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