2 * Incoming Mail Checker
4 * (c) 2005 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";
52 static clist options, patterns;
53 static struct options global_options;
64 int last_size, last_pos;
66 int last_total, last_new;
73 static struct mbox **mbox_array;
74 static int num_mboxes, mbox_array_size;
76 static void redraw_line(int i);
77 static void rethink_display(void);
80 add_pattern(char *patt)
82 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
83 strcpy(n->pattern, patt);
84 if (patt = strchr(n->pattern, '='))
91 clist_add_tail(&patterns, &n->n);
97 struct passwd *p = getpwuid(getuid());
99 die("You don't exist, go away!");
100 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
101 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
106 init_options(struct options *o)
110 o->hide_if_empty = -1;
117 setup_options(struct mbox *b)
119 b->o = global_options;
120 CLIST_FOR_EACH(struct option_node *, n, options)
121 if (!fnmatch(n->pattern, b->name, 0))
123 debug("\tApplied options %s\n", n->pattern);
124 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
127 MERGE(hide_if_empty);
135 mbox_name(char *path)
137 char *c = strrchr(path, '/');
138 return c ? (c+1) : path;
142 add_mbox(clist *l, char *path, char *name)
144 struct mbox *b = xmalloc(sizeof(*b));
145 bzero(b, sizeof(*b));
146 b->path = xstrdup(path);
147 b->name = xstrdup(name);
151 cnode *prev = l->head.prev;
152 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
154 clist_insert_after(&b->n, prev);
157 clist_add_tail(l, &b->n);
163 del_mbox(struct mbox *b)
172 find_mbox(clist *l, char *path)
174 CLIST_FOR_EACH(struct mbox *, b, *l)
175 if (!strcmp(b->path, path))
181 mbox_active_p(struct mbox *b)
183 if (b->o.priority < minimum_priority)
191 mbox_visible_p(struct mbox *b)
193 if (!mbox_active_p(b))
197 if (b->o.hide_if_empty && !b->total)
203 add_snippet(char **ppos, char *term, unsigned char *add)
207 while (*add && pos < term)
215 else if (*add >= 0x7f)
232 prepare_snippet(struct mbox *b, char *sender, char *subject)
234 while (*sender == ' ' || *sender == '\t')
236 while (*subject == ' ' || *subject == '\t')
239 char *pos = b->snippet;
240 char *term = b->snippet + sizeof(b->snippet) - 1;
243 add_snippet(&pos, term, sender);
244 add_snippet(&pos, term, ": ");
247 add_snippet(&pos, term, subject);
249 add_snippet(&pos, term, "No subject");
252 static int mb_fd, mb_pos;
253 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
258 mb_cc = mb_end = mb_buf;
265 lseek(mb_fd, pos, SEEK_SET);
272 return mb_pos - (mb_end - mb_cc);
278 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
287 mb_end = mb_buf + len;
296 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
307 mb_check(const char *p, int len)
311 if (mb_get() != *p++)
318 scan_mbox(struct mbox *b, struct stat *st)
320 char buf[1024], sender[1024], subject[1024];
322 const char from[] = "\nFrom ";
326 b->total = b->new = 0;
331 /* FIXME: Locking! */
333 mb_fd = open(b->path, O_RDONLY);
336 debug("[open failed: %m] ");
337 b->total = b->new = -1;
343 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh)
345 mb_seek(b->last_pos);
346 if (mb_check(from, 6))
348 debug("[incremental] ");
353 debug("[incremental failed] ");
359 if (!mb_check(from+1, 5))
361 debug("[inconsistent] ");
362 b->total = b->new = -1;
365 b->total = b->new = 0;
366 b->last_total = b->last_new = 0;
370 b->total = b->last_total;
371 b->new = b->last_new;
376 b->last_pos = mb_tell() - 5;
378 b->last_pos--; // last_pos should be the previous \n character
379 b->last_total = b->total;
380 b->last_new = b->new;
381 while ((c = mb_get()) >= 0 && c != '\n')
395 debug("[truncated] ");
406 while (c == ' ' || c == '\t');
414 if (i < sizeof(buf) - 1)
420 if (!strncasecmp(buf, "Status:", 7))
422 else if (!strncasecmp(buf, "From:", 5))
423 strcpy(sender, buf+5);
424 else if (!strncasecmp(buf, "Subject:", 8))
425 strcpy(subject, buf+8);
432 prepare_snippet(b, sender, subject);
453 debug("Searching for mailboxes...\n");
454 last_scan_time = time(NULL);
455 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
457 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
459 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
460 if (err && err != GLOB_NOMATCH)
461 die("Failed to glob %s: %m", p->pattern);
462 for (uns i=0; i<g.gl_pathc; i++)
464 char *name = g.gl_pathv[i];
465 struct mbox *b = find_mbox(&mboxes, name);
468 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
469 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
479 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
485 debug("Lost mailbox %s\n", b->name);
492 debug("Scanning mailboxes...\n");
493 CLIST_FOR_EACH(struct mbox *, b, mboxes)
496 debug("%s: ", b->name);
497 if (!mbox_active_p(b))
503 b->force_refresh = 1;
504 if (stat(b->path, &st) < 0)
506 b->total = b->new = -1;
509 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
512 redraw_line(b->index);
516 b->last_time = st.st_mtime;
517 b->last_size = st.st_size;
518 debug("%d %d (stopped at %d of %d)\n", b->total, b->new, b->last_pos, b->last_size);
521 redraw_line(b->index);
525 debug("not changed\n");
526 b->force_refresh = 0;
530 debug("Scan finished\n");
531 last_scan_time = time(NULL);
535 static int cursor_at, cursor_max;
544 static int attrs[2][2][M_MAX]; // active, hilite, status
552 struct mbox *b = mbox_array[i];
553 int cc = (cursor_at == i);
554 int hi = b->o.highlight;
556 attrset(attrs[cc][hi][M_IDLE]);
562 attrset(attrs[cc][hi][M_NEW]);
563 printw("%-20s ", b->name);
566 else if (b->scanning)
568 attrset(attrs[cc][hi][M_SCAN]);
569 printw("[SCANNING]");
571 else if (b->total < 0)
573 attrset(attrs[cc][hi][M_BAD]);
578 attrset(attrs[cc][hi][M_IDLE]);
579 printw("%6d ", b->total);
582 attrset(attrs[cc][hi][M_NEW]);
583 printw("%6d ", b->new);
584 attrset(attrs[cc][hi][M_IDLE]);
585 int age = (last_scan_time - b->last_time);
589 printw("%2d min ", age/60);
590 else if (age < 86400)
591 printw("%2d hrs ", age/3600);
594 if (b->o.snippets && b->snippet[0])
597 getyx(stdscr, yy, xx);
598 int remains = COLS-1-xx;
600 printw("%-.*s", remains, b->snippet);
605 attrset(attrs[0][0][M_IDLE]);
612 cursor_max = num_mboxes;
613 if (cursor_max > LINES-1)
614 cursor_max = LINES-1;
615 if (cursor_at >= cursor_max)
616 cursor_at = cursor_max - 1;
620 for (int i=0; i<cursor_max; i++)
625 printw("(no mailboxes found)");
633 rethink_display(void)
638 CLIST_FOR_EACH(struct mbox *, b, mboxes)
639 if (mbox_visible_p(b))
642 if (i >= num_mboxes || mbox_array[i] != b)
645 if (i >= mbox_array_size)
647 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
648 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
652 if (b->o.beep && b->new > b->last_beep_new)
654 b->last_beep_new = b->new;
666 if (beeeep && allow_bells)
677 intrflush(stdscr, FALSE);
678 keypad(stdscr, TRUE);
681 static const int attrs_mono[2][M_MAX] = {
682 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_BAD] = A_DIM },
683 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_BAD] = A_DIM },
685 for (int i=0; i<2; i++)
686 for (int j=0; j<M_MAX; j++)
688 attrs[0][i][j] = attrs_mono[i][j];
689 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
695 if (COLOR_PAIRS >= 5)
697 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
698 init_pair(2, COLOR_RED, COLOR_BLACK);
699 init_pair(3, COLOR_WHITE, COLOR_BLUE);
700 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
701 init_pair(5, COLOR_RED, COLOR_BLUE);
702 static const int attrs_color[2][2][M_MAX] = {
703 [0][0] = { [M_IDLE] = 0, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1), [M_BAD] = COLOR_PAIR(2) },
704 [0][1] = { [M_IDLE] = A_BOLD, [M_SCAN] = COLOR_PAIR(1), [M_NEW] = COLOR_PAIR(1) | A_BOLD, [M_BAD] = COLOR_PAIR(2) | A_BOLD },
705 [1][0] = { [M_IDLE] = COLOR_PAIR(3), [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4), [M_BAD] = COLOR_PAIR(5) },
706 [1][1] = { [M_IDLE] = COLOR_PAIR(3) | A_BOLD, [M_SCAN] = COLOR_PAIR(4), [M_NEW] = COLOR_PAIR(4) | A_BOLD, [M_BAD] = COLOR_PAIR(5) | A_BOLD },
708 memcpy(attrs, attrs_color, sizeof(attrs));
720 print_status(char *status)
724 printw("%s", status);
730 scan_and_redraw(void)
732 print_status("Busy...");
740 if (i >= 0 && i < cursor_max && i != cursor_at)
750 next_active(int since, int step)
754 since = (since+cursor_max) % cursor_max;
755 step = (step+cursor_max) % cursor_max;
761 struct mbox *b = mbox_array[i];
762 if (b->new && b->o.priority > bestp)
765 bestp = b->o.priority;
767 i = (i+step) % cursor_max;
775 #define STR(c) STR2(c)
780 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
783 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
784 -d\t\t\tLog debug messages to stderr\n\
785 -i\t\t\tInclude user's INBOX\n\
786 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
787 -o <pattern>=<opts>\tSet mailbox options\n\
788 -o <opts>\t\tSet default options for all mailboxes\n\
789 -p <pri>\t\tSet minimum priority to show\n\
791 Mailbox options (set with `-o', use upper case to negate):\n\
792 0-9\t\t\tSet mailbox priority (0=default)\n\
793 b\t\t\tBeep when a message arrives\n\
794 e\t\t\tHide from display if empty\n\
795 h\t\t\tHide from display\n\
796 s\t\t\tShow message snippets\n\
797 t\t\t\tHighlight the entry\n\
799 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
800 It can be freely distributed and used according to the GNU GPL v2.\n\
806 parse_options(char *c)
810 if (sep = strchr(c, '='))
812 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
813 memcpy(n->pattern, c, sep-c);
814 n->pattern[sep-c] = 0;
815 clist_add_tail(&options, &n->n);
825 if (x >= '0' && x <= '9')
826 o->priority = x - '0';
829 int value = !!islower(x);
836 o->hide_if_empty = value;
845 o->highlight = value;
848 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
855 main(int argc, char **argv)
858 clist_init(&options);
859 clist_init(&patterns);
862 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
866 check_interval = atol(optarg);
867 if (check_interval <= 0)
880 parse_options(optarg);
883 minimum_priority = atol(optarg);
888 while (optind < argc)
889 add_pattern(argv[optind++]);
898 time_t now = time(NULL);
899 int remains = last_scan_time + check_interval - now;
900 if (remains <= 0 || force_refresh)
905 halfdelay((remains > 255) ? 255 : remains);
914 move_cursor(cursor_at+1);
918 move_cursor(cursor_at-1);
928 move_cursor(cursor_max-1);
931 next_active(cursor_at+1, 1);
934 next_active(cursor_at-1, -1);
938 if (cursor_at < cursor_max)
940 struct mbox *b = mbox_array[cursor_at];
941 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
942 sprintf(cmd, run_cmd, b->path);
948 b->force_refresh = 1;
953 clearok(stdscr, TRUE);
962 print_status("Bells and whistles are now enabled. Toot!");
966 print_status("Bells and whistles are now disabled. Pssst!");
969 if (ch >= '0' && ch <= '9')
971 minimum_priority = ch - '0';
975 debug("Pressed unknown key %d\n", ch);