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