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