]> mj.ucw.cz Git - checkmail.git/blob - cm.c
01d683d9ec37dc4f979bc1187705b21d76a7691a
[checkmail.git] / cm.c
1 /*
2  *      Incoming Mail Checker
3  *
4  *      (c) 2005--2015 Martin Mares <mj@ucw.cz>
5  *      (c) 2020--2021 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
6  */
7
8 #define _GNU_SOURCE
9
10 #include <ctype.h>
11 #include <dirent.h>
12 #include <fcntl.h>
13 #include <fnmatch.h>
14 #include <getopt.h>
15 #include <glob.h>
16 #include <pwd.h>
17 #include <stdio.h>
18 #include <stdlib.h>
19 #include <string.h>
20 #include <sys/stat.h>
21 #include <sys/types.h>
22 #include <sys/wait.h>
23 #include <time.h>
24 #include <unistd.h>
25 #include <errno.h>
26
27 #ifdef CONFIG_WIDE_CURSES
28 #include <ncurses.h>
29 #else
30 #include <curses.h>
31 #endif
32
33 #include "util.h"
34 #include "clists.h"
35 #include "charset.h"
36
37 static int check_interval = 30;
38 static int osd_repeat_interval = 0; // 0 is disable
39 static int force_refresh;
40 static int allow_bells = 1;
41 static int allow_osd = 1;
42 static int minimum_priority;
43 static time_t last_scan_time;
44 static time_t last_osd_repeat_time;
45 static char *run_cmd = "mutt -f %s";
46 static int simple_tab;
47
48 struct options {
49   int priority;
50   int hide;
51   int hide_if_empty;
52   int highlight;
53   int beep;
54   int snippets;
55   int show_flagged;
56   int sender_personal;
57   int sender_mbox;
58   int hotkey;
59   int led;
60   int osd;
61   int osd_repeat;
62   int osd_list;
63   int unread_is_new;
64   int sort_order;
65 };
66
67 struct option_node {
68   cnode n;
69   struct options o;
70   char pattern[1];
71 };
72
73 struct pattern_node {
74   cnode n;
75   char *name;
76   char pattern[1];
77 };
78
79 static clist options, patterns;
80 static struct options global_options = {
81   .sender_personal = 1,
82   .sort_order = 1000,
83   .osd_repeat = 1,
84   .osd_list = 1,
85 };
86
87 #define MDIR_MAX_NAME_LEN 128
88
89 enum inotify_status
90 {
91   INOTIFY_OFF,
92   INOTIFY_ON,
93   INOTIFY_ERROR,
94 };
95
96 struct mbox {
97   cnode n;
98   enum inotify_status inotify_status;
99   struct options o;
100   char *name;
101   char *path;
102   int index;
103   int scanning;
104   int seen;
105   time_t last_time;
106   time_t best_time;
107   time_t display_valid_until;
108   int last_size, last_pos;
109   int total, new, flagged;
110   int last_total, last_new, last_flagged;
111   int last_beep_new;
112   int force_refresh;
113   int best_is_new;
114   char sender_snippet[128];
115   char subject_snippet[128];
116   char mdir_best[MDIR_MAX_NAME_LEN];
117   int osd_repeat_last_total, osd_repeat_last_new, osd_repeat_last_flagged;
118   char osd_repeat_last_sender_snippet[128];
119   char osd_repeat_last_subject_snippet[128];
120 };
121
122 static clist mboxes;
123 static struct mbox **mbox_array;
124 static int num_mboxes, mbox_array_size;
125
126 struct osd_opt_node {
127   cnode n;
128   char *val;
129   char key[1];
130 };
131
132 static clist osd_opts;
133 static clist osd_repeat_opts;
134 static char *osd_mbox_opt="%a";
135 static char *osd_mbox_opt_separator=" ";
136
137 static void redraw_line(int i);
138 static void rethink_display(int notify);
139
140 static int inotify_fd;
141 static bool inotify_active;
142 static void inotify_cm_init(void);
143 static void inotify_add_mbox(struct mbox * b);
144 static void inotify_delete_mbox(struct mbox * b);
145
146 static void
147 add_pattern(char *patt)
148 {
149   struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
150   strcpy(n->pattern, patt);
151   if (patt = strchr(n->pattern, '='))
152     {
153       *patt++ = 0;
154       n->name = patt;
155     }
156   else
157     n->name = NULL;
158   clist_add_tail(&patterns, &n->n);
159 }
160
161 static void
162 add_inbox(void)
163 {
164   struct passwd *p = getpwuid(getuid());
165   if (!p)
166     die("You don't exist, go away!");
167   char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
168   sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
169   add_pattern(buf);
170 }
171
172 static void
173 init_options(struct options *o)
174 {
175   o->priority = -1;
176   o->hide = -1;
177   o->hide_if_empty = -1;
178   o->beep = -1;
179   o->highlight = -1;
180   o->snippets = -1;
181   o->show_flagged = -1;
182   o->sender_personal = -1;
183   o->sender_mbox = -1;
184   o->hotkey = -1;
185   o->led = -1;
186   o->osd = -1;
187   o->osd_repeat = -1;
188   o->osd_list = -1;
189   o->unread_is_new = -1;
190   o->sort_order = -1;
191 }
192
193 static void
194 setup_options(struct mbox *b)
195 {
196   b->o = global_options;
197   CLIST_FOR_EACH(struct option_node *, n, options)
198     if (!fnmatch(n->pattern, b->name, 0))
199       {
200         debug("\tApplied options %s\n", n->pattern);
201 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
202         MERGE(priority);
203         MERGE(hide);
204         MERGE(hide_if_empty);
205         MERGE(highlight);
206         MERGE(beep);
207         MERGE(snippets);
208         MERGE(show_flagged);
209         MERGE(sender_personal);
210         MERGE(sender_mbox);
211         MERGE(hotkey);
212         MERGE(led);
213         MERGE(osd);
214         MERGE(osd_repeat);
215         MERGE(osd_list);
216         MERGE(unread_is_new);
217         MERGE(sort_order);
218       }
219 }
220
221 static void
222 add_osd_opt(char *arg, struct clist *opts)
223 {
224   struct osd_opt_node *n = xmalloc(sizeof(*n) + strlen(arg));
225   strcpy(n->key, arg);
226   n->val = strchr(n->key, '=');
227   if (!n->val)
228     die("Malformed OSD option");
229   *n->val++ = 0;
230   clist_add_tail(opts, &n->n);
231 }
232
233 static char *
234 mbox_name(char *path)
235 {
236   char *c = strrchr(path, '/');
237   if (c)
238     c++;
239   else
240     c = path;
241   if (*c == '.')
242     c++;
243   return c;
244 }
245
246 static struct mbox *
247 new_mbox(char *path, char *name)
248 {
249   struct mbox *b = xmalloc(sizeof(*b));
250   bzero(b, sizeof(*b));
251   b->inotify_status = INOTIFY_OFF;
252   b->path = xstrdup(path);
253   b->name = xstrdup(name);
254   return b;
255 }
256
257 static void
258 add_mbox(clist *l, struct mbox *b)
259 {
260   if (b->name)
261     {
262       cnode *prev = l->head.prev;
263       struct mbox *prev_mbox;
264       while (prev != &l->head &&
265           ((prev_mbox = (struct mbox *)prev)->o.sort_order > b->o.sort_order ||
266            prev_mbox->o.sort_order == b->o.sort_order && strcmp(((struct mbox *)prev)->name, b->name) > 0))
267         prev = prev->prev;
268       clist_insert_after(&b->n, prev);
269     }
270   else
271     clist_add_tail(l, &b->n);
272 }
273
274 static void
275 del_mbox(struct mbox *b)
276 {
277   clist_remove(&b->n);
278   free(b->path);
279   free(b->name);
280   free(b);
281 }
282
283 static struct mbox *
284 find_mbox(clist *l, char *path)
285 {
286   CLIST_FOR_EACH(struct mbox *, b, *l)
287     if (!strcmp(b->path, path))
288       return b;
289   return NULL;
290 }
291
292 static inline int
293 mbox_active_p(struct mbox *b)
294 {
295   if (b->o.priority < minimum_priority)
296     return 0;
297   if (b->o.hide)
298     return 0;
299   return 1;
300 }
301
302 static inline int
303 mbox_visible_p(struct mbox *b)
304 {
305   if (!mbox_active_p(b))
306     return 0;
307   if (b->scanning < 0)
308     return 1;
309   if (b->o.hide_if_empty && !b->total)
310     return 0;
311   return 1;
312 }
313
314 static void
315 prepare_snippets(struct mbox *b, char *sender, char *subject)
316 {
317   char *pos, *term;
318
319   while (*sender == ' ' || *sender == '\t')
320     sender++;
321   while (*subject == ' ' || *subject == '\t')
322     subject++;
323
324   pos = b->sender_snippet;
325   term = pos + sizeof(b->sender_snippet) - 1;
326   if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
327     add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
328   else
329     *pos = 0;
330
331   pos = b->subject_snippet;
332   term = pos + sizeof(b->subject_snippet) - 1;
333   if (subject[0])
334     add_subject_snippet(&pos, term, subject);
335   else
336     add_snippet_raw(&pos, term, "No subject");
337 }
338
339 static void
340 build_snippet(char *buf, char *term, struct mbox *b)
341 {
342   if (b->sender_snippet[0])
343     {
344       add_snippet(&buf, term, b->sender_snippet);
345       add_snippet_raw(&buf, term, ": ");
346     }
347   add_snippet(&buf, term, b->subject_snippet);
348 }
349
350 static int mb_fd, mb_pos, mb_seekable;
351 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
352
353 static void
354 mb_reset(int pos)
355 {
356   mb_cc = mb_end = mb_buf;
357   mb_pos = pos;
358 }
359
360 static void
361 mb_seek(uns pos)
362 {
363   lseek(mb_fd, pos, SEEK_SET);
364   mb_reset(pos);
365 }
366
367 static int
368 mb_tell(void)
369 {
370   return mb_pos - (mb_end - mb_cc);
371 }
372
373 static int
374 mb_ll_get(void)
375 {
376   int len = read(mb_fd, mb_buf, sizeof(mb_buf));
377   mb_cc = mb_buf;
378   if (len <= 0)
379     {
380       mb_end = mb_buf;
381       return -1;
382     }
383   else
384     {
385       mb_end = mb_buf + len;
386       mb_pos += len;
387       return *mb_cc++;
388     }
389 }
390
391 static inline int
392 mb_get(void)
393 {
394   return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
395 }
396
397 static void
398 mb_unget(int c)
399 {
400   if (c >= 0)
401     mb_cc--;
402 }
403
404 static int
405 mb_check(const char *p, int len)
406 {
407   while (len--)
408     {
409       if (mb_get() != *p++)
410         return 0;
411     }
412   return 1;
413 }
414
415 static void
416 mb_skip(int len)
417 {
418   while (len)
419     {
420       int avail = mb_end - mb_cc;
421       if (!avail)
422         {
423           if (mb_seekable && len >= (int) sizeof(mb_buf))
424             {
425               int next = len - len % sizeof(mb_buf);
426               mb_seek(mb_tell() + next);
427               len -= next;
428               continue;
429             }
430           if (mb_ll_get() < 0)
431             return;
432           len--;
433         }
434       else
435         {
436           int next = (avail < len) ? avail : len;
437           len -= next;
438           mb_cc += next;
439         }
440     }
441 }
442
443 #define HDR_BUF_SIZE 1024
444
445 static int
446 read_hdr_field(char *buf)
447 {
448   uns i = 0;
449   for (;;)
450     {
451       int c = mb_get();
452       if (c < 0)
453         {
454           debug("[truncated] ");
455           return -1;
456         }
457       if (c == '\n')
458         {
459           if (!i)
460             break;
461           int fold = -1;
462           do
463             {
464               fold++;
465               c = mb_get();
466             }
467           while (c == ' ' || c == '\t');
468           mb_unget(c);
469           if (!fold)
470             break;
471           c = ' ';
472         }
473       if (c == '\r')
474         continue;
475       if (i < HDR_BUF_SIZE - 1)
476         buf[i++] = c;
477     }
478   buf[i] = 0;
479   if (debug_mode > 2)
480     debug("Header: <%s>\n", buf);
481   return buf[0] ? 1 : 0;
482 }
483
484 static void
485 scan_mbox(struct mbox *b, struct stat *st)
486 {
487   char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE];
488   int c;
489   int compressed = 0;
490   const char from[] = "\nFrom ";
491
492   b->best_time = st->st_mtime;
493   if (!st->st_size)
494     {
495       b->total = b->new = b->flagged = 0;
496       b->last_pos = 0;
497       return;
498     }
499
500   mb_fd = open(b->path, O_RDONLY);
501   if (mb_fd < 0)
502     {
503       debug("[open failed: %m] ");
504       b->total = b->new = b->flagged = -1;
505       return;
506     }
507   mb_seekable = 1;
508
509   char signature[2];
510   c = read(mb_fd, signature, 2);
511   lseek(mb_fd, 0, SEEK_SET);
512
513   if (c == 2 && !memcmp(signature, "\037\213", 2)) //gzip
514     {
515       debug("[decompressing] ");
516       int fds[2];
517       if (pipe(fds))
518         die("pipe failed: %m");
519       int pid = fork();
520       if (pid < 0)
521         die("fork failed: %m");
522       if (!pid)
523         {
524           if (dup2(mb_fd, 0) < 0 || dup2(fds[1], 1) < 0)
525             die("dup2 failed: %m");
526           close(fds[0]);
527           close(fds[1]);
528           close(mb_fd);
529           execlp("gzip", "gzip", "-cd", NULL);
530           die("Cannot execute gzip: %m");
531         }
532       close(fds[1]);
533       close(mb_fd);
534       mb_fd = fds[0];
535       compressed = 1;
536       mb_seekable = 0;
537     }
538   mb_reset(0);
539
540   int incremental = 0;
541   if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && mb_seekable)
542     {
543       mb_seek(b->last_pos);
544       if (mb_check(from, 6))
545         {
546           debug("[incremental] ");
547           incremental = 1;
548         }
549       else
550         {
551           debug("[incremental failed] ");
552           mb_seek(0);
553         }
554     }
555   if (!incremental)
556     {
557       if (!mb_check(from+1, 5))
558         {
559           debug("[inconsistent] ");
560           b->total = b->new = b->flagged = -1;
561           goto done;
562         }
563       b->total = b->new = b->flagged = 0;
564       b->last_total = b->last_new = b->last_flagged = 0;
565       b->best_is_new = 0;
566     }
567   else
568     {
569       b->total = b->last_total;
570       b->new = b->last_new;
571       b->flagged = b->last_flagged;
572     }
573
574   for(;;)
575     {
576       b->last_pos = mb_tell() - 5;
577       if (b->last_pos)
578         b->last_pos--;          // last_pos should be the previous \n character
579       b->last_total = b->total;
580       b->last_new = b->new;
581       b->last_flagged = b->flagged;
582       while ((c = mb_get()) >= 0 && c != '\n')
583         ;
584
585       int content_length = -1;
586       int new = 1;
587       int flagged = 0;
588       sender[0] = 0;
589       subject[0] = 0;
590       for (;;)
591         {
592           int c = read_hdr_field(buf);
593           if (c < 0)
594             goto done;
595           if (!c)
596             break;
597           if (!strncasecmp(buf, "Status:", 7))
598             {
599               if (!b->o.unread_is_new || strchr(buf + 7, 'R'))
600                 new = 0;
601             }
602           else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
603             flagged = 1;
604           else if (!strncasecmp(buf, "From:", 5))
605             strcpy(sender, buf+5);
606           else if (!strncasecmp(buf, "Subject:", 8))
607             strcpy(subject, buf+8);
608           else if (!strncasecmp(buf, "Content-Length:", 15))
609             content_length = atoi(buf + 15);
610         }
611
612       b->total++;
613       if (new)
614         b->new++;
615       if (flagged)
616         b->flagged++;
617       if (debug_mode > 1)
618         debug("new=%d flagged=%d len=%d sender=<%s> subject=<%s>\n", new, flagged, content_length, sender, subject);
619       if (new || (flagged && !b->best_is_new))
620         {
621           b->best_is_new = new;
622           prepare_snippets(b, sender, subject);
623         }
624
625       if (content_length >= 0)
626         mb_skip(content_length);
627
628       int ct = 1;
629       while (from[ct])
630         {
631           c = mb_get();
632           if (c < 0)
633             goto done;
634           if (c != from[ct++])
635             ct = (c == '\n');
636         }
637     }
638
639  done:
640   close(mb_fd);
641   if (compressed)
642     {
643       int status;
644       if (wait(&status) < 0 || !WIFEXITED(status) || WEXITSTATUS(status))
645         b->total = b->new = b->flagged = -1;
646     }
647 }
648
649 static time_t
650 mdir_mtime(struct mbox *b)
651 {
652   time_t mtime = 0;
653   char path[strlen(b->path) + 5];
654
655   for (int new=0; new<2; new++)
656     {
657       sprintf(path, "%s/%s", b->path, (new ? "new" : "cur"));
658       struct stat st;
659       if (stat(path, &st) < 0)
660         debug("[cannot stat %s] ", path);
661       else if (st.st_mtime > mtime)
662         mtime = st.st_mtime;
663     }
664   return mtime;
665 }
666
667 static void
668 mdir_get_snippet(struct mbox *b)
669 {
670   char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE];
671   sender[0] = 0;
672   subject[0] = 0;
673
674   char path[strlen(b->path) + MDIR_MAX_NAME_LEN];
675   sprintf(path, "%s/%s", b->path, b->mdir_best);
676   mb_fd = open(path, O_RDONLY);
677   if (mb_fd < 0)
678     {
679       debug("[open failed: %m] ");
680       prepare_snippets(b, sender, subject);
681       return;
682     }
683   mb_seekable = 1;
684   mb_reset(0);
685
686   while (read_hdr_field(buf) > 0)
687     {
688       if (!strncasecmp(buf, "From:", 5))
689         strcpy(sender, buf+5);
690       else if (!strncasecmp(buf, "Subject:", 8))
691         strcpy(subject, buf+8);
692     }
693
694   close(mb_fd);
695   prepare_snippets(b, sender, subject);
696 }
697
698 static void
699 scan_mdir(struct mbox *b)
700 {
701   int dir_len = strlen(b->path);
702   char path[dir_len + MDIR_MAX_NAME_LEN];
703   strcpy(path, b->path);
704
705   b->total = b->new = b->flagged = 0;
706   b->best_time = 0;
707   b->best_is_new = 0;
708
709   for (int new=0; new<2; new++)
710     {
711       strcpy(path + dir_len, (new ? "/new" : "/cur"));
712       DIR *d = opendir(path);
713       if (!d)
714         {
715           debug("[cannot open %s: %m] ");
716           continue;
717         }
718       struct dirent *de;
719       while (de = readdir(d))
720         {
721           char *name = de->d_name;
722           if (name[0] == '.' || strlen(name) + 10 > MDIR_MAX_NAME_LEN)
723             continue;
724           path[dir_len + 4] = '/';
725           strcpy(path + dir_len + 5, name);
726
727           char *colon = strchr(name, ':');
728           int seen = 0;
729           int flagged = 0;
730           if (colon && colon[1] == '2' && colon[2] == ',')
731             {
732               // Another comma can separate extended flags (Dovecot extension)
733               for (int i=3; colon[i] && colon[i] != ','; i++)
734                 switch (colon[i])
735                   {
736                   case 'S':
737                     seen = 1;
738                     break;
739                   case 'F':
740                     flagged = 1;
741                     break;
742                   }
743             }
744
745           int is_new = new;
746           if (b->o.unread_is_new && !seen)
747             is_new = 1;
748
749           b->total++;
750           if (is_new)
751             b->new++;
752           if (flagged)
753             b->flagged++;
754           if (debug_mode > 1)
755             debug("%s: new=%d flagged=%d seen=%d\n", path, is_new, flagged, seen);
756
757           if (is_new || flagged)
758             {
759               // Need to pick the best message to display
760               struct stat st;
761               if (stat(path, &st) < 0)
762                 debug("[cannot stat %s: %m] ", path);
763               else if (is_new > b->best_is_new || is_new == b->best_is_new && st.st_mtime > b->best_time)
764                 {
765                   b->best_is_new = is_new;
766                   b->best_time = st.st_mtime;
767                   strcpy(b->mdir_best, path + dir_len + 1);
768                 }
769             }
770         }
771       closedir(d);
772     }
773
774   if (b->best_time && b->o.snippets)
775     {
776       debug("best <%s> ", b->mdir_best);
777       mdir_get_snippet(b);
778     }
779 }
780
781 static void
782 scan(int notify)
783 {
784   debug("Searching for mailboxes (notify=%d)...\n", notify);
785   last_scan_time = time(NULL);
786   CLIST_FOR_EACH(struct pattern_node *, p, patterns)
787     {
788       debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
789       glob_t g;
790       int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
791       if (err && err != GLOB_NOMATCH)
792         die("Failed to glob %s: %m", p->pattern);
793       for (uns i=0; i<g.gl_pathc; i++)
794         {
795           char *name = g.gl_pathv[i];
796           struct mbox *b = find_mbox(&mboxes, name);
797           if (!b)
798             {
799               b = new_mbox(name, (p->name ? p->name : mbox_name(name)));
800               debug("Discovered mailbox %s (%s)\n", b->name, b->path);
801               setup_options(b);
802               add_mbox(&mboxes, b);
803               b->scanning = -1;
804             }
805           b->seen = 1;
806         }
807       globfree(&g);
808     }
809
810   struct mbox *tmp;
811   if(inotify_active)
812   CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
813     {
814       if(b->inotify_status != INOTIFY_ON)
815         inotify_add_mbox(b);
816     }
817
818
819   CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
820     {
821       if (b->seen)
822         b->seen = 0;
823       else
824         {
825           debug("Lost mailbox %s\n", b->name);
826           inotify_delete_mbox(b);
827           del_mbox(b);
828         }
829     }
830
831   rethink_display(0);
832
833   debug("Scanning mailboxes...\n");
834   CLIST_FOR_EACH(struct mbox *, b, mboxes)
835     {
836       struct stat st;
837       debug("%s: ", b->name);
838       if (!mbox_active_p(b))
839         {
840           debug("inactive\n");
841           continue;
842         }
843       if (force_refresh)
844         b->force_refresh = 1;
845       if (stat(b->path, &st) < 0)
846         {
847           b->total = b->new = b->flagged = -1;
848           debug("%m\n");
849           continue;
850         }
851
852       time_t current_mtime;
853       int current_size;
854       int is_mdir;
855       if (S_ISREG(st.st_mode))
856         {
857           // Regular mailbox
858           is_mdir = 0;
859           current_mtime = st.st_mtime;
860           current_size = st.st_size;
861           debug("[mbox] ");
862         }
863       else if (S_ISDIR(st.st_mode))
864         {
865           // Maildir
866           is_mdir = 1;
867           current_mtime = mdir_mtime(b);
868           current_size = 0;
869           debug("[mdir] ");
870         }
871       else
872         {
873           debug("neither file nor directory\n");
874           continue;
875         }
876
877       if (!b->last_time || current_mtime != b->last_time || current_size != b->last_size || b->force_refresh)
878         {
879           b->scanning = 1;
880           redraw_line(b->index);
881           refresh();
882
883           if (is_mdir)
884             scan_mdir(b);
885           else
886             scan_mbox(b, &st);
887           b->last_time = current_mtime;
888           b->last_size = current_size;
889           debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
890
891           b->scanning = 0;
892           redraw_line(b->index);
893           refresh();
894           b->force_refresh = 0;
895         }
896       else if (b->display_valid_until <= last_scan_time)
897         {
898           debug("not changed, but needs redraw\n");
899           redraw_line(b->index);
900         }
901       else
902         debug("not changed\n");
903     }
904   force_refresh = 0;
905
906   debug("Scan finished\n");
907   last_scan_time = time(NULL);
908   rethink_display(notify);
909 }
910
911 #ifdef CONFIG_X11
912
913 #include <X11/Xlib.h>
914 #include <X11/Xatom.h>
915
916 static Display *x11_dpy;
917 static unsigned leds_care, leds_have, leds_want;
918
919 static Atom osd_pty;
920 static unsigned osd_care;
921 #define OSD_MSG_SIZE 1024
922 static char osd_last_msg[OSD_MSG_SIZE];
923 static time_t osd_last_time;
924
925 static void
926 x11_init(void)
927 {
928   leds_care = (global_options.led >= 0 ? (1 << global_options.led) : 0);
929   osd_care = (global_options.osd >= 0);
930   CLIST_FOR_EACH(struct option_node *, o, options)
931     {
932       if (o->o.led > 0)
933         leds_care |= (1 << o->o.led);
934       if (o->o.osd > 0)
935         osd_care = 1;
936     }
937   if (osd_repeat_interval)
938     osd_care = 1;
939
940   if (!leds_care && !osd_care)
941     {
942       debug("X11: No mailbox wants LEDs or OSD\n");
943       return;
944     }
945   if (!getenv("DISPLAY"))
946     {
947       debug("X11: Do not have X display\n");
948       return;
949     }
950   if (!(x11_dpy = XOpenDisplay(NULL)))
951     die("Cannot open X display, although the DISPLAY variable is set");
952
953   if (osd_care)
954     {
955       osd_pty = XInternAtom(x11_dpy, "OSD_QUEUE", False);
956       if (!osd_pty)
957         die("Cannot intern OSD_QUEUE atom");
958
959       // If OSD options contain no message, add one
960       int seen_msg = 0;
961       CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
962         if (!n->key[0])
963           seen_msg = 1;
964       if (!seen_msg)
965         {
966           add_osd_opt("=%40f", &osd_opts);
967           add_osd_opt("=%40s", &osd_opts);
968           add_osd_opt("=", &osd_opts);
969           add_osd_opt("=(and %N more)", &osd_opts);
970         }
971     }
972
973   leds_have = ~0U;
974   debug("X11: Initialized\n");
975 }
976
977 static void
978 sync_leds(void)
979 {
980   if (leds_want == leds_have)
981     return;
982
983   debug("LEDS: have %02x, want %02x, care %02x\n", leds_have, leds_want, leds_care);
984   for (int i=1; i<10; i++)
985     if (leds_care & (leds_have ^ leds_want) & (1 << i))
986       {
987         XKeyboardControl cc;
988         cc.led = i;
989         cc.led_mode = (leds_want & (1 << i)) ? LedModeOn : LedModeOff;
990         XChangeKeyboardControl(x11_dpy, KBLed | KBLedMode, &cc);
991       }
992   XFlush(x11_dpy);
993   leds_have = leds_want;
994 }
995
996 static void
997 rethink_leds(void)
998 {
999   if (!leds_care || !x11_dpy)
1000     return;
1001
1002   leds_want = 0;
1003   CLIST_FOR_EACH(struct mbox *, b, mboxes)
1004     if (b->o.led > 0 && b->new)
1005       leds_want |= (1 << b->o.led);
1006   sync_leds();
1007 }
1008
1009 static bool // 1 if string is to long
1010 append_osd_string(char * dest, unsigned *pos, char * source)
1011 {
1012   *pos += snprintf(dest+*pos, OSD_MSG_SIZE-*pos-1, "%s", source);
1013   if (*pos > OSD_MSG_SIZE-1)
1014     {
1015       *pos = sprintf(dest, "OSD message too long!");
1016       return 1;
1017     }
1018   return 0;
1019 }
1020
1021 struct osd_params {
1022   struct mbox *mbox;
1023   int new;
1024   int flagged;
1025   int total;
1026   clist *mboxes;
1027 };
1028
1029 static int format_osd_string(char *dest, char *src, struct osd_params *par);
1030
1031 static void
1032 format_osd_string_mailboxes(char * dest, bool all, struct osd_params *par)
1033 {
1034   if (!par->mboxes)
1035     {
1036       sprintf(dest, "[no mboxes]");
1037       return;
1038     }
1039   unsigned pos = 0;
1040   struct osd_params p = *par;
1041   bool is_first=1;
1042   CLIST_FOR_EACH(struct mbox *, b, *par->mboxes)
1043     {
1044       if (b->o.osd_list)
1045         if (all || b->new)
1046           {
1047             p.mboxes = 0;
1048             p.mbox = b;
1049             p.new = b->new;
1050             p.flagged = b->flagged;
1051             p.total = b->total;
1052             char buf[OSD_MSG_SIZE];
1053             if (!is_first)
1054               {
1055                 if (format_osd_string(buf, osd_mbox_opt_separator, &p))
1056                   if (append_osd_string(dest, &pos, buf)) break;
1057               }
1058             if (!format_osd_string(buf, osd_mbox_opt, &p))
1059               continue;
1060             if (append_osd_string(dest, &pos, buf)) break;
1061             is_first = 0;
1062           }
1063     }
1064   dest[pos] = 0;
1065 }
1066
1067 static int
1068 format_osd_string(char *dest, char *src, struct osd_params *par)
1069 {
1070   char *stop = dest + OSD_MSG_SIZE - 1;
1071   char buf[OSD_MSG_SIZE];
1072
1073   while (*src && dest < stop)
1074     {
1075       if (*src == '%')
1076         {
1077           src++;
1078           int size = 0;
1079           while (*src >= '0' && *src <= '9')
1080             size = 10*size + *src++ - '0';
1081           if (!size || size > stop-dest)
1082             size = dest-stop;
1083
1084           int spec = *src++;
1085           if (!spec)
1086             break;
1087
1088           char *arg = buf;
1089           switch (spec)
1090             {
1091             case 'f':
1092               arg = par->mbox ? par->mbox->sender_snippet : "";
1093               break;
1094             case 's':
1095               arg = par->mbox ? par->mbox->subject_snippet : "";
1096               break;
1097             case 'a':
1098               arg = par->mbox ? par->mbox->name : "";
1099               break;
1100             case 'A':
1101               arg = par->mbox ? par->mbox->path : "";
1102               break;
1103             case 'n':
1104               snprintf(buf, sizeof(buf), "%d", par->new);
1105               break;
1106             case 'F':
1107               snprintf(buf, sizeof(buf), "%d", par->flagged);
1108               break;
1109             case 'N':
1110               if (par->new < 2)
1111                 return 0;
1112               snprintf(buf, sizeof(buf), "%d", par->new - 1);
1113               break;
1114             case 'm':
1115               snprintf(buf, sizeof(buf), "%d", par->total);
1116               break;
1117             case 't':
1118               snprintf(buf, sizeof(buf), "%lld", (long long)time(NULL));
1119               break;
1120             case 'b':
1121               format_osd_string_mailboxes(buf, 0, par);
1122               break;
1123             case 'B':
1124               format_osd_string_mailboxes(buf, 1, par);
1125               break;
1126             case '%':
1127               arg = "%";
1128               break;
1129             default:
1130               arg = "???";
1131               break;
1132             }
1133
1134           while (*arg && size)
1135             {
1136               *dest++ = *arg++;
1137               size--;
1138             }
1139         }
1140       else
1141         *dest++ = *src++;
1142     }
1143   *dest = 0;
1144   return 1;
1145 }
1146
1147 static void
1148 format_osd(char *msg, struct clist *opts, struct osd_params *par)
1149 {
1150   unsigned pos = 0;
1151   unsigned have_text = 0;
1152   CLIST_FOR_EACH(struct osd_opt_node *, n, *opts)
1153     {
1154       char buf[OSD_MSG_SIZE];
1155       if (!format_osd_string(buf, n->val, par))
1156         continue;
1157       if (!n->key[0] && buf[0])
1158         have_text = 1;
1159       pos += snprintf(msg+pos, OSD_MSG_SIZE-pos-1, "%s:%s\n", n->key, buf);
1160       if (pos > OSD_MSG_SIZE-1)
1161         {
1162           pos = sprintf(msg, "OSD message too long!\n");
1163           break;
1164         }
1165     }
1166   if (have_text)
1167     msg[pos++] = '\n';
1168   else
1169     pos = 0;
1170   msg[pos] = 0;
1171 }
1172
1173 static void
1174 debug_osd_msg(char *msg)
1175 {
1176   if (!debug_mode)
1177     return;
1178   fprintf(stderr, "OSD: <");
1179   while (*msg)
1180     {
1181       fputc((*msg != '\n' ? *msg : '|'), stderr);
1182       msg++;
1183     }
1184   fprintf(stderr, ">\n");
1185 }
1186
1187 static void
1188 osd_send(char * msg)
1189 {
1190   if (msg[0])
1191     {
1192       debug("OSD: Sending to daemon\n");
1193       XChangeProperty(x11_dpy, DefaultRootWindow(x11_dpy), osd_pty, XA_STRING, 8, PropModeAppend, (unsigned char *) msg, strlen(msg));
1194       XFlush(x11_dpy);
1195     }
1196 }
1197
1198 static struct osd_params
1199 create_osd_params(bool (* if_use_box)(struct mbox *))
1200 {
1201   struct osd_params p = { .mbox = NULL, .new = 0, .flagged=0, .total=0, .mboxes=&mboxes };
1202   CLIST_FOR_EACH(struct mbox *, b, mboxes)
1203           if (if_use_box(b))
1204             {
1205               p.new += b->new;
1206               p.flagged += b->flagged;
1207               p.total += b->new;
1208               if (b->new && b->best_time > osd_last_time && (!p.mbox || p.mbox->o.priority < b->o.priority))
1209                 p.mbox = b;
1210             }
1211   return p;
1212 }
1213
1214 static bool if_use_box_osd(struct mbox *b)
1215 {
1216   return b->o.osd > 0;
1217 }
1218
1219 static bool if_use_box_osd_repeat(struct mbox *b)
1220 {
1221   return b->o.osd_repeat > 0;
1222 }
1223
1224 static void
1225 rethink_osd(int notify)
1226 {
1227   if (!osd_care || !x11_dpy || !allow_osd)
1228     {
1229       osd_last_msg[0] = 0;
1230       return;
1231     }
1232
1233   struct osd_params p = create_osd_params(if_use_box_osd);
1234
1235   char new_msg[OSD_MSG_SIZE];
1236   if(p.mbox)
1237     format_osd(new_msg, &osd_opts, &p);
1238   else
1239     new_msg[0]='\0';
1240   debug_osd_msg(new_msg);
1241   if (strcmp(new_msg, osd_last_msg))
1242     {
1243       strcpy(osd_last_msg, new_msg);
1244       if (notify)
1245         osd_send(new_msg);
1246       else
1247         debug("OSD: No changes\n");
1248       osd_last_time = time(NULL);
1249     }
1250 }
1251
1252 static void
1253 x11_cleanup(void)
1254 {
1255   if (!x11_dpy)
1256     return;
1257
1258   leds_want = 0;
1259   sync_leds();
1260 }
1261
1262 static void
1263 osd_repeat(void)
1264 {
1265   if(!osd_repeat_interval) return;
1266   last_osd_repeat_time = time(NULL);
1267   char new_msg[OSD_MSG_SIZE];
1268   struct osd_params p = create_osd_params(if_use_box_osd_repeat);
1269   format_osd(new_msg, &osd_repeat_opts, &p);
1270   debug_osd_msg(new_msg);
1271   osd_send(new_msg);
1272   CLIST_FOR_EACH(struct mbox *, b, mboxes)
1273     {
1274              b->osd_repeat_last_total           = b->total           ;
1275              b->osd_repeat_last_new             = b->new             ;
1276              b->osd_repeat_last_flagged         = b->flagged         ;
1277       strcpy(b->osd_repeat_last_sender_snippet  , b->sender_snippet );
1278       strcpy(b->osd_repeat_last_subject_snippet , b->subject_snippet);
1279     }
1280 }
1281
1282 static void
1283 rethink_osd_repeat(void)
1284 {
1285   if(!osd_repeat_interval) return;
1286   bool change = 0;
1287   CLIST_FOR_EACH(struct mbox *, b, mboxes)
1288     {
1289       if(       b->osd_repeat_last_total           != b->total           ) change = 1;
1290       if(       b->osd_repeat_last_new             != b->new             ) change = 1;
1291       if(       b->osd_repeat_last_flagged         != b->flagged         ) change = 1;
1292       if(strcmp(b->osd_repeat_last_sender_snippet  ,  b->sender_snippet )) change = 1;
1293       if(strcmp(b->osd_repeat_last_subject_snippet ,  b->subject_snippet)) change = 1;
1294     }
1295   if(change) osd_repeat();
1296 }
1297
1298 #else
1299
1300 static void x11_init(void) { }
1301 static void rethink_leds(void) { }
1302 static void rethink_osd(int notify UNUSED) { }
1303 static void x11_cleanup(void) { }
1304 static void osd_repeat(void) { }
1305 static void rethink_osd_repeat(void) { }
1306
1307 #endif
1308
1309 static int cursor_at, cursor_max;
1310
1311 static unsigned is_active, is_pos;      // incremental search
1312 static char is_buf[64];
1313
1314 enum {
1315   M_IDLE,
1316   M_SCAN,
1317   M_NEW,
1318   M_FLAG,
1319   M_BAD,
1320   M_INCSEARCH,
1321   M_MAX
1322 };
1323 static int attrs[3][2][M_MAX];          // active (2=incsearch), hilite, status
1324
1325 static void
1326 redraw_line(int i)
1327 {
1328   move(i, 0);
1329   if (i < cursor_max)
1330     {
1331       struct mbox *b = mbox_array[i];
1332       int cc = (cursor_at == i ? (is_active ? 2 : 1) : 0);
1333       int hi = b->o.highlight;
1334       unsigned namepos = 0;
1335       unsigned namelen = strlen(b->name);
1336       int valid = 3600;
1337
1338       attrset(attrs[cc][hi][M_IDLE]);
1339       if (b->o.hotkey)
1340         printw("%c ", b->o.hotkey);
1341       else if (cc)
1342         printw("> ");
1343       else
1344         printw("  ");
1345       if (cc == 2)
1346         {
1347           attrset(attrs[cc][hi][M_INCSEARCH]);
1348           for (namepos=0; namepos < is_pos && namepos < 20; namepos++)
1349             addch(is_buf[namepos]);
1350         }
1351       if (b->new)
1352         attrset(attrs[cc][hi][M_NEW]);
1353       else if (b->flagged && b->o.show_flagged)
1354         attrset(attrs[cc][hi][M_FLAG]);
1355       else
1356         attrset(attrs[cc][hi][M_IDLE]);
1357       while (namepos < namelen)
1358         addch(b->name[namepos++]);
1359       while (namepos++ < 20)
1360         addch(' ');
1361       if (b->scanning < 0)
1362         ;
1363       else if (b->scanning)
1364         {
1365           attrset(attrs[cc][hi][M_SCAN]);
1366           printw("[SCANNING]");
1367         }
1368       else if (b->total < 0)
1369         {
1370           attrset(attrs[cc][hi][M_BAD]);
1371           printw("BROKEN");
1372         }
1373       else
1374         {
1375           attrset(attrs[cc][hi][M_IDLE]);
1376           printw("%6d ", b->total);
1377           int snip = 0;
1378           if (b->new)
1379             {
1380               attrset(attrs[cc][hi][M_NEW]);
1381               printw("%6d  ", b->new);
1382               attrset(attrs[cc][hi][M_IDLE]);
1383               int age = (last_scan_time - b->best_time);
1384               if (age < 0)
1385                 age = 0;
1386               if (age < 3600)
1387                 {
1388                   printw("%2d min  ", age/60);
1389                   valid = 60;
1390                 }
1391               else if (age < 86400)
1392                 printw("%2d hr%c  ", age/3600, (age >= 7200 ? 's' : ' '));
1393               else
1394                 printw("        ");
1395               snip = 1;
1396             }
1397           else if (b->flagged && b->o.show_flagged)
1398             {
1399               attrset(attrs[cc][hi][M_FLAG]);
1400               printw("%6d  ", b->flagged);
1401               attrset(attrs[cc][hi][M_IDLE]);
1402               printw("        ");
1403               attrset(attrs[cc][0][M_FLAG]);    /* We avoid the highlight intentionally */
1404               snip = 1;
1405             }
1406           if (snip && b->o.snippets)
1407             {
1408               int xx, yy;
1409               getyx(stdscr, yy, xx);
1410               int remains = COLS-1-xx;
1411               (void) yy;
1412
1413               char snip[256];
1414               build_snippet(snip, snip + sizeof(snip) - 1, b);
1415
1416               if (snip[0] && remains > 2)
1417                 {
1418 #ifdef CONFIG_WIDE_CURSES
1419                   size_t len = strlen(snip)+1;
1420                   wchar_t snip2[len];
1421                   mbstowcs(snip2, snip, len);
1422                   addnwstr(snip2, remains);
1423 #else
1424                   printw("%-.*s", remains, snip);
1425 #endif
1426                 }
1427             }
1428         }
1429       b->display_valid_until = last_scan_time + valid;
1430     }
1431   attrset(attrs[0][0][M_IDLE]);
1432   clrtoeol();
1433 }
1434
1435 static void
1436 redraw_all(void)
1437 {
1438   cursor_max = num_mboxes;
1439   if (cursor_max > LINES-1)
1440     cursor_max = LINES-1;
1441   if (cursor_at >= cursor_max)
1442     cursor_at = cursor_max - 1;
1443   if (cursor_at < 0)
1444     cursor_at = 0;
1445
1446   for (int i=0; i<cursor_max; i++)
1447     redraw_line(i);
1448   move(cursor_max, 0);
1449   if (!cursor_max)
1450     {
1451       printw("(no mailboxes found)");
1452       clrtoeol();
1453       move(1, 0);
1454     }
1455   clrtobot();
1456 }
1457
1458 static void
1459 rethink_display(int notify)
1460 {
1461   int i = 0;
1462   int changed = 0;
1463   int beeeep = 0;
1464   CLIST_FOR_EACH(struct mbox *, b, mboxes)
1465     if (mbox_visible_p(b))
1466       {
1467         b->index = i;
1468         if (i >= num_mboxes || mbox_array[i] != b)
1469           {
1470             changed = 1;
1471             if (i >= mbox_array_size)
1472               {
1473                 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
1474                 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
1475               }
1476             mbox_array[i] = b;
1477           }
1478         if (b->o.beep && b->new > b->last_beep_new)
1479           beeeep = 1;
1480         b->last_beep_new = b->new;
1481         i++;
1482       }
1483   if (i != num_mboxes)
1484     changed = 1;
1485   num_mboxes = i;
1486
1487   if (changed)
1488     {
1489       redraw_all();
1490       refresh();
1491     }
1492   rethink_leds();
1493   rethink_osd(notify);
1494   rethink_osd_repeat();
1495   if (beeeep && allow_bells && notify)
1496     beep();
1497 }
1498
1499 static void
1500 term_init(void)
1501 {
1502   initscr();
1503   cbreak();
1504   noecho();
1505   nonl();
1506   intrflush(stdscr, FALSE);
1507   keypad(stdscr, TRUE);
1508   curs_set(0);
1509
1510   static const int attrs_mono[2][M_MAX] = {
1511         [0] = { [M_IDLE] = 0,
1512                 [M_SCAN] = A_BOLD,
1513                 [M_NEW] = A_BOLD,
1514                 [M_FLAG] = 0,
1515                 [M_BAD] = A_DIM,
1516                 [M_INCSEARCH] = A_REVERSE },
1517         [1] = { [M_IDLE] = 0,
1518                 [M_SCAN] = A_BOLD,
1519                 [M_NEW] = A_REVERSE | A_BOLD,
1520                 [M_FLAG] = A_REVERSE,
1521                 [M_BAD] = A_DIM,
1522                 [M_INCSEARCH] = A_REVERSE },
1523   };
1524   for (int i=0; i<2; i++)
1525     for (int j=0; j<M_MAX; j++)
1526       {
1527         attrs[0][i][j] = attrs_mono[i][j];
1528         attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
1529         attrs[2][i][j] = attrs_mono[i][j] | A_UNDERLINE;
1530       }
1531
1532   if (has_colors())
1533     {
1534       start_color();
1535       if (COLOR_PAIRS >= 12)
1536         {
1537           init_pair(1, COLOR_YELLOW, COLOR_BLACK);
1538           init_pair(2, COLOR_RED, COLOR_BLACK);
1539           init_pair(3, COLOR_WHITE, COLOR_BLUE);
1540           init_pair(4, COLOR_YELLOW, COLOR_BLUE);
1541           init_pair(5, COLOR_RED, COLOR_BLUE);
1542           init_pair(6, COLOR_GREEN, COLOR_BLACK);
1543           init_pair(7, COLOR_GREEN, COLOR_BLUE);
1544           init_pair(8, COLOR_WHITE, COLOR_MAGENTA);
1545           init_pair(9, COLOR_YELLOW, COLOR_MAGENTA);
1546           init_pair(10, COLOR_GREEN, COLOR_MAGENTA);
1547           init_pair(11, COLOR_RED, COLOR_MAGENTA);
1548           init_pair(12, COLOR_BLACK, COLOR_YELLOW);
1549           static const int attrs_color[3][2][M_MAX] = {
1550              [0][0] = { [M_IDLE] = 0,
1551                         [M_SCAN] = COLOR_PAIR(1),
1552                         [M_NEW] = COLOR_PAIR(1),
1553                         [M_FLAG] = COLOR_PAIR(6),
1554                         [M_BAD] = COLOR_PAIR(2) },
1555              [0][1] = { [M_IDLE] = A_BOLD,
1556                         [M_SCAN] = COLOR_PAIR(1),
1557                         [M_NEW] = COLOR_PAIR(1) | A_BOLD,
1558                         [M_FLAG] = COLOR_PAIR(6) | A_BOLD,
1559                         [M_BAD] = COLOR_PAIR(2) | A_BOLD },
1560              [1][0] = { [M_IDLE] = COLOR_PAIR(3),
1561                         [M_SCAN] = COLOR_PAIR(4),
1562                         [M_NEW] = COLOR_PAIR(4),
1563                         [M_FLAG] = COLOR_PAIR(7),
1564                         [M_BAD] = COLOR_PAIR(5) },
1565              [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD,
1566                         [M_SCAN] = COLOR_PAIR(4),
1567                         [M_NEW] = COLOR_PAIR(4) | A_BOLD,
1568                         [M_FLAG] = COLOR_PAIR(7) | A_BOLD,
1569                         [M_BAD] = COLOR_PAIR(5) | A_BOLD },
1570              [2][0] = { [M_IDLE] = COLOR_PAIR(8),
1571                         [M_SCAN] = COLOR_PAIR(9),
1572                         [M_NEW] = COLOR_PAIR(9),
1573                         [M_FLAG] = COLOR_PAIR(10),
1574                         [M_BAD] = COLOR_PAIR(11),
1575                         [M_INCSEARCH] = COLOR_PAIR(12) | A_DIM },
1576              [2][1] = { [M_IDLE] = COLOR_PAIR(8) | A_BOLD,
1577                         [M_SCAN] = COLOR_PAIR(9),
1578                         [M_NEW] = COLOR_PAIR(9) | A_BOLD,
1579                         [M_FLAG] = COLOR_PAIR(10) | A_BOLD,
1580                         [M_BAD] = COLOR_PAIR(11) | A_BOLD,
1581                         [M_INCSEARCH] = COLOR_PAIR(12) },
1582           };
1583           memcpy(attrs, attrs_color, sizeof(attrs));
1584         }
1585     }
1586 }
1587
1588 static void
1589 term_cleanup(void)
1590 {
1591   endwin();
1592 }
1593
1594 static void
1595 print_status(char *status)
1596 {
1597   move(LINES-1, 0);
1598   if (status)
1599     printw("%s", status);
1600   clrtoeol();
1601   refresh();
1602 }
1603
1604 static void
1605 scan_and_redraw(int notify)
1606 {
1607   print_status("Busy...");
1608   scan(notify);
1609   print_status(NULL);
1610 }
1611
1612 static void
1613 move_cursor(int i)
1614 {
1615   if (i >= 0 && i < cursor_max && i != cursor_at)
1616     {
1617       int old = cursor_at;
1618       cursor_at = i;
1619       redraw_line(old);
1620       redraw_line(i);
1621     }
1622 }
1623
1624 static void
1625 next_active(int since, int step)
1626 {
1627   if (!cursor_max)
1628     return;
1629   since = (since+cursor_max) % cursor_max;
1630   step = (step+cursor_max) % cursor_max;
1631   int besti = -1;
1632   int bestp = -1;
1633   int i = since;
1634   do
1635     {
1636       struct mbox *b = mbox_array[i];
1637       if (simple_tab)
1638         {
1639           if (b->new)
1640             {
1641               besti = i;
1642               break;
1643             }
1644         }
1645       else
1646         {
1647           if (b->new && b->o.priority > bestp)
1648             {
1649               besti = i;
1650               bestp = b->o.priority;
1651             }
1652         }
1653       i = (i+step) % cursor_max;
1654     }
1655   while (i != since);
1656   if (besti >= 0)
1657     move_cursor(besti);
1658 }
1659
1660 static void
1661 mbox_run(struct mbox *b)
1662 {
1663   char cmd[strlen(run_cmd) + strlen(b->path) + 16];
1664   sprintf(cmd, run_cmd, b->path);
1665   term_cleanup();
1666   system(cmd);
1667   term_init();
1668   redraw_all();
1669   refresh();
1670   b->force_refresh = 1;
1671   scan_and_redraw(0);
1672 }
1673
1674 static void
1675 enter_incsearch(void)
1676 {
1677   print_status("Incremental search...");
1678   is_active = 1;
1679   is_pos = 0;
1680   redraw_line(cursor_at);
1681 }
1682
1683 static int
1684 handle_incsearch(int ch)
1685 {
1686   if ((ch == KEY_BACKSPACE || ch == KEY_DC) && is_pos)
1687     --is_pos;
1688   else if (ch >= ' ' && ch <= '~')
1689     {
1690       if (is_pos < sizeof(is_buf) - 1)
1691         is_buf[is_pos++] = ch;
1692     }
1693   else
1694     {
1695       print_status(NULL);
1696       is_active = 0;
1697       is_pos = 0;
1698       redraw_line(cursor_at);
1699       return 0;
1700     }
1701
1702   is_buf[is_pos] = 0;
1703   for (int i=0; i<cursor_max; i++)
1704     {
1705       struct mbox *b = mbox_array[i];
1706       if (!strncmp(b->name, is_buf, is_pos))
1707         {
1708           if (i != cursor_at)
1709             {
1710               move_cursor(i);
1711               return 1;
1712             }
1713           break;
1714         }
1715     }
1716
1717   redraw_line(cursor_at);
1718   return 1;
1719 }
1720
1721 static void
1722 without_inotify_wait(int timeout)
1723 {
1724   timeout *= 10;
1725   halfdelay((timeout > 255) ? 255 : timeout);
1726 }
1727 #ifdef CONFIG_INOTIFY
1728
1729 #include <sys/inotify.h>
1730
1731 static void
1732 inotify_cm_init(void)
1733 {
1734   if (!inotify_active) return;
1735   inotify_fd = inotify_init();
1736   if (inotify_fd < 0)
1737     die("Inotify init faild.\n");
1738 }
1739
1740 static void
1741 inotify_add_mbox(struct mbox * b)
1742 {
1743   if (!inotify_active) return;
1744   b->inotify_status = INOTIFY_ON;
1745   char *path = xmalloc(strlen(b->path)+100);
1746   struct stat st;
1747   if (stat(b->path, &st) < 0)
1748     {
1749       debug("Get stat of %s faild\n", b->path);
1750       return;
1751     }
1752   if (S_ISREG(st.st_mode))
1753     {
1754       // Regular mailbox
1755       sprintf(path, "%s", b->path);
1756       if (inotify_add_watch(inotify_fd, path,  IN_MOVED_TO | IN_MOVED_FROM | IN_DELETE | IN_CLOSE_WRITE ) < 0)
1757         {
1758           debug("Inotify add %s faild\n", path);
1759           b->inotify_status = INOTIFY_ERROR;
1760         }
1761       else
1762           debug("Inotify add %s OK\n", path);
1763     }
1764   else if (S_ISDIR(st.st_mode))
1765     {
1766       // Maildir
1767       char *watch_dir[] = {"cur", "new"};
1768       for (int i=0; i < (int)(sizeof(watch_dir)/sizeof(*watch_dir)); i++)
1769         {
1770           sprintf(path, "%s/%s", b->path, watch_dir[i]);
1771           if (inotify_add_watch(inotify_fd, path,  IN_MOVED_TO | IN_MOVED_FROM | IN_DELETE | IN_CLOSE_WRITE ) < 0)
1772             {
1773               debug("Inotify add %s faild\n", path);
1774               b->inotify_status = INOTIFY_ERROR;
1775             }
1776         }
1777     }
1778   else
1779     {
1780       debug("neither file nor directory\n");
1781       b->inotify_status = INOTIFY_ERROR;
1782       return;
1783     }
1784   free(path);
1785 }
1786
1787 static void
1788 inotify_delete_mbox(struct mbox * b)
1789 {
1790   (void)b;
1791   // TODO
1792 }
1793
1794 static bool // true if inotify change detected
1795 inotify_wait(int timeout)
1796 {
1797   if(!inotify_active)
1798     {
1799       without_inotify_wait(timeout);
1800       return 0;
1801     }
1802
1803   bool inotify_change = 0;
1804
1805   struct timeval tv;
1806   tv.tv_sec = timeout;
1807   tv.tv_usec = 0;
1808   while (true)
1809     {
1810       fd_set rfds;
1811       FD_ZERO(&rfds);
1812       FD_SET(0, &rfds);
1813       FD_SET(inotify_fd, &rfds);
1814
1815       debug("Select begin (timeout %d %d)\n", tv.tv_sec, tv.tv_usec);
1816       int ret = select(1+inotify_fd, &rfds, NULL, NULL, &tv);
1817       debug("Select end\n");
1818       if (ret>0 && FD_ISSET(0, &rfds))
1819         return inotify_change;
1820       if (ret>0 && FD_ISSET(inotify_fd, &rfds))
1821         {
1822           debug("Read notify begin\n");
1823           char buf[4096] __attribute__ ((aligned(__alignof__(struct inotify_event))));
1824           ssize_t len = read(inotify_fd, buf, sizeof(buf));
1825           if (len == -1 && errno != EAGAIN)
1826               perror ("read inotify faild");
1827           inotify_change = 1;
1828           debug("Read notify end\n");
1829         }
1830       else
1831         return inotify_change;
1832       tv.tv_sec = 0;
1833       tv.tv_usec = 1000;
1834     }
1835 }
1836
1837 #else
1838
1839 static void
1840 inotify_cm_init(void){}
1841
1842 static void
1843 inotify_add_mbox(struct mbox *){}
1844
1845 static void
1846 inotify_delete_mbox(struct mbox *){}
1847
1848 static bool
1849 inotify_wait(int timeout)
1850 {
1851   without_inotify_wait(timeout);
1852   return 0;
1853 }
1854
1855 #endif
1856
1857 #define STR2(c) #c
1858 #define STR(c) STR2(c)
1859
1860 static void NONRET
1861 usage(void)
1862 {
1863   fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
1864 \n\
1865 Options:\n\
1866 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
1867 -d\t\t\tLog debug messages to stderr\n\
1868 -i\t\t\tInclude user's INBOX (/var/mail/<user>=INBOX)\n\
1869 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
1870 -o <pattern>=<opts>\tSet mailbox options\n\
1871 -o <opts>\t\tSet default options for all mailboxes\n\
1872 -p <pri>\t\tSet minimum priority to show\n\
1873 -s <key>=<val>\t\tSet on-screen display options (consult OSDD docs)\n\
1874 -r <key>=<val>\t\tSet on-screen display repeat notification options (consult OSDD docs)\n\
1875 -b <val>\t\tSet on-screen display mailbox specification string (for %%b %%B expansions)\n\
1876 -B <val>\t\tSet on-screen display mailbox specification string separator (for %%b %%B expansions)\n\
1877 -C <interval>\t\tSend repeat notifications every <interval> seconds (default is disable repeat not.)\n\
1878 -t\t\t\tLet TAB select the next mailbox with new mail, no matter what priority it has\n\
1879 -f\t\t\tEnable inotify (wait for file system changes)\n\
1880 \n\
1881 On-screen display replacement (in all OSD values, notification lines and -b -B options):\n\
1882 %%%%\t\t%%\n\
1883 %%f\t\tFrom - mail author name (last unread mail if any exist)\n\
1884 %%s\t\tMail subject (last unread mail if any exist)\n\
1885 %%a\t\tMailbox name of last unread mail (if any exit) or actual mailbox in -b\n\
1886 %%A\t\tMailbox path of last unread mail (if any exit) or actual mailbox in -b\n\
1887 %%n\t\tCount of new mails\n\
1888 %%N\t\tCount of new mails minus one; if is less than one, whole line will be hidden\n\
1889 %%F\t\tCount of flagged mails\n\
1890 %%m\t\tCount of all mails\n\
1891 %%t\t\tActual unix time\n\
1892 %%b\t\tPrint all mailboxes with at least one new mail (format by -b -B options)\n\
1893 %%B\t\tPrint all mailbox (format by -b -B options)\n\
1894 \n\
1895 Mailbox options (set with `-o', use upper case to negate):\n\
1896 0-9\t\t\tSet mailbox priority (0=default)\n\
1897 b\t\t\tBeep when a message arrives\n\
1898 d\t\t\tSend an on-screen-display message (requires OSDD)\n\
1899 x\t\t\tCount this mailbox in on-screen display repeat notification (default on)\n\
1900 y\t\t\tUse this mailbox in notification mailbox list (%%b %%B expansions) (default on)\n\
1901 e\t\t\tHide from display if empty\n\
1902 f\t\t\tShow flagged messages if there are no new ones\n\
1903 h\t\t\tHide from display\n\
1904 l<led>\t\t\tLight a keyboard led (1-9) if running on X display\n\
1905 m\t\t\tShow mailbox name of the sender\n\
1906 o\t\t\tCount old, but unread messages as new\n\
1907 p\t\t\tShow personal info (full name) of the sender\n\
1908 r<int>\t\t\tSort order (default=1000)\n\
1909 s\t\t\tShow message snippets\n\
1910 t\t\t\tHighlight the entry\n\
1911 !<key>\t\t\tSet hot key\n\
1912 \n\
1913 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>, Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>\n\
1914 It can be freely distributed and used according to the GNU GPL v2.\n\
1915 ");
1916   exit(1);
1917 }
1918
1919 static void
1920 parse_options(char *c)
1921 {
1922   struct options *o;
1923   char *sep;
1924   if (sep = strchr(c, '='))
1925     {
1926       struct option_node *n = xmalloc(sizeof(*n) + sep-c);
1927       memcpy(n->pattern, c, sep-c);
1928       n->pattern[sep-c] = 0;
1929       clist_add_tail(&options, &n->n);
1930       o = &n->o;
1931       init_options(o);
1932       c = sep+1;
1933     }
1934   else
1935     o = &global_options;
1936
1937   int x;
1938   while (x = *c++)
1939     if (x >= '0' && x <= '9')
1940       o->priority = x - '0';
1941     else if (x == '!' && *c)
1942       o->hotkey = *c++;
1943     else if (x == 'l' && *c >= '1' && *c <= '9')
1944       o->led = *c++ - '0';
1945     else if (x == 'r' && *c >= '0' && *c <= '9')
1946       {
1947         o->sort_order = 0;
1948         while (*c >= '0' && *c <= '9')
1949           o->sort_order = 10*o->sort_order + *c++ - '0';
1950       }
1951     else
1952       {
1953         int value = !!islower(x);
1954         switch (tolower(x))
1955           {
1956           case 'b':
1957             o->beep = value;
1958             break;
1959           case 'x':
1960             o->osd_repeat = value;
1961             break;
1962           case 'y':
1963             o->osd_list = value;
1964             break;
1965           case 'd':
1966             o->osd = value;
1967             break;
1968           case 'e':
1969             o->hide_if_empty = value;
1970             break;
1971           case 'f':
1972             o->show_flagged = value;
1973             break;
1974           case 'h':
1975             o->hide = value;
1976             break;
1977           case 'm':
1978             o->sender_mbox = value;
1979             break;
1980           case 'o':
1981             o->unread_is_new = value;
1982             break;
1983           case 'p':
1984             o->sender_personal = value;
1985             break;
1986           case 's':
1987             o->snippets = value;
1988             break;
1989           case 't':
1990             o->highlight = value;
1991             break;
1992           default:
1993             fprintf(stderr, "Invalid mailbox option `%c'\n", x);
1994             usage();
1995           }
1996       }
1997 }
1998
1999 int
2000 main(int argc, char **argv)
2001 {
2002   clist_init(&mboxes);
2003   clist_init(&options);
2004   clist_init(&patterns);
2005   clist_init(&osd_opts);
2006   clist_init(&osd_repeat_opts);
2007
2008   int c;
2009   while ((c = getopt(argc, argv, "c:C:dim:o:p:s:r:b:B:tif")) >= 0)
2010     switch (c)
2011       {
2012       case 'c':
2013         check_interval = atol(optarg);
2014         if (check_interval <= 0)
2015           usage();
2016         break;
2017       case 'C':
2018         osd_repeat_interval = atol(optarg);
2019         if (check_interval <= 0)
2020           usage();
2021         break;
2022       case 'd':
2023         debug_mode++;
2024         break;
2025       case 'i':
2026         add_inbox();
2027         break;
2028       case 'm':
2029         run_cmd = optarg;
2030         break;
2031       case 'o':
2032         parse_options(optarg);
2033         break;
2034       case 'p':
2035         minimum_priority = atol(optarg);
2036         break;
2037       case 's':
2038         add_osd_opt(optarg, &osd_opts);
2039         break;
2040       case 'r':
2041         add_osd_opt(optarg, &osd_repeat_opts);
2042         break;
2043       case 'b':
2044         osd_mbox_opt = optarg;
2045         break;
2046       case 'B':
2047         osd_mbox_opt_separator = optarg;
2048         break;
2049       case 't':
2050         simple_tab = 1;
2051         break;
2052       case 'f':
2053         inotify_active = 1;
2054         break;
2055       default:
2056         usage();
2057       }
2058   while (optind < argc)
2059     add_pattern(argv[optind++]);
2060
2061   charset_init();
2062   term_init();
2063   x11_init();
2064   inotify_cm_init();
2065   scan_and_redraw(0);
2066   next_active(0, 1);
2067
2068
2069   int inotify_rescan = 0;
2070   int should_exit = 0;
2071 restart:
2072   while (!should_exit)
2073     {
2074       time_t now = time(NULL);
2075       int remains = last_scan_time + check_interval - now;
2076       int osd_repeat_remains = last_osd_repeat_time + osd_repeat_interval - now;
2077       if(!osd_repeat_interval) osd_repeat_remains = 1<<30;
2078       if (force_refresh)
2079         scan_and_redraw(0);
2080       if (remains <= 0 || inotify_rescan)
2081         {
2082           if (inotify_rescan) debug("Inotify rescan\n");
2083           inotify_rescan = 0;
2084           scan_and_redraw(1);
2085         }
2086       if (osd_repeat_remains <= 0)
2087         {
2088           osd_repeat();
2089         }
2090       else
2091         {
2092           nodelay(stdscr,1);
2093           int ch = getch();
2094           if (ch < 0)
2095             {
2096               int min_remains = remains;
2097               if(min_remains > osd_repeat_remains) min_remains = osd_repeat_remains;
2098               inotify_rescan |= inotify_wait(min_remains);
2099               ch = getch();
2100             }
2101           if (ch < 0)
2102             continue;
2103           if (is_active && handle_incsearch(ch))
2104             {
2105               refresh();
2106               continue;
2107             }
2108           for (int i=0; i<num_mboxes; i++)
2109             if (ch == mbox_array[i]->o.hotkey)
2110               {
2111                 if (i < cursor_max)
2112                   cursor_at = i;
2113                 mbox_run(mbox_array[i]);
2114                 goto restart;
2115               }
2116           switch (ch)
2117             {
2118             case 'q':
2119               should_exit = 1;
2120               break;
2121             case 'j':
2122             case KEY_DOWN:
2123               move_cursor(cursor_at+1);
2124               break;
2125             case 'k':
2126             case KEY_UP:
2127               move_cursor(cursor_at-1);
2128               break;
2129             case '^':
2130             case KEY_HOME:
2131             case KEY_PPAGE:
2132               move_cursor(0);
2133               break;
2134             case '$':
2135             case KEY_END:
2136             case KEY_NPAGE:
2137               move_cursor(cursor_max-1);
2138               break;
2139             case '\t':
2140               next_active(cursor_at+1, 1);
2141               break;
2142             case '`':
2143               next_active(cursor_at-1, -1);
2144               break;
2145             case '\r':
2146             case '\n':
2147               if (cursor_at < cursor_max)
2148                 mbox_run(mbox_array[cursor_at]);
2149               break;
2150             case 'l' & 0x1f:
2151               clearok(stdscr, TRUE);
2152               redraw_all();
2153               refresh();
2154               break;
2155             case 'r' & 0x1f:
2156               force_refresh = 1;
2157               break;
2158             case 'b':
2159               allow_bells = 1;
2160               print_status("Bells and whistles are now enabled. Toot!");
2161               break;
2162             case 'B':
2163               allow_bells = 0;
2164               print_status("Bells and whistles are now disabled. Pssst!");
2165               break;
2166             case 'd':
2167               allow_osd = 1;
2168               print_status("On-screen display is now enabled.");
2169               break;
2170             case 'D':
2171               allow_osd = 0;
2172               print_status("On-screen display is now disabled. Watch your step.");
2173               break;
2174             case '/':
2175               enter_incsearch();
2176               break;
2177             default:
2178               if (ch >= '0' && ch <= '9')
2179                 {
2180                   minimum_priority = ch - '0';
2181                   scan_and_redraw(0);
2182                 }
2183               else
2184                 debug("Pressed unknown key %d\n", ch);
2185             }
2186           refresh();
2187         }
2188     }
2189
2190   x11_cleanup();
2191   term_cleanup();
2192   return 0;
2193 }