]> mj.ucw.cz Git - checkmail.git/blob - cm.c
Added an option to treat old, but still unread messages as new
[checkmail.git] / cm.c
1 /*
2  *      Incoming Mail Checker
3  *
4  *      (c) 2005--2010 Martin Mares <mj@ucw.cz>
5  */
6
7 #define _GNU_SOURCE
8
9 #include <stdio.h>
10 #include <string.h>
11 #include <stdlib.h>
12 #include <ctype.h>
13 #include <getopt.h>
14 #include <fcntl.h>
15 #include <glob.h>
16 #include <fnmatch.h>
17 #include <sys/stat.h>
18 #include <unistd.h>
19 #include <pwd.h>
20 #include <time.h>
21 #include <sys/types.h>
22 #include <sys/wait.h>
23
24 #ifdef CONFIG_WIDE_CURSES
25 #include <ncursesw/ncurses.h>
26 #else
27 #include <curses.h>
28 #endif
29
30 #include "util.h"
31 #include "clists.h"
32 #include "charset.h"
33
34 static int check_interval = 30;
35 static int force_refresh;
36 static int allow_bells = 1;
37 static int allow_osd = 1;
38 static int minimum_priority;
39 static time_t last_scan_time;
40 static char *run_cmd = "mutt -f %s";
41
42 struct options {
43   int priority;
44   int hide;
45   int hide_if_empty;
46   int highlight;
47   int beep;
48   int snippets;
49   int show_flagged;
50   int sender_personal;
51   int sender_mbox;
52   int hotkey;
53   int led;
54   int osd;
55   int unread_is_new;
56 };
57
58 struct option_node {
59   cnode n;
60   struct options o;
61   char pattern[1];
62 };
63
64 struct pattern_node {
65   cnode n;
66   char *name;
67   char pattern[1];
68 };
69
70 static clist options, patterns;
71 static struct options global_options = {
72   .sender_personal = 1
73 };
74
75 struct mbox {
76   cnode n;
77   struct options o;
78   char *name;
79   char *path;
80   int index;
81   int scanning;
82   int seen;
83   time_t last_time;
84   time_t display_valid_until;
85   int last_size, last_pos;
86   int total, new, flagged;
87   int last_total, last_new, last_flagged;
88   int last_beep_new;
89   int force_refresh;
90   int snippet_is_new;
91   char sender_snippet[128];
92   char subject_snippet[128];
93 };
94
95 static clist mboxes;
96 static struct mbox **mbox_array;
97 static int num_mboxes, mbox_array_size;
98
99 struct osd_opt_node {
100   cnode n;
101   char *val;
102   char key[1];
103 };
104
105 static clist osd_opts;
106
107 static void redraw_line(int i);
108 static void rethink_display(int notify);
109
110 static void
111 add_pattern(char *patt)
112 {
113   struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
114   strcpy(n->pattern, patt);
115   if (patt = strchr(n->pattern, '='))
116     {
117       *patt++ = 0;
118       n->name = patt;
119     }
120   else
121     n->name = NULL;
122   clist_add_tail(&patterns, &n->n);
123 }
124
125 static void
126 add_inbox(void)
127 {
128   struct passwd *p = getpwuid(getuid());
129   if (!p)
130     die("You don't exist, go away!");
131   char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
132   sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
133   add_pattern(buf);
134 }
135
136 static void
137 init_options(struct options *o)
138 {
139   o->priority = -1;
140   o->hide = -1;
141   o->hide_if_empty = -1;
142   o->beep = -1;
143   o->highlight = -1;
144   o->snippets = -1;
145   o->show_flagged = -1;
146   o->sender_personal = -1;
147   o->sender_mbox = -1;
148   o->hotkey = -1;
149   o->led = -1;
150   o->osd = -1;
151   o->unread_is_new = -1;
152 }
153
154 static void
155 setup_options(struct mbox *b)
156 {
157   b->o = global_options;
158   CLIST_FOR_EACH(struct option_node *, n, options)
159     if (!fnmatch(n->pattern, b->name, 0))
160       {
161         debug("\tApplied options %s\n", n->pattern);
162 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
163         MERGE(priority);
164         MERGE(hide);
165         MERGE(hide_if_empty);
166         MERGE(highlight);
167         MERGE(beep);
168         MERGE(snippets);
169         MERGE(show_flagged);
170         MERGE(sender_personal);
171         MERGE(sender_mbox);
172         MERGE(hotkey);
173         MERGE(led);
174         MERGE(osd);
175         MERGE(unread_is_new);
176       }
177 }
178
179 static void
180 add_osd_opt(char *arg)
181 {
182   struct osd_opt_node *n = xmalloc(sizeof(*n) + strlen(arg));
183   strcpy(n->key, arg);
184   n->val = strchr(n->key, '=');
185   if (!n->val)
186     die("Malformed OSD option");
187   *n->val++ = 0;
188   clist_add_tail(&osd_opts, &n->n);
189 }
190
191 static char *
192 mbox_name(char *path)
193 {
194   char *c = strrchr(path, '/');
195   return c ? (c+1) : path;
196 }
197
198 static struct mbox *
199 add_mbox(clist *l, char *path, char *name)
200 {
201   struct mbox *b = xmalloc(sizeof(*b));
202   bzero(b, sizeof(*b));
203   b->path = xstrdup(path);
204   b->name = xstrdup(name);
205
206   if (name)
207     {
208       cnode *prev = l->head.prev;
209       while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
210         prev = prev->prev;
211       clist_insert_after(&b->n, prev);
212     }
213   else
214     clist_add_tail(l, &b->n);
215
216   return b;
217 }
218
219 static void
220 del_mbox(struct mbox *b)
221 {
222   clist_remove(&b->n);
223   free(b->path);
224   free(b->name);
225   free(b);
226 }
227
228 static struct mbox *
229 find_mbox(clist *l, char *path)
230 {
231   CLIST_FOR_EACH(struct mbox *, b, *l)
232     if (!strcmp(b->path, path))
233       return b;
234   return NULL;
235 }
236
237 static inline int
238 mbox_active_p(struct mbox *b)
239 {
240   if (b->o.priority < minimum_priority)
241     return 0;
242   if (b->o.hide)
243     return 0;
244   return 1;
245 }
246
247 static inline int
248 mbox_visible_p(struct mbox *b)
249 {
250   if (!mbox_active_p(b))
251     return 0;
252   if (b->scanning < 0)
253     return 1;
254   if (b->o.hide_if_empty && !b->total)
255     return 0;
256   return 1;
257 }
258
259 static void
260 prepare_snippets(struct mbox *b, char *sender, char *subject)
261 {
262   char *pos, *term;
263
264   while (*sender == ' ' || *sender == '\t')
265     sender++;
266   while (*subject == ' ' || *subject == '\t')
267     subject++;
268
269   pos = b->sender_snippet;
270   term = pos + sizeof(b->sender_snippet) - 1;
271   if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
272     add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
273   else
274     *pos = 0;
275
276   pos = b->subject_snippet;
277   term = pos + sizeof(b->subject_snippet) - 1;
278   if (subject[0])
279     add_subject_snippet(&pos, term, subject);
280   else
281     add_snippet_raw(&pos, term, "No subject");
282 }
283
284 static void
285 build_snippet(char *buf, char *term, struct mbox *b)
286 {
287   if (b->sender_snippet[0])
288     {
289       add_snippet(&buf, term, b->sender_snippet);
290       add_snippet_raw(&buf, term, ": ");
291     }
292   add_snippet(&buf, term, b->subject_snippet);
293 }
294
295 static int mb_fd, mb_pos;
296 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
297
298 static void
299 mb_reset(int pos)
300 {
301   mb_cc = mb_end = mb_buf;
302   mb_pos = pos;
303 }
304
305 static void
306 mb_seek(uns pos)
307 {
308   lseek(mb_fd, pos, SEEK_SET);
309   mb_reset(pos);
310 }
311
312 static int
313 mb_tell(void)
314 {
315   return mb_pos - (mb_end - mb_cc);
316 }
317
318 static int
319 mb_ll_get(void)
320 {
321   int len = read(mb_fd, mb_buf, sizeof(mb_buf));
322   mb_cc = mb_buf;
323   if (len <= 0)
324     {
325       mb_end = mb_buf;
326       return -1;
327     }
328   else
329     {
330       mb_end = mb_buf + len;
331       mb_pos += len;
332       return *mb_cc++;
333     }
334 }
335
336 static inline int
337 mb_get(void)
338 {
339   return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
340 }
341
342 static void
343 mb_unget(int c)
344 {
345   if (c >= 0)
346     mb_cc--;
347 }
348
349 static int
350 mb_check(const char *p, int len)
351 {
352   while (len--)
353     {
354       if (mb_get() != *p++)
355         return 0;
356     }
357   return 1;
358 }
359
360 static void
361 mb_skip(int len)
362 {
363   while (len)
364     {
365       int avail = mb_end - mb_cc;
366       if (!avail)
367         {
368           if (mb_ll_get() < 0)
369             return;
370           len--;
371         }
372       else
373         {
374           int next = (avail < len) ? avail : len;
375           len -= next;
376           mb_cc += next;
377         }
378     }
379 }
380
381 static void
382 scan_mbox(struct mbox *b, struct stat *st)
383 {
384   char buf[1024], sender[1024], subject[1024];
385   int c;
386   int compressed = 0;
387   const char from[] = "\nFrom ";
388
389   if (!st->st_size)
390     {
391       b->total = b->new = b->flagged = 0;
392       b->last_pos = 0;
393       return;
394     }
395
396   /* FIXME: Should we do some locking? */
397
398   mb_fd = open(b->path, O_RDONLY);
399   if (mb_fd < 0)
400     {
401       debug("[open failed: %m] ");
402       b->total = b->new = b->flagged = -1;
403       return;
404     }
405
406   char signature[2];
407   c = read(mb_fd, signature, 2);
408   lseek(mb_fd, 0, SEEK_SET);
409
410   if (c == 2 && !memcmp(signature, "\037\213", 2)) //gzip
411     {
412       debug("[decompressing] ");
413       int fds[2];
414       if (pipe(fds))
415         die("pipe failed: %m");
416       int pid = fork();
417       if (pid < 0)
418         die("fork failed: %m");
419       if (!pid)
420         {
421           if (dup2(mb_fd, 0) < 0 || dup2(fds[1], 1) < 0)
422             die("dup2 failed: %m");
423           close(fds[0]);
424           close(fds[1]);
425           close(mb_fd);
426           execlp("gzip", "gzip", "-cd", NULL);
427           die("Cannot execute gzip: %m");
428         }
429       close(fds[1]);
430       close(mb_fd);
431       mb_fd = fds[0];
432       compressed = 1;
433     }
434   mb_reset(0);
435
436   int incremental = 0;
437   if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && !compressed)
438     {
439       mb_seek(b->last_pos);
440       if (mb_check(from, 6))
441         {
442           debug("[incremental] ");
443           incremental = 1;
444         }
445       else
446         {
447           debug("[incremental failed] ");
448           mb_seek(0);
449         }
450     }
451   if (!incremental)
452     {
453       if (!mb_check(from+1, 5))
454         {
455           debug("[inconsistent] ");
456           b->total = b->new = b->flagged = -1;
457           goto done;
458         }
459       b->total = b->new = b->flagged = 0;
460       b->last_total = b->last_new = b->last_flagged = 0;
461       b->snippet_is_new = 0;
462     }
463   else
464     {
465       b->total = b->last_total;
466       b->new = b->last_new;
467       b->flagged = b->last_flagged;
468     }
469
470   for(;;)
471     {
472       b->last_pos = mb_tell() - 5;
473       if (b->last_pos)
474         b->last_pos--;          // last_pos should be the previous \n character
475       b->last_total = b->total;
476       b->last_new = b->new;
477       b->last_flagged = b->flagged;
478       while ((c = mb_get()) >= 0 && c != '\n')
479         ;
480
481       int content_length = -1;
482       int new = 1;
483       int flagged = 0;
484       sender[0] = 0;
485       subject[0] = 0;
486       for (;;)
487         {
488           uns i = 0;
489           for (;;)
490             {
491               c = mb_get();
492               if (c < 0)
493                 {
494                   debug("[truncated] ");
495                   goto done;
496                 }
497               if (c == '\n')
498                 {
499                   int fold = -1;
500                   do
501                     {
502                       fold++;
503                       c = mb_get();
504                     }
505                   while (c == ' ' || c == '\t');
506                   mb_unget(c);
507                   if (!fold)
508                     break;
509                   c = ' ';
510                 }
511               if (c == '\r')
512                 continue;
513               if (i < sizeof(buf) - 1)
514                 buf[i++] = c;
515             }
516           buf[i] = 0;
517           if (!buf[0])
518             break;
519           if (!strncasecmp(buf, "Status:", 7))
520             {
521               if (!b->o.unread_is_new || strchr(buf + 7, 'R'))
522                 new = 0;
523             }
524           else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
525             flagged = 1;
526           else if (!strncasecmp(buf, "From:", 5))
527             strcpy(sender, buf+5);
528           else if (!strncasecmp(buf, "Subject:", 8))
529             strcpy(subject, buf+8);
530           else if (!strncasecmp(buf, "Content-Length:", 15))
531             content_length = atoi(buf + 15);
532         }
533
534       b->total++;
535       if (new)
536         b->new++;
537       if (flagged)
538         b->flagged++;
539       if (debug_mode > 1)
540         debug("new=%d flagged=%d len=%d sender=<%s> subject=<%s>\n", new, flagged, content_length, sender, subject);
541       if (new || (flagged && !b->snippet_is_new))
542         {
543           b->snippet_is_new = new;
544           prepare_snippets(b, sender, subject);
545         }
546
547       if (content_length >= 0)
548         mb_skip(content_length);
549
550       int ct = 1;
551       while (from[ct])
552         {
553           c = mb_get();
554           if (c < 0)
555             goto done;
556           if (c != from[ct++])
557             ct = (c == '\n');
558         }
559     }
560
561  done:
562   close(mb_fd);
563   if (compressed)
564     {
565       int status;
566       if (wait(&status) < 0 || !WIFEXITED(status) || WEXITSTATUS(status))
567         b->total = b->new = b->flagged = -1;
568     }
569 }
570
571 static void
572 scan(int notify)
573 {
574   debug("Searching for mailboxes (notify=%d)...\n", notify);
575   last_scan_time = time(NULL);
576   CLIST_FOR_EACH(struct pattern_node *, p, patterns)
577     {
578       debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
579       glob_t g;
580       int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
581       if (err && err != GLOB_NOMATCH)
582         die("Failed to glob %s: %m", p->pattern);
583       for (uns i=0; i<g.gl_pathc; i++)
584         {
585           char *name = g.gl_pathv[i];
586           struct mbox *b = find_mbox(&mboxes, name);
587           if (!b)
588             {
589               b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
590               debug("Discovered mailbox %s (%s)\n", b->name, b->path);
591               setup_options(b);
592               b->scanning = -1;
593             }
594           b->seen = 1;
595         }
596       globfree(&g);
597     }
598
599   struct mbox *tmp;
600   CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
601     {
602       if (b->seen)
603         b->seen = 0;
604       else
605         {
606           debug("Lost mailbox %s\n", b->name);
607           del_mbox(b);
608         }
609     }
610
611   rethink_display(0);
612
613   debug("Scanning mailboxes...\n");
614   CLIST_FOR_EACH(struct mbox *, b, mboxes)
615     {
616       struct stat st;
617       debug("%s: ", b->name);
618       if (!mbox_active_p(b))
619         {
620           debug("inactive\n");
621           continue;
622         }
623       if (force_refresh)
624         b->force_refresh = 1;
625       if (stat(b->path, &st) < 0)
626         {
627           b->total = b->new = b->flagged = -1;
628           debug("%m\n");
629         }
630       else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
631         {
632           b->scanning = 1;
633           redraw_line(b->index);
634           refresh();
635
636           scan_mbox(b, &st);
637           b->last_time = st.st_mtime;
638           b->last_size = st.st_size;
639           debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
640
641           b->scanning = 0;
642           redraw_line(b->index);
643           refresh();
644         }
645       else if (b->display_valid_until <= last_scan_time)
646         {
647           debug("not changed, but needs redraw\n");
648           redraw_line(b->index);
649         }
650       else
651         debug("not changed\n");
652       b->force_refresh = 0;
653     }
654   force_refresh = 0;
655
656   debug("Scan finished\n");
657   last_scan_time = time(NULL);
658   rethink_display(notify);
659 }
660
661 #ifdef CONFIG_X11
662
663 #include <X11/Xlib.h>
664 #include <X11/Xatom.h>
665
666 static Display *x11_dpy;
667 static unsigned leds_care, leds_have, leds_want;
668
669 static Atom osd_pty;
670 static unsigned osd_care;
671 #define OSD_MSG_SIZE 1024
672 static char osd_last_msg[OSD_MSG_SIZE];
673 static time_t osd_last_time;
674
675 static void
676 x11_init(void)
677 {
678   leds_care = (global_options.led >= 0 ? (1 << global_options.led) : 0);
679   osd_care = (global_options.osd >= 0);
680   CLIST_FOR_EACH(struct option_node *, o, options)
681     {
682       if (o->o.led > 0)
683         leds_care |= (1 << o->o.led);
684       if (o->o.osd > 0)
685         osd_care = 1;
686     }
687
688   if (!leds_care && !osd_care)
689     {
690       debug("X11: No mailbox wants LEDs or OSD\n");
691       return;
692     }
693   if (!getenv("DISPLAY"))
694     {
695       debug("X11: Do not have X display\n");
696       return;
697     }
698   if (!(x11_dpy = XOpenDisplay(NULL)))
699     die("Cannot open X display, although the DISPLAY variable is set");
700
701   if (osd_care)
702     {
703       osd_pty = XInternAtom(x11_dpy, "OSD_QUEUE", False);
704       if (!osd_pty)
705         die("Cannot intern OSD_QUEUE atom");
706
707       // If OSD options contain no message, add one
708       int seen_msg = 0;
709       CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
710         if (!n->key[0])
711           seen_msg = 1;
712       if (!seen_msg)
713         {
714           add_osd_opt("=%40f");
715           add_osd_opt("=%40s");
716           add_osd_opt("=");
717           add_osd_opt("=(and %m more)");
718         }
719     }
720
721   leds_have = ~0U;
722   debug("X11: Initialized\n");
723 }
724
725 static void
726 sync_leds(void)
727 {
728   if (leds_want == leds_have)
729     return;
730
731   debug("LEDS: have %02x, want %02x, care %02x\n", leds_have, leds_want, leds_care);
732   for (int i=1; i<10; i++)
733     if (leds_care & (leds_have ^ leds_want) & (1 << i))
734       {
735         XKeyboardControl cc;
736         cc.led = i;
737         cc.led_mode = (leds_want & (1 << i)) ? LedModeOn : LedModeOff;
738         XChangeKeyboardControl(x11_dpy, KBLed | KBLedMode, &cc);
739       }
740   XFlush(x11_dpy);
741   leds_have = leds_want;
742 }
743
744 static void
745 rethink_leds(void)
746 {
747   if (!leds_care || !x11_dpy)
748     return;
749
750   leds_want = 0;
751   CLIST_FOR_EACH(struct mbox *, b, mboxes)
752     if (b->o.led > 0 && b->new)
753       leds_want |= (1 << b->o.led);
754   sync_leds();
755 }
756
757 struct osd_params {
758   struct mbox *mbox;
759   int total_new;
760 };
761
762 static int
763 format_osd_string(char *dest, char *src, struct osd_params *par)
764 {
765   char *stop = dest + OSD_MSG_SIZE - 1;
766   char numbuf[16];
767
768   while (*src && dest < stop)
769     {
770       if (*src == '%')
771         {
772           src++;
773           int size = 0;
774           while (*src >= '0' && *src <= '9')
775             size = 10*size + *src++ - '0';
776           if (!size || size > stop-dest)
777             size = dest-stop;
778
779           int spec = *src++;
780           if (!spec)
781             break;
782
783           char *arg = numbuf;
784           switch (spec)
785             {
786             case 'f':
787               arg = par->mbox->sender_snippet;
788               break;
789             case 's':
790               arg = par->mbox->subject_snippet;
791               break;
792             case 'n':
793               snprintf(numbuf, sizeof(numbuf), "%d", par->total_new);
794               break;
795             case 'm':
796               if (par->total_new < 2)
797                 return 0;
798               snprintf(numbuf, sizeof(numbuf), "%d", par->total_new - 1);
799               break;
800             case '%':
801               arg = "%";
802               break;
803             default:
804               arg = "???";
805               break;
806             }
807
808           while (*arg && size)
809             {
810               *dest++ = *arg++;
811               size--;
812             }
813         }
814       else
815         *dest++ = *src++;
816     }
817   *dest = 0;
818   return 1;
819 }
820
821 static void
822 format_osd(char *msg, struct osd_params *par)
823 {
824   if (!par->mbox)
825     {
826       msg[0] = 0;
827       return;
828     }
829
830   unsigned pos = 0;
831   unsigned have_text = 0;
832   CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
833     {
834       char buf[OSD_MSG_SIZE];
835       if (!format_osd_string(buf, n->val, par))
836         continue;
837       if (!n->key[0] && buf[0])
838         have_text = 1;
839       pos += snprintf(msg+pos, OSD_MSG_SIZE-pos-1, "%s:%s\n", n->key, buf);
840       if (pos > OSD_MSG_SIZE-1)
841         {
842           pos = sprintf(msg, "OSD message too long!\n");
843           break;
844         }
845     }
846   if (have_text)
847     msg[pos++] = '\n';
848   else
849     pos = 0;
850   msg[pos] = 0;
851 }
852
853 static void
854 debug_osd_msg(char *msg)
855 {
856   if (!debug_mode)
857     return;
858   fprintf(stderr, "OSD: <");
859   while (*msg)
860     {
861       fputc((*msg != '\n' ? *msg : '|'), stderr);
862       msg++;
863     }
864   fprintf(stderr, ">\n");
865 }
866
867 static void
868 rethink_osd(int notify)
869 {
870   if (!osd_care || !x11_dpy || !allow_osd)
871     {
872       osd_last_msg[0] = 0;
873       return;
874     }
875
876   struct osd_params p = { .mbox = NULL, .total_new = 0 };
877   CLIST_FOR_EACH(struct mbox *, b, mboxes)
878     if (b->o.osd > 0)
879       {
880         p.total_new += b->new;
881         if (b->new && b->last_time > osd_last_time && (!p.mbox || p.mbox->o.priority < b->o.priority))
882           p.mbox = b;
883       }
884
885   char new_msg[OSD_MSG_SIZE];
886   format_osd(new_msg, &p);
887   debug_osd_msg(new_msg);
888   if (strcmp(new_msg, osd_last_msg))
889     {
890       strcpy(osd_last_msg, new_msg);
891       if (notify && new_msg[0])
892         {
893           debug("OSD: Sending to daemon\n");
894           XChangeProperty(x11_dpy, DefaultRootWindow(x11_dpy), osd_pty, XA_STRING, 8, PropModeAppend, (unsigned char *) new_msg, strlen(new_msg));
895           XFlush(x11_dpy);
896         }
897       else
898         debug("OSD: No changes\n");
899       osd_last_time = time(NULL);
900     }
901 }
902
903 static void
904 x11_cleanup(void)
905 {
906   if (!x11_dpy)
907     return;
908
909   leds_want = 0;
910   sync_leds();
911 }
912
913 #else
914
915 static void x11_init(void) { }
916 static void rethink_leds(void) { }
917 static void rethink_osd(int notify UNUSED) { }
918 static void x11_cleanup(void) { }
919
920 #endif
921
922 static int cursor_at, cursor_max;
923
924 static unsigned is_active, is_pos;      // incremental search
925 static char is_buf[64];
926
927 enum {
928   M_IDLE,
929   M_SCAN,
930   M_NEW,
931   M_FLAG,
932   M_BAD,
933   M_INCSEARCH,
934   M_MAX
935 };
936 static int attrs[3][2][M_MAX];          // active (2=incsearch), hilite, status
937
938 static void
939 redraw_line(int i)
940 {
941   move(i, 0);
942   if (i < cursor_max)
943     {
944       struct mbox *b = mbox_array[i];
945       int cc = (cursor_at == i ? (is_active ? 2 : 1) : 0);
946       int hi = b->o.highlight;
947       unsigned namepos = 0;
948       unsigned namelen = strlen(b->name);
949       int valid = 3600;
950
951       attrset(attrs[cc][hi][M_IDLE]);
952       if (b->o.hotkey)
953         printw("%c ", b->o.hotkey);
954       else if (cc)
955         printw("> ");
956       else
957         printw("  ");
958       if (cc == 2)
959         {
960           attrset(attrs[cc][hi][M_INCSEARCH]);
961           for (namepos=0; namepos < is_pos && namepos < 20; namepos++)
962             addch(is_buf[namepos]);
963         }
964       if (b->new)
965         attrset(attrs[cc][hi][M_NEW]);
966       else if (b->flagged && b->o.show_flagged)
967         attrset(attrs[cc][hi][M_FLAG]);
968       else
969         attrset(attrs[cc][hi][M_IDLE]);
970       while (namepos < namelen)
971         addch(b->name[namepos++]);
972       while (namepos++ < 20)
973         addch(' ');
974       if (b->scanning < 0)
975         ;
976       else if (b->scanning)
977         {
978           attrset(attrs[cc][hi][M_SCAN]);
979           printw("[SCANNING]");
980         }
981       else if (b->total < 0)
982         {
983           attrset(attrs[cc][hi][M_BAD]);
984           printw("BROKEN");
985         }
986       else
987         {
988           attrset(attrs[cc][hi][M_IDLE]);
989           printw("%6d ", b->total);
990           int snip = 0;
991           if (b->new)
992             {
993               attrset(attrs[cc][hi][M_NEW]);
994               printw("%6d  ", b->new);
995               attrset(attrs[cc][hi][M_IDLE]);
996               int age = (last_scan_time - b->last_time);
997               if (age < 0)
998                 age = 0;
999               if (age < 3600)
1000                 {
1001                   printw("%2d min  ", age/60);
1002                   valid = 60;
1003                 }
1004               else if (age < 86400)
1005                 printw("%2d hr%c  ", age/3600, (age >= 7200 ? 's' : ' '));
1006               else
1007                 printw("        ");
1008               snip = 1;
1009             }
1010           else if (b->flagged && b->o.show_flagged)
1011             {
1012               attrset(attrs[cc][hi][M_FLAG]);
1013               printw("%6d  ", b->flagged);
1014               attrset(attrs[cc][hi][M_IDLE]);
1015               printw("        ");
1016               attrset(attrs[cc][0][M_FLAG]);    /* We avoid the highlight intentionally */
1017               snip = 1;
1018             }
1019           if (snip && b->o.snippets)
1020             {
1021               int xx, yy;
1022               getyx(stdscr, yy, xx);
1023               int remains = COLS-1-xx;
1024               (void) yy;
1025
1026               char snip[256];
1027               build_snippet(snip, snip + sizeof(snip) - 1, b);
1028
1029               if (snip[0] && remains > 2)
1030                 {
1031 #ifdef CONFIG_WIDE_CURSES
1032                   size_t len = strlen(snip)+1;
1033                   wchar_t snip2[len];
1034                   mbstowcs(snip2, snip, len);
1035                   addnwstr(snip2, remains);
1036 #else
1037                   printw("%-.*s", remains, snip);
1038 #endif
1039                 }
1040             }
1041         }
1042       b->display_valid_until = last_scan_time + valid;
1043     }
1044   attrset(attrs[0][0][M_IDLE]);
1045   clrtoeol();
1046 }
1047
1048 static void
1049 redraw_all(void)
1050 {
1051   cursor_max = num_mboxes;
1052   if (cursor_max > LINES-1)
1053     cursor_max = LINES-1;
1054   if (cursor_at >= cursor_max)
1055     cursor_at = cursor_max - 1;
1056   if (cursor_at < 0)
1057     cursor_at = 0;
1058
1059   for (int i=0; i<cursor_max; i++)
1060     redraw_line(i);
1061   move(cursor_max, 0);
1062   if (!cursor_max)
1063     {
1064       printw("(no mailboxes found)");
1065       clrtoeol();
1066       move(1, 0);
1067     }
1068   clrtobot();
1069 }
1070
1071 static void
1072 rethink_display(int notify)
1073 {
1074   int i = 0;
1075   int changed = 0;
1076   int beeeep = 0;
1077   CLIST_FOR_EACH(struct mbox *, b, mboxes)
1078     if (mbox_visible_p(b))
1079       {
1080         b->index = i;
1081         if (i >= num_mboxes || mbox_array[i] != b)
1082           {
1083             changed = 1;
1084             if (i >= mbox_array_size)
1085               {
1086                 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
1087                 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
1088               }
1089             mbox_array[i] = b;
1090           }
1091         if (b->o.beep && b->new > b->last_beep_new)
1092           beeeep = 1;
1093         b->last_beep_new = b->new;
1094         i++;
1095       }
1096   if (i != num_mboxes)
1097     changed = 1;
1098   num_mboxes = i;
1099
1100   if (changed)
1101     {
1102       redraw_all();
1103       refresh();
1104     }
1105   rethink_leds();
1106   rethink_osd(notify);
1107   if (beeeep && allow_bells && notify)
1108     beep();
1109 }
1110
1111 static void
1112 term_init(void)
1113 {
1114   initscr();
1115   cbreak();
1116   noecho();
1117   nonl();
1118   intrflush(stdscr, FALSE);
1119   keypad(stdscr, TRUE);
1120   curs_set(0);
1121
1122   static const int attrs_mono[2][M_MAX] = {
1123         [0] = { [M_IDLE] = 0,
1124                 [M_SCAN] = A_BOLD,
1125                 [M_NEW] = A_BOLD,
1126                 [M_FLAG] = 0,
1127                 [M_BAD] = A_DIM,
1128                 [M_INCSEARCH] = A_REVERSE },
1129         [1] = { [M_IDLE] = 0,
1130                 [M_SCAN] = A_BOLD,
1131                 [M_NEW] = A_REVERSE | A_BOLD,
1132                 [M_FLAG] = A_REVERSE,
1133                 [M_BAD] = A_DIM,
1134                 [M_INCSEARCH] = A_REVERSE },
1135   };
1136   for (int i=0; i<2; i++)
1137     for (int j=0; j<M_MAX; j++)
1138       {
1139         attrs[0][i][j] = attrs_mono[i][j];
1140         attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
1141         attrs[2][i][j] = attrs_mono[i][j] | A_UNDERLINE;
1142       }
1143
1144   if (has_colors())
1145     {
1146       start_color();
1147       if (COLOR_PAIRS >= 12)
1148         {
1149           init_pair(1, COLOR_YELLOW, COLOR_BLACK);
1150           init_pair(2, COLOR_RED, COLOR_BLACK);
1151           init_pair(3, COLOR_WHITE, COLOR_BLUE);
1152           init_pair(4, COLOR_YELLOW, COLOR_BLUE);
1153           init_pair(5, COLOR_RED, COLOR_BLUE);
1154           init_pair(6, COLOR_GREEN, COLOR_BLACK);
1155           init_pair(7, COLOR_GREEN, COLOR_BLUE);
1156           init_pair(8, COLOR_WHITE, COLOR_MAGENTA);
1157           init_pair(9, COLOR_YELLOW, COLOR_MAGENTA);
1158           init_pair(10, COLOR_GREEN, COLOR_MAGENTA);
1159           init_pair(11, COLOR_RED, COLOR_MAGENTA);
1160           init_pair(12, COLOR_BLACK, COLOR_YELLOW);
1161           static const int attrs_color[3][2][M_MAX] = {
1162              [0][0] = { [M_IDLE] = 0,
1163                         [M_SCAN] = COLOR_PAIR(1),
1164                         [M_NEW] = COLOR_PAIR(1),
1165                         [M_FLAG] = COLOR_PAIR(6),
1166                         [M_BAD] = COLOR_PAIR(2) },
1167              [0][1] = { [M_IDLE] = A_BOLD,
1168                         [M_SCAN] = COLOR_PAIR(1),
1169                         [M_NEW] = COLOR_PAIR(1) | A_BOLD,
1170                         [M_FLAG] = COLOR_PAIR(6) | A_BOLD,
1171                         [M_BAD] = COLOR_PAIR(2) | A_BOLD },
1172              [1][0] = { [M_IDLE] = COLOR_PAIR(3),
1173                         [M_SCAN] = COLOR_PAIR(4),
1174                         [M_NEW] = COLOR_PAIR(4),
1175                         [M_FLAG] = COLOR_PAIR(7),
1176                         [M_BAD] = COLOR_PAIR(5) },
1177              [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD,
1178                         [M_SCAN] = COLOR_PAIR(4),
1179                         [M_NEW] = COLOR_PAIR(4) | A_BOLD,
1180                         [M_FLAG] = COLOR_PAIR(7) | A_BOLD,
1181                         [M_BAD] = COLOR_PAIR(5) | A_BOLD },
1182              [2][0] = { [M_IDLE] = COLOR_PAIR(8),
1183                         [M_SCAN] = COLOR_PAIR(9),
1184                         [M_NEW] = COLOR_PAIR(9),
1185                         [M_FLAG] = COLOR_PAIR(10),
1186                         [M_BAD] = COLOR_PAIR(11),
1187                         [M_INCSEARCH] = COLOR_PAIR(12) | A_DIM },
1188              [2][1] = { [M_IDLE] = COLOR_PAIR(8) | A_BOLD,
1189                         [M_SCAN] = COLOR_PAIR(9),
1190                         [M_NEW] = COLOR_PAIR(9) | A_BOLD,
1191                         [M_FLAG] = COLOR_PAIR(10) | A_BOLD,
1192                         [M_BAD] = COLOR_PAIR(11) | A_BOLD,
1193                         [M_INCSEARCH] = COLOR_PAIR(12) },
1194           };
1195           memcpy(attrs, attrs_color, sizeof(attrs));
1196         }
1197     }
1198 }
1199
1200 static void
1201 term_cleanup(void)
1202 {
1203   endwin();
1204 }
1205
1206 static void
1207 print_status(char *status)
1208 {
1209   move(LINES-1, 0);
1210   if (status)
1211     printw("%s", status);
1212   clrtoeol();
1213   refresh();
1214 }
1215
1216 static void
1217 scan_and_redraw(int notify)
1218 {
1219   print_status("Busy...");
1220   scan(notify);
1221   print_status(NULL);
1222 }
1223
1224 static void
1225 move_cursor(int i)
1226 {
1227   if (i >= 0 && i < cursor_max && i != cursor_at)
1228     {
1229       int old = cursor_at;
1230       cursor_at = i;
1231       redraw_line(old);
1232       redraw_line(i);
1233     }
1234 }
1235
1236 static void
1237 next_active(int since, int step)
1238 {
1239   if (!cursor_max)
1240     return;
1241   since = (since+cursor_max) % cursor_max;
1242   step = (step+cursor_max) % cursor_max;
1243   int besti = -1;
1244   int bestp = -1;
1245   int i = since;
1246   do
1247     {
1248       struct mbox *b = mbox_array[i];
1249       if (b->new && b->o.priority > bestp)
1250         {
1251           besti = i;
1252           bestp = b->o.priority;
1253         }
1254       i = (i+step) % cursor_max;
1255     }
1256   while (i != since);
1257   if (besti >= 0)
1258     move_cursor(besti);
1259 }
1260
1261 static void
1262 mbox_run(struct mbox *b)
1263 {
1264   char cmd[strlen(run_cmd) + strlen(b->path) + 16];
1265   sprintf(cmd, run_cmd, b->path);
1266   term_cleanup();
1267   system(cmd);
1268   term_init();
1269   redraw_all();
1270   refresh();
1271   b->force_refresh = 1;
1272   scan_and_redraw(0);
1273 }
1274
1275 static void
1276 enter_incsearch(void)
1277 {
1278   print_status("Incremental search...");
1279   is_active = 1;
1280   is_pos = 0;
1281   redraw_line(cursor_at);
1282 }
1283
1284 static int
1285 handle_incsearch(int ch)
1286 {
1287   if ((ch == KEY_BACKSPACE || ch == KEY_DC) && is_pos)
1288     --is_pos;
1289   else if (ch >= ' ' && ch <= '~')
1290     {
1291       if (is_pos < sizeof(is_buf) - 1)
1292         is_buf[is_pos++] = ch;
1293     }
1294   else
1295     {
1296       print_status(NULL);
1297       is_active = 0;
1298       is_pos = 0;
1299       redraw_line(cursor_at);
1300       return 0;
1301     }
1302
1303   is_buf[is_pos] = 0;
1304   for (int i=0; i<cursor_max; i++)
1305     {
1306       struct mbox *b = mbox_array[i];
1307       if (!strncmp(b->name, is_buf, is_pos))
1308         {
1309           if (i != cursor_at)
1310             {
1311               move_cursor(i);
1312               return 1;
1313             }
1314           break;
1315         }
1316     }
1317
1318   redraw_line(cursor_at);
1319   return 1;
1320 }
1321
1322 #define STR2(c) #c
1323 #define STR(c) STR2(c)
1324
1325 static void NONRET
1326 usage(void)
1327 {
1328   fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
1329 \n\
1330 Options:\n\
1331 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
1332 -d\t\t\tLog debug messages to stderr\n\
1333 -i\t\t\tInclude user's INBOX\n\
1334 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
1335 -o <pattern>=<opts>\tSet mailbox options\n\
1336 -o <opts>\t\tSet default options for all mailboxes\n\
1337 -p <pri>\t\tSet minimum priority to show\n\
1338 -s <key>=<val>\t\tSet on-screen display options (consult OSDD docs)\n\
1339 \n\
1340 Mailbox options (set with `-o', use upper case to negate):\n\
1341 0-9\t\t\tSet mailbox priority (0=default)\n\
1342 b\t\t\tBeep when a message arrives\n\
1343 d\t\t\tSend an on-screen-display message (requires OSDD)\n\
1344 e\t\t\tHide from display if empty\n\
1345 f\t\t\tShow flagged messages if there are no new ones\n\
1346 h\t\t\tHide from display\n\
1347 l<led>\t\t\tLight a keyboard led (1-9) if running on X display\n\
1348 m\t\t\tShow mailbox name of the sender\n\
1349 o\t\t\tCount old, but unread messages as new\n\
1350 p\t\t\tShow personal info (full name) of the sender\n\
1351 s\t\t\tShow message snippets\n\
1352 t\t\t\tHighlight the entry\n\
1353 !<key>\t\t\tSet hot key\n\
1354 \n\
1355 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
1356 It can be freely distributed and used according to the GNU GPL v2.\n\
1357 ");
1358   exit(1);
1359 }
1360
1361 static void
1362 parse_options(char *c)
1363 {
1364   struct options *o;
1365   char *sep;
1366   if (sep = strchr(c, '='))
1367     {
1368       struct option_node *n = xmalloc(sizeof(*n) + sep-c);
1369       memcpy(n->pattern, c, sep-c);
1370       n->pattern[sep-c] = 0;
1371       clist_add_tail(&options, &n->n);
1372       o = &n->o;
1373       init_options(o);
1374       c = sep+1;
1375     }
1376   else
1377     o = &global_options;
1378
1379   int x;
1380   while (x = *c++)
1381     if (x >= '0' && x <= '9')
1382       o->priority = x - '0';
1383     else if (x == '!' && *c)
1384       o->hotkey = *c++;
1385     else if (x == 'l' && *c >= '1' && *c <= '9')
1386       o->led = *c++ - '0';
1387     else
1388       {
1389         int value = !!islower(x);
1390         switch (tolower(x))
1391           {
1392           case 'b':
1393             o->beep = value;
1394             break;
1395           case 'd':
1396             o->osd = value;
1397             break;
1398           case 'e':
1399             o->hide_if_empty = value;
1400             break;
1401           case 'f':
1402             o->show_flagged = value;
1403             break;
1404           case 'h':
1405             o->hide = value;
1406             break;
1407           case 'm':
1408             o->sender_mbox = value;
1409             break;
1410           case 'o':
1411             o->unread_is_new = value;
1412             break;
1413           case 'p':
1414             o->sender_personal = value;
1415             break;
1416           case 's':
1417             o->snippets = value;
1418             break;
1419           case 't':
1420             o->highlight = value;
1421             break;
1422           default:
1423             fprintf(stderr, "Invalid mailbox option `%c'\n", x);
1424             usage();
1425           }
1426       }
1427 }
1428
1429 int
1430 main(int argc, char **argv)
1431 {
1432   clist_init(&mboxes);
1433   clist_init(&options);
1434   clist_init(&patterns);
1435   clist_init(&osd_opts);
1436
1437   int c;
1438   while ((c = getopt(argc, argv, "c:dim:o:p:s:")) >= 0)
1439     switch (c)
1440       {
1441       case 'c':
1442         check_interval = atol(optarg);
1443         if (check_interval <= 0)
1444           usage();
1445         break;
1446       case 'd':
1447         debug_mode++;
1448         break;
1449       case 'i':
1450         add_inbox();
1451         break;
1452       case 'm':
1453         run_cmd = optarg;
1454         break;
1455       case 'o':
1456         parse_options(optarg);
1457         break;
1458       case 'p':
1459         minimum_priority = atol(optarg);
1460         break;
1461       case 's':
1462         add_osd_opt(optarg);
1463         break;
1464       default:
1465         usage();
1466       }
1467   while (optind < argc)
1468     add_pattern(argv[optind++]);
1469
1470   charset_init();
1471   term_init();
1472   x11_init();
1473   scan_and_redraw(0);
1474   next_active(0, 1);
1475
1476   int should_exit = 0;
1477 restart:
1478   while (!should_exit)
1479     {
1480       time_t now = time(NULL);
1481       int remains = last_scan_time + check_interval - now;
1482       if (force_refresh)
1483         scan_and_redraw(0);
1484       if (remains <= 0)
1485         scan_and_redraw(1);
1486       else
1487         {
1488           remains *= 10;
1489           halfdelay((remains > 255) ? 255 : remains);
1490           int ch = getch();
1491           if (ch < 0)
1492             continue;
1493           if (is_active && handle_incsearch(ch))
1494             {
1495               refresh();
1496               continue;
1497             }
1498           for (int i=0; i<num_mboxes; i++)
1499             if (ch == mbox_array[i]->o.hotkey)
1500               {
1501                 if (i < cursor_max)
1502                   cursor_at = i;
1503                 mbox_run(mbox_array[i]);
1504                 goto restart;
1505               }
1506           switch (ch)
1507             {
1508             case 'q':
1509               should_exit = 1;
1510               break;
1511             case 'j':
1512             case KEY_DOWN:
1513               move_cursor(cursor_at+1);
1514               break;
1515             case 'k':
1516             case KEY_UP:
1517               move_cursor(cursor_at-1);
1518               break;
1519             case '^':
1520             case KEY_HOME:
1521             case KEY_PPAGE:
1522               move_cursor(0);
1523               break;
1524             case '$':
1525             case KEY_END:
1526             case KEY_NPAGE:
1527               move_cursor(cursor_max-1);
1528               break;
1529             case '\t':
1530               next_active(cursor_at+1, 1);
1531               break;
1532             case '`':
1533               next_active(cursor_at-1, -1);
1534               break;
1535             case '\r':
1536             case '\n':
1537               if (cursor_at < cursor_max)
1538                 mbox_run(mbox_array[cursor_at]);
1539               break;
1540             case 'l' & 0x1f:
1541               clearok(stdscr, TRUE);
1542               redraw_all();
1543               refresh();
1544               break;
1545             case 'r' & 0x1f:
1546               force_refresh = 1;
1547               break;
1548             case 'b':
1549               allow_bells = 1;
1550               print_status("Bells and whistles are now enabled. Toot!");
1551               break;
1552             case 'B':
1553               allow_bells = 0;
1554               print_status("Bells and whistles are now disabled. Pssst!");
1555               break;
1556             case 'd':
1557               allow_osd = 1;
1558               print_status("On-screen display is now enabled.");
1559               break;
1560             case 'D':
1561               allow_osd = 0;
1562               print_status("On-screen display is now disabled. Watch your step.");
1563               break;
1564             case '/':
1565               enter_incsearch();
1566               break;
1567             default:
1568               if (ch >= '0' && ch <= '9')
1569                 {
1570                   minimum_priority = ch - '0';
1571                   scan_and_redraw(0);
1572                 }
1573               else
1574                 debug("Pressed unknown key %d\n", ch);
1575             }
1576           refresh();
1577         }
1578     }
1579
1580   x11_cleanup();
1581   term_cleanup();
1582   return 0;
1583 }