2 * Incoming Mail Checker
4 * (c) 2005--2007 Martin Mares <mj@ucw.cz>
25 static int check_interval = 30;
26 static int force_refresh;
27 static int allow_bells = 1;
28 static int minimum_priority;
29 static time_t last_scan_time;
30 static char *run_cmd = "mutt -f %s";
54 static clist options, patterns;
55 static struct options global_options;
66 int last_size, last_pos;
67 int total, new, flagged;
68 int last_total, last_new;
76 static struct mbox **mbox_array;
77 static int num_mboxes, mbox_array_size;
79 static void redraw_line(int i);
80 static void rethink_display(void);
83 add_pattern(char *patt)
85 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
86 strcpy(n->pattern, patt);
87 if (patt = strchr(n->pattern, '='))
94 clist_add_tail(&patterns, &n->n);
100 struct passwd *p = getpwuid(getuid());
102 die("You don't exist, go away!");
103 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
104 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
109 init_options(struct options *o)
113 o->hide_if_empty = -1;
117 o->show_flagged = -1;
121 setup_options(struct mbox *b)
123 b->o = global_options;
124 CLIST_FOR_EACH(struct option_node *, n, options)
125 if (!fnmatch(n->pattern, b->name, 0))
127 debug("\tApplied options %s\n", n->pattern);
128 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
131 MERGE(hide_if_empty);
140 mbox_name(char *path)
142 char *c = strrchr(path, '/');
143 return c ? (c+1) : path;
147 add_mbox(clist *l, char *path, char *name)
149 struct mbox *b = xmalloc(sizeof(*b));
150 bzero(b, sizeof(*b));
151 b->path = xstrdup(path);
152 b->name = xstrdup(name);
156 cnode *prev = l->head.prev;
157 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
159 clist_insert_after(&b->n, prev);
162 clist_add_tail(l, &b->n);
168 del_mbox(struct mbox *b)
177 find_mbox(clist *l, char *path)
179 CLIST_FOR_EACH(struct mbox *, b, *l)
180 if (!strcmp(b->path, path))
186 mbox_active_p(struct mbox *b)
188 if (b->o.priority < minimum_priority)
196 mbox_visible_p(struct mbox *b)
198 if (!mbox_active_p(b))
202 if (b->o.hide_if_empty && !b->total)
208 do_add_snippet(char **ppos, char *term, unsigned char *add)
212 while (*add && pos < term)
220 else if (*add >= 0x7f)
237 add_snippet(char **ppos, char *term, unsigned char *add)
240 char *buf = xmalloc(strlen(add) + 1);
242 rfc2047_decode(&buf);
243 do_add_snippet(ppos, term, buf);
246 do_add_snippet(ppos, term, add);
251 prepare_snippet(struct mbox *b, char *sender, char *subject)
253 while (*sender == ' ' || *sender == '\t')
255 while (*subject == ' ' || *subject == '\t')
258 char *pos = b->snippet;
259 char *term = b->snippet + sizeof(b->snippet) - 1;
262 add_snippet(&pos, term, sender);
263 add_snippet(&pos, term, ": ");
266 add_snippet(&pos, term, subject);
268 add_snippet(&pos, term, "No subject");
271 static int mb_fd, mb_pos;
272 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
277 mb_cc = mb_end = mb_buf;
284 lseek(mb_fd, pos, SEEK_SET);
291 return mb_pos - (mb_end - mb_cc);
297 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
306 mb_end = mb_buf + len;
315 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
326 mb_check(const char *p, int len)
330 if (mb_get() != *p++)
337 scan_mbox(struct mbox *b, struct stat *st)
339 char buf[1024], sender[1024], subject[1024];
341 const char from[] = "\nFrom ";
345 b->total = b->new = 0;
350 /* FIXME: Locking! */
352 mb_fd = open(b->path, O_RDONLY);
355 debug("[open failed: %m] ");
356 b->total = b->new = -1;
362 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
364 mb_seek(b->last_pos);
365 if (mb_check(from, 6))
367 debug("[incremental] ");
372 debug("[incremental failed] ");
378 if (!mb_check(from+1, 5))
380 debug("[inconsistent] ");
381 b->total = b->new = -1;
384 b->total = b->new = 0;
385 b->last_total = b->last_new = 0;
386 b->snippet_is_new = 0;
390 b->total = b->last_total;
391 b->new = b->last_new;
396 b->last_pos = mb_tell() - 5;
398 b->last_pos--; // last_pos should be the previous \n character
399 b->last_total = b->total;
400 b->last_new = b->new;
401 while ((c = mb_get()) >= 0 && c != '\n')
416 debug("[truncated] ");
427 while (c == ' ' || c == '\t');
435 if (i < sizeof(buf) - 1)
441 if (!strncasecmp(buf, "Status:", 7))
443 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
445 else if (!strncasecmp(buf, "From:", 5))
446 strcpy(sender, buf+5);
447 else if (!strncasecmp(buf, "Subject:", 8))
448 strcpy(subject, buf+8);
456 if (new || (flagged && !b->snippet_is_new))
458 b->snippet_is_new = new;
459 prepare_snippet(b, sender, subject);
480 debug("Searching for mailboxes...\n");
481 last_scan_time = time(NULL);
482 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
484 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
486 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
487 if (err && err != GLOB_NOMATCH)
488 die("Failed to glob %s: %m", p->pattern);
489 for (uns i=0; i<g.gl_pathc; i++)
491 char *name = g.gl_pathv[i];
492 struct mbox *b = find_mbox(&mboxes, name);
495 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
496 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
506 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
512 debug("Lost mailbox %s\n", b->name);
519 debug("Scanning mailboxes...\n");
520 CLIST_FOR_EACH(struct mbox *, b, mboxes)
523 debug("%s: ", b->name);
524 if (!mbox_active_p(b))
530 b->force_refresh = 1;
531 if (stat(b->path, &st) < 0)
533 b->total = b->new = -1;
536 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
539 redraw_line(b->index);
543 b->last_time = st.st_mtime;
544 b->last_size = st.st_size;
545 debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
548 redraw_line(b->index);
552 debug("not changed\n");
553 b->force_refresh = 0;
557 debug("Scan finished\n");
558 last_scan_time = time(NULL);
562 static int cursor_at, cursor_max;
572 static int attrs[2][2][M_MAX]; // active, hilite, status
580 struct mbox *b = mbox_array[i];
581 int cc = (cursor_at == i);
582 int hi = b->o.highlight;
584 attrset(attrs[cc][hi][M_IDLE]);
590 attrset(attrs[cc][hi][M_NEW]);
592 attrset(attrs[cc][hi][M_FLAG]);
593 printw("%-20s ", b->name);
596 else if (b->scanning)
598 attrset(attrs[cc][hi][M_SCAN]);
599 printw("[SCANNING]");
601 else if (b->total < 0)
603 attrset(attrs[cc][hi][M_BAD]);
608 attrset(attrs[cc][hi][M_IDLE]);
609 printw("%6d ", b->total);
613 attrset(attrs[cc][hi][M_NEW]);
614 printw("%6d ", b->new);
615 attrset(attrs[cc][hi][M_IDLE]);
616 int age = (last_scan_time - b->last_time);
620 printw("%2d min ", age/60);
621 else if (age < 86400)
622 printw("%2d hrs ", age/3600);
629 attrset(attrs[cc][hi][M_FLAG]);
630 printw("%6d ", b->flagged);
631 attrset(attrs[cc][hi][M_IDLE]);
633 snip = b->o.show_flagged;
635 if (snip && b->o.snippets && b->snippet[0])
638 getyx(stdscr, yy, xx);
639 int remains = COLS-1-xx;
641 printw("%-.*s", remains, b->snippet);
645 attrset(attrs[0][0][M_IDLE]);
652 cursor_max = num_mboxes;
653 if (cursor_max > LINES-1)
654 cursor_max = LINES-1;
655 if (cursor_at >= cursor_max)
656 cursor_at = cursor_max - 1;
660 for (int i=0; i<cursor_max; i++)
665 printw("(no mailboxes found)");
673 rethink_display(void)
678 CLIST_FOR_EACH(struct mbox *, b, mboxes)
679 if (mbox_visible_p(b))
682 if (i >= num_mboxes || mbox_array[i] != b)
685 if (i >= mbox_array_size)
687 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
688 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
692 if (b->o.beep && b->new > b->last_beep_new)
694 b->last_beep_new = b->new;
706 if (beeeep && allow_bells)
717 intrflush(stdscr, FALSE);
718 keypad(stdscr, TRUE);
721 static const int attrs_mono[2][M_MAX] = {
722 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
723 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
725 for (int i=0; i<2; i++)
726 for (int j=0; j<M_MAX; j++)
728 attrs[0][i][j] = attrs_mono[i][j];
729 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
735 if (COLOR_PAIRS >= 5)
737 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
738 init_pair(2, COLOR_RED, COLOR_BLACK);
739 init_pair(3, COLOR_WHITE, COLOR_BLUE);
740 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
741 init_pair(5, COLOR_RED, COLOR_BLUE);
742 init_pair(6, COLOR_GREEN, COLOR_BLACK);
743 init_pair(7, COLOR_GREEN, COLOR_BLUE);
744 static const int attrs_color[2][2][M_MAX] = {
745 [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) },
746 [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 },
747 [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) },
748 [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 },
750 memcpy(attrs, attrs_color, sizeof(attrs));
762 print_status(char *status)
766 printw("%s", status);
772 scan_and_redraw(void)
774 print_status("Busy...");
782 if (i >= 0 && i < cursor_max && i != cursor_at)
792 next_active(int since, int step)
796 since = (since+cursor_max) % cursor_max;
797 step = (step+cursor_max) % cursor_max;
803 struct mbox *b = mbox_array[i];
804 if (b->new && b->o.priority > bestp)
807 bestp = b->o.priority;
809 i = (i+step) % cursor_max;
817 #define STR(c) STR2(c)
822 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
825 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
826 -d\t\t\tLog debug messages to stderr\n\
827 -i\t\t\tInclude user's INBOX\n\
828 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
829 -o <pattern>=<opts>\tSet mailbox options\n\
830 -o <opts>\t\tSet default options for all mailboxes\n\
831 -p <pri>\t\tSet minimum priority to show\n\
833 Mailbox options (set with `-o', use upper case to negate):\n\
834 0-9\t\t\tSet mailbox priority (0=default)\n\
835 b\t\t\tBeep when a message arrives\n\
836 e\t\t\tHide from display if empty\n\
837 f\t\t\tShow flagged messages if there are no new ones\n\
838 h\t\t\tHide from display\n\
839 s\t\t\tShow message snippets\n\
840 t\t\t\tHighlight the entry\n\
842 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
843 It can be freely distributed and used according to the GNU GPL v2.\n\
849 parse_options(char *c)
853 if (sep = strchr(c, '='))
855 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
856 memcpy(n->pattern, c, sep-c);
857 n->pattern[sep-c] = 0;
858 clist_add_tail(&options, &n->n);
868 if (x >= '0' && x <= '9')
869 o->priority = x - '0';
872 int value = !!islower(x);
879 o->hide_if_empty = value;
882 o->show_flagged = value;
891 o->highlight = value;
894 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
901 main(int argc, char **argv)
904 clist_init(&options);
905 clist_init(&patterns);
908 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
912 check_interval = atol(optarg);
913 if (check_interval <= 0)
926 parse_options(optarg);
929 minimum_priority = atol(optarg);
934 while (optind < argc)
935 add_pattern(argv[optind++]);
945 time_t now = time(NULL);
946 int remains = last_scan_time + check_interval - now;
947 if (remains <= 0 || force_refresh)
952 halfdelay((remains > 255) ? 255 : remains);
961 move_cursor(cursor_at+1);
965 move_cursor(cursor_at-1);
975 move_cursor(cursor_max-1);
978 next_active(cursor_at+1, 1);
981 next_active(cursor_at-1, -1);
985 if (cursor_at < cursor_max)
987 struct mbox *b = mbox_array[cursor_at];
988 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
989 sprintf(cmd, run_cmd, b->path);
995 b->force_refresh = 1;
1000 clearok(stdscr, TRUE);
1009 print_status("Bells and whistles are now enabled. Toot!");
1013 print_status("Bells and whistles are now disabled. Pssst!");
1016 if (ch >= '0' && ch <= '9')
1018 minimum_priority = ch - '0';
1022 debug("Pressed unknown key %d\n", ch);