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