2 * Incoming Mail Checker
4 * (c) 2005--2008 Martin Mares <mj@ucw.cz>
22 #ifdef CONFIG_WIDE_CURSES
23 #include <ncursesw/ncurses.h>
32 static int check_interval = 30;
33 static int force_refresh;
34 static int allow_bells = 1;
35 static int minimum_priority;
36 static time_t last_scan_time;
37 static char *run_cmd = "mutt -f %s";
64 static clist options, patterns;
65 static struct options global_options = {
78 int last_size, last_pos;
79 int total, new, flagged;
80 int last_total, last_new, last_flagged;
88 static struct mbox **mbox_array;
89 static int num_mboxes, mbox_array_size;
91 static void redraw_line(int i);
92 static void rethink_display(void);
95 add_pattern(char *patt)
97 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
98 strcpy(n->pattern, patt);
99 if (patt = strchr(n->pattern, '='))
106 clist_add_tail(&patterns, &n->n);
112 struct passwd *p = getpwuid(getuid());
114 die("You don't exist, go away!");
115 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
116 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
121 init_options(struct options *o)
125 o->hide_if_empty = -1;
129 o->show_flagged = -1;
130 o->sender_personal = -1;
136 setup_options(struct mbox *b)
138 b->o = global_options;
139 CLIST_FOR_EACH(struct option_node *, n, options)
140 if (!fnmatch(n->pattern, b->name, 0))
142 debug("\tApplied options %s\n", n->pattern);
143 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
146 MERGE(hide_if_empty);
151 MERGE(sender_personal);
158 mbox_name(char *path)
160 char *c = strrchr(path, '/');
161 return c ? (c+1) : path;
165 add_mbox(clist *l, char *path, char *name)
167 struct mbox *b = xmalloc(sizeof(*b));
168 bzero(b, sizeof(*b));
169 b->path = xstrdup(path);
170 b->name = xstrdup(name);
174 cnode *prev = l->head.prev;
175 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
177 clist_insert_after(&b->n, prev);
180 clist_add_tail(l, &b->n);
186 del_mbox(struct mbox *b)
195 find_mbox(clist *l, char *path)
197 CLIST_FOR_EACH(struct mbox *, b, *l)
198 if (!strcmp(b->path, path))
204 mbox_active_p(struct mbox *b)
206 if (b->o.priority < minimum_priority)
214 mbox_visible_p(struct mbox *b)
216 if (!mbox_active_p(b))
220 if (b->o.hide_if_empty && !b->total)
226 prepare_snippet(struct mbox *b, char *sender, char *subject)
228 while (*sender == ' ' || *sender == '\t')
230 while (*subject == ' ' || *subject == '\t')
233 char *pos = b->snippet;
234 char *term = b->snippet + sizeof(b->snippet) - 1;
235 if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
237 add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
238 add_snippet(&pos, term, ": ");
241 add_subject_snippet(&pos, term, subject);
243 add_snippet(&pos, term, "No subject");
246 static int mb_fd, mb_pos;
247 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
252 mb_cc = mb_end = mb_buf;
259 lseek(mb_fd, pos, SEEK_SET);
266 return mb_pos - (mb_end - mb_cc);
272 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
281 mb_end = mb_buf + len;
290 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
301 mb_check(const char *p, int len)
305 if (mb_get() != *p++)
312 scan_mbox(struct mbox *b, struct stat *st)
314 char buf[1024], sender[1024], subject[1024];
316 const char from[] = "\nFrom ";
320 b->total = b->new = b->flagged = 0;
325 /* FIXME: Should we do some locking? */
327 mb_fd = open(b->path, O_RDONLY);
330 debug("[open failed: %m] ");
331 b->total = b->new = b->flagged = -1;
337 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
339 mb_seek(b->last_pos);
340 if (mb_check(from, 6))
342 debug("[incremental] ");
347 debug("[incremental failed] ");
353 if (!mb_check(from+1, 5))
355 debug("[inconsistent] ");
356 b->total = b->new = b->flagged = -1;
359 b->total = b->new = b->flagged = 0;
360 b->last_total = b->last_new = b->last_flagged = 0;
361 b->snippet_is_new = 0;
365 b->total = b->last_total;
366 b->new = b->last_new;
367 b->flagged = b->last_flagged;
372 b->last_pos = mb_tell() - 5;
374 b->last_pos--; // last_pos should be the previous \n character
375 b->last_total = b->total;
376 b->last_new = b->new;
377 b->last_flagged = b->flagged;
378 while ((c = mb_get()) >= 0 && c != '\n')
393 debug("[truncated] ");
404 while (c == ' ' || c == '\t');
412 if (i < sizeof(buf) - 1)
418 if (!strncasecmp(buf, "Status:", 7))
420 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
422 else if (!strncasecmp(buf, "From:", 5))
423 strcpy(sender, buf+5);
424 else if (!strncasecmp(buf, "Subject:", 8))
425 strcpy(subject, buf+8);
433 if (new || (flagged && !b->snippet_is_new))
435 b->snippet_is_new = new;
436 prepare_snippet(b, sender, subject);
457 debug("Searching for mailboxes...\n");
458 last_scan_time = time(NULL);
459 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
461 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
463 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
464 if (err && err != GLOB_NOMATCH)
465 die("Failed to glob %s: %m", p->pattern);
466 for (uns i=0; i<g.gl_pathc; i++)
468 char *name = g.gl_pathv[i];
469 struct mbox *b = find_mbox(&mboxes, name);
472 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
473 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
483 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
489 debug("Lost mailbox %s\n", b->name);
496 debug("Scanning mailboxes...\n");
497 CLIST_FOR_EACH(struct mbox *, b, mboxes)
500 debug("%s: ", b->name);
501 if (!mbox_active_p(b))
507 b->force_refresh = 1;
508 if (stat(b->path, &st) < 0)
510 b->total = b->new = b->flagged = -1;
513 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
516 redraw_line(b->index);
520 b->last_time = st.st_mtime;
521 b->last_size = st.st_size;
522 debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
525 redraw_line(b->index);
529 debug("not changed\n");
530 b->force_refresh = 0;
534 debug("Scan finished\n");
535 last_scan_time = time(NULL);
539 static int cursor_at, cursor_max;
549 static int attrs[2][2][M_MAX]; // active, hilite, status
557 struct mbox *b = mbox_array[i];
558 int cc = (cursor_at == i);
559 int hi = b->o.highlight;
561 attrset(attrs[cc][hi][M_IDLE]);
563 printw("%c ", b->o.hotkey);
569 attrset(attrs[cc][hi][M_NEW]);
570 else if (b->flagged && b->o.show_flagged)
571 attrset(attrs[cc][hi][M_FLAG]);
572 printw("%-20s ", b->name);
575 else if (b->scanning)
577 attrset(attrs[cc][hi][M_SCAN]);
578 printw("[SCANNING]");
580 else if (b->total < 0)
582 attrset(attrs[cc][hi][M_BAD]);
587 attrset(attrs[cc][hi][M_IDLE]);
588 printw("%6d ", b->total);
592 attrset(attrs[cc][hi][M_NEW]);
593 printw("%6d ", b->new);
594 attrset(attrs[cc][hi][M_IDLE]);
595 int age = (last_scan_time - b->last_time);
599 printw("%2d min ", age/60);
600 else if (age < 86400)
601 printw("%2d hrs ", age/3600);
606 else if (b->flagged && b->o.show_flagged)
608 attrset(attrs[cc][hi][M_FLAG]);
609 printw("%6d ", b->flagged);
610 attrset(attrs[cc][hi][M_IDLE]);
612 attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */
615 if (snip && b->o.snippets && b->snippet[0])
618 getyx(stdscr, yy, xx);
619 int remains = COLS-1-xx;
622 #ifdef CONFIG_WIDE_CURSES
623 size_t len = strlen(b->snippet)+1;
625 mbstowcs(snip, b->snippet, len);
626 addnwstr(snip, remains);
628 printw("%-.*s", remains, b->snippet);
634 attrset(attrs[0][0][M_IDLE]);
641 cursor_max = num_mboxes;
642 if (cursor_max > LINES-1)
643 cursor_max = LINES-1;
644 if (cursor_at >= cursor_max)
645 cursor_at = cursor_max - 1;
649 for (int i=0; i<cursor_max; i++)
654 printw("(no mailboxes found)");
662 rethink_display(void)
667 CLIST_FOR_EACH(struct mbox *, b, mboxes)
668 if (mbox_visible_p(b))
671 if (i >= num_mboxes || mbox_array[i] != b)
674 if (i >= mbox_array_size)
676 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
677 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
681 if (b->o.beep && b->new > b->last_beep_new)
683 b->last_beep_new = b->new;
695 if (beeeep && allow_bells)
706 intrflush(stdscr, FALSE);
707 keypad(stdscr, TRUE);
710 static const int attrs_mono[2][M_MAX] = {
711 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
712 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
714 for (int i=0; i<2; i++)
715 for (int j=0; j<M_MAX; j++)
717 attrs[0][i][j] = attrs_mono[i][j];
718 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
724 if (COLOR_PAIRS >= 5)
726 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
727 init_pair(2, COLOR_RED, COLOR_BLACK);
728 init_pair(3, COLOR_WHITE, COLOR_BLUE);
729 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
730 init_pair(5, COLOR_RED, COLOR_BLUE);
731 init_pair(6, COLOR_GREEN, COLOR_BLACK);
732 init_pair(7, COLOR_GREEN, COLOR_BLUE);
733 static const int attrs_color[2][2][M_MAX] = {
734 [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) },
735 [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 },
736 [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) },
737 [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 },
739 memcpy(attrs, attrs_color, sizeof(attrs));
751 print_status(char *status)
755 printw("%s", status);
761 scan_and_redraw(void)
763 print_status("Busy...");
771 if (i >= 0 && i < cursor_max && i != cursor_at)
781 next_active(int since, int step)
785 since = (since+cursor_max) % cursor_max;
786 step = (step+cursor_max) % cursor_max;
792 struct mbox *b = mbox_array[i];
793 if (b->new && b->o.priority > bestp)
796 bestp = b->o.priority;
798 i = (i+step) % cursor_max;
806 mbox_run(struct mbox *b)
808 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
809 sprintf(cmd, run_cmd, b->path);
815 b->force_refresh = 1;
820 #define STR(c) STR2(c)
825 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
828 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
829 -d\t\t\tLog debug messages to stderr\n\
830 -i\t\t\tInclude user's INBOX\n\
831 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
832 -o <pattern>=<opts>\tSet mailbox options\n\
833 -o <opts>\t\tSet default options for all mailboxes\n\
834 -p <pri>\t\tSet minimum priority to show\n\
836 Mailbox options (set with `-o', use upper case to negate):\n\
837 0-9\t\t\tSet mailbox priority (0=default)\n\
838 b\t\t\tBeep when a message arrives\n\
839 e\t\t\tHide from display if empty\n\
840 f\t\t\tShow flagged messages if there are no new ones\n\
841 h\t\t\tHide from display\n\
842 m\t\t\tShow mailbox name of the sender\n\
843 p\t\t\tShow personal info (full name) of the sender\n\
844 s\t\t\tShow message snippets\n\
845 t\t\t\tHighlight the entry\n\
846 !<key>\t\t\tSet hot key\n\
848 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
849 It can be freely distributed and used according to the GNU GPL v2.\n\
855 parse_options(char *c)
859 if (sep = strchr(c, '='))
861 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
862 memcpy(n->pattern, c, sep-c);
863 n->pattern[sep-c] = 0;
864 clist_add_tail(&options, &n->n);
874 if (x >= '0' && x <= '9')
875 o->priority = x - '0';
876 else if (x == '!' && *c)
880 int value = !!islower(x);
887 o->hide_if_empty = value;
890 o->show_flagged = value;
896 o->sender_mbox = value;
899 o->sender_personal = value;
905 o->highlight = value;
908 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
915 main(int argc, char **argv)
918 clist_init(&options);
919 clist_init(&patterns);
922 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
926 check_interval = atol(optarg);
927 if (check_interval <= 0)
940 parse_options(optarg);
943 minimum_priority = atol(optarg);
948 while (optind < argc)
949 add_pattern(argv[optind++]);
960 time_t now = time(NULL);
961 int remains = last_scan_time + check_interval - now;
962 if (remains <= 0 || force_refresh)
967 halfdelay((remains > 255) ? 255 : remains);
969 for (int i=0; i<num_mboxes; i++)
970 if (ch == mbox_array[i]->o.hotkey)
974 mbox_run(mbox_array[i]);
984 move_cursor(cursor_at+1);
988 move_cursor(cursor_at-1);
998 move_cursor(cursor_max-1);
1001 next_active(cursor_at+1, 1);
1004 next_active(cursor_at-1, -1);
1008 if (cursor_at < cursor_max)
1009 mbox_run(mbox_array[cursor_at]);
1012 clearok(stdscr, TRUE);
1021 print_status("Bells and whistles are now enabled. Toot!");
1025 print_status("Bells and whistles are now disabled. Pssst!");
1028 if (ch >= '0' && ch <= '9')
1030 minimum_priority = ch - '0';
1034 debug("Pressed unknown key %d\n", ch);