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 snip = b->o.show_flagged;
602 if (snip && b->o.snippets && b->snippet[0])
605 getyx(stdscr, yy, xx);
606 int remains = COLS-1-xx;
608 printw("%-.*s", remains, b->snippet);
612 attrset(attrs[0][0][M_IDLE]);
619 cursor_max = num_mboxes;
620 if (cursor_max > LINES-1)
621 cursor_max = LINES-1;
622 if (cursor_at >= cursor_max)
623 cursor_at = cursor_max - 1;
627 for (int i=0; i<cursor_max; i++)
632 printw("(no mailboxes found)");
640 rethink_display(void)
645 CLIST_FOR_EACH(struct mbox *, b, mboxes)
646 if (mbox_visible_p(b))
649 if (i >= num_mboxes || mbox_array[i] != b)
652 if (i >= mbox_array_size)
654 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
655 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
659 if (b->o.beep && b->new > b->last_beep_new)
661 b->last_beep_new = b->new;
673 if (beeeep && allow_bells)
684 intrflush(stdscr, FALSE);
685 keypad(stdscr, TRUE);
688 static const int attrs_mono[2][M_MAX] = {
689 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
690 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
692 for (int i=0; i<2; i++)
693 for (int j=0; j<M_MAX; j++)
695 attrs[0][i][j] = attrs_mono[i][j];
696 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
702 if (COLOR_PAIRS >= 5)
704 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
705 init_pair(2, COLOR_RED, COLOR_BLACK);
706 init_pair(3, COLOR_WHITE, COLOR_BLUE);
707 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
708 init_pair(5, COLOR_RED, COLOR_BLUE);
709 init_pair(6, COLOR_GREEN, COLOR_BLACK);
710 init_pair(7, COLOR_GREEN, COLOR_BLUE);
711 static const int attrs_color[2][2][M_MAX] = {
712 [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) },
713 [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 },
714 [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) },
715 [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 },
717 memcpy(attrs, attrs_color, sizeof(attrs));
729 print_status(char *status)
733 printw("%s", status);
739 scan_and_redraw(void)
741 print_status("Busy...");
749 if (i >= 0 && i < cursor_max && i != cursor_at)
759 next_active(int since, int step)
763 since = (since+cursor_max) % cursor_max;
764 step = (step+cursor_max) % cursor_max;
770 struct mbox *b = mbox_array[i];
771 if (b->new && b->o.priority > bestp)
774 bestp = b->o.priority;
776 i = (i+step) % cursor_max;
784 #define STR(c) STR2(c)
789 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
792 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
793 -d\t\t\tLog debug messages to stderr\n\
794 -i\t\t\tInclude user's INBOX\n\
795 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
796 -o <pattern>=<opts>\tSet mailbox options\n\
797 -o <opts>\t\tSet default options for all mailboxes\n\
798 -p <pri>\t\tSet minimum priority to show\n\
800 Mailbox options (set with `-o', use upper case to negate):\n\
801 0-9\t\t\tSet mailbox priority (0=default)\n\
802 b\t\t\tBeep when a message arrives\n\
803 e\t\t\tHide from display if empty\n\
804 f\t\t\tShow flagged messages if there are no new ones\n\
805 h\t\t\tHide from display\n\
806 m\t\t\tShow mailbox name of the sender\n\
807 p\t\t\tShow personal info (full name) of the sender\n\
808 s\t\t\tShow message snippets\n\
809 t\t\t\tHighlight the entry\n\
811 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
812 It can be freely distributed and used according to the GNU GPL v2.\n\
818 parse_options(char *c)
822 if (sep = strchr(c, '='))
824 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
825 memcpy(n->pattern, c, sep-c);
826 n->pattern[sep-c] = 0;
827 clist_add_tail(&options, &n->n);
837 if (x >= '0' && x <= '9')
838 o->priority = x - '0';
841 int value = !!islower(x);
848 o->hide_if_empty = value;
851 o->show_flagged = value;
857 o->sender_mbox = value;
860 o->sender_personal = value;
866 o->highlight = value;
869 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
876 main(int argc, char **argv)
879 clist_init(&options);
880 clist_init(&patterns);
883 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
887 check_interval = atol(optarg);
888 if (check_interval <= 0)
901 parse_options(optarg);
904 minimum_priority = atol(optarg);
909 while (optind < argc)
910 add_pattern(argv[optind++]);
920 time_t now = time(NULL);
921 int remains = last_scan_time + check_interval - now;
922 if (remains <= 0 || force_refresh)
927 halfdelay((remains > 255) ? 255 : remains);
936 move_cursor(cursor_at+1);
940 move_cursor(cursor_at-1);
950 move_cursor(cursor_max-1);
953 next_active(cursor_at+1, 1);
956 next_active(cursor_at-1, -1);
960 if (cursor_at < cursor_max)
962 struct mbox *b = mbox_array[cursor_at];
963 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
964 sprintf(cmd, run_cmd, b->path);
970 b->force_refresh = 1;
975 clearok(stdscr, TRUE);
984 print_status("Bells and whistles are now enabled. Toot!");
988 print_status("Bells and whistles are now disabled. Pssst!");
991 if (ch >= '0' && ch <= '9')
993 minimum_priority = ch - '0';
997 debug("Pressed unknown key %d\n", ch);