+/*
+ * Incoming Mail Checker
+ *
+ * (c) 2005 Martin Mares <mj@ucw.cz>
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <pwd.h>
+#include <time.h>
+#include <curses.h>
+
+#include "util.h"
+#include "clists.h"
+
+static int check_interval = 30;
+static int force_refresh;
+static int lock_mboxes;
+static time_t last_scan_time;
+static char *run_cmd = "mutt -f %s";
+
+struct mbox {
+ cnode n;
+ int index;
+ int hilited;
+ int scanning;
+ int seen;
+ time_t last_time;
+ int last_size, last_pos;
+ char *name;
+ char *path;
+ int total, new;
+ int last_total, last_new;
+ int force_refresh;
+};
+
+static clist mboxes, hilites, patterns;
+static struct mbox **mbox_array;
+static int num_mboxes;
+
+static void redraw_line(int i);
+static void redraw_all(void);
+
+static char *
+mbox_name(char *path)
+{
+ char *c = strrchr(path, '/');
+ return c ? (c+1) : path;
+}
+
+static struct mbox *
+add_mbox(clist *l, char *path, char *name)
+{
+ struct mbox *b = xmalloc(sizeof(*b));
+ bzero(b, sizeof(*b));
+ b->path = xstrdup(path);
+ b->name = xstrdup(name);
+
+ if (name)
+ {
+ cnode *prev = l->head.prev;
+ while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
+ prev = prev->prev;
+ clist_insert_after(&b->n, prev);
+ }
+ else
+ clist_add_tail(l, &b->n);
+
+ return b;
+}
+
+static void
+del_mbox(struct mbox *b)
+{
+ clist_remove(&b->n);
+ free(b->path);
+ free(b->name);
+ free(b);
+}
+
+static struct mbox *
+find_mbox(clist *l, char *path)
+{
+ CLIST_FOR_EACH(struct mbox *, b, *l)
+ if (!strcmp(b->path, path))
+ return b;
+ return NULL;
+}
+
+static void
+add_inbox(clist *l)
+{
+ struct passwd *p = getpwuid(getuid());
+ if (!p)
+ die("You don't exist, go away!");
+ char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 1];
+ sprintf(buf, "/var/mail/%s", p->pw_name);
+ add_mbox(l, buf, "INBOX");
+}
+
+static int mb_fd, mb_pos;
+static unsigned char mb_buf[4096], *mb_cc, *mb_end;
+
+static void
+mb_reset(int pos)
+{
+ mb_cc = mb_end = mb_buf;
+ mb_pos = pos;
+}
+
+static int
+mb_seek(uns pos)
+{
+ lseek(mb_fd, pos, SEEK_SET);
+ mb_reset(pos);
+}
+
+static int
+mb_tell(void)
+{
+ return mb_pos - (mb_end - mb_cc);
+}
+
+static int
+mb_ll_get(void)
+{
+ int len = read(mb_fd, mb_buf, sizeof(mb_buf));
+ mb_cc = mb_buf;
+ if (len <= 0)
+ {
+ mb_end = mb_buf;
+ return -1;
+ }
+ else
+ {
+ mb_end = mb_buf + len;
+ mb_pos += len;
+ return *mb_cc++;
+ }
+}
+
+static inline int
+mb_get(void)
+{
+ return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
+}
+
+static int
+mb_check(const char *p, int len)
+{
+ while (len--)
+ {
+ if (mb_get() != *p++)
+ return 0;
+ }
+ return 1;
+}
+
+static void
+scan_mbox(struct mbox *b, struct stat *st)
+{
+ char buf[1024];
+ int c;
+ const char from[] = "\nFrom ";
+
+ if (!st->st_size)
+ {
+ b->total = b->new = 0;
+ b->last_pos = 0;
+ return;
+ }
+
+ /* FIXME: Locking! */
+
+ mb_fd = open(b->path, O_RDONLY);
+ if (mb_fd < 0)
+ {
+ debug("[open failed: %m] ");
+ b->total = b->new = -1;
+ return;
+ }
+ mb_reset(0);
+
+ int incremental = 0;
+ if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
+ {
+ mb_seek(b->last_pos);
+ if (mb_check(from, 6))
+ {
+ debug("[incremental] ");
+ incremental = 1;
+ }
+ else
+ {
+ debug("[incremental failed] ");
+ mb_seek(0);
+ }
+ }
+ if (!incremental)
+ {
+ if (!mb_check(from+1, 5))
+ {
+ debug("[inconsistent] ");
+ b->total = b->new = -1;
+ goto done;
+ }
+ b->total = b->new = 0;
+ b->last_total = b->last_new = 0;
+ }
+ else
+ {
+ b->total = b->last_total;
+ b->new = b->last_new;
+ }
+
+ for(;;)
+ {
+ b->last_pos = mb_tell() - 5;
+ if (b->last_pos)
+ b->last_pos--; // last_pos should be the previous \n character
+ b->last_total = b->total;
+ b->last_new = b->new;
+ while ((c = mb_get()) >= 0 && c != '\n')
+ ;
+
+ int new = 1;
+ for (;;)
+ {
+ uns i = 0;
+ for (;;)
+ {
+ c = mb_get();
+ if (c < 0)
+ {
+ debug("[truncated] ");
+ goto done;
+ }
+ if (c == '\n')
+ break;
+ if (c == '\r')
+ continue;
+ if (i < sizeof(buf) - 1)
+ buf[i++] = c;
+ }
+ buf[i] = 0;
+ if (!buf[0])
+ break;
+ if (!strncasecmp(buf, "Status:", 7))
+ new = 0;
+ }
+
+ b->total++;
+ if (new)
+ b->new++;
+
+ int ct = 1;
+ while (from[ct])
+ {
+ c = mb_get();
+ if (c < 0)
+ goto done;
+ if (c != from[ct++])
+ ct = (c == '\n');
+ }
+ }
+
+ done:
+ close(mb_fd);
+}
+
+static void
+scan(void)
+{
+ debug("Searching for mailboxes...\n");
+ int changed = 0;
+ CLIST_FOR_EACH(struct mbox *, p, patterns)
+ {
+ glob_t g;
+ int err = glob(p->path, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
+ if (err && err != GLOB_NOMATCH)
+ die("Failed to glob %s: %m", p->path);
+ for (uns i=0; i<g.gl_pathc; i++)
+ {
+ char *name = g.gl_pathv[i];
+ struct mbox *b = find_mbox(&mboxes, name);
+ if (!b)
+ {
+ b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
+ if (find_mbox(&hilites, b->name))
+ b->hilited = 1;
+ debug("Discovered mailbox %s (%s) hilited=%d\n", b->name, b->path, b->hilited);
+ b->scanning = -1;
+ changed = 1;
+ }
+ b->seen = 1;
+ }
+ globfree(&g);
+ }
+
+ num_mboxes = 0;
+ struct mbox *tmp;
+ CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
+ {
+ if (b->seen)
+ {
+ num_mboxes++;
+ b->seen = 0;
+ }
+ else
+ {
+ debug("Lost mailbox %s\n", b->name);
+ changed = 1;
+ del_mbox(b);
+ }
+ }
+
+ if (changed)
+ {
+ debug("Reallocating mailbox array...\n");
+ free(mbox_array);
+ mbox_array = xmalloc(sizeof(struct mbox *) * (num_mboxes+1));
+ int i = 0;
+ CLIST_FOR_EACH(struct mbox *, b, mboxes)
+ {
+ b->index = i;
+ mbox_array[i++] = b;
+ }
+ redraw_all();
+ refresh();
+ }
+
+ debug("Scanning mailboxes...\n");
+ CLIST_FOR_EACH(struct mbox *, b, mboxes)
+ {
+ struct stat st;
+ debug("%s: ", b->name);
+ if (force_refresh)
+ b->force_refresh = 1;
+ if (stat(b->path, &st) < 0)
+ {
+ b->total = b->new = -1;
+ debug("%m\n");
+ }
+ else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
+ {
+ b->scanning = 1;
+ redraw_line(b->index);
+ refresh();
+
+ scan_mbox(b, &st);
+ b->last_time = st.st_mtime;
+ b->last_size = st.st_size;
+ debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
+
+ b->scanning = 0;
+ redraw_line(b->index);
+ refresh();
+ }
+ else
+ debug("not changed\n");
+ b->force_refresh = 0;
+ }
+ force_refresh = 0;
+
+ debug("Scan finished\n");
+ last_scan_time = time(NULL);
+}
+
+static int cursor_at, cursor_max;
+
+enum {
+ M_IDLE,
+ M_SCAN,
+ M_NEW,
+ M_BAD,
+ M_MAX
+};
+static int attrs[2][2][M_MAX]; // active, hilite, status
+
+static void
+redraw_line(int i)
+{
+ move(i, 0);
+ if (i < cursor_max)
+ {
+ struct mbox *b = mbox_array[i];
+ int cc = (cursor_at == i);
+ int hi = b->hilited;
+
+ attrset(attrs[cc][hi][M_IDLE]);
+ if (cc)
+ printw("> ");
+ else
+ printw(" ");
+ if (b->new)
+ attrset(attrs[cc][hi][M_NEW]);
+ printw("%-20s ", b->name);
+ if (b->scanning < 0)
+ ;
+ else if (b->scanning)
+ {
+ attrset(attrs[cc][hi][M_SCAN]);
+ printw("[SCANNING]");
+ }
+ else if (b->total < 0)
+ {
+ attrset(attrs[cc][hi][M_BAD]);
+ printw("BROKEN");
+ }
+ else
+ {
+ attrset(attrs[cc][hi][M_IDLE]);
+ printw("%4d ", b->total);
+ if (b->new)
+ {
+ attrset(attrs[cc][hi][M_NEW]);
+ printw("%4d", b->new);
+ }
+ }
+ attrset(attrs[cc][hi][M_IDLE]);
+ }
+ else
+ attrset(attrs[0][0][M_IDLE]);
+ clrtoeol();
+}
+
+static void
+redraw_all(void)
+{
+ cursor_max = num_mboxes;
+ if (cursor_max > LINES-1)
+ cursor_max = LINES-1;
+ if (cursor_at >= cursor_max)
+ cursor_at = cursor_max - 1;
+ if (cursor_at < 0)
+ cursor_at = 0;
+
+ for (int i=0; i<cursor_max; i++)
+ redraw_line(i);
+ move(cursor_max, 0);
+ if (!cursor_max)
+ {
+ printw("(no mailboxes found)");
+ move(1, 0);
+ }
+ clrtobot();
+}
+
+static void
+term_init(void)
+{
+ initscr();
+ cbreak();
+ noecho();
+ nonl();
+ intrflush(stdscr, FALSE);
+ keypad(stdscr, TRUE);
+ curs_set(0);
+
+ static const int attrs_mono[2][M_MAX] = {
+ [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_BAD] = A_DIM },
+ [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_BAD] = A_DIM },
+ };
+ for (int i=0; i<2; i++)
+ for (int j=0; j<M_MAX; j++)
+ {
+ attrs[0][i][j] = attrs_mono[i][j];
+ attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
+ }
+
+ if (has_colors())
+ {
+ start_color();
+ if (COLOR_PAIRS >= 5)
+ {
+ init_pair(1, COLOR_YELLOW, COLOR_BLACK);
+ init_pair(2, COLOR_RED, COLOR_BLACK);
+ init_pair(3, COLOR_WHITE, COLOR_BLUE);
+ init_pair(4, COLOR_YELLOW, COLOR_BLUE);
+ init_pair(5, COLOR_RED, COLOR_BLUE);
+ static const int attrs_color[2][2][M_MAX] = {
+ [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_BAD] = COLOR_PAIR(2) },
+ [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 },
+ [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_BAD] = COLOR_PAIR(5) },
+ [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 },
+ };
+ memcpy(attrs, attrs_color, sizeof(attrs));
+ }
+ }
+ clear();
+}
+
+static void
+term_cleanup(void)
+{
+ endwin();
+}
+
+static void
+scan_and_redraw(void)
+{
+ move(LINES-1, 0);
+ printw("Busy...");
+ refresh();
+ scan();
+ redraw_all();
+ refresh();
+}
+
+static void
+move_cursor(int i)
+{
+ if (i >= 0 && i < cursor_max && i != cursor_at)
+ {
+ int old = cursor_at;
+ cursor_at = i;
+ redraw_line(old);
+ redraw_line(i);
+ }
+}
+
+static void
+next_active(int since)
+{
+ if (!cursor_max)
+ return;
+ since %= cursor_max;
+ for (int pass=1; pass >= 0; pass--)
+ {
+ int i = since;
+ do {
+ if (mbox_array[i]->hilited >= pass && mbox_array[i]->new)
+ {
+ move_cursor(i);
+ return;
+ }
+ i = (i+1) % cursor_max;
+ } while (i != since);
+ }
+}
+
+static void NONRET
+usage(void)
+{
+ fprintf(stderr, "Usage: cm [<options>] <mbox-patterns>\n\
+\n\
+Options:\n\
+-c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
+-d\t\t\tLog debug messages to stderr\n\
+-h <mbox>\t\tHighlight and prefer the specified mailbox\n\
+-i\t\t\tInclude user's INBOX\n\
+-l\t\t\tLock mailboxes when scanning\n\
+-m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
+");
+ exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+ clist_init(&mboxes);
+ clist_init(&hilites);
+ clist_init(&patterns);
+
+ int c;
+ while ((c = getopt(argc, argv, "c:dh:ilm:")) >= 0)
+ switch (c)
+ {
+ case 'c':
+ check_interval = atol(optarg);
+ if (check_interval <= 0)
+ usage();
+ break;
+ case 'd':
+ debug_mode++;
+ break;
+ case 'h':
+ add_mbox(&hilites, optarg, NULL);
+ break;
+ case 'i':
+ add_inbox(&patterns);
+ break;
+ case 'l':
+ lock_mboxes = 1;
+ break;
+ case 'm':
+ run_cmd = optarg;
+ break;
+ default:
+ usage();
+ }
+ while (optind < argc)
+ add_mbox(&patterns, argv[optind++], NULL);
+
+ term_init();
+ scan_and_redraw();
+ next_active(0);
+
+ int should_exit = 0;
+ while (!should_exit)
+ {
+ time_t now = time(NULL);
+ int remains = last_scan_time + check_interval - now;
+ if (remains <= 0 || force_refresh)
+ scan_and_redraw();
+ else
+ {
+ remains *= 10;
+ halfdelay((remains > 255) ? 255 : remains);
+ int ch = getch();
+ switch (ch)
+ {
+ case 'q':
+ should_exit = 1;
+ break;
+ case 'j':
+ case KEY_DOWN:
+ move_cursor(cursor_at+1);
+ break;
+ case 'k':
+ case KEY_UP:
+ move_cursor(cursor_at-1);
+ break;
+ case '0':
+ case KEY_HOME:
+ case KEY_PPAGE:
+ move_cursor(0);
+ break;
+ case '$':
+ case KEY_END:
+ case KEY_NPAGE:
+ move_cursor(cursor_max-1);
+ break;
+ case '\t':
+ next_active(cursor_at+1);
+ break;
+ case '\r':
+ case '\n':
+ if (cursor_at < cursor_max)
+ {
+ struct mbox *b = mbox_array[cursor_at];
+ char cmd[strlen(run_cmd) + strlen(b->path) + 16];
+ sprintf(cmd, run_cmd, b->path);
+ term_cleanup();
+ system(cmd);
+ term_init();
+ redraw_all();
+ refresh();
+ b->force_refresh = 1;
+ scan_and_redraw();
+ }
+ break;
+ case 'r' & 0x1f:
+ force_refresh = 1;
+ break;
+ }
+ refresh();
+ }
+ }
+
+ term_cleanup();
+ return 0;
+}