2 * Incoming Mail Checker
4 * (c) 2005--2007 Martin Mares <mj@ucw.cz>
24 static int check_interval = 30;
25 static int force_refresh;
26 static int allow_bells = 1;
27 static int minimum_priority;
28 static time_t last_scan_time;
29 static char *run_cmd = "mutt -f %s";
53 static clist options, patterns;
54 static struct options global_options;
65 int last_size, last_pos;
66 int total, new, flagged;
67 int last_total, last_new;
75 static struct mbox **mbox_array;
76 static int num_mboxes, mbox_array_size;
78 static void redraw_line(int i);
79 static void rethink_display(void);
82 add_pattern(char *patt)
84 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
85 strcpy(n->pattern, patt);
86 if (patt = strchr(n->pattern, '='))
93 clist_add_tail(&patterns, &n->n);
99 struct passwd *p = getpwuid(getuid());
101 die("You don't exist, go away!");
102 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
103 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
108 init_options(struct options *o)
112 o->hide_if_empty = -1;
116 o->show_flagged = -1;
120 setup_options(struct mbox *b)
122 b->o = global_options;
123 CLIST_FOR_EACH(struct option_node *, n, options)
124 if (!fnmatch(n->pattern, b->name, 0))
126 debug("\tApplied options %s\n", n->pattern);
127 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
130 MERGE(hide_if_empty);
139 mbox_name(char *path)
141 char *c = strrchr(path, '/');
142 return c ? (c+1) : path;
146 add_mbox(clist *l, char *path, char *name)
148 struct mbox *b = xmalloc(sizeof(*b));
149 bzero(b, sizeof(*b));
150 b->path = xstrdup(path);
151 b->name = xstrdup(name);
155 cnode *prev = l->head.prev;
156 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
158 clist_insert_after(&b->n, prev);
161 clist_add_tail(l, &b->n);
167 del_mbox(struct mbox *b)
176 find_mbox(clist *l, char *path)
178 CLIST_FOR_EACH(struct mbox *, b, *l)
179 if (!strcmp(b->path, path))
185 mbox_active_p(struct mbox *b)
187 if (b->o.priority < minimum_priority)
195 mbox_visible_p(struct mbox *b)
197 if (!mbox_active_p(b))
201 if (b->o.hide_if_empty && !b->total)
207 add_snippet(char **ppos, char *term, unsigned char *add)
211 while (*add && pos < term)
219 else if (*add >= 0x7f)
236 prepare_snippet(struct mbox *b, char *sender, char *subject)
238 while (*sender == ' ' || *sender == '\t')
240 while (*subject == ' ' || *subject == '\t')
243 char *pos = b->snippet;
244 char *term = b->snippet + sizeof(b->snippet) - 1;
247 add_snippet(&pos, term, sender);
248 add_snippet(&pos, term, ": ");
251 add_snippet(&pos, term, subject);
253 add_snippet(&pos, term, "No subject");
256 static int mb_fd, mb_pos;
257 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
262 mb_cc = mb_end = mb_buf;
269 lseek(mb_fd, pos, SEEK_SET);
276 return mb_pos - (mb_end - mb_cc);
282 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
291 mb_end = mb_buf + len;
300 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
311 mb_check(const char *p, int len)
315 if (mb_get() != *p++)
322 scan_mbox(struct mbox *b, struct stat *st)
324 char buf[1024], sender[1024], subject[1024];
326 const char from[] = "\nFrom ";
330 b->total = b->new = 0;
335 /* FIXME: Locking! */
337 mb_fd = open(b->path, O_RDONLY);
340 debug("[open failed: %m] ");
341 b->total = b->new = -1;
347 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
349 mb_seek(b->last_pos);
350 if (mb_check(from, 6))
352 debug("[incremental] ");
357 debug("[incremental failed] ");
363 if (!mb_check(from+1, 5))
365 debug("[inconsistent] ");
366 b->total = b->new = -1;
369 b->total = b->new = 0;
370 b->last_total = b->last_new = 0;
371 b->snippet_is_new = 0;
375 b->total = b->last_total;
376 b->new = b->last_new;
381 b->last_pos = mb_tell() - 5;
383 b->last_pos--; // last_pos should be the previous \n character
384 b->last_total = b->total;
385 b->last_new = b->new;
386 while ((c = mb_get()) >= 0 && c != '\n')
401 debug("[truncated] ");
412 while (c == ' ' || c == '\t');
420 if (i < sizeof(buf) - 1)
426 if (!strncasecmp(buf, "Status:", 7))
428 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
430 else if (!strncasecmp(buf, "From:", 5))
431 strcpy(sender, buf+5);
432 else if (!strncasecmp(buf, "Subject:", 8))
433 strcpy(subject, buf+8);
441 if (new || (flagged && !b->snippet_is_new))
443 b->snippet_is_new = new;
444 prepare_snippet(b, sender, subject);
465 debug("Searching for mailboxes...\n");
466 last_scan_time = time(NULL);
467 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
469 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
471 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
472 if (err && err != GLOB_NOMATCH)
473 die("Failed to glob %s: %m", p->pattern);
474 for (uns i=0; i<g.gl_pathc; i++)
476 char *name = g.gl_pathv[i];
477 struct mbox *b = find_mbox(&mboxes, name);
480 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
481 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
491 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
497 debug("Lost mailbox %s\n", b->name);
504 debug("Scanning mailboxes...\n");
505 CLIST_FOR_EACH(struct mbox *, b, mboxes)
508 debug("%s: ", b->name);
509 if (!mbox_active_p(b))
515 b->force_refresh = 1;
516 if (stat(b->path, &st) < 0)
518 b->total = b->new = -1;
521 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
524 redraw_line(b->index);
528 b->last_time = st.st_mtime;
529 b->last_size = st.st_size;
530 debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
533 redraw_line(b->index);
537 debug("not changed\n");
538 b->force_refresh = 0;
542 debug("Scan finished\n");
543 last_scan_time = time(NULL);
547 static int cursor_at, cursor_max;
557 static int attrs[2][2][M_MAX]; // active, hilite, status
565 struct mbox *b = mbox_array[i];
566 int cc = (cursor_at == i);
567 int hi = b->o.highlight;
569 attrset(attrs[cc][hi][M_IDLE]);
575 attrset(attrs[cc][hi][M_NEW]);
577 attrset(attrs[cc][hi][M_FLAG]);
578 printw("%-20s ", b->name);
581 else if (b->scanning)
583 attrset(attrs[cc][hi][M_SCAN]);
584 printw("[SCANNING]");
586 else if (b->total < 0)
588 attrset(attrs[cc][hi][M_BAD]);
593 attrset(attrs[cc][hi][M_IDLE]);
594 printw("%6d ", b->total);
598 attrset(attrs[cc][hi][M_NEW]);
599 printw("%6d ", b->new);
600 attrset(attrs[cc][hi][M_IDLE]);
601 int age = (last_scan_time - b->last_time);
605 printw("%2d min ", age/60);
606 else if (age < 86400)
607 printw("%2d hrs ", age/3600);
614 attrset(attrs[cc][hi][M_FLAG]);
615 printw("%6d ", b->flagged);
616 attrset(attrs[cc][hi][M_IDLE]);
618 snip = b->o.show_flagged;
620 if (snip && b->o.snippets && b->snippet[0])
623 getyx(stdscr, yy, xx);
624 int remains = COLS-1-xx;
626 printw("%-.*s", remains, b->snippet);
630 attrset(attrs[0][0][M_IDLE]);
637 cursor_max = num_mboxes;
638 if (cursor_max > LINES-1)
639 cursor_max = LINES-1;
640 if (cursor_at >= cursor_max)
641 cursor_at = cursor_max - 1;
645 for (int i=0; i<cursor_max; i++)
650 printw("(no mailboxes found)");
658 rethink_display(void)
663 CLIST_FOR_EACH(struct mbox *, b, mboxes)
664 if (mbox_visible_p(b))
667 if (i >= num_mboxes || mbox_array[i] != b)
670 if (i >= mbox_array_size)
672 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
673 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
677 if (b->o.beep && b->new > b->last_beep_new)
679 b->last_beep_new = b->new;
691 if (beeeep && allow_bells)
702 intrflush(stdscr, FALSE);
703 keypad(stdscr, TRUE);
706 static const int attrs_mono[2][M_MAX] = {
707 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
708 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
710 for (int i=0; i<2; i++)
711 for (int j=0; j<M_MAX; j++)
713 attrs[0][i][j] = attrs_mono[i][j];
714 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
720 if (COLOR_PAIRS >= 5)
722 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
723 init_pair(2, COLOR_RED, COLOR_BLACK);
724 init_pair(3, COLOR_WHITE, COLOR_BLUE);
725 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
726 init_pair(5, COLOR_RED, COLOR_BLUE);
727 init_pair(6, COLOR_GREEN, COLOR_BLACK);
728 init_pair(7, COLOR_GREEN, COLOR_BLUE);
729 static const int attrs_color[2][2][M_MAX] = {
730 [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) },
731 [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 },
732 [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) },
733 [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 },
735 memcpy(attrs, attrs_color, sizeof(attrs));
747 print_status(char *status)
751 printw("%s", status);
757 scan_and_redraw(void)
759 print_status("Busy...");
767 if (i >= 0 && i < cursor_max && i != cursor_at)
777 next_active(int since, int step)
781 since = (since+cursor_max) % cursor_max;
782 step = (step+cursor_max) % cursor_max;
788 struct mbox *b = mbox_array[i];
789 if (b->new && b->o.priority > bestp)
792 bestp = b->o.priority;
794 i = (i+step) % cursor_max;
802 #define STR(c) STR2(c)
807 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
810 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
811 -d\t\t\tLog debug messages to stderr\n\
812 -i\t\t\tInclude user's INBOX\n\
813 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
814 -o <pattern>=<opts>\tSet mailbox options\n\
815 -o <opts>\t\tSet default options for all mailboxes\n\
816 -p <pri>\t\tSet minimum priority to show\n\
818 Mailbox options (set with `-o', use upper case to negate):\n\
819 0-9\t\t\tSet mailbox priority (0=default)\n\
820 b\t\t\tBeep when a message arrives\n\
821 e\t\t\tHide from display if empty\n\
822 f\t\t\tShow flagged messages if there are no new ones\n\
823 h\t\t\tHide from display\n\
824 s\t\t\tShow message snippets\n\
825 t\t\t\tHighlight the entry\n\
827 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
828 It can be freely distributed and used according to the GNU GPL v2.\n\
834 parse_options(char *c)
838 if (sep = strchr(c, '='))
840 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
841 memcpy(n->pattern, c, sep-c);
842 n->pattern[sep-c] = 0;
843 clist_add_tail(&options, &n->n);
853 if (x >= '0' && x <= '9')
854 o->priority = x - '0';
857 int value = !!islower(x);
864 o->hide_if_empty = value;
867 o->show_flagged = value;
876 o->highlight = value;
879 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
886 main(int argc, char **argv)
889 clist_init(&options);
890 clist_init(&patterns);
893 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
897 check_interval = atol(optarg);
898 if (check_interval <= 0)
911 parse_options(optarg);
914 minimum_priority = atol(optarg);
919 while (optind < argc)
920 add_pattern(argv[optind++]);
929 time_t now = time(NULL);
930 int remains = last_scan_time + check_interval - now;
931 if (remains <= 0 || force_refresh)
936 halfdelay((remains > 255) ? 255 : remains);
945 move_cursor(cursor_at+1);
949 move_cursor(cursor_at-1);
959 move_cursor(cursor_max-1);
962 next_active(cursor_at+1, 1);
965 next_active(cursor_at-1, -1);
969 if (cursor_at < cursor_max)
971 struct mbox *b = mbox_array[cursor_at];
972 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
973 sprintf(cmd, run_cmd, b->path);
979 b->force_refresh = 1;
984 clearok(stdscr, TRUE);
993 print_status("Bells and whistles are now enabled. Toot!");
997 print_status("Bells and whistles are now disabled. Pssst!");
1000 if (ch >= '0' && ch <= '9')
1002 minimum_priority = ch - '0';
1006 debug("Pressed unknown key %d\n", ch);