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";
56 static clist options, patterns;
57 static struct options global_options = {
70 int last_size, last_pos;
71 int total, new, flagged;
72 int last_total, last_new, last_flagged;
80 static struct mbox **mbox_array;
81 static int num_mboxes, mbox_array_size;
83 static void redraw_line(int i);
84 static void rethink_display(void);
87 add_pattern(char *patt)
89 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
90 strcpy(n->pattern, patt);
91 if (patt = strchr(n->pattern, '='))
98 clist_add_tail(&patterns, &n->n);
104 struct passwd *p = getpwuid(getuid());
106 die("You don't exist, go away!");
107 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
108 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
113 init_options(struct options *o)
117 o->hide_if_empty = -1;
121 o->show_flagged = -1;
122 o->sender_personal = -1;
127 setup_options(struct mbox *b)
129 b->o = global_options;
130 CLIST_FOR_EACH(struct option_node *, n, options)
131 if (!fnmatch(n->pattern, b->name, 0))
133 debug("\tApplied options %s\n", n->pattern);
134 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
137 MERGE(hide_if_empty);
142 MERGE(sender_personal);
148 mbox_name(char *path)
150 char *c = strrchr(path, '/');
151 return c ? (c+1) : path;
155 add_mbox(clist *l, char *path, char *name)
157 struct mbox *b = xmalloc(sizeof(*b));
158 bzero(b, sizeof(*b));
159 b->path = xstrdup(path);
160 b->name = xstrdup(name);
164 cnode *prev = l->head.prev;
165 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
167 clist_insert_after(&b->n, prev);
170 clist_add_tail(l, &b->n);
176 del_mbox(struct mbox *b)
185 find_mbox(clist *l, char *path)
187 CLIST_FOR_EACH(struct mbox *, b, *l)
188 if (!strcmp(b->path, path))
194 mbox_active_p(struct mbox *b)
196 if (b->o.priority < minimum_priority)
204 mbox_visible_p(struct mbox *b)
206 if (!mbox_active_p(b))
210 if (b->o.hide_if_empty && !b->total)
216 prepare_snippet(struct mbox *b, char *sender, char *subject)
218 while (*sender == ' ' || *sender == '\t')
220 while (*subject == ' ' || *subject == '\t')
223 char *pos = b->snippet;
224 char *term = b->snippet + sizeof(b->snippet) - 1;
225 if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
227 add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
228 add_snippet(&pos, term, ": ");
231 add_subject_snippet(&pos, term, subject);
233 add_snippet(&pos, term, "No subject");
236 static int mb_fd, mb_pos;
237 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
242 mb_cc = mb_end = mb_buf;
249 lseek(mb_fd, pos, SEEK_SET);
256 return mb_pos - (mb_end - mb_cc);
262 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
271 mb_end = mb_buf + len;
280 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
291 mb_check(const char *p, int len)
295 if (mb_get() != *p++)
302 scan_mbox(struct mbox *b, struct stat *st)
304 char buf[1024], sender[1024], subject[1024];
306 const char from[] = "\nFrom ";
310 b->total = b->new = b->flagged = 0;
315 /* FIXME: Locking! */
317 mb_fd = open(b->path, O_RDONLY);
320 debug("[open failed: %m] ");
321 b->total = b->new = b->flagged = -1;
327 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
329 mb_seek(b->last_pos);
330 if (mb_check(from, 6))
332 debug("[incremental] ");
337 debug("[incremental failed] ");
343 if (!mb_check(from+1, 5))
345 debug("[inconsistent] ");
346 b->total = b->new = b->flagged = -1;
349 b->total = b->new = b->flagged = 0;
350 b->last_total = b->last_new = b->last_flagged = 0;
351 b->snippet_is_new = 0;
355 b->total = b->last_total;
356 b->new = b->last_new;
357 b->flagged = b->last_flagged;
362 b->last_pos = mb_tell() - 5;
364 b->last_pos--; // last_pos should be the previous \n character
365 b->last_total = b->total;
366 b->last_new = b->new;
367 b->last_flagged = b->flagged;
368 while ((c = mb_get()) >= 0 && c != '\n')
383 debug("[truncated] ");
394 while (c == ' ' || c == '\t');
402 if (i < sizeof(buf) - 1)
408 if (!strncasecmp(buf, "Status:", 7))
410 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
412 else if (!strncasecmp(buf, "From:", 5))
413 strcpy(sender, buf+5);
414 else if (!strncasecmp(buf, "Subject:", 8))
415 strcpy(subject, buf+8);
423 if (new || (flagged && !b->snippet_is_new))
425 b->snippet_is_new = new;
426 prepare_snippet(b, sender, subject);
447 debug("Searching for mailboxes...\n");
448 last_scan_time = time(NULL);
449 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
451 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
453 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
454 if (err && err != GLOB_NOMATCH)
455 die("Failed to glob %s: %m", p->pattern);
456 for (uns i=0; i<g.gl_pathc; i++)
458 char *name = g.gl_pathv[i];
459 struct mbox *b = find_mbox(&mboxes, name);
462 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
463 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
473 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
479 debug("Lost mailbox %s\n", b->name);
486 debug("Scanning mailboxes...\n");
487 CLIST_FOR_EACH(struct mbox *, b, mboxes)
490 debug("%s: ", b->name);
491 if (!mbox_active_p(b))
497 b->force_refresh = 1;
498 if (stat(b->path, &st) < 0)
500 b->total = b->new = b->flagged = -1;
503 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
506 redraw_line(b->index);
510 b->last_time = st.st_mtime;
511 b->last_size = st.st_size;
512 debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
515 redraw_line(b->index);
519 debug("not changed\n");
520 b->force_refresh = 0;
524 debug("Scan finished\n");
525 last_scan_time = time(NULL);
529 static int cursor_at, cursor_max;
539 static int attrs[2][2][M_MAX]; // active, hilite, status
547 struct mbox *b = mbox_array[i];
548 int cc = (cursor_at == i);
549 int hi = b->o.highlight;
551 attrset(attrs[cc][hi][M_IDLE]);
557 attrset(attrs[cc][hi][M_NEW]);
559 attrset(attrs[cc][hi][M_FLAG]);
560 printw("%-20s ", b->name);
563 else if (b->scanning)
565 attrset(attrs[cc][hi][M_SCAN]);
566 printw("[SCANNING]");
568 else if (b->total < 0)
570 attrset(attrs[cc][hi][M_BAD]);
575 attrset(attrs[cc][hi][M_IDLE]);
576 printw("%6d ", b->total);
580 attrset(attrs[cc][hi][M_NEW]);
581 printw("%6d ", b->new);
582 attrset(attrs[cc][hi][M_IDLE]);
583 int age = (last_scan_time - b->last_time);
587 printw("%2d min ", age/60);
588 else if (age < 86400)
589 printw("%2d hrs ", age/3600);
596 attrset(attrs[cc][hi][M_FLAG]);
597 printw("%6d ", b->flagged);
598 attrset(attrs[cc][hi][M_IDLE]);
600 attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */
601 snip = b->o.show_flagged;
603 if (snip && b->o.snippets && b->snippet[0])
606 getyx(stdscr, yy, xx);
607 int remains = COLS-1-xx;
609 printw("%-.*s", remains, b->snippet);
613 attrset(attrs[0][0][M_IDLE]);
620 cursor_max = num_mboxes;
621 if (cursor_max > LINES-1)
622 cursor_max = LINES-1;
623 if (cursor_at >= cursor_max)
624 cursor_at = cursor_max - 1;
628 for (int i=0; i<cursor_max; i++)
633 printw("(no mailboxes found)");
641 rethink_display(void)
646 CLIST_FOR_EACH(struct mbox *, b, mboxes)
647 if (mbox_visible_p(b))
650 if (i >= num_mboxes || mbox_array[i] != b)
653 if (i >= mbox_array_size)
655 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
656 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
660 if (b->o.beep && b->new > b->last_beep_new)
662 b->last_beep_new = b->new;
674 if (beeeep && allow_bells)
685 intrflush(stdscr, FALSE);
686 keypad(stdscr, TRUE);
689 static const int attrs_mono[2][M_MAX] = {
690 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
691 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
693 for (int i=0; i<2; i++)
694 for (int j=0; j<M_MAX; j++)
696 attrs[0][i][j] = attrs_mono[i][j];
697 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
703 if (COLOR_PAIRS >= 5)
705 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
706 init_pair(2, COLOR_RED, COLOR_BLACK);
707 init_pair(3, COLOR_WHITE, COLOR_BLUE);
708 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
709 init_pair(5, COLOR_RED, COLOR_BLUE);
710 init_pair(6, COLOR_GREEN, COLOR_BLACK);
711 init_pair(7, COLOR_GREEN, COLOR_BLUE);
712 static const int attrs_color[2][2][M_MAX] = {
713 [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) },
714 [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 },
715 [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) },
716 [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 },
718 memcpy(attrs, attrs_color, sizeof(attrs));
730 print_status(char *status)
734 printw("%s", status);
740 scan_and_redraw(void)
742 print_status("Busy...");
750 if (i >= 0 && i < cursor_max && i != cursor_at)
760 next_active(int since, int step)
764 since = (since+cursor_max) % cursor_max;
765 step = (step+cursor_max) % cursor_max;
771 struct mbox *b = mbox_array[i];
772 if (b->new && b->o.priority > bestp)
775 bestp = b->o.priority;
777 i = (i+step) % cursor_max;
785 #define STR(c) STR2(c)
790 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
793 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
794 -d\t\t\tLog debug messages to stderr\n\
795 -i\t\t\tInclude user's INBOX\n\
796 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
797 -o <pattern>=<opts>\tSet mailbox options\n\
798 -o <opts>\t\tSet default options for all mailboxes\n\
799 -p <pri>\t\tSet minimum priority to show\n\
801 Mailbox options (set with `-o', use upper case to negate):\n\
802 0-9\t\t\tSet mailbox priority (0=default)\n\
803 b\t\t\tBeep when a message arrives\n\
804 e\t\t\tHide from display if empty\n\
805 f\t\t\tShow flagged messages if there are no new ones\n\
806 h\t\t\tHide from display\n\
807 m\t\t\tShow mailbox name of the sender\n\
808 p\t\t\tShow personal info (full name) of the sender\n\
809 s\t\t\tShow message snippets\n\
810 t\t\t\tHighlight the entry\n\
812 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
813 It can be freely distributed and used according to the GNU GPL v2.\n\
819 parse_options(char *c)
823 if (sep = strchr(c, '='))
825 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
826 memcpy(n->pattern, c, sep-c);
827 n->pattern[sep-c] = 0;
828 clist_add_tail(&options, &n->n);
838 if (x >= '0' && x <= '9')
839 o->priority = x - '0';
842 int value = !!islower(x);
849 o->hide_if_empty = value;
852 o->show_flagged = value;
858 o->sender_mbox = value;
861 o->sender_personal = value;
867 o->highlight = value;
870 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
877 main(int argc, char **argv)
880 clist_init(&options);
881 clist_init(&patterns);
884 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
888 check_interval = atol(optarg);
889 if (check_interval <= 0)
902 parse_options(optarg);
905 minimum_priority = atol(optarg);
910 while (optind < argc)
911 add_pattern(argv[optind++]);
921 time_t now = time(NULL);
922 int remains = last_scan_time + check_interval - now;
923 if (remains <= 0 || force_refresh)
928 halfdelay((remains > 255) ? 255 : remains);
937 move_cursor(cursor_at+1);
941 move_cursor(cursor_at-1);
951 move_cursor(cursor_max-1);
954 next_active(cursor_at+1, 1);
957 next_active(cursor_at-1, -1);
961 if (cursor_at < cursor_max)
963 struct mbox *b = mbox_array[cursor_at];
964 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
965 sprintf(cmd, run_cmd, b->path);
971 b->force_refresh = 1;
976 clearok(stdscr, TRUE);
985 print_status("Bells and whistles are now enabled. Toot!");
989 print_status("Bells and whistles are now disabled. Pssst!");
992 if (ch >= '0' && ch <= '9')
994 minimum_priority = ch - '0';
998 debug("Pressed unknown key %d\n", ch);