]> mj.ucw.cz Git - checkmail.git/blob - cm.c
b9b2050266fced91e61fc8854426ee3cedd87fbc
[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
690   char msg[1024];
691   unsigned pos = 0;
692   CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
693     {
694       pos += snprintf(msg+pos, sizeof(msg)-pos-1, "%s:%s\n", n->key, n->val);
695       if (pos > sizeof(msg)-1)
696         {
697           pos = sprintf(msg, "OSD message too long!\n");
698           break;
699         }
700     }
701   msg[pos++] = '\n';
702
703   XChangeProperty(x11_dpy, DefaultRootWindow(x11_dpy), osd_pty, XA_STRING, 8, PropModeAppend, (unsigned char *) msg, pos);
704   XFlush(x11_dpy);
705 }
706
707 static void
708 rethink_leds(void)
709 {
710   if (!x11_dpy)
711     return;
712
713   leds_want = 0;
714   osd_want = 0;
715   CLIST_FOR_EACH(struct mbox *, b, mboxes)
716     {
717       if (b->o.led > 0 && b->new)
718         leds_want |= (1 << b->o.led);
719       if (b->o.osd > 0 && b->new)
720         osd_want = 1;
721     }
722   sync_leds();
723   sync_osd();
724 }
725
726 static void
727 x11_cleanup(void)
728 {
729   if (!x11_dpy)
730     return;
731
732   leds_want = 0;
733   sync_leds();
734 }
735
736 #else
737
738 static void x11_init(void) { }
739 static void rethink_leds(void) { }
740 static void x11_cleanup(void) { }
741
742 #endif
743
744 static int cursor_at, cursor_max;
745
746 enum {
747   M_IDLE,
748   M_SCAN,
749   M_NEW,
750   M_FLAG,
751   M_BAD,
752   M_MAX
753 };
754 static int attrs[2][2][M_MAX];          // active, hilite, status
755
756 static void
757 redraw_line(int i)
758 {
759   move(i, 0);
760   if (i < cursor_max)
761     {
762       struct mbox *b = mbox_array[i];
763       int cc = (cursor_at == i);
764       int hi = b->o.highlight;
765
766       attrset(attrs[cc][hi][M_IDLE]);
767       if (b->o.hotkey)
768         printw("%c ", b->o.hotkey);
769       else if (cc)
770         printw("> ");
771       else
772         printw("  ");
773       if (b->new)
774         attrset(attrs[cc][hi][M_NEW]);
775       else if (b->flagged && b->o.show_flagged)
776         attrset(attrs[cc][hi][M_FLAG]);
777       printw("%-20s ", b->name);
778       if (b->scanning < 0)
779         ;
780       else if (b->scanning)
781         {
782           attrset(attrs[cc][hi][M_SCAN]);
783           printw("[SCANNING]");
784         }
785       else if (b->total < 0)
786         {
787           attrset(attrs[cc][hi][M_BAD]);
788           printw("BROKEN");
789         }
790       else
791         {
792           attrset(attrs[cc][hi][M_IDLE]);
793           printw("%6d ", b->total);
794           int snip = 0;
795           if (b->new)
796             {
797               attrset(attrs[cc][hi][M_NEW]);
798               printw("%6d  ", b->new);
799               attrset(attrs[cc][hi][M_IDLE]);
800               int age = (last_scan_time - b->last_time);
801               if (age < 0)
802                 age = 0;
803               if (age < 3600)
804                 printw("%2d min  ", age/60);
805               else if (age < 86400)
806                 printw("%2d hrs  ", age/3600);
807               else
808                 printw("        ");
809               snip = 1;
810             }
811           else if (b->flagged && b->o.show_flagged)
812             {
813               attrset(attrs[cc][hi][M_FLAG]);
814               printw("%6d  ", b->flagged);
815               attrset(attrs[cc][hi][M_IDLE]);
816               printw("        ");
817               attrset(attrs[cc][0][M_FLAG]);    /* We avoid the highlight intentionally */
818               snip = 1;
819             }
820           if (snip && b->o.snippets && b->snippet[0])
821             {
822               int xx, yy;
823               getyx(stdscr, yy, xx);
824               int remains = COLS-1-xx;
825               if (remains > 2)
826                 {
827 #ifdef CONFIG_WIDE_CURSES
828                   size_t len = strlen(b->snippet)+1;
829                   wchar_t snip[len];
830                   mbstowcs(snip, b->snippet, len);
831                   addnwstr(snip, remains);
832 #else
833                   printw("%-.*s", remains, b->snippet);
834 #endif
835                 }
836             }
837         }
838     }
839   attrset(attrs[0][0][M_IDLE]);
840   clrtoeol();
841 }
842
843 static void
844 redraw_all(void)
845 {
846   cursor_max = num_mboxes;
847   if (cursor_max > LINES-1)
848     cursor_max = LINES-1;
849   if (cursor_at >= cursor_max)
850     cursor_at = cursor_max - 1;
851   if (cursor_at < 0)
852     cursor_at = 0;
853
854   for (int i=0; i<cursor_max; i++)
855     redraw_line(i);
856   move(cursor_max, 0);
857   if (!cursor_max)
858     {
859       printw("(no mailboxes found)");
860       clrtoeol();
861       move(1, 0);
862     }
863   clrtobot();
864 }
865
866 static void
867 rethink_display(void)
868 {
869   int i = 0;
870   int changed = 0;
871   int beeeep = 0;
872   CLIST_FOR_EACH(struct mbox *, b, mboxes)
873     if (mbox_visible_p(b))
874       {
875         b->index = i;
876         if (i >= num_mboxes || mbox_array[i] != b)
877           {
878             changed = 1;
879             if (i >= mbox_array_size)
880               {
881                 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
882                 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
883               }
884             mbox_array[i] = b;
885           }
886         if (b->o.beep && b->new > b->last_beep_new)
887           beeeep = 1;
888         b->last_beep_new = b->new;
889         i++;
890       }
891   if (i != num_mboxes)
892     changed = 1;
893   num_mboxes = i;
894
895   if (changed)
896     {
897       redraw_all();
898       refresh();
899     }
900   rethink_leds();
901   if (beeeep && allow_bells)
902     beep();
903 }
904
905 static void
906 term_init(void)
907 {
908   initscr();
909   cbreak();
910   noecho();
911   nonl();
912   intrflush(stdscr, FALSE);
913   keypad(stdscr, TRUE);
914   curs_set(0);
915
916   static const int attrs_mono[2][M_MAX] = {
917     [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
918     [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
919   };
920   for (int i=0; i<2; i++)
921     for (int j=0; j<M_MAX; j++)
922       {
923         attrs[0][i][j] = attrs_mono[i][j];
924         attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
925       }
926
927   if (has_colors())
928     {
929       start_color();
930       if (COLOR_PAIRS >= 5)
931         {
932           init_pair(1, COLOR_YELLOW, COLOR_BLACK);
933           init_pair(2, COLOR_RED, COLOR_BLACK);
934           init_pair(3, COLOR_WHITE, COLOR_BLUE);
935           init_pair(4, COLOR_YELLOW, COLOR_BLUE);
936           init_pair(5, COLOR_RED, COLOR_BLUE);
937           init_pair(6, COLOR_GREEN, COLOR_BLACK);
938           init_pair(7, COLOR_GREEN, COLOR_BLUE);
939           static const int attrs_color[2][2][M_MAX] = {
940             [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_FLAG] = COLOR_PAIR(6), [M_BAD] = COLOR_PAIR(2) },
941             [0][1] = { [M_IDLE] = A_BOLD, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1) | A_BOLD, [M_FLAG] = COLOR_PAIR(6) | A_BOLD, [M_BAD] = COLOR_PAIR(2) | A_BOLD },
942             [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_FLAG] = COLOR_PAIR(7), [M_BAD] = COLOR_PAIR(5) },
943             [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD, [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4) | A_BOLD, [M_FLAG] = COLOR_PAIR(7) | A_BOLD, [M_BAD] = COLOR_PAIR(5) | A_BOLD },
944           };
945           memcpy(attrs, attrs_color, sizeof(attrs));
946         }
947     }
948 }
949
950 static void
951 term_cleanup(void)
952 {
953   endwin();
954 }
955
956 static void
957 print_status(char *status)
958 {
959   move(LINES-1, 0);
960   if (status)
961     printw("%s", status);
962   clrtoeol();
963   refresh();
964 }
965
966 static void
967 scan_and_redraw(void)
968 {
969   print_status("Busy...");
970   scan();
971   print_status(NULL);
972 }
973
974 static void
975 move_cursor(int i)
976 {
977   if (i >= 0 && i < cursor_max && i != cursor_at)
978     {
979       int old = cursor_at;
980       cursor_at = i;
981       redraw_line(old);
982       redraw_line(i);
983     }
984 }
985
986 static void
987 next_active(int since, int step)
988 {
989   if (!cursor_max)
990     return;
991   since = (since+cursor_max) % cursor_max;
992   step = (step+cursor_max) % cursor_max;
993   int besti = -1;
994   int bestp = -1;
995   int i = since;
996   do
997     {
998       struct mbox *b = mbox_array[i];
999       if (b->new && b->o.priority > bestp)
1000         {
1001           besti = i;
1002           bestp = b->o.priority;
1003         }
1004       i = (i+step) % cursor_max;
1005     }
1006   while (i != since);
1007   if (besti >= 0)
1008     move_cursor(besti);
1009 }
1010
1011 static void
1012 mbox_run(struct mbox *b)
1013 {
1014   char cmd[strlen(run_cmd) + strlen(b->path) + 16];
1015   sprintf(cmd, run_cmd, b->path);
1016   term_cleanup();
1017   system(cmd);
1018   term_init();
1019   redraw_all();
1020   refresh();
1021   b->force_refresh = 1;
1022   scan_and_redraw();
1023 }
1024
1025 #define STR2(c) #c
1026 #define STR(c) STR2(c)
1027
1028 static void NONRET
1029 usage(void)
1030 {
1031   fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
1032 \n\
1033 Options:\n\
1034 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
1035 -d\t\t\tLog debug messages to stderr\n\
1036 -i\t\t\tInclude user's INBOX\n\
1037 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
1038 -o <pattern>=<opts>\tSet mailbox options\n\
1039 -o <opts>\t\tSet default options for all mailboxes\n\
1040 -p <pri>\t\tSet minimum priority to show\n\
1041 -s <key>=<val>\t\tSet on-screen display options (consult OSDD docs)\n\
1042 \n\
1043 Mailbox options (set with `-o', use upper case to negate):\n\
1044 0-9\t\t\tSet mailbox priority (0=default)\n\
1045 b\t\t\tBeep when a message arrives\n\
1046 d\t\t\tSend an on-screen-display message (requires OSDD)\n\
1047 e\t\t\tHide from display if empty\n\
1048 f\t\t\tShow flagged messages if there are no new ones\n\
1049 h\t\t\tHide from display\n\
1050 l<led>\t\t\tLight a keyboard led (1-9) if running on X display\n\
1051 m\t\t\tShow mailbox name of the sender\n\
1052 p\t\t\tShow personal info (full name) of the sender\n\
1053 s\t\t\tShow message snippets\n\
1054 t\t\t\tHighlight the entry\n\
1055 !<key>\t\t\tSet hot key\n\
1056 \n\
1057 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
1058 It can be freely distributed and used according to the GNU GPL v2.\n\
1059 ");
1060   exit(1);
1061 }
1062
1063 static void
1064 parse_options(char *c)
1065 {
1066   struct options *o;
1067   char *sep;
1068   if (sep = strchr(c, '='))
1069     {
1070       struct option_node *n = xmalloc(sizeof(*n) + sep-c);
1071       memcpy(n->pattern, c, sep-c);
1072       n->pattern[sep-c] = 0;
1073       clist_add_tail(&options, &n->n);
1074       o = &n->o;
1075       init_options(o);
1076       c = sep+1;
1077     }
1078   else
1079     o = &global_options;
1080
1081   int x;
1082   while (x = *c++)
1083     if (x >= '0' && x <= '9')
1084       o->priority = x - '0';
1085     else if (x == '!' && *c)
1086       o->hotkey = *c++;
1087     else if (x == 'l' && *c >= '1' && *c <= '9')
1088       o->led = *c++ - '0';
1089     else
1090       {
1091         int value = !!islower(x);
1092         switch (tolower(x))
1093           {
1094           case 'b':
1095             o->beep = value;
1096             break;
1097           case 'd':
1098             o->osd = value;
1099             break;
1100           case 'e':
1101             o->hide_if_empty = value;
1102             break;
1103           case 'f':
1104             o->show_flagged = value;
1105             break;
1106           case 'h':
1107             o->hide = value;
1108             break;
1109           case 'm':
1110             o->sender_mbox = value;
1111             break;
1112           case 'p':
1113             o->sender_personal = value;
1114             break;
1115           case 's':
1116             o->snippets = value;
1117             break;
1118           case 't':
1119             o->highlight = value;
1120             break;
1121           default:
1122             fprintf(stderr, "Invalid mailbox option `%c'\n", x);
1123             usage();
1124           }
1125       }
1126 }
1127
1128 int
1129 main(int argc, char **argv)
1130 {
1131   clist_init(&mboxes);
1132   clist_init(&options);
1133   clist_init(&patterns);
1134   clist_init(&osd_opts);
1135
1136   int c;
1137   while ((c = getopt(argc, argv, "c:dim:o:p:s:")) >= 0)
1138     switch (c)
1139       {
1140       case 'c':
1141         check_interval = atol(optarg);
1142         if (check_interval <= 0)
1143           usage();
1144         break;
1145       case 'd':
1146         debug_mode++;
1147         break;
1148       case 'i':
1149         add_inbox();
1150         break;
1151       case 'm':
1152         run_cmd = optarg;
1153         break;
1154       case 'o':
1155         parse_options(optarg);
1156         break;
1157       case 'p':
1158         minimum_priority = atol(optarg);
1159         break;
1160       case 's':
1161         add_osd_opt(optarg);
1162         break;
1163       default:
1164         usage();
1165       }
1166   while (optind < argc)
1167     add_pattern(argv[optind++]);
1168
1169   charset_init();
1170   term_init();
1171   x11_init();
1172   scan_and_redraw();
1173   next_active(0, 1);
1174
1175   int should_exit = 0;
1176 restart:
1177   while (!should_exit)
1178     {
1179       time_t now = time(NULL);
1180       int remains = last_scan_time + check_interval - now;
1181       if (remains <= 0 || force_refresh)
1182         scan_and_redraw();
1183       else
1184         {
1185           remains *= 10;
1186           halfdelay((remains > 255) ? 255 : remains);
1187           int ch = getch();
1188           for (int i=0; i<num_mboxes; i++)
1189             if (ch == mbox_array[i]->o.hotkey)
1190               {
1191                 if (i < cursor_max)
1192                   cursor_at = i;
1193                 mbox_run(mbox_array[i]);
1194                 goto restart;
1195               }
1196           switch (ch)
1197             {
1198             case 'q':
1199               should_exit = 1;
1200               break;
1201             case 'j':
1202             case KEY_DOWN:
1203               move_cursor(cursor_at+1);
1204               break;
1205             case 'k':
1206             case KEY_UP:
1207               move_cursor(cursor_at-1);
1208               break;
1209             case '^':
1210             case KEY_HOME:
1211             case KEY_PPAGE:
1212               move_cursor(0);
1213               break;
1214             case '$':
1215             case KEY_END:
1216             case KEY_NPAGE:
1217               move_cursor(cursor_max-1);
1218               break;
1219             case '\t':
1220               next_active(cursor_at+1, 1);
1221               break;
1222             case '`':
1223               next_active(cursor_at-1, -1);
1224               break;
1225             case '\r':
1226             case '\n':
1227               if (cursor_at < cursor_max)
1228                 mbox_run(mbox_array[cursor_at]);
1229               break;
1230             case 'l' & 0x1f:
1231               clearok(stdscr, TRUE);
1232               redraw_all();
1233               refresh();
1234               break;
1235             case 'r' & 0x1f:
1236               force_refresh = 1;
1237               break;
1238             case 'b':
1239               allow_bells = 1;
1240               print_status("Bells and whistles are now enabled. Toot!");
1241               break;
1242             case 'B':
1243               allow_bells = 0;
1244               print_status("Bells and whistles are now disabled. Pssst!");
1245               break;
1246             case 'd':
1247               allow_osd = 1;
1248               print_status("On-screen display is now enabled.");
1249               break;
1250             case 'D':
1251               allow_osd = 0;
1252               print_status("On-screen display is now disabled. Watch your step.");
1253               break;
1254             default:
1255               if (ch >= '0' && ch <= '9')
1256                 {
1257                   minimum_priority = ch - '0';
1258                   scan_and_redraw();
1259                 }
1260               else
1261                 debug("Pressed unknown key %d\n", ch);
1262             }
1263           refresh();
1264         }
1265     }
1266
1267   x11_cleanup();
1268   term_cleanup();
1269   return 0;
1270 }