]> mj.ucw.cz Git - checkmail.git/blob - cm.c
09dce992e942ea22dbc6bf48e1ef663ac5b833dd
[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   last_scan_time = time(NULL);
282   int changed = 0;
283   CLIST_FOR_EACH(struct mbox *, p, patterns)
284     {
285       glob_t g;
286       int err = glob(p->path, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
287       if (err && err != GLOB_NOMATCH)
288         die("Failed to glob %s: %m", p->path);
289       for (uns i=0; i<g.gl_pathc; i++)
290         {
291           char *name = g.gl_pathv[i];
292           struct mbox *b = find_mbox(&mboxes, name);
293           if (!b)
294             {
295               b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
296               if (find_mbox(&hilites, b->name))
297                 b->hilited = 1;
298               debug("Discovered mailbox %s (%s) hilited=%d\n", b->name, b->path, b->hilited);
299               b->scanning = -1;
300               changed = 1;
301             }
302           b->seen = 1;
303         }
304       globfree(&g);
305     }
306
307   num_mboxes = 0;
308   struct mbox *tmp;
309   CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
310     {
311       if (b->seen)
312         {
313           num_mboxes++;
314           b->seen = 0;
315         }
316       else
317         {
318           debug("Lost mailbox %s\n", b->name);
319           changed = 1;
320           del_mbox(b);
321         }
322     }
323
324   if (changed)
325     {
326       debug("Reallocating mailbox array...\n");
327       free(mbox_array);
328       mbox_array = xmalloc(sizeof(struct mbox *) * (num_mboxes+1));
329       int i = 0;
330       CLIST_FOR_EACH(struct mbox *, b, mboxes)
331         {
332           b->index = i;
333           mbox_array[i++] = b;
334         }
335       redraw_all();
336       refresh();
337     }
338
339   debug("Scanning mailboxes...\n");
340   CLIST_FOR_EACH(struct mbox *, b, mboxes)
341     {
342       struct stat st;
343       debug("%s: ", b->name);
344       if (force_refresh)
345         b->force_refresh = 1;
346       if (stat(b->path, &st) < 0)
347         {
348           b->total = b->new = -1;
349           debug("%m\n");
350         }
351       else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
352         {
353           b->scanning = 1;
354           redraw_line(b->index);
355           refresh();
356
357           scan_mbox(b, &st);
358           b->last_time = st.st_mtime;
359           b->last_size = st.st_size;
360           debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
361
362           b->scanning = 0;
363           redraw_line(b->index);
364           refresh();
365         }
366       else
367         debug("not changed\n");
368       b->force_refresh = 0;
369     }
370   force_refresh = 0;
371
372   debug("Scan finished\n");
373   last_scan_time = time(NULL);
374 }
375
376 static int cursor_at, cursor_max;
377
378 enum {
379   M_IDLE,
380   M_SCAN,
381   M_NEW,
382   M_BAD,
383   M_MAX
384 };
385 static int attrs[2][2][M_MAX];          // active, hilite, status
386
387 static void
388 redraw_line(int i)
389 {
390   move(i, 0);
391   if (i < cursor_max)
392     {
393       struct mbox *b = mbox_array[i];
394       int cc = (cursor_at == i);
395       int hi = b->hilited;
396
397       attrset(attrs[cc][hi][M_IDLE]);
398       if (cc)
399         printw("> ");
400       else
401         printw("  ");
402       if (b->new)
403         attrset(attrs[cc][hi][M_NEW]);
404       printw("%-20s ", b->name);
405       if (b->scanning < 0)
406         ;
407       else if (b->scanning)
408         {
409           attrset(attrs[cc][hi][M_SCAN]);
410           printw("[SCANNING]");
411         }
412       else if (b->total < 0)
413         {
414           attrset(attrs[cc][hi][M_BAD]);
415           printw("BROKEN");
416         }
417       else
418         {
419           attrset(attrs[cc][hi][M_IDLE]);
420           printw("%6d ", b->total);
421           if (b->new)
422             {
423               attrset(attrs[cc][hi][M_NEW]);
424               printw("%6d  ", b->new);
425               attrset(attrs[cc][hi][M_IDLE]);
426               int age = (last_scan_time - b->last_time);
427               if (age < 0)
428                 age = 0;
429               if (age < 3600)
430                 printw("%2d min", age/60);
431               else if (age < 86400)
432                 printw("%2d hrs", age/3600);
433             }
434         }
435       attrset(attrs[cc][hi][M_IDLE]);
436     }
437   else
438     attrset(attrs[0][0][M_IDLE]);
439   clrtoeol();
440 }
441
442 static void
443 redraw_all(void)
444 {
445   cursor_max = num_mboxes;
446   if (cursor_max > LINES-1)
447     cursor_max = LINES-1;
448   if (cursor_at >= cursor_max)
449     cursor_at = cursor_max - 1;
450   if (cursor_at < 0)
451     cursor_at = 0;
452
453   for (int i=0; i<cursor_max; i++)
454     redraw_line(i);
455   move(cursor_max, 0);
456   if (!cursor_max)
457     {
458       printw("(no mailboxes found)");
459       move(1, 0);
460     }
461   clrtobot();
462 }
463
464 static void
465 term_init(void)
466 {
467   initscr();
468   cbreak();
469   noecho();
470   nonl();
471   intrflush(stdscr, FALSE);
472   keypad(stdscr, TRUE);
473   curs_set(0);
474
475   static const int attrs_mono[2][M_MAX] = {
476     [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_BAD] = A_DIM },
477     [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_BAD] = A_DIM },
478   };
479   for (int i=0; i<2; i++)
480     for (int j=0; j<M_MAX; j++)
481       {
482         attrs[0][i][j] = attrs_mono[i][j];
483         attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
484       }
485
486   if (has_colors())
487     {
488       start_color();
489       if (COLOR_PAIRS >= 5)
490         {
491           init_pair(1, COLOR_YELLOW, COLOR_BLACK);
492           init_pair(2, COLOR_RED, COLOR_BLACK);
493           init_pair(3, COLOR_WHITE, COLOR_BLUE);
494           init_pair(4, COLOR_YELLOW, COLOR_BLUE);
495           init_pair(5, COLOR_RED, COLOR_BLUE);
496           static const int attrs_color[2][2][M_MAX] = {
497             [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_BAD] = COLOR_PAIR(2) },
498             [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 },
499             [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_BAD] = COLOR_PAIR(5) },
500             [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 },
501           };
502           memcpy(attrs, attrs_color, sizeof(attrs));
503         }
504     }
505   clear();
506 }
507
508 static void
509 term_cleanup(void)
510 {
511   endwin();
512 }
513
514 static void
515 scan_and_redraw(void)
516 {
517   move(LINES-1, 0);
518   printw("Busy...");
519   refresh();
520   scan();
521   redraw_all();
522   refresh();
523 }
524
525 static void
526 move_cursor(int i)
527 {
528   if (i >= 0 && i < cursor_max && i != cursor_at)
529     {
530       int old = cursor_at;
531       cursor_at = i;
532       redraw_line(old);
533       redraw_line(i);
534     }
535 }
536
537 static void
538 next_active(int since)
539 {
540   if (!cursor_max)
541     return;
542   since %= cursor_max;
543   for (int pass=1; pass >= 0; pass--)
544     {
545       int i = since;
546       do {
547         if (mbox_array[i]->hilited >= pass && mbox_array[i]->new)
548           {
549             move_cursor(i);
550             return;
551           }
552         i = (i+1) % cursor_max;
553       } while (i != since);
554     }
555 }
556
557 static void NONRET
558 usage(void)
559 {
560   fprintf(stderr, "Usage: cm [<options>] <mbox-patterns>\n\
561 \n\
562 Options:\n\
563 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
564 -d\t\t\tLog debug messages to stderr\n\
565 -h <mbox>\t\tHighlight and prefer the specified mailbox\n\
566 -i\t\t\tInclude user's INBOX\n\
567 -l\t\t\tLock mailboxes when scanning\n\
568 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
569 \n\
570 CheckMail 0.1, (c) 2005 Martin Mares <mj@ucw.cz>\n\
571 It can be freely distributed and used according to the GNU GPL v2.\n\
572 ");
573   exit(1);
574 }
575
576 int
577 main(int argc, char **argv)
578 {
579   clist_init(&mboxes);
580   clist_init(&hilites);
581   clist_init(&patterns);
582
583   int c;
584   while ((c = getopt(argc, argv, "c:dh:ilm:")) >= 0)
585     switch (c)
586       {
587       case 'c':
588         check_interval = atol(optarg);
589         if (check_interval <= 0)
590           usage();
591         break;
592       case 'd':
593         debug_mode++;
594         break;
595       case 'h':
596         add_mbox(&hilites, optarg, NULL);
597         break;
598       case 'i':
599         add_inbox(&patterns);
600         break;
601       case 'l':
602         lock_mboxes = 1;
603         break;
604       case 'm':
605         run_cmd = optarg;
606         break;
607       default:
608         usage();
609       }
610   while (optind < argc)
611     add_mbox(&patterns, argv[optind++], NULL);
612
613   term_init();
614   scan_and_redraw();
615   next_active(0);
616
617   int should_exit = 0;
618   while (!should_exit)
619     {
620       time_t now = time(NULL);
621       int remains = last_scan_time + check_interval - now;
622       if (remains <= 0 || force_refresh)
623         scan_and_redraw();
624       else
625         {
626           remains *= 10;
627           halfdelay((remains > 255) ? 255 : remains);
628           int ch = getch();
629           switch (ch)
630             {
631             case 'q':
632               should_exit = 1;
633               break;
634             case 'j':
635             case KEY_DOWN:
636               move_cursor(cursor_at+1);
637               break;
638             case 'k':
639             case KEY_UP:
640               move_cursor(cursor_at-1);
641               break;
642             case '0':
643             case KEY_HOME:
644             case KEY_PPAGE:
645               move_cursor(0);
646               break;
647             case '$':
648             case KEY_END:
649             case KEY_NPAGE:
650               move_cursor(cursor_max-1);
651               break;
652             case '\t':
653               next_active(cursor_at+1);
654               break;
655             case '\r':
656             case '\n':
657               if (cursor_at < cursor_max)
658                 {
659                   struct mbox *b = mbox_array[cursor_at];
660                   char cmd[strlen(run_cmd) + strlen(b->path) + 16];
661                   sprintf(cmd, run_cmd, b->path);
662                   term_cleanup();
663                   system(cmd);
664                   term_init();
665                   redraw_all();
666                   refresh();
667                   b->force_refresh = 1;
668                   scan_and_redraw();
669                 }
670               break;
671             case 'r' & 0x1f:
672               force_refresh = 1;
673               break;
674             }
675           refresh();
676         }
677     }
678
679   term_cleanup();
680   return 0;
681 }