2 * Incoming Mail Checker
4 * (c) 2005--2008 Martin Mares <mj@ucw.cz>
21 #include <sys/types.h>
24 #ifdef CONFIG_WIDE_CURSES
25 #include <ncursesw/ncurses.h>
34 static int check_interval = 30;
35 static int force_refresh;
36 static int allow_bells = 1;
37 static int minimum_priority;
38 static time_t last_scan_time;
39 static char *run_cmd = "mutt -f %s";
66 static clist options, patterns;
67 static struct options global_options = {
80 int last_size, last_pos;
81 int total, new, flagged;
82 int last_total, last_new, last_flagged;
90 static struct mbox **mbox_array;
91 static int num_mboxes, mbox_array_size;
93 static void redraw_line(int i);
94 static void rethink_display(void);
97 add_pattern(char *patt)
99 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
100 strcpy(n->pattern, patt);
101 if (patt = strchr(n->pattern, '='))
108 clist_add_tail(&patterns, &n->n);
114 struct passwd *p = getpwuid(getuid());
116 die("You don't exist, go away!");
117 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
118 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
123 init_options(struct options *o)
127 o->hide_if_empty = -1;
131 o->show_flagged = -1;
132 o->sender_personal = -1;
138 setup_options(struct mbox *b)
140 b->o = global_options;
141 CLIST_FOR_EACH(struct option_node *, n, options)
142 if (!fnmatch(n->pattern, b->name, 0))
144 debug("\tApplied options %s\n", n->pattern);
145 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
148 MERGE(hide_if_empty);
153 MERGE(sender_personal);
160 mbox_name(char *path)
162 char *c = strrchr(path, '/');
163 return c ? (c+1) : path;
167 add_mbox(clist *l, char *path, char *name)
169 struct mbox *b = xmalloc(sizeof(*b));
170 bzero(b, sizeof(*b));
171 b->path = xstrdup(path);
172 b->name = xstrdup(name);
176 cnode *prev = l->head.prev;
177 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
179 clist_insert_after(&b->n, prev);
182 clist_add_tail(l, &b->n);
188 del_mbox(struct mbox *b)
197 find_mbox(clist *l, char *path)
199 CLIST_FOR_EACH(struct mbox *, b, *l)
200 if (!strcmp(b->path, path))
206 mbox_active_p(struct mbox *b)
208 if (b->o.priority < minimum_priority)
216 mbox_visible_p(struct mbox *b)
218 if (!mbox_active_p(b))
222 if (b->o.hide_if_empty && !b->total)
228 prepare_snippet(struct mbox *b, char *sender, char *subject)
230 while (*sender == ' ' || *sender == '\t')
232 while (*subject == ' ' || *subject == '\t')
235 char *pos = b->snippet;
236 char *term = b->snippet + sizeof(b->snippet) - 1;
237 if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
239 add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
240 add_snippet(&pos, term, ": ");
243 add_subject_snippet(&pos, term, subject);
245 add_snippet(&pos, term, "No subject");
248 static int mb_fd, mb_pos;
249 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
254 mb_cc = mb_end = mb_buf;
261 lseek(mb_fd, pos, SEEK_SET);
268 return mb_pos - (mb_end - mb_cc);
274 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
283 mb_end = mb_buf + len;
292 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
303 mb_check(const char *p, int len)
307 if (mb_get() != *p++)
314 scan_mbox(struct mbox *b, struct stat *st)
316 char buf[1024], sender[1024], subject[1024];
319 const char from[] = "\nFrom ";
323 b->total = b->new = b->flagged = 0;
328 /* FIXME: Should we do some locking? */
330 mb_fd = open(b->path, O_RDONLY);
333 debug("[open failed: %m] ");
334 b->total = b->new = b->flagged = -1;
339 c = read(mb_fd, signature, 2);
340 lseek(mb_fd, 0, SEEK_SET);
342 if (c == 2 && !memcmp(signature, "\037\213", 2)) //gzip
344 debug("[decompressing] ");
347 die("pipe failed: %m");
350 die("fork failed: %m");
353 if (dup2(mb_fd, 0) < 0 || dup2(fds[1], 1) < 0)
354 die("dup2 failed: %m");
358 execlp("gzip", "gzip", "-cd", NULL);
359 die("Cannot execute gzip: %m");
369 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && !compressed)
371 mb_seek(b->last_pos);
372 if (mb_check(from, 6))
374 debug("[incremental] ");
379 debug("[incremental failed] ");
385 if (!mb_check(from+1, 5))
387 debug("[inconsistent] ");
388 b->total = b->new = b->flagged = -1;
391 b->total = b->new = b->flagged = 0;
392 b->last_total = b->last_new = b->last_flagged = 0;
393 b->snippet_is_new = 0;
397 b->total = b->last_total;
398 b->new = b->last_new;
399 b->flagged = b->last_flagged;
404 b->last_pos = mb_tell() - 5;
406 b->last_pos--; // last_pos should be the previous \n character
407 b->last_total = b->total;
408 b->last_new = b->new;
409 b->last_flagged = b->flagged;
410 while ((c = mb_get()) >= 0 && c != '\n')
425 debug("[truncated] ");
436 while (c == ' ' || c == '\t');
444 if (i < sizeof(buf) - 1)
450 if (!strncasecmp(buf, "Status:", 7))
452 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
454 else if (!strncasecmp(buf, "From:", 5))
455 strcpy(sender, buf+5);
456 else if (!strncasecmp(buf, "Subject:", 8))
457 strcpy(subject, buf+8);
465 if (new || (flagged && !b->snippet_is_new))
467 b->snippet_is_new = new;
468 prepare_snippet(b, sender, subject);
487 if (wait(&status) < 0 || !WIFEXITED(status) || WEXITSTATUS(status))
488 b->total = b->new = b->flagged = -1;
495 debug("Searching for mailboxes...\n");
496 last_scan_time = time(NULL);
497 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
499 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
501 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
502 if (err && err != GLOB_NOMATCH)
503 die("Failed to glob %s: %m", p->pattern);
504 for (uns i=0; i<g.gl_pathc; i++)
506 char *name = g.gl_pathv[i];
507 struct mbox *b = find_mbox(&mboxes, name);
510 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
511 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
521 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
527 debug("Lost mailbox %s\n", b->name);
534 debug("Scanning mailboxes...\n");
535 CLIST_FOR_EACH(struct mbox *, b, mboxes)
538 debug("%s: ", b->name);
539 if (!mbox_active_p(b))
545 b->force_refresh = 1;
546 if (stat(b->path, &st) < 0)
548 b->total = b->new = b->flagged = -1;
551 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
554 redraw_line(b->index);
558 b->last_time = st.st_mtime;
559 b->last_size = st.st_size;
560 debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
563 redraw_line(b->index);
567 debug("not changed\n");
568 b->force_refresh = 0;
572 debug("Scan finished\n");
573 last_scan_time = time(NULL);
577 static int cursor_at, cursor_max;
587 static int attrs[2][2][M_MAX]; // active, hilite, status
595 struct mbox *b = mbox_array[i];
596 int cc = (cursor_at == i);
597 int hi = b->o.highlight;
599 attrset(attrs[cc][hi][M_IDLE]);
601 printw("%c ", b->o.hotkey);
607 attrset(attrs[cc][hi][M_NEW]);
608 else if (b->flagged && b->o.show_flagged)
609 attrset(attrs[cc][hi][M_FLAG]);
610 printw("%-20s ", b->name);
613 else if (b->scanning)
615 attrset(attrs[cc][hi][M_SCAN]);
616 printw("[SCANNING]");
618 else if (b->total < 0)
620 attrset(attrs[cc][hi][M_BAD]);
625 attrset(attrs[cc][hi][M_IDLE]);
626 printw("%6d ", b->total);
630 attrset(attrs[cc][hi][M_NEW]);
631 printw("%6d ", b->new);
632 attrset(attrs[cc][hi][M_IDLE]);
633 int age = (last_scan_time - b->last_time);
637 printw("%2d min ", age/60);
638 else if (age < 86400)
639 printw("%2d hrs ", age/3600);
644 else if (b->flagged && b->o.show_flagged)
646 attrset(attrs[cc][hi][M_FLAG]);
647 printw("%6d ", b->flagged);
648 attrset(attrs[cc][hi][M_IDLE]);
650 attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */
653 if (snip && b->o.snippets && b->snippet[0])
656 getyx(stdscr, yy, xx);
657 int remains = COLS-1-xx;
660 #ifdef CONFIG_WIDE_CURSES
661 size_t len = strlen(b->snippet)+1;
663 mbstowcs(snip, b->snippet, len);
664 addnwstr(snip, remains);
666 printw("%-.*s", remains, b->snippet);
672 attrset(attrs[0][0][M_IDLE]);
679 cursor_max = num_mboxes;
680 if (cursor_max > LINES-1)
681 cursor_max = LINES-1;
682 if (cursor_at >= cursor_max)
683 cursor_at = cursor_max - 1;
687 for (int i=0; i<cursor_max; i++)
692 printw("(no mailboxes found)");
700 rethink_display(void)
705 CLIST_FOR_EACH(struct mbox *, b, mboxes)
706 if (mbox_visible_p(b))
709 if (i >= num_mboxes || mbox_array[i] != b)
712 if (i >= mbox_array_size)
714 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
715 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
719 if (b->o.beep && b->new > b->last_beep_new)
721 b->last_beep_new = b->new;
733 if (beeeep && allow_bells)
744 intrflush(stdscr, FALSE);
745 keypad(stdscr, TRUE);
748 static const int attrs_mono[2][M_MAX] = {
749 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
750 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
752 for (int i=0; i<2; i++)
753 for (int j=0; j<M_MAX; j++)
755 attrs[0][i][j] = attrs_mono[i][j];
756 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
762 if (COLOR_PAIRS >= 5)
764 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
765 init_pair(2, COLOR_RED, COLOR_BLACK);
766 init_pair(3, COLOR_WHITE, COLOR_BLUE);
767 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
768 init_pair(5, COLOR_RED, COLOR_BLUE);
769 init_pair(6, COLOR_GREEN, COLOR_BLACK);
770 init_pair(7, COLOR_GREEN, COLOR_BLUE);
771 static const int attrs_color[2][2][M_MAX] = {
772 [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) },
773 [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 },
774 [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) },
775 [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 },
777 memcpy(attrs, attrs_color, sizeof(attrs));
789 print_status(char *status)
793 printw("%s", status);
799 scan_and_redraw(void)
801 print_status("Busy...");
809 if (i >= 0 && i < cursor_max && i != cursor_at)
819 next_active(int since, int step)
823 since = (since+cursor_max) % cursor_max;
824 step = (step+cursor_max) % cursor_max;
830 struct mbox *b = mbox_array[i];
831 if (b->new && b->o.priority > bestp)
834 bestp = b->o.priority;
836 i = (i+step) % cursor_max;
844 mbox_run(struct mbox *b)
846 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
847 sprintf(cmd, run_cmd, b->path);
853 b->force_refresh = 1;
858 #define STR(c) STR2(c)
863 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
866 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
867 -d\t\t\tLog debug messages to stderr\n\
868 -i\t\t\tInclude user's INBOX\n\
869 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
870 -o <pattern>=<opts>\tSet mailbox options\n\
871 -o <opts>\t\tSet default options for all mailboxes\n\
872 -p <pri>\t\tSet minimum priority to show\n\
874 Mailbox options (set with `-o', use upper case to negate):\n\
875 0-9\t\t\tSet mailbox priority (0=default)\n\
876 b\t\t\tBeep when a message arrives\n\
877 e\t\t\tHide from display if empty\n\
878 f\t\t\tShow flagged messages if there are no new ones\n\
879 h\t\t\tHide from display\n\
880 m\t\t\tShow mailbox name of the sender\n\
881 p\t\t\tShow personal info (full name) of the sender\n\
882 s\t\t\tShow message snippets\n\
883 t\t\t\tHighlight the entry\n\
884 !<key>\t\t\tSet hot key\n\
886 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
887 It can be freely distributed and used according to the GNU GPL v2.\n\
893 parse_options(char *c)
897 if (sep = strchr(c, '='))
899 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
900 memcpy(n->pattern, c, sep-c);
901 n->pattern[sep-c] = 0;
902 clist_add_tail(&options, &n->n);
912 if (x >= '0' && x <= '9')
913 o->priority = x - '0';
914 else if (x == '!' && *c)
918 int value = !!islower(x);
925 o->hide_if_empty = value;
928 o->show_flagged = value;
934 o->sender_mbox = value;
937 o->sender_personal = value;
943 o->highlight = value;
946 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
953 main(int argc, char **argv)
956 clist_init(&options);
957 clist_init(&patterns);
960 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
964 check_interval = atol(optarg);
965 if (check_interval <= 0)
978 parse_options(optarg);
981 minimum_priority = atol(optarg);
986 while (optind < argc)
987 add_pattern(argv[optind++]);
998 time_t now = time(NULL);
999 int remains = last_scan_time + check_interval - now;
1000 if (remains <= 0 || force_refresh)
1005 halfdelay((remains > 255) ? 255 : remains);
1007 for (int i=0; i<num_mboxes; i++)
1008 if (ch == mbox_array[i]->o.hotkey)
1012 mbox_run(mbox_array[i]);
1022 move_cursor(cursor_at+1);
1026 move_cursor(cursor_at-1);
1036 move_cursor(cursor_max-1);
1039 next_active(cursor_at+1, 1);
1042 next_active(cursor_at-1, -1);
1046 if (cursor_at < cursor_max)
1047 mbox_run(mbox_array[cursor_at]);
1050 clearok(stdscr, TRUE);
1059 print_status("Bells and whistles are now enabled. Toot!");
1063 print_status("Bells and whistles are now disabled. Pssst!");
1066 if (ch >= '0' && ch <= '9')
1068 minimum_priority = ch - '0';
1072 debug("Pressed unknown key %d\n", ch);