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