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";
54 static clist options, patterns;
55 static struct options global_options;
66 int last_size, last_pos;
67 int total, new, flagged;
68 int last_total, last_new, last_flagged;
76 static struct mbox **mbox_array;
77 static int num_mboxes, mbox_array_size;
79 static void redraw_line(int i);
80 static void rethink_display(void);
83 add_pattern(char *patt)
85 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
86 strcpy(n->pattern, patt);
87 if (patt = strchr(n->pattern, '='))
94 clist_add_tail(&patterns, &n->n);
100 struct passwd *p = getpwuid(getuid());
102 die("You don't exist, go away!");
103 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
104 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
109 init_options(struct options *o)
113 o->hide_if_empty = -1;
117 o->show_flagged = -1;
121 setup_options(struct mbox *b)
123 b->o = global_options;
124 CLIST_FOR_EACH(struct option_node *, n, options)
125 if (!fnmatch(n->pattern, b->name, 0))
127 debug("\tApplied options %s\n", n->pattern);
128 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
131 MERGE(hide_if_empty);
140 mbox_name(char *path)
142 char *c = strrchr(path, '/');
143 return c ? (c+1) : path;
147 add_mbox(clist *l, char *path, char *name)
149 struct mbox *b = xmalloc(sizeof(*b));
150 bzero(b, sizeof(*b));
151 b->path = xstrdup(path);
152 b->name = xstrdup(name);
156 cnode *prev = l->head.prev;
157 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
159 clist_insert_after(&b->n, prev);
162 clist_add_tail(l, &b->n);
168 del_mbox(struct mbox *b)
177 find_mbox(clist *l, char *path)
179 CLIST_FOR_EACH(struct mbox *, b, *l)
180 if (!strcmp(b->path, path))
186 mbox_active_p(struct mbox *b)
188 if (b->o.priority < minimum_priority)
196 mbox_visible_p(struct mbox *b)
198 if (!mbox_active_p(b))
202 if (b->o.hide_if_empty && !b->total)
208 prepare_snippet(struct mbox *b, char *sender, char *subject)
210 while (*sender == ' ' || *sender == '\t')
212 while (*subject == ' ' || *subject == '\t')
215 char *pos = b->snippet;
216 char *term = b->snippet + sizeof(b->snippet) - 1;
219 add_snippet(&pos, term, sender);
220 add_snippet(&pos, term, ": ");
223 add_snippet(&pos, term, subject);
225 add_snippet(&pos, term, "No subject");
228 static int mb_fd, mb_pos;
229 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
234 mb_cc = mb_end = mb_buf;
241 lseek(mb_fd, pos, SEEK_SET);
248 return mb_pos - (mb_end - mb_cc);
254 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
263 mb_end = mb_buf + len;
272 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
283 mb_check(const char *p, int len)
287 if (mb_get() != *p++)
294 scan_mbox(struct mbox *b, struct stat *st)
296 char buf[1024], sender[1024], subject[1024];
298 const char from[] = "\nFrom ";
302 b->total = b->new = b->flagged = 0;
307 /* FIXME: Locking! */
309 mb_fd = open(b->path, O_RDONLY);
312 debug("[open failed: %m] ");
313 b->total = b->new = b->flagged = -1;
319 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
321 mb_seek(b->last_pos);
322 if (mb_check(from, 6))
324 debug("[incremental] ");
329 debug("[incremental failed] ");
335 if (!mb_check(from+1, 5))
337 debug("[inconsistent] ");
338 b->total = b->new = b->flagged = -1;
341 b->total = b->new = b->flagged = 0;
342 b->last_total = b->last_new = b->last_flagged = 0;
343 b->snippet_is_new = 0;
347 b->total = b->last_total;
348 b->new = b->last_new;
349 b->flagged = b->last_flagged;
354 b->last_pos = mb_tell() - 5;
356 b->last_pos--; // last_pos should be the previous \n character
357 b->last_total = b->total;
358 b->last_new = b->new;
359 b->last_flagged = b->flagged;
360 while ((c = mb_get()) >= 0 && c != '\n')
375 debug("[truncated] ");
386 while (c == ' ' || c == '\t');
394 if (i < sizeof(buf) - 1)
400 if (!strncasecmp(buf, "Status:", 7))
402 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
404 else if (!strncasecmp(buf, "From:", 5))
405 strcpy(sender, buf+5);
406 else if (!strncasecmp(buf, "Subject:", 8))
407 strcpy(subject, buf+8);
415 if (new || (flagged && !b->snippet_is_new))
417 b->snippet_is_new = new;
418 prepare_snippet(b, sender, subject);
439 debug("Searching for mailboxes...\n");
440 last_scan_time = time(NULL);
441 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
443 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
445 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
446 if (err && err != GLOB_NOMATCH)
447 die("Failed to glob %s: %m", p->pattern);
448 for (uns i=0; i<g.gl_pathc; i++)
450 char *name = g.gl_pathv[i];
451 struct mbox *b = find_mbox(&mboxes, name);
454 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
455 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
465 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
471 debug("Lost mailbox %s\n", b->name);
478 debug("Scanning mailboxes...\n");
479 CLIST_FOR_EACH(struct mbox *, b, mboxes)
482 debug("%s: ", b->name);
483 if (!mbox_active_p(b))
489 b->force_refresh = 1;
490 if (stat(b->path, &st) < 0)
492 b->total = b->new = b->flagged = -1;
495 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
498 redraw_line(b->index);
502 b->last_time = st.st_mtime;
503 b->last_size = st.st_size;
504 debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
507 redraw_line(b->index);
511 debug("not changed\n");
512 b->force_refresh = 0;
516 debug("Scan finished\n");
517 last_scan_time = time(NULL);
521 static int cursor_at, cursor_max;
531 static int attrs[2][2][M_MAX]; // active, hilite, status
539 struct mbox *b = mbox_array[i];
540 int cc = (cursor_at == i);
541 int hi = b->o.highlight;
543 attrset(attrs[cc][hi][M_IDLE]);
549 attrset(attrs[cc][hi][M_NEW]);
551 attrset(attrs[cc][hi][M_FLAG]);
552 printw("%-20s ", b->name);
555 else if (b->scanning)
557 attrset(attrs[cc][hi][M_SCAN]);
558 printw("[SCANNING]");
560 else if (b->total < 0)
562 attrset(attrs[cc][hi][M_BAD]);
567 attrset(attrs[cc][hi][M_IDLE]);
568 printw("%6d ", b->total);
572 attrset(attrs[cc][hi][M_NEW]);
573 printw("%6d ", b->new);
574 attrset(attrs[cc][hi][M_IDLE]);
575 int age = (last_scan_time - b->last_time);
579 printw("%2d min ", age/60);
580 else if (age < 86400)
581 printw("%2d hrs ", age/3600);
588 attrset(attrs[cc][hi][M_FLAG]);
589 printw("%6d ", b->flagged);
590 attrset(attrs[cc][hi][M_IDLE]);
592 snip = b->o.show_flagged;
594 if (snip && b->o.snippets && b->snippet[0])
597 getyx(stdscr, yy, xx);
598 int remains = COLS-1-xx;
600 printw("%-.*s", remains, b->snippet);
604 attrset(attrs[0][0][M_IDLE]);
611 cursor_max = num_mboxes;
612 if (cursor_max > LINES-1)
613 cursor_max = LINES-1;
614 if (cursor_at >= cursor_max)
615 cursor_at = cursor_max - 1;
619 for (int i=0; i<cursor_max; i++)
624 printw("(no mailboxes found)");
632 rethink_display(void)
637 CLIST_FOR_EACH(struct mbox *, b, mboxes)
638 if (mbox_visible_p(b))
641 if (i >= num_mboxes || mbox_array[i] != b)
644 if (i >= mbox_array_size)
646 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
647 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
651 if (b->o.beep && b->new > b->last_beep_new)
653 b->last_beep_new = b->new;
665 if (beeeep && allow_bells)
676 intrflush(stdscr, FALSE);
677 keypad(stdscr, TRUE);
680 static const int attrs_mono[2][M_MAX] = {
681 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
682 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
684 for (int i=0; i<2; i++)
685 for (int j=0; j<M_MAX; j++)
687 attrs[0][i][j] = attrs_mono[i][j];
688 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
694 if (COLOR_PAIRS >= 5)
696 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
697 init_pair(2, COLOR_RED, COLOR_BLACK);
698 init_pair(3, COLOR_WHITE, COLOR_BLUE);
699 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
700 init_pair(5, COLOR_RED, COLOR_BLUE);
701 init_pair(6, COLOR_GREEN, COLOR_BLACK);
702 init_pair(7, COLOR_GREEN, COLOR_BLUE);
703 static const int attrs_color[2][2][M_MAX] = {
704 [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) },
705 [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 },
706 [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) },
707 [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 },
709 memcpy(attrs, attrs_color, sizeof(attrs));
721 print_status(char *status)
725 printw("%s", status);
731 scan_and_redraw(void)
733 print_status("Busy...");
741 if (i >= 0 && i < cursor_max && i != cursor_at)
751 next_active(int since, int step)
755 since = (since+cursor_max) % cursor_max;
756 step = (step+cursor_max) % cursor_max;
762 struct mbox *b = mbox_array[i];
763 if (b->new && b->o.priority > bestp)
766 bestp = b->o.priority;
768 i = (i+step) % cursor_max;
776 #define STR(c) STR2(c)
781 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
784 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
785 -d\t\t\tLog debug messages to stderr\n\
786 -i\t\t\tInclude user's INBOX\n\
787 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
788 -o <pattern>=<opts>\tSet mailbox options\n\
789 -o <opts>\t\tSet default options for all mailboxes\n\
790 -p <pri>\t\tSet minimum priority to show\n\
792 Mailbox options (set with `-o', use upper case to negate):\n\
793 0-9\t\t\tSet mailbox priority (0=default)\n\
794 b\t\t\tBeep when a message arrives\n\
795 e\t\t\tHide from display if empty\n\
796 f\t\t\tShow flagged messages if there are no new ones\n\
797 h\t\t\tHide from display\n\
798 s\t\t\tShow message snippets\n\
799 t\t\t\tHighlight the entry\n\
801 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
802 It can be freely distributed and used according to the GNU GPL v2.\n\
808 parse_options(char *c)
812 if (sep = strchr(c, '='))
814 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
815 memcpy(n->pattern, c, sep-c);
816 n->pattern[sep-c] = 0;
817 clist_add_tail(&options, &n->n);
827 if (x >= '0' && x <= '9')
828 o->priority = x - '0';
831 int value = !!islower(x);
838 o->hide_if_empty = value;
841 o->show_flagged = value;
850 o->highlight = value;
853 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
860 main(int argc, char **argv)
863 clist_init(&options);
864 clist_init(&patterns);
867 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
871 check_interval = atol(optarg);
872 if (check_interval <= 0)
885 parse_options(optarg);
888 minimum_priority = atol(optarg);
893 while (optind < argc)
894 add_pattern(argv[optind++]);
904 time_t now = time(NULL);
905 int remains = last_scan_time + check_interval - now;
906 if (remains <= 0 || force_refresh)
911 halfdelay((remains > 255) ? 255 : remains);
920 move_cursor(cursor_at+1);
924 move_cursor(cursor_at-1);
934 move_cursor(cursor_max-1);
937 next_active(cursor_at+1, 1);
940 next_active(cursor_at-1, -1);
944 if (cursor_at < cursor_max)
946 struct mbox *b = mbox_array[cursor_at];
947 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
948 sprintf(cmd, run_cmd, b->path);
954 b->force_refresh = 1;
959 clearok(stdscr, TRUE);
968 print_status("Bells and whistles are now enabled. Toot!");
972 print_status("Bells and whistles are now disabled. Pssst!");
975 if (ch >= '0' && ch <= '9')
977 minimum_priority = ch - '0';
981 debug("Pressed unknown key %d\n", ch);