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";
57 static clist options, patterns;
58 static struct options global_options = {
71 int last_size, last_pos;
72 int total, new, flagged;
73 int last_total, last_new, last_flagged;
81 static struct mbox **mbox_array;
82 static int num_mboxes, mbox_array_size;
84 static void redraw_line(int i);
85 static void rethink_display(void);
88 add_pattern(char *patt)
90 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
91 strcpy(n->pattern, patt);
92 if (patt = strchr(n->pattern, '='))
99 clist_add_tail(&patterns, &n->n);
105 struct passwd *p = getpwuid(getuid());
107 die("You don't exist, go away!");
108 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
109 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
114 init_options(struct options *o)
118 o->hide_if_empty = -1;
122 o->show_flagged = -1;
123 o->sender_personal = -1;
129 setup_options(struct mbox *b)
131 b->o = global_options;
132 CLIST_FOR_EACH(struct option_node *, n, options)
133 if (!fnmatch(n->pattern, b->name, 0))
135 debug("\tApplied options %s\n", n->pattern);
136 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
139 MERGE(hide_if_empty);
144 MERGE(sender_personal);
151 mbox_name(char *path)
153 char *c = strrchr(path, '/');
154 return c ? (c+1) : path;
158 add_mbox(clist *l, char *path, char *name)
160 struct mbox *b = xmalloc(sizeof(*b));
161 bzero(b, sizeof(*b));
162 b->path = xstrdup(path);
163 b->name = xstrdup(name);
167 cnode *prev = l->head.prev;
168 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
170 clist_insert_after(&b->n, prev);
173 clist_add_tail(l, &b->n);
179 del_mbox(struct mbox *b)
188 find_mbox(clist *l, char *path)
190 CLIST_FOR_EACH(struct mbox *, b, *l)
191 if (!strcmp(b->path, path))
197 mbox_active_p(struct mbox *b)
199 if (b->o.priority < minimum_priority)
207 mbox_visible_p(struct mbox *b)
209 if (!mbox_active_p(b))
213 if (b->o.hide_if_empty && !b->total)
219 prepare_snippet(struct mbox *b, char *sender, char *subject)
221 while (*sender == ' ' || *sender == '\t')
223 while (*subject == ' ' || *subject == '\t')
226 char *pos = b->snippet;
227 char *term = b->snippet + sizeof(b->snippet) - 1;
228 if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
230 add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
231 add_snippet(&pos, term, ": ");
234 add_subject_snippet(&pos, term, subject);
236 add_snippet(&pos, term, "No subject");
239 static int mb_fd, mb_pos;
240 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
245 mb_cc = mb_end = mb_buf;
252 lseek(mb_fd, pos, SEEK_SET);
259 return mb_pos - (mb_end - mb_cc);
265 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
274 mb_end = mb_buf + len;
283 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
294 mb_check(const char *p, int len)
298 if (mb_get() != *p++)
305 scan_mbox(struct mbox *b, struct stat *st)
307 char buf[1024], sender[1024], subject[1024];
309 const char from[] = "\nFrom ";
313 b->total = b->new = b->flagged = 0;
318 /* FIXME: Locking! */
320 mb_fd = open(b->path, O_RDONLY);
323 debug("[open failed: %m] ");
324 b->total = b->new = b->flagged = -1;
330 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
332 mb_seek(b->last_pos);
333 if (mb_check(from, 6))
335 debug("[incremental] ");
340 debug("[incremental failed] ");
346 if (!mb_check(from+1, 5))
348 debug("[inconsistent] ");
349 b->total = b->new = b->flagged = -1;
352 b->total = b->new = b->flagged = 0;
353 b->last_total = b->last_new = b->last_flagged = 0;
354 b->snippet_is_new = 0;
358 b->total = b->last_total;
359 b->new = b->last_new;
360 b->flagged = b->last_flagged;
365 b->last_pos = mb_tell() - 5;
367 b->last_pos--; // last_pos should be the previous \n character
368 b->last_total = b->total;
369 b->last_new = b->new;
370 b->last_flagged = b->flagged;
371 while ((c = mb_get()) >= 0 && c != '\n')
386 debug("[truncated] ");
397 while (c == ' ' || c == '\t');
405 if (i < sizeof(buf) - 1)
411 if (!strncasecmp(buf, "Status:", 7))
413 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
415 else if (!strncasecmp(buf, "From:", 5))
416 strcpy(sender, buf+5);
417 else if (!strncasecmp(buf, "Subject:", 8))
418 strcpy(subject, buf+8);
426 if (new || (flagged && !b->snippet_is_new))
428 b->snippet_is_new = new;
429 prepare_snippet(b, sender, subject);
450 debug("Searching for mailboxes...\n");
451 last_scan_time = time(NULL);
452 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
454 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
456 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
457 if (err && err != GLOB_NOMATCH)
458 die("Failed to glob %s: %m", p->pattern);
459 for (uns i=0; i<g.gl_pathc; i++)
461 char *name = g.gl_pathv[i];
462 struct mbox *b = find_mbox(&mboxes, name);
465 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
466 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
476 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
482 debug("Lost mailbox %s\n", b->name);
489 debug("Scanning mailboxes...\n");
490 CLIST_FOR_EACH(struct mbox *, b, mboxes)
493 debug("%s: ", b->name);
494 if (!mbox_active_p(b))
500 b->force_refresh = 1;
501 if (stat(b->path, &st) < 0)
503 b->total = b->new = b->flagged = -1;
506 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
509 redraw_line(b->index);
513 b->last_time = st.st_mtime;
514 b->last_size = st.st_size;
515 debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
518 redraw_line(b->index);
522 debug("not changed\n");
523 b->force_refresh = 0;
527 debug("Scan finished\n");
528 last_scan_time = time(NULL);
532 static int cursor_at, cursor_max;
542 static int attrs[2][2][M_MAX]; // active, hilite, status
550 struct mbox *b = mbox_array[i];
551 int cc = (cursor_at == i);
552 int hi = b->o.highlight;
554 attrset(attrs[cc][hi][M_IDLE]);
556 printw("%c ", b->o.hotkey);
562 attrset(attrs[cc][hi][M_NEW]);
564 attrset(attrs[cc][hi][M_FLAG]);
565 printw("%-20s ", b->name);
568 else if (b->scanning)
570 attrset(attrs[cc][hi][M_SCAN]);
571 printw("[SCANNING]");
573 else if (b->total < 0)
575 attrset(attrs[cc][hi][M_BAD]);
580 attrset(attrs[cc][hi][M_IDLE]);
581 printw("%6d ", b->total);
585 attrset(attrs[cc][hi][M_NEW]);
586 printw("%6d ", b->new);
587 attrset(attrs[cc][hi][M_IDLE]);
588 int age = (last_scan_time - b->last_time);
592 printw("%2d min ", age/60);
593 else if (age < 86400)
594 printw("%2d hrs ", age/3600);
601 attrset(attrs[cc][hi][M_FLAG]);
602 printw("%6d ", b->flagged);
603 attrset(attrs[cc][hi][M_IDLE]);
605 attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */
606 snip = b->o.show_flagged;
608 if (snip && b->o.snippets && b->snippet[0])
611 getyx(stdscr, yy, xx);
612 int remains = COLS-1-xx;
614 printw("%-.*s", remains, b->snippet);
618 attrset(attrs[0][0][M_IDLE]);
625 cursor_max = num_mboxes;
626 if (cursor_max > LINES-1)
627 cursor_max = LINES-1;
628 if (cursor_at >= cursor_max)
629 cursor_at = cursor_max - 1;
633 for (int i=0; i<cursor_max; i++)
638 printw("(no mailboxes found)");
646 rethink_display(void)
651 CLIST_FOR_EACH(struct mbox *, b, mboxes)
652 if (mbox_visible_p(b))
655 if (i >= num_mboxes || mbox_array[i] != b)
658 if (i >= mbox_array_size)
660 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
661 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
665 if (b->o.beep && b->new > b->last_beep_new)
667 b->last_beep_new = b->new;
679 if (beeeep && allow_bells)
690 intrflush(stdscr, FALSE);
691 keypad(stdscr, TRUE);
694 static const int attrs_mono[2][M_MAX] = {
695 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
696 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
698 for (int i=0; i<2; i++)
699 for (int j=0; j<M_MAX; j++)
701 attrs[0][i][j] = attrs_mono[i][j];
702 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
708 if (COLOR_PAIRS >= 5)
710 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
711 init_pair(2, COLOR_RED, COLOR_BLACK);
712 init_pair(3, COLOR_WHITE, COLOR_BLUE);
713 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
714 init_pair(5, COLOR_RED, COLOR_BLUE);
715 init_pair(6, COLOR_GREEN, COLOR_BLACK);
716 init_pair(7, COLOR_GREEN, COLOR_BLUE);
717 static const int attrs_color[2][2][M_MAX] = {
718 [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) },
719 [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 },
720 [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) },
721 [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 },
723 memcpy(attrs, attrs_color, sizeof(attrs));
735 print_status(char *status)
739 printw("%s", status);
745 scan_and_redraw(void)
747 print_status("Busy...");
755 if (i >= 0 && i < cursor_max && i != cursor_at)
765 next_active(int since, int step)
769 since = (since+cursor_max) % cursor_max;
770 step = (step+cursor_max) % cursor_max;
776 struct mbox *b = mbox_array[i];
777 if (b->new && b->o.priority > bestp)
780 bestp = b->o.priority;
782 i = (i+step) % cursor_max;
790 mbox_run(struct mbox *b)
792 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
793 sprintf(cmd, run_cmd, b->path);
799 b->force_refresh = 1;
804 #define STR(c) STR2(c)
809 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
812 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
813 -d\t\t\tLog debug messages to stderr\n\
814 -i\t\t\tInclude user's INBOX\n\
815 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
816 -o <pattern>=<opts>\tSet mailbox options\n\
817 -o <opts>\t\tSet default options for all mailboxes\n\
818 -p <pri>\t\tSet minimum priority to show\n\
820 Mailbox options (set with `-o', use upper case to negate):\n\
821 0-9\t\t\tSet mailbox priority (0=default)\n\
822 b\t\t\tBeep when a message arrives\n\
823 e\t\t\tHide from display if empty\n\
824 f\t\t\tShow flagged messages if there are no new ones\n\
825 h\t\t\tHide from display\n\
826 m\t\t\tShow mailbox name of the sender\n\
827 p\t\t\tShow personal info (full name) of the sender\n\
828 s\t\t\tShow message snippets\n\
829 t\t\t\tHighlight the entry\n\
830 !<key>\t\t\tSet hot key\n\
832 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
833 It can be freely distributed and used according to the GNU GPL v2.\n\
839 parse_options(char *c)
843 if (sep = strchr(c, '='))
845 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
846 memcpy(n->pattern, c, sep-c);
847 n->pattern[sep-c] = 0;
848 clist_add_tail(&options, &n->n);
858 if (x >= '0' && x <= '9')
859 o->priority = x - '0';
860 else if (x == '!' && *c)
864 int value = !!islower(x);
871 o->hide_if_empty = value;
874 o->show_flagged = value;
880 o->sender_mbox = value;
883 o->sender_personal = value;
889 o->highlight = value;
892 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
899 main(int argc, char **argv)
902 clist_init(&options);
903 clist_init(&patterns);
906 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
910 check_interval = atol(optarg);
911 if (check_interval <= 0)
924 parse_options(optarg);
927 minimum_priority = atol(optarg);
932 while (optind < argc)
933 add_pattern(argv[optind++]);
944 time_t now = time(NULL);
945 int remains = last_scan_time + check_interval - now;
946 if (remains <= 0 || force_refresh)
951 halfdelay((remains > 255) ? 255 : remains);
953 for (int i=0; i<num_mboxes; i++)
954 if (ch == mbox_array[i]->o.hotkey)
958 mbox_run(mbox_array[i]);
968 move_cursor(cursor_at+1);
972 move_cursor(cursor_at-1);
982 move_cursor(cursor_max-1);
985 next_active(cursor_at+1, 1);
988 next_active(cursor_at-1, -1);
992 if (cursor_at < cursor_max)
993 mbox_run(mbox_array[cursor_at]);
996 clearok(stdscr, TRUE);
1005 print_status("Bells and whistles are now enabled. Toot!");
1009 print_status("Bells and whistles are now disabled. Pssst!");
1012 if (ch >= '0' && ch <= '9')
1014 minimum_priority = ch - '0';
1018 debug("Pressed unknown key %d\n", ch);