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