]> mj.ucw.cz Git - checkmail.git/blob - cm.c
Initial revision
[checkmail.git] / cm.c
1 /*
2  *      Incoming Mail Checker
3  *
4  *      (c) 2005 Martin Mares <mj@ucw.cz>
5  */
6
7 #include <stdio.h>
8 #include <string.h>
9 #include <stdlib.h>
10 #include <getopt.h>
11 #include <fcntl.h>
12 #include <glob.h>
13 #include <sys/stat.h>
14 #include <unistd.h>
15 #include <pwd.h>
16 #include <time.h>
17 #include <curses.h>
18
19 #include "util.h"
20 #include "clists.h"
21
22 static int check_interval = 30;
23 static int force_refresh;
24 static int lock_mboxes;
25 static time_t last_scan_time;
26 static char *run_cmd = "mutt -f %s";
27
28 struct mbox {
29   cnode n;
30   int index;
31   int hilited;
32   int scanning;
33   int seen;
34   time_t last_time;
35   int last_size, last_pos;
36   char *name;
37   char *path;
38   int total, new;
39   int last_total, last_new;
40   int force_refresh;
41 };
42
43 static clist mboxes, hilites, patterns;
44 static struct mbox **mbox_array;
45 static int num_mboxes;
46
47 static void redraw_line(int i);
48 static void redraw_all(void);
49
50 static char *
51 mbox_name(char *path)
52 {
53   char *c = strrchr(path, '/');
54   return c ? (c+1) : path;
55 }
56
57 static struct mbox *
58 add_mbox(clist *l, char *path, char *name)
59 {
60   struct mbox *b = xmalloc(sizeof(*b));
61   bzero(b, sizeof(*b));
62   b->path = xstrdup(path);
63   b->name = xstrdup(name);
64
65   if (name)
66     {
67       cnode *prev = l->head.prev;
68       while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
69         prev = prev->prev;
70       clist_insert_after(&b->n, prev);
71     }
72   else
73     clist_add_tail(l, &b->n);
74
75   return b;
76 }
77
78 static void
79 del_mbox(struct mbox *b)
80 {
81   clist_remove(&b->n);
82   free(b->path);
83   free(b->name);
84   free(b);
85 }
86
87 static struct mbox *
88 find_mbox(clist *l, char *path)
89 {
90   CLIST_FOR_EACH(struct mbox *, b, *l)
91     if (!strcmp(b->path, path))
92       return b;
93   return NULL;
94 }
95
96 static void
97 add_inbox(clist *l)
98 {
99   struct passwd *p = getpwuid(getuid());
100   if (!p)
101     die("You don't exist, go away!");
102   char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 1];
103   sprintf(buf, "/var/mail/%s", p->pw_name);
104   add_mbox(l, buf, "INBOX");
105 }
106
107 static int mb_fd, mb_pos;
108 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
109
110 static void
111 mb_reset(int pos)
112 {
113   mb_cc = mb_end = mb_buf;
114   mb_pos = pos;
115 }
116
117 static int
118 mb_seek(uns pos)
119 {
120   lseek(mb_fd, pos, SEEK_SET);
121   mb_reset(pos);
122 }
123
124 static int
125 mb_tell(void)
126 {
127   return mb_pos - (mb_end - mb_cc);
128 }
129
130 static int
131 mb_ll_get(void)
132 {
133   int len = read(mb_fd, mb_buf, sizeof(mb_buf));
134   mb_cc = mb_buf;
135   if (len <= 0)
136     {
137       mb_end = mb_buf;
138       return -1;
139     }
140   else
141     {
142       mb_end = mb_buf + len;
143       mb_pos += len;
144       return *mb_cc++;
145     }
146 }
147
148 static inline int
149 mb_get(void)
150 {
151   return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
152 }
153
154 static int
155 mb_check(const char *p, int len)
156 {
157   while (len--)
158     {
159       if (mb_get() != *p++)
160         return 0;
161     }
162   return 1;
163 }
164
165 static void
166 scan_mbox(struct mbox *b, struct stat *st)
167 {
168   char buf[1024];
169   int c;
170   const char from[] = "\nFrom ";
171
172   if (!st->st_size)
173     {
174       b->total = b->new = 0;
175       b->last_pos = 0;
176       return;
177     }
178
179   /* FIXME: Locking! */
180
181   mb_fd = open(b->path, O_RDONLY);
182   if (mb_fd < 0)
183     {
184       debug("[open failed: %m] ");
185       b->total = b->new = -1;
186       return;
187     }
188   mb_reset(0);
189
190   int incremental = 0;
191   if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
192     {
193       mb_seek(b->last_pos);
194       if (mb_check(from, 6))
195         {
196           debug("[incremental] ");
197           incremental = 1;
198         }
199       else
200         {
201           debug("[incremental failed] ");
202           mb_seek(0);
203         }
204     }
205   if (!incremental)
206     {
207       if (!mb_check(from+1, 5))
208         {
209           debug("[inconsistent] ");
210           b->total = b->new = -1;
211           goto done;
212         }
213       b->total = b->new = 0;
214       b->last_total = b->last_new = 0;
215     }
216   else
217     {
218       b->total = b->last_total;
219       b->new = b->last_new;
220     }
221
222   for(;;)
223     {
224       b->last_pos = mb_tell() - 5;
225       if (b->last_pos)
226         b->last_pos--;          // last_pos should be the previous \n character
227       b->last_total = b->total;
228       b->last_new = b->new;
229       while ((c = mb_get()) >= 0 && c != '\n')
230         ;
231
232       int new = 1;
233       for (;;)
234         {
235           uns i = 0;
236           for (;;)
237             {
238               c = mb_get();
239               if (c < 0)
240                 {
241                   debug("[truncated] ");
242                   goto done;
243                 }
244               if (c == '\n')
245                 break;
246               if (c == '\r')
247                 continue;
248               if (i < sizeof(buf) - 1)
249                 buf[i++] = c;
250             }
251           buf[i] = 0;
252           if (!buf[0])
253             break;
254           if (!strncasecmp(buf, "Status:", 7))
255             new = 0;
256         }
257
258       b->total++;
259       if (new)
260         b->new++;
261
262       int ct = 1;
263       while (from[ct])
264         {
265           c = mb_get();
266           if (c < 0)
267             goto done;
268           if (c != from[ct++])
269             ct = (c == '\n');
270         }
271     }
272
273  done:
274   close(mb_fd);
275 }
276
277 static void
278 scan(void)
279 {
280   debug("Searching for mailboxes...\n");
281   int changed = 0;
282   CLIST_FOR_EACH(struct mbox *, p, patterns)
283     {
284       glob_t g;
285       int err = glob(p->path, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
286       if (err && err != GLOB_NOMATCH)
287         die("Failed to glob %s: %m", p->path);
288       for (uns i=0; i<g.gl_pathc; i++)
289         {
290           char *name = g.gl_pathv[i];
291           struct mbox *b = find_mbox(&mboxes, name);
292           if (!b)
293             {
294               b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
295               if (find_mbox(&hilites, b->name))
296                 b->hilited = 1;
297               debug("Discovered mailbox %s (%s) hilited=%d\n", b->name, b->path, b->hilited);
298               b->scanning = -1;
299               changed = 1;
300             }
301           b->seen = 1;
302         }
303       globfree(&g);
304     }
305
306   num_mboxes = 0;
307   struct mbox *tmp;
308   CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
309     {
310       if (b->seen)
311         {
312           num_mboxes++;
313           b->seen = 0;
314         }
315       else
316         {
317           debug("Lost mailbox %s\n", b->name);
318           changed = 1;
319           del_mbox(b);
320         }
321     }
322
323   if (changed)
324     {
325       debug("Reallocating mailbox array...\n");
326       free(mbox_array);
327       mbox_array = xmalloc(sizeof(struct mbox *) * (num_mboxes+1));
328       int i = 0;
329       CLIST_FOR_EACH(struct mbox *, b, mboxes)
330         {
331           b->index = i;
332           mbox_array[i++] = b;
333         }
334       redraw_all();
335       refresh();
336     }
337
338   debug("Scanning mailboxes...\n");
339   CLIST_FOR_EACH(struct mbox *, b, mboxes)
340     {
341       struct stat st;
342       debug("%s: ", b->name);
343       if (force_refresh)
344         b->force_refresh = 1;
345       if (stat(b->path, &st) < 0)
346         {
347           b->total = b->new = -1;
348           debug("%m\n");
349         }
350       else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
351         {
352           b->scanning = 1;
353           redraw_line(b->index);
354           refresh();
355
356           scan_mbox(b, &st);
357           b->last_time = st.st_mtime;
358           b->last_size = st.st_size;
359           debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
360
361           b->scanning = 0;
362           redraw_line(b->index);
363           refresh();
364         }
365       else
366         debug("not changed\n");
367       b->force_refresh = 0;
368     }
369   force_refresh = 0;
370
371   debug("Scan finished\n");
372   last_scan_time = time(NULL);
373 }
374
375 static int cursor_at, cursor_max;
376
377 enum {
378   M_IDLE,
379   M_SCAN,
380   M_NEW,
381   M_BAD,
382   M_MAX
383 };
384 static int attrs[2][2][M_MAX];          // active, hilite, status
385
386 static void
387 redraw_line(int i)
388 {
389   move(i, 0);
390   if (i < cursor_max)
391     {
392       struct mbox *b = mbox_array[i];
393       int cc = (cursor_at == i);
394       int hi = b->hilited;
395
396       attrset(attrs[cc][hi][M_IDLE]);
397       if (cc)
398         printw("> ");
399       else
400         printw("  ");
401       if (b->new)
402         attrset(attrs[cc][hi][M_NEW]);
403       printw("%-20s ", b->name);
404       if (b->scanning < 0)
405         ;
406       else if (b->scanning)
407         {
408           attrset(attrs[cc][hi][M_SCAN]);
409           printw("[SCANNING]");
410         }
411       else if (b->total < 0)
412         {
413           attrset(attrs[cc][hi][M_BAD]);
414           printw("BROKEN");
415         }
416       else
417         {
418           attrset(attrs[cc][hi][M_IDLE]);
419           printw("%4d ", b->total);
420           if (b->new)
421             {
422               attrset(attrs[cc][hi][M_NEW]);
423               printw("%4d", b->new);
424             }
425         }
426       attrset(attrs[cc][hi][M_IDLE]);
427     }
428   else
429     attrset(attrs[0][0][M_IDLE]);
430   clrtoeol();
431 }
432
433 static void
434 redraw_all(void)
435 {
436   cursor_max = num_mboxes;
437   if (cursor_max > LINES-1)
438     cursor_max = LINES-1;
439   if (cursor_at >= cursor_max)
440     cursor_at = cursor_max - 1;
441   if (cursor_at < 0)
442     cursor_at = 0;
443
444   for (int i=0; i<cursor_max; i++)
445     redraw_line(i);
446   move(cursor_max, 0);
447   if (!cursor_max)
448     {
449       printw("(no mailboxes found)");
450       move(1, 0);
451     }
452   clrtobot();
453 }
454
455 static void
456 term_init(void)
457 {
458   initscr();
459   cbreak();
460   noecho();
461   nonl();
462   intrflush(stdscr, FALSE);
463   keypad(stdscr, TRUE);
464   curs_set(0);
465
466   static const int attrs_mono[2][M_MAX] = {
467     [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_BAD] = A_DIM },
468     [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_BAD] = A_DIM },
469   };
470   for (int i=0; i<2; i++)
471     for (int j=0; j<M_MAX; j++)
472       {
473         attrs[0][i][j] = attrs_mono[i][j];
474         attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
475       }
476
477   if (has_colors())
478     {
479       start_color();
480       if (COLOR_PAIRS >= 5)
481         {
482           init_pair(1, COLOR_YELLOW, COLOR_BLACK);
483           init_pair(2, COLOR_RED, COLOR_BLACK);
484           init_pair(3, COLOR_WHITE, COLOR_BLUE);
485           init_pair(4, COLOR_YELLOW, COLOR_BLUE);
486           init_pair(5, COLOR_RED, COLOR_BLUE);
487           static const int attrs_color[2][2][M_MAX] = {
488             [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_BAD] = COLOR_PAIR(2) },
489             [0][1] = { [M_IDLE] = A_BOLD, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1) | A_BOLD, [M_BAD] = COLOR_PAIR(2) | A_BOLD },
490             [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_BAD] = COLOR_PAIR(5) },
491             [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD, [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4) | A_BOLD, [M_BAD] = COLOR_PAIR(5) | A_BOLD },
492           };
493           memcpy(attrs, attrs_color, sizeof(attrs));
494         }
495     }
496   clear();
497 }
498
499 static void
500 term_cleanup(void)
501 {
502   endwin();
503 }
504
505 static void
506 scan_and_redraw(void)
507 {
508   move(LINES-1, 0);
509   printw("Busy...");
510   refresh();
511   scan();
512   redraw_all();
513   refresh();
514 }
515
516 static void
517 move_cursor(int i)
518 {
519   if (i >= 0 && i < cursor_max && i != cursor_at)
520     {
521       int old = cursor_at;
522       cursor_at = i;
523       redraw_line(old);
524       redraw_line(i);
525     }
526 }
527
528 static void
529 next_active(int since)
530 {
531   if (!cursor_max)
532     return;
533   since %= cursor_max;
534   for (int pass=1; pass >= 0; pass--)
535     {
536       int i = since;
537       do {
538         if (mbox_array[i]->hilited >= pass && mbox_array[i]->new)
539           {
540             move_cursor(i);
541             return;
542           }
543         i = (i+1) % cursor_max;
544       } while (i != since);
545     }
546 }
547
548 static void NONRET
549 usage(void)
550 {
551   fprintf(stderr, "Usage: cm [<options>] <mbox-patterns>\n\
552 \n\
553 Options:\n\
554 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
555 -d\t\t\tLog debug messages to stderr\n\
556 -h <mbox>\t\tHighlight and prefer the specified mailbox\n\
557 -i\t\t\tInclude user's INBOX\n\
558 -l\t\t\tLock mailboxes when scanning\n\
559 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
560 ");
561   exit(1);
562 }
563
564 int
565 main(int argc, char **argv)
566 {
567   clist_init(&mboxes);
568   clist_init(&hilites);
569   clist_init(&patterns);
570
571   int c;
572   while ((c = getopt(argc, argv, "c:dh:ilm:")) >= 0)
573     switch (c)
574       {
575       case 'c':
576         check_interval = atol(optarg);
577         if (check_interval <= 0)
578           usage();
579         break;
580       case 'd':
581         debug_mode++;
582         break;
583       case 'h':
584         add_mbox(&hilites, optarg, NULL);
585         break;
586       case 'i':
587         add_inbox(&patterns);
588         break;
589       case 'l':
590         lock_mboxes = 1;
591         break;
592       case 'm':
593         run_cmd = optarg;
594         break;
595       default:
596         usage();
597       }
598   while (optind < argc)
599     add_mbox(&patterns, argv[optind++], NULL);
600
601   term_init();
602   scan_and_redraw();
603   next_active(0);
604
605   int should_exit = 0;
606   while (!should_exit)
607     {
608       time_t now = time(NULL);
609       int remains = last_scan_time + check_interval - now;
610       if (remains <= 0 || force_refresh)
611         scan_and_redraw();
612       else
613         {
614           remains *= 10;
615           halfdelay((remains > 255) ? 255 : remains);
616           int ch = getch();
617           switch (ch)
618             {
619             case 'q':
620               should_exit = 1;
621               break;
622             case 'j':
623             case KEY_DOWN:
624               move_cursor(cursor_at+1);
625               break;
626             case 'k':
627             case KEY_UP:
628               move_cursor(cursor_at-1);
629               break;
630             case '0':
631             case KEY_HOME:
632             case KEY_PPAGE:
633               move_cursor(0);
634               break;
635             case '$':
636             case KEY_END:
637             case KEY_NPAGE:
638               move_cursor(cursor_max-1);
639               break;
640             case '\t':
641               next_active(cursor_at+1);
642               break;
643             case '\r':
644             case '\n':
645               if (cursor_at < cursor_max)
646                 {
647                   struct mbox *b = mbox_array[cursor_at];
648                   char cmd[strlen(run_cmd) + strlen(b->path) + 16];
649                   sprintf(cmd, run_cmd, b->path);
650                   term_cleanup();
651                   system(cmd);
652                   term_init();
653                   redraw_all();
654                   refresh();
655                   b->force_refresh = 1;
656                   scan_and_redraw();
657                 }
658               break;
659             case 'r' & 0x1f:
660               force_refresh = 1;
661               break;
662             }
663           refresh();
664         }
665     }
666
667   term_cleanup();
668   return 0;
669 }