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