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