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