X-Git-Url: http://mj.ucw.cz/gitweb/?a=blobdiff_plain;f=cm.c;h=9bc036672c4139ee175a48b34239bf0b0698bc52;hb=8d4bc94b9732a8a8fb966fc61a5d6a97568c353d;hp=8b4e3e5ce18adc3fcb6e4079cfbac0fba7f09722;hpb=60687231eb923234756020bf9d9cccd20551fd90;p=checkmail.git diff --git a/cm.c b/cm.c index 8b4e3e5..9bc0366 100644 --- a/cm.c +++ b/cm.c @@ -1,51 +1,198 @@ /* * Incoming Mail Checker * - * (c) 2005 Martin Mares + * (c) 2005--2015 Martin Mares */ -#include -#include -#include -#include +#define _GNU_SOURCE + +#include +#include #include +#include +#include #include -#include -#include #include +#include +#include +#include +#include +#include +#include #include +#include + +#ifdef CONFIG_WIDE_CURSES +#include +#else #include +#endif #include "util.h" #include "clists.h" +#include "charset.h" static int check_interval = 30; static int force_refresh; -static int lock_mboxes; +static int allow_bells = 1; +static int allow_osd = 1; +static int minimum_priority; static time_t last_scan_time; static char *run_cmd = "mutt -f %s"; +static int simple_tab; + +struct options { + int priority; + int hide; + int hide_if_empty; + int highlight; + int beep; + int snippets; + int show_flagged; + int sender_personal; + int sender_mbox; + int hotkey; + int led; + int osd; + int unread_is_new; +}; + +struct option_node { + cnode n; + struct options o; + char pattern[1]; +}; + +struct pattern_node { + cnode n; + char *name; + char pattern[1]; +}; + +static clist options, patterns; +static struct options global_options = { + .sender_personal = 1 +}; + +#define MDIR_MAX_NAME_LEN 128 struct mbox { cnode n; + struct options o; + char *name; + char *path; int index; - int hilited; int scanning; int seen; time_t last_time; + time_t best_time; + time_t display_valid_until; int last_size, last_pos; - char *name; - char *path; - int total, new; - int last_total, last_new; + int total, new, flagged; + int last_total, last_new, last_flagged; + int last_beep_new; int force_refresh; + int best_is_new; + char sender_snippet[128]; + char subject_snippet[128]; + char mdir_best[MDIR_MAX_NAME_LEN]; }; -static clist mboxes, hilites, patterns; +static clist mboxes; static struct mbox **mbox_array; -static int num_mboxes; +static int num_mboxes, mbox_array_size; + +struct osd_opt_node { + cnode n; + char *val; + char key[1]; +}; + +static clist osd_opts; static void redraw_line(int i); -static void redraw_all(void); +static void rethink_display(int notify); + +static void +add_pattern(char *patt) +{ + struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt)); + strcpy(n->pattern, patt); + if (patt = strchr(n->pattern, '=')) + { + *patt++ = 0; + n->name = patt; + } + else + n->name = NULL; + clist_add_tail(&patterns, &n->n); +} + +static void +add_inbox(void) +{ + struct passwd *p = getpwuid(getuid()); + if (!p) + die("You don't exist, go away!"); + char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7]; + sprintf(buf, "/var/mail/%s=INBOX", p->pw_name); + add_pattern(buf); +} + +static void +init_options(struct options *o) +{ + o->priority = -1; + o->hide = -1; + o->hide_if_empty = -1; + o->beep = -1; + o->highlight = -1; + o->snippets = -1; + o->show_flagged = -1; + o->sender_personal = -1; + o->sender_mbox = -1; + o->hotkey = -1; + o->led = -1; + o->osd = -1; + o->unread_is_new = -1; +} + +static void +setup_options(struct mbox *b) +{ + b->o = global_options; + CLIST_FOR_EACH(struct option_node *, n, options) + if (!fnmatch(n->pattern, b->name, 0)) + { + debug("\tApplied options %s\n", n->pattern); +#define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f + MERGE(priority); + MERGE(hide); + MERGE(hide_if_empty); + MERGE(highlight); + MERGE(beep); + MERGE(snippets); + MERGE(show_flagged); + MERGE(sender_personal); + MERGE(sender_mbox); + MERGE(hotkey); + MERGE(led); + MERGE(osd); + MERGE(unread_is_new); + } +} + +static void +add_osd_opt(char *arg) +{ + struct osd_opt_node *n = xmalloc(sizeof(*n) + strlen(arg)); + strcpy(n->key, arg); + n->val = strchr(n->key, '='); + if (!n->val) + die("Malformed OSD option"); + *n->val++ = 0; + clist_add_tail(&osd_opts, &n->n); +} static char * mbox_name(char *path) @@ -93,18 +240,65 @@ find_mbox(clist *l, char *path) return NULL; } +static inline int +mbox_active_p(struct mbox *b) +{ + if (b->o.priority < minimum_priority) + return 0; + if (b->o.hide) + return 0; + return 1; +} + +static inline int +mbox_visible_p(struct mbox *b) +{ + if (!mbox_active_p(b)) + return 0; + if (b->scanning < 0) + return 1; + if (b->o.hide_if_empty && !b->total) + return 0; + return 1; +} + static void -add_inbox(clist *l) +prepare_snippets(struct mbox *b, char *sender, char *subject) { - 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"); + char *pos, *term; + + while (*sender == ' ' || *sender == '\t') + sender++; + while (*subject == ' ' || *subject == '\t') + subject++; + + pos = b->sender_snippet; + term = pos + sizeof(b->sender_snippet) - 1; + if (sender[0] && (b->o.sender_mbox || b->o.sender_personal)) + add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal); + else + *pos = 0; + + pos = b->subject_snippet; + term = pos + sizeof(b->subject_snippet) - 1; + if (subject[0]) + add_subject_snippet(&pos, term, subject); + else + add_snippet_raw(&pos, term, "No subject"); } -static int mb_fd, mb_pos; +static void +build_snippet(char *buf, char *term, struct mbox *b) +{ + if (b->sender_snippet[0]) + { + add_snippet(&buf, term, b->sender_snippet); + add_snippet_raw(&buf, term, ": "); + } + add_snippet(&buf, term, b->subject_snippet); +} + +static int mb_fd, mb_pos, mb_seekable; static unsigned char mb_buf[4096], *mb_cc, *mb_end; static void @@ -114,7 +308,7 @@ mb_reset(int pos) mb_pos = pos; } -static int +static void mb_seek(uns pos) { lseek(mb_fd, pos, SEEK_SET); @@ -151,6 +345,13 @@ mb_get(void) return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get(); } +static void +mb_unget(int c) +{ + if (c >= 0) + mb_cc--; +} + static int mb_check(const char *p, int len) { @@ -162,33 +363,133 @@ mb_check(const char *p, int len) return 1; } +static void +mb_skip(int len) +{ + while (len) + { + int avail = mb_end - mb_cc; + if (!avail) + { + if (mb_seekable && len >= (int) sizeof(mb_buf)) + { + int next = len - len % sizeof(mb_buf); + mb_seek(mb_tell() + next); + len -= next; + continue; + } + if (mb_ll_get() < 0) + return; + len--; + } + else + { + int next = (avail < len) ? avail : len; + len -= next; + mb_cc += next; + } + } +} + +#define HDR_BUF_SIZE 1024 + +static int +read_hdr_field(char *buf) +{ + uns i = 0; + for (;;) + { + int c = mb_get(); + if (c < 0) + { + debug("[truncated] "); + return -1; + } + if (c == '\n') + { + if (!i) + break; + int fold = -1; + do + { + fold++; + c = mb_get(); + } + while (c == ' ' || c == '\t'); + mb_unget(c); + if (!fold) + break; + c = ' '; + } + if (c == '\r') + continue; + if (i < HDR_BUF_SIZE - 1) + buf[i++] = c; + } + buf[i] = 0; + if (debug_mode > 2) + debug("Header: <%s>\n", buf); + return buf[0] ? 1 : 0; +} + static void scan_mbox(struct mbox *b, struct stat *st) { - char buf[1024]; + char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE]; int c; + int compressed = 0; const char from[] = "\nFrom "; + b->best_time = st->st_mtime; if (!st->st_size) { - b->total = b->new = 0; + b->total = b->new = b->flagged = 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; + b->total = b->new = b->flagged = -1; return; } + mb_seekable = 1; + + char signature[2]; + c = read(mb_fd, signature, 2); + lseek(mb_fd, 0, SEEK_SET); + + if (c == 2 && !memcmp(signature, "\037\213", 2)) //gzip + { + debug("[decompressing] "); + int fds[2]; + if (pipe(fds)) + die("pipe failed: %m"); + int pid = fork(); + if (pid < 0) + die("fork failed: %m"); + if (!pid) + { + if (dup2(mb_fd, 0) < 0 || dup2(fds[1], 1) < 0) + die("dup2 failed: %m"); + close(fds[0]); + close(fds[1]); + close(mb_fd); + execlp("gzip", "gzip", "-cd", NULL); + die("Cannot execute gzip: %m"); + } + close(fds[1]); + close(mb_fd); + mb_fd = fds[0]; + compressed = 1; + mb_seekable = 0; + } mb_reset(0); int incremental = 0; - if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh) + if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && mb_seekable) { mb_seek(b->last_pos); if (mb_check(from, 6)) @@ -207,16 +508,18 @@ scan_mbox(struct mbox *b, struct stat *st) if (!mb_check(from+1, 5)) { debug("[inconsistent] "); - b->total = b->new = -1; + b->total = b->new = b->flagged = -1; goto done; } - b->total = b->new = 0; - b->last_total = b->last_new = 0; + b->total = b->new = b->flagged = 0; + b->last_total = b->last_new = b->last_flagged = 0; + b->best_is_new = 0; } else { b->total = b->last_total; b->new = b->last_new; + b->flagged = b->last_flagged; } for(;;) @@ -226,38 +529,52 @@ scan_mbox(struct mbox *b, struct stat *st) b->last_pos--; // last_pos should be the previous \n character b->last_total = b->total; b->last_new = b->new; + b->last_flagged = b->flagged; while ((c = mb_get()) >= 0 && c != '\n') ; + int content_length = -1; int new = 1; + int flagged = 0; + sender[0] = 0; + subject[0] = 0; 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]) + int c = read_hdr_field(buf); + if (c < 0) + goto done; + if (!c) break; if (!strncasecmp(buf, "Status:", 7)) - new = 0; + { + if (!b->o.unread_is_new || strchr(buf + 7, 'R')) + new = 0; + } + else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F')) + flagged = 1; + else if (!strncasecmp(buf, "From:", 5)) + strcpy(sender, buf+5); + else if (!strncasecmp(buf, "Subject:", 8)) + strcpy(subject, buf+8); + else if (!strncasecmp(buf, "Content-Length:", 15)) + content_length = atoi(buf + 15); } b->total++; if (new) b->new++; + if (flagged) + b->flagged++; + if (debug_mode > 1) + debug("new=%d flagged=%d len=%d sender=<%s> subject=<%s>\n", new, flagged, content_length, sender, subject); + if (new || (flagged && !b->best_is_new)) + { + b->best_is_new = new; + prepare_snippets(b, sender, subject); + } + + if (content_length >= 0) + mb_skip(content_length); int ct = 1; while (from[ct]) @@ -272,19 +589,158 @@ scan_mbox(struct mbox *b, struct stat *st) done: close(mb_fd); + if (compressed) + { + int status; + if (wait(&status) < 0 || !WIFEXITED(status) || WEXITSTATUS(status)) + b->total = b->new = b->flagged = -1; + } +} + +static time_t +mdir_mtime(struct mbox *b) +{ + time_t mtime = 0; + char path[strlen(b->path) + 5]; + + for (int new=0; new<2; new++) + { + sprintf(path, "%s/%s", b->path, (new ? "new" : "cur")); + struct stat st; + if (stat(path, &st) < 0) + debug("[cannot stat %s] ", path); + else if (st.st_mtime > mtime) + mtime = st.st_mtime; + } + return mtime; +} + +static void +mdir_get_snippet(struct mbox *b) +{ + char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE]; + sender[0] = 0; + subject[0] = 0; + + char path[strlen(b->path) + MDIR_MAX_NAME_LEN]; + sprintf(path, "%s/%s", b->path, b->mdir_best); + mb_fd = open(path, O_RDONLY); + if (mb_fd < 0) + { + debug("[open failed: %m] "); + prepare_snippets(b, sender, subject); + return; + } + mb_seekable = 1; + mb_reset(0); + + while (read_hdr_field(buf) > 0) + { + if (!strncasecmp(buf, "From:", 5)) + strcpy(sender, buf+5); + else if (!strncasecmp(buf, "Subject:", 8)) + strcpy(subject, buf+8); + } + + close(mb_fd); + prepare_snippets(b, sender, subject); } static void -scan(void) +scan_mdir(struct mbox *b) { - debug("Searching for mailboxes...\n"); - int changed = 0; - CLIST_FOR_EACH(struct mbox *, p, patterns) + int dir_len = strlen(b->path); + char path[dir_len + MDIR_MAX_NAME_LEN]; + strcpy(path, b->path); + + b->total = b->new = b->flagged = 0; + b->best_time = 0; + b->best_is_new = 0; + + for (int new=0; new<2; new++) { + strcpy(path + dir_len, (new ? "/new" : "/cur")); + DIR *d = opendir(path); + if (!d) + { + debug("[cannot open %s: %m] "); + continue; + } + struct dirent *de; + while (de = readdir(d)) + { + char *name = de->d_name; + if (name[0] == '.' || strlen(name) + 10 > MDIR_MAX_NAME_LEN) + continue; + path[dir_len + 4] = '/'; + strcpy(path + dir_len + 5, name); + + char *colon = strchr(name, ':'); + int seen = 0; + int flagged = 0; + if (colon && colon[1] == '2' && colon[2] == ',') + { + // Another comma can separate extended flags (Dovecot extension) + for (int i=3; colon[i] && colon[i] != ','; i++) + switch (colon[i]) + { + case 'S': + seen = 1; + break; + case 'F': + flagged = 1; + break; + } + } + + int is_new = new; + if (b->o.unread_is_new && !seen) + is_new = 1; + + b->total++; + if (is_new) + b->new++; + if (flagged) + b->flagged++; + if (debug_mode > 1) + debug("%s: new=%d flagged=%d seen=%d\n", path, is_new, flagged, seen); + + if (is_new || flagged) + { + // Need to pick the best message to display + struct stat st; + if (stat(path, &st) < 0) + debug("[cannot stat %s: %m] ", path); + else if (is_new > b->best_is_new || is_new == b->best_is_new && st.st_mtime > b->best_time) + { + b->best_is_new = is_new; + b->best_time = st.st_mtime; + strcpy(b->mdir_best, path + dir_len + 1); + } + } + } + closedir(d); + } + + if (b->best_time && b->o.snippets) + { + debug("best <%s> ", b->mdir_best); + mdir_get_snippet(b); + } +} + +static void +scan(int notify) +{ + debug("Searching for mailboxes (notify=%d)...\n", notify); + last_scan_time = time(NULL); + CLIST_FOR_EACH(struct pattern_node *, p, patterns) + { + debug("Trying pattern %s (name %s)\n", p->pattern, p->name); glob_t g; - int err = glob(p->path, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g); + int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g); if (err && err != GLOB_NOMATCH) - die("Failed to glob %s: %m", p->path); + die("Failed to glob %s: %m", p->pattern); for (uns i=0; iname ? 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); + debug("Discovered mailbox %s (%s)\n", b->name, b->path); + setup_options(b); 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; - } + 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(); - } + rethink_display(0); 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 (!mbox_active_p(b)) + { + debug("inactive\n"); + continue; + } + b->force_refresh = force_refresh; if (stat(b->path, &st) < 0) { - b->total = b->new = -1; + b->total = b->new = b->flagged = -1; debug("%m\n"); + continue; + } + + time_t current_mtime; + int current_size; + int is_mdir; + if (S_ISREG(st.st_mode)) + { + // Regular mailbox + is_mdir = 0; + current_mtime = st.st_mtime; + current_size = st.st_size; + debug("[mbox] "); + } + else if (S_ISDIR(st.st_mode)) + { + // Maildir + is_mdir = 1; + current_mtime = mdir_mtime(b); + current_size = 0; + debug("[mdir] "); } - else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh) + else + { + debug("neither file nor directory\n"); + continue; + } + + if (!b->last_time || current_mtime != b->last_time || current_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); + if (is_mdir) + scan_mdir(b); + else + scan_mbox(b, &st); + b->last_time = current_mtime; + b->last_size = current_size; + debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size); b->scanning = 0; redraw_line(b->index); refresh(); + b->force_refresh = 0; + } + else if (b->display_valid_until <= last_scan_time) + { + debug("not changed, but needs redraw\n"); + redraw_line(b->index); } else debug("not changed\n"); - b->force_refresh = 0; } force_refresh = 0; debug("Scan finished\n"); last_scan_time = time(NULL); + rethink_display(notify); +} + +#ifdef CONFIG_X11 + +#include +#include + +static Display *x11_dpy; +static unsigned leds_care, leds_have, leds_want; + +static Atom osd_pty; +static unsigned osd_care; +#define OSD_MSG_SIZE 1024 +static char osd_last_msg[OSD_MSG_SIZE]; +static time_t osd_last_time; + +static void +x11_init(void) +{ + leds_care = (global_options.led >= 0 ? (1 << global_options.led) : 0); + osd_care = (global_options.osd >= 0); + CLIST_FOR_EACH(struct option_node *, o, options) + { + if (o->o.led > 0) + leds_care |= (1 << o->o.led); + if (o->o.osd > 0) + osd_care = 1; + } + + if (!leds_care && !osd_care) + { + debug("X11: No mailbox wants LEDs or OSD\n"); + return; + } + if (!getenv("DISPLAY")) + { + debug("X11: Do not have X display\n"); + return; + } + if (!(x11_dpy = XOpenDisplay(NULL))) + die("Cannot open X display, although the DISPLAY variable is set"); + + if (osd_care) + { + osd_pty = XInternAtom(x11_dpy, "OSD_QUEUE", False); + if (!osd_pty) + die("Cannot intern OSD_QUEUE atom"); + + // If OSD options contain no message, add one + int seen_msg = 0; + CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts) + if (!n->key[0]) + seen_msg = 1; + if (!seen_msg) + { + add_osd_opt("=%40f"); + add_osd_opt("=%40s"); + add_osd_opt("="); + add_osd_opt("=(and %m more)"); + } + } + + leds_have = ~0U; + debug("X11: Initialized\n"); +} + +static void +sync_leds(void) +{ + if (leds_want == leds_have) + return; + + debug("LEDS: have %02x, want %02x, care %02x\n", leds_have, leds_want, leds_care); + for (int i=1; i<10; i++) + if (leds_care & (leds_have ^ leds_want) & (1 << i)) + { + XKeyboardControl cc; + cc.led = i; + cc.led_mode = (leds_want & (1 << i)) ? LedModeOn : LedModeOff; + XChangeKeyboardControl(x11_dpy, KBLed | KBLedMode, &cc); + } + XFlush(x11_dpy); + leds_have = leds_want; +} + +static void +rethink_leds(void) +{ + if (!leds_care || !x11_dpy) + return; + + leds_want = 0; + CLIST_FOR_EACH(struct mbox *, b, mboxes) + if (b->o.led > 0 && b->new) + leds_want |= (1 << b->o.led); + sync_leds(); +} + +struct osd_params { + struct mbox *mbox; + int total_new; +}; + +static int +format_osd_string(char *dest, char *src, struct osd_params *par) +{ + char *stop = dest + OSD_MSG_SIZE - 1; + char numbuf[16]; + + while (*src && dest < stop) + { + if (*src == '%') + { + src++; + int size = 0; + while (*src >= '0' && *src <= '9') + size = 10*size + *src++ - '0'; + if (!size || size > stop-dest) + size = dest-stop; + + int spec = *src++; + if (!spec) + break; + + char *arg = numbuf; + switch (spec) + { + case 'f': + arg = par->mbox->sender_snippet; + break; + case 's': + arg = par->mbox->subject_snippet; + break; + case 'n': + snprintf(numbuf, sizeof(numbuf), "%d", par->total_new); + break; + case 'm': + if (par->total_new < 2) + return 0; + snprintf(numbuf, sizeof(numbuf), "%d", par->total_new - 1); + break; + case '%': + arg = "%"; + break; + default: + arg = "???"; + break; + } + + while (*arg && size) + { + *dest++ = *arg++; + size--; + } + } + else + *dest++ = *src++; + } + *dest = 0; + return 1; } +static void +format_osd(char *msg, struct osd_params *par) +{ + if (!par->mbox) + { + msg[0] = 0; + return; + } + + unsigned pos = 0; + unsigned have_text = 0; + CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts) + { + char buf[OSD_MSG_SIZE]; + if (!format_osd_string(buf, n->val, par)) + continue; + if (!n->key[0] && buf[0]) + have_text = 1; + pos += snprintf(msg+pos, OSD_MSG_SIZE-pos-1, "%s:%s\n", n->key, buf); + if (pos > OSD_MSG_SIZE-1) + { + pos = sprintf(msg, "OSD message too long!\n"); + break; + } + } + if (have_text) + msg[pos++] = '\n'; + else + pos = 0; + msg[pos] = 0; +} + +static void +debug_osd_msg(char *msg) +{ + if (!debug_mode) + return; + fprintf(stderr, "OSD: <"); + while (*msg) + { + fputc((*msg != '\n' ? *msg : '|'), stderr); + msg++; + } + fprintf(stderr, ">\n"); +} + +static void +rethink_osd(int notify) +{ + if (!osd_care || !x11_dpy || !allow_osd) + { + osd_last_msg[0] = 0; + return; + } + + struct osd_params p = { .mbox = NULL, .total_new = 0 }; + CLIST_FOR_EACH(struct mbox *, b, mboxes) + if (b->o.osd > 0) + { + p.total_new += b->new; + if (b->new && b->best_time > osd_last_time && (!p.mbox || p.mbox->o.priority < b->o.priority)) + p.mbox = b; + } + + char new_msg[OSD_MSG_SIZE]; + format_osd(new_msg, &p); + debug_osd_msg(new_msg); + if (strcmp(new_msg, osd_last_msg)) + { + strcpy(osd_last_msg, new_msg); + if (notify && new_msg[0]) + { + debug("OSD: Sending to daemon\n"); + XChangeProperty(x11_dpy, DefaultRootWindow(x11_dpy), osd_pty, XA_STRING, 8, PropModeAppend, (unsigned char *) new_msg, strlen(new_msg)); + XFlush(x11_dpy); + } + else + debug("OSD: No changes\n"); + osd_last_time = time(NULL); + } +} + +static void +x11_cleanup(void) +{ + if (!x11_dpy) + return; + + leds_want = 0; + sync_leds(); +} + +#else + +static void x11_init(void) { } +static void rethink_leds(void) { } +static void rethink_osd(int notify UNUSED) { } +static void x11_cleanup(void) { } + +#endif + static int cursor_at, cursor_max; +static unsigned is_active, is_pos; // incremental search +static char is_buf[64]; + enum { M_IDLE, M_SCAN, M_NEW, + M_FLAG, M_BAD, + M_INCSEARCH, M_MAX }; -static int attrs[2][2][M_MAX]; // active, hilite, status +static int attrs[3][2][M_MAX]; // active (2=incsearch), hilite, status static void redraw_line(int i) @@ -390,17 +1132,35 @@ redraw_line(int i) if (i < cursor_max) { struct mbox *b = mbox_array[i]; - int cc = (cursor_at == i); - int hi = b->hilited; + int cc = (cursor_at == i ? (is_active ? 2 : 1) : 0); + int hi = b->o.highlight; + unsigned namepos = 0; + unsigned namelen = strlen(b->name); + int valid = 3600; attrset(attrs[cc][hi][M_IDLE]); - if (cc) + if (b->o.hotkey) + printw("%c ", b->o.hotkey); + else if (cc) printw("> "); else printw(" "); + if (cc == 2) + { + attrset(attrs[cc][hi][M_INCSEARCH]); + for (namepos=0; namepos < is_pos && namepos < 20; namepos++) + addch(is_buf[namepos]); + } if (b->new) attrset(attrs[cc][hi][M_NEW]); - printw("%-20s ", b->name); + else if (b->flagged && b->o.show_flagged) + attrset(attrs[cc][hi][M_FLAG]); + else + attrset(attrs[cc][hi][M_IDLE]); + while (namepos < namelen) + addch(b->name[namepos++]); + while (namepos++ < 20) + addch(' '); if (b->scanning < 0) ; else if (b->scanning) @@ -416,17 +1176,62 @@ redraw_line(int i) else { attrset(attrs[cc][hi][M_IDLE]); - printw("%4d ", b->total); + printw("%6d ", b->total); + int snip = 0; if (b->new) { attrset(attrs[cc][hi][M_NEW]); - printw("%4d", b->new); + printw("%6d ", b->new); + attrset(attrs[cc][hi][M_IDLE]); + int age = (last_scan_time - b->best_time); + if (age < 0) + age = 0; + if (age < 3600) + { + printw("%2d min ", age/60); + valid = 60; + } + else if (age < 86400) + printw("%2d hr%c ", age/3600, (age >= 7200 ? 's' : ' ')); + else + printw(" "); + snip = 1; + } + else if (b->flagged && b->o.show_flagged) + { + attrset(attrs[cc][hi][M_FLAG]); + printw("%6d ", b->flagged); + attrset(attrs[cc][hi][M_IDLE]); + printw(" "); + attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */ + snip = 1; + } + if (snip && b->o.snippets) + { + int xx, yy; + getyx(stdscr, yy, xx); + int remains = COLS-1-xx; + (void) yy; + + char snip[256]; + build_snippet(snip, snip + sizeof(snip) - 1, b); + + if (snip[0] && remains > 2) + { +#ifdef CONFIG_WIDE_CURSES + size_t len = strlen(snip)+1; + wchar_t snip2[len]; + mbstowcs(snip2, snip, len); + addnwstr(snip2, remains); +#else + printw("%-.*s", remains, snip); +#endif + } } } - attrset(attrs[cc][hi][M_IDLE]); + b->display_valid_until = last_scan_time + valid; } - else - attrset(attrs[0][0][M_IDLE]); + attrset(attrs[0][0][M_IDLE]); clrtoeol(); } @@ -447,11 +1252,52 @@ redraw_all(void) if (!cursor_max) { printw("(no mailboxes found)"); + clrtoeol(); move(1, 0); } clrtobot(); } +static void +rethink_display(int notify) +{ + int i = 0; + int changed = 0; + int beeeep = 0; + CLIST_FOR_EACH(struct mbox *, b, mboxes) + if (mbox_visible_p(b)) + { + b->index = i; + if (i >= num_mboxes || mbox_array[i] != b) + { + changed = 1; + if (i >= mbox_array_size) + { + mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16); + mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size); + } + mbox_array[i] = b; + } + if (b->o.beep && b->new > b->last_beep_new) + beeeep = 1; + b->last_beep_new = b->new; + i++; + } + if (i != num_mboxes) + changed = 1; + num_mboxes = i; + + if (changed) + { + redraw_all(); + refresh(); + } + rethink_leds(); + rethink_osd(notify); + if (beeeep && allow_bells && notify) + beep(); +} + static void term_init(void) { @@ -464,36 +1310,81 @@ term_init(void) 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 }, + [0] = { [M_IDLE] = 0, + [M_SCAN] = A_BOLD, + [M_NEW] = A_BOLD, + [M_FLAG] = 0, + [M_BAD] = A_DIM, + [M_INCSEARCH] = A_REVERSE }, + [1] = { [M_IDLE] = 0, + [M_SCAN] = A_BOLD, + [M_NEW] = A_REVERSE | A_BOLD, + [M_FLAG] = A_REVERSE, + [M_BAD] = A_DIM, + [M_INCSEARCH] = A_REVERSE }, }; for (int i=0; i<2; i++) for (int j=0; j= 5) + if (COLOR_PAIRS >= 12) { 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 }, + init_pair(6, COLOR_GREEN, COLOR_BLACK); + init_pair(7, COLOR_GREEN, COLOR_BLUE); + init_pair(8, COLOR_WHITE, COLOR_MAGENTA); + init_pair(9, COLOR_YELLOW, COLOR_MAGENTA); + init_pair(10, COLOR_GREEN, COLOR_MAGENTA); + init_pair(11, COLOR_RED, COLOR_MAGENTA); + init_pair(12, COLOR_BLACK, COLOR_YELLOW); + static const int attrs_color[3][2][M_MAX] = { + [0][0] = { [M_IDLE] = 0, + [M_SCAN] = COLOR_PAIR(1), + [M_NEW] = COLOR_PAIR(1), + [M_FLAG] = COLOR_PAIR(6), + [M_BAD] = COLOR_PAIR(2) }, + [0][1] = { [M_IDLE] = A_BOLD, + [M_SCAN] = COLOR_PAIR(1), + [M_NEW] = COLOR_PAIR(1) | A_BOLD, + [M_FLAG] = COLOR_PAIR(6) | 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_FLAG] = COLOR_PAIR(7), + [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_FLAG] = COLOR_PAIR(7) | A_BOLD, + [M_BAD] = COLOR_PAIR(5) | A_BOLD }, + [2][0] = { [M_IDLE] = COLOR_PAIR(8), + [M_SCAN] = COLOR_PAIR(9), + [M_NEW] = COLOR_PAIR(9), + [M_FLAG] = COLOR_PAIR(10), + [M_BAD] = COLOR_PAIR(11), + [M_INCSEARCH] = COLOR_PAIR(12) | A_DIM }, + [2][1] = { [M_IDLE] = COLOR_PAIR(8) | A_BOLD, + [M_SCAN] = COLOR_PAIR(9), + [M_NEW] = COLOR_PAIR(9) | A_BOLD, + [M_FLAG] = COLOR_PAIR(10) | A_BOLD, + [M_BAD] = COLOR_PAIR(11) | A_BOLD, + [M_INCSEARCH] = COLOR_PAIR(12) }, }; memcpy(attrs, attrs_color, sizeof(attrs)); } } - clear(); } static void @@ -503,16 +1394,23 @@ term_cleanup(void) } static void -scan_and_redraw(void) +print_status(char *status) { move(LINES-1, 0); - printw("Busy..."); - refresh(); - scan(); - redraw_all(); + if (status) + printw("%s", status); + clrtoeol(); refresh(); } +static void +scan_and_redraw(int notify) +{ + print_status("Busy..."); + scan(notify); + print_status(NULL); +} + static void move_cursor(int i) { @@ -526,50 +1424,220 @@ move_cursor(int i) } static void -next_active(int since) +next_active(int since, int step) { if (!cursor_max) return; - since %= cursor_max; - for (int pass=1; pass >= 0; pass--) + since = (since+cursor_max) % cursor_max; + step = (step+cursor_max) % cursor_max; + int besti = -1; + int bestp = -1; + int i = since; + do { - 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); + struct mbox *b = mbox_array[i]; + if (simple_tab) + { + if (b->new) + { + besti = i; + break; + } + } + else + { + if (b->new && b->o.priority > bestp) + { + besti = i; + bestp = b->o.priority; + } + } + i = (i+step) % cursor_max; } + while (i != since); + if (besti >= 0) + move_cursor(besti); } +static void +mbox_run(struct mbox *b) +{ + 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(0); +} + +static void +enter_incsearch(void) +{ + print_status("Incremental search..."); + is_active = 1; + is_pos = 0; + redraw_line(cursor_at); +} + +static int +handle_incsearch(int ch) +{ + if ((ch == KEY_BACKSPACE || ch == KEY_DC) && is_pos) + --is_pos; + else if (ch >= ' ' && ch <= '~') + { + if (is_pos < sizeof(is_buf) - 1) + is_buf[is_pos++] = ch; + } + else + { + print_status(NULL); + is_active = 0; + is_pos = 0; + redraw_line(cursor_at); + return 0; + } + + is_buf[is_pos] = 0; + for (int i=0; iname, is_buf, is_pos)) + { + if (i != cursor_at) + { + move_cursor(i); + return 1; + } + break; + } + } + + redraw_line(cursor_at); + return 1; +} + +#define STR2(c) #c +#define STR(c) STR2(c) + static void NONRET usage(void) { - fprintf(stderr, "Usage: cm [] \n\ + fprintf(stderr, "Usage: cm [] [ | [=]] ...\n\ \n\ Options:\n\ -c \t\tScan mailboxes every seconds (default is 30)\n\ -d\t\t\tLog debug messages to stderr\n\ --h \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 \t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\ +-o =\tSet mailbox options\n\ +-o \t\tSet default options for all mailboxes\n\ +-p \t\tSet minimum priority to show\n\ +-s =\t\tSet on-screen display options (consult OSDD docs)\n\ +-t\t\t\tLet TAB select the next mailbox with new mail, no matter what priority it has\n\ +\n\ +Mailbox options (set with `-o', use upper case to negate):\n\ +0-9\t\t\tSet mailbox priority (0=default)\n\ +b\t\t\tBeep when a message arrives\n\ +d\t\t\tSend an on-screen-display message (requires OSDD)\n\ +e\t\t\tHide from display if empty\n\ +f\t\t\tShow flagged messages if there are no new ones\n\ +h\t\t\tHide from display\n\ +l\t\t\tLight a keyboard led (1-9) if running on X display\n\ +m\t\t\tShow mailbox name of the sender\n\ +o\t\t\tCount old, but unread messages as new\n\ +p\t\t\tShow personal info (full name) of the sender\n\ +s\t\t\tShow message snippets\n\ +t\t\t\tHighlight the entry\n\ +!\t\t\tSet hot key\n\ +\n\ +CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares \n\ +It can be freely distributed and used according to the GNU GPL v2.\n\ "); exit(1); } +static void +parse_options(char *c) +{ + struct options *o; + char *sep; + if (sep = strchr(c, '=')) + { + struct option_node *n = xmalloc(sizeof(*n) + sep-c); + memcpy(n->pattern, c, sep-c); + n->pattern[sep-c] = 0; + clist_add_tail(&options, &n->n); + o = &n->o; + init_options(o); + c = sep+1; + } + else + o = &global_options; + + int x; + while (x = *c++) + if (x >= '0' && x <= '9') + o->priority = x - '0'; + else if (x == '!' && *c) + o->hotkey = *c++; + else if (x == 'l' && *c >= '1' && *c <= '9') + o->led = *c++ - '0'; + else + { + int value = !!islower(x); + switch (tolower(x)) + { + case 'b': + o->beep = value; + break; + case 'd': + o->osd = value; + break; + case 'e': + o->hide_if_empty = value; + break; + case 'f': + o->show_flagged = value; + break; + case 'h': + o->hide = value; + break; + case 'm': + o->sender_mbox = value; + break; + case 'o': + o->unread_is_new = value; + break; + case 'p': + o->sender_personal = value; + break; + case 's': + o->snippets = value; + break; + case 't': + o->highlight = value; + break; + default: + fprintf(stderr, "Invalid mailbox option `%c'\n", x); + usage(); + } + } +} + int main(int argc, char **argv) { clist_init(&mboxes); - clist_init(&hilites); + clist_init(&options); clist_init(&patterns); + clist_init(&osd_opts); int c; - while ((c = getopt(argc, argv, "c:dh:ilm:")) >= 0) + while ((c = getopt(argc, argv, "c:dim:o:p:s:t")) >= 0) switch (c) { case 'c': @@ -580,40 +1648,66 @@ main(int argc, char **argv) case 'd': debug_mode++; break; - case 'h': - add_mbox(&hilites, optarg, NULL); - break; case 'i': - add_inbox(&patterns); - break; - case 'l': - lock_mboxes = 1; + add_inbox(); break; case 'm': run_cmd = optarg; break; + case 'o': + parse_options(optarg); + break; + case 'p': + minimum_priority = atol(optarg); + break; + case 's': + add_osd_opt(optarg); + break; + case 't': + simple_tab = 1; + break; default: usage(); } while (optind < argc) - add_mbox(&patterns, argv[optind++], NULL); + add_pattern(argv[optind++]); + charset_init(); term_init(); - scan_and_redraw(); - next_active(0); + x11_init(); + scan_and_redraw(0); + next_active(0, 1); int should_exit = 0; +restart: while (!should_exit) { time_t now = time(NULL); int remains = last_scan_time + check_interval - now; - if (remains <= 0 || force_refresh) - scan_and_redraw(); + if (force_refresh) + scan_and_redraw(0); + if (remains <= 0) + scan_and_redraw(1); else { remains *= 10; halfdelay((remains > 255) ? 255 : remains); int ch = getch(); + if (ch < 0) + continue; + if (is_active && handle_incsearch(ch)) + { + refresh(); + continue; + } + for (int i=0; io.hotkey) + { + if (i < cursor_max) + cursor_at = i; + mbox_run(mbox_array[i]); + goto restart; + } switch (ch) { case 'q': @@ -627,7 +1721,7 @@ main(int argc, char **argv) case KEY_UP: move_cursor(cursor_at-1); break; - case '0': + case '^': case KEY_HOME: case KEY_PPAGE: move_cursor(0); @@ -638,32 +1732,57 @@ main(int argc, char **argv) move_cursor(cursor_max-1); break; case '\t': - next_active(cursor_at+1); + next_active(cursor_at+1, 1); + break; + case '`': + next_active(cursor_at-1, -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(); - } + mbox_run(mbox_array[cursor_at]); + break; + case 'l' & 0x1f: + clearok(stdscr, TRUE); + redraw_all(); + refresh(); break; case 'r' & 0x1f: force_refresh = 1; break; + case 'b': + allow_bells = 1; + print_status("Bells and whistles are now enabled. Toot!"); + break; + case 'B': + allow_bells = 0; + print_status("Bells and whistles are now disabled. Pssst!"); + break; + case 'd': + allow_osd = 1; + print_status("On-screen display is now enabled."); + break; + case 'D': + allow_osd = 0; + print_status("On-screen display is now disabled. Watch your step."); + break; + case '/': + enter_incsearch(); + break; + default: + if (ch >= '0' && ch <= '9') + { + minimum_priority = ch - '0'; + scan_and_redraw(0); + } + else + debug("Pressed unknown key %d\n", ch); } refresh(); } } + x11_cleanup(); term_cleanup(); return 0; }