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