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