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