2 * Incoming Mail Checker
4 * (c) 2005--2008 Martin Mares <mj@ucw.cz>
21 #include <sys/types.h>
24 #ifdef CONFIG_WIDE_CURSES
25 #include <ncursesw/ncurses.h>
34 static int check_interval = 30;
35 static int force_refresh;
36 static int allow_bells = 1;
37 static int minimum_priority;
38 static time_t last_scan_time;
39 static char *run_cmd = "mutt -f %s";
67 static clist options, patterns;
68 static struct options global_options = {
81 int last_size, last_pos;
82 int total, new, flagged;
83 int last_total, last_new, last_flagged;
91 static struct mbox **mbox_array;
92 static int num_mboxes, mbox_array_size;
94 static void redraw_line(int i);
95 static void rethink_display(void);
98 add_pattern(char *patt)
100 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
101 strcpy(n->pattern, patt);
102 if (patt = strchr(n->pattern, '='))
109 clist_add_tail(&patterns, &n->n);
115 struct passwd *p = getpwuid(getuid());
117 die("You don't exist, go away!");
118 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
119 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
124 init_options(struct options *o)
128 o->hide_if_empty = -1;
132 o->show_flagged = -1;
133 o->sender_personal = -1;
140 setup_options(struct mbox *b)
142 b->o = global_options;
143 CLIST_FOR_EACH(struct option_node *, n, options)
144 if (!fnmatch(n->pattern, b->name, 0))
146 debug("\tApplied options %s\n", n->pattern);
147 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
150 MERGE(hide_if_empty);
155 MERGE(sender_personal);
163 mbox_name(char *path)
165 char *c = strrchr(path, '/');
166 return c ? (c+1) : path;
170 add_mbox(clist *l, char *path, char *name)
172 struct mbox *b = xmalloc(sizeof(*b));
173 bzero(b, sizeof(*b));
174 b->path = xstrdup(path);
175 b->name = xstrdup(name);
179 cnode *prev = l->head.prev;
180 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
182 clist_insert_after(&b->n, prev);
185 clist_add_tail(l, &b->n);
191 del_mbox(struct mbox *b)
200 find_mbox(clist *l, char *path)
202 CLIST_FOR_EACH(struct mbox *, b, *l)
203 if (!strcmp(b->path, path))
209 mbox_active_p(struct mbox *b)
211 if (b->o.priority < minimum_priority)
219 mbox_visible_p(struct mbox *b)
221 if (!mbox_active_p(b))
225 if (b->o.hide_if_empty && !b->total)
231 prepare_snippet(struct mbox *b, char *sender, char *subject)
233 while (*sender == ' ' || *sender == '\t')
235 while (*subject == ' ' || *subject == '\t')
238 char *pos = b->snippet;
239 char *term = b->snippet + sizeof(b->snippet) - 1;
240 if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
242 add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
243 add_snippet(&pos, term, ": ");
246 add_subject_snippet(&pos, term, subject);
248 add_snippet(&pos, term, "No subject");
251 static int mb_fd, mb_pos;
252 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
257 mb_cc = mb_end = mb_buf;
264 lseek(mb_fd, pos, SEEK_SET);
271 return mb_pos - (mb_end - mb_cc);
277 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
286 mb_end = mb_buf + len;
295 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
306 mb_check(const char *p, int len)
310 if (mb_get() != *p++)
317 scan_mbox(struct mbox *b, struct stat *st)
319 char buf[1024], sender[1024], subject[1024];
322 const char from[] = "\nFrom ";
326 b->total = b->new = b->flagged = 0;
331 /* FIXME: Should we do some locking? */
333 mb_fd = open(b->path, O_RDONLY);
336 debug("[open failed: %m] ");
337 b->total = b->new = b->flagged = -1;
342 c = read(mb_fd, signature, 2);
343 lseek(mb_fd, 0, SEEK_SET);
345 if (c == 2 && !memcmp(signature, "\037\213", 2)) //gzip
347 debug("[decompressing] ");
350 die("pipe failed: %m");
353 die("fork failed: %m");
356 if (dup2(mb_fd, 0) < 0 || dup2(fds[1], 1) < 0)
357 die("dup2 failed: %m");
361 execlp("gzip", "gzip", "-cd", NULL);
362 die("Cannot execute gzip: %m");
372 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && !compressed)
374 mb_seek(b->last_pos);
375 if (mb_check(from, 6))
377 debug("[incremental] ");
382 debug("[incremental failed] ");
388 if (!mb_check(from+1, 5))
390 debug("[inconsistent] ");
391 b->total = b->new = b->flagged = -1;
394 b->total = b->new = b->flagged = 0;
395 b->last_total = b->last_new = b->last_flagged = 0;
396 b->snippet_is_new = 0;
400 b->total = b->last_total;
401 b->new = b->last_new;
402 b->flagged = b->last_flagged;
407 b->last_pos = mb_tell() - 5;
409 b->last_pos--; // last_pos should be the previous \n character
410 b->last_total = b->total;
411 b->last_new = b->new;
412 b->last_flagged = b->flagged;
413 while ((c = mb_get()) >= 0 && c != '\n')
428 debug("[truncated] ");
439 while (c == ' ' || c == '\t');
447 if (i < sizeof(buf) - 1)
453 if (!strncasecmp(buf, "Status:", 7))
455 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
457 else if (!strncasecmp(buf, "From:", 5))
458 strcpy(sender, buf+5);
459 else if (!strncasecmp(buf, "Subject:", 8))
460 strcpy(subject, buf+8);
468 if (new || (flagged && !b->snippet_is_new))
470 b->snippet_is_new = new;
471 prepare_snippet(b, sender, subject);
490 if (wait(&status) < 0 || !WIFEXITED(status) || WEXITSTATUS(status))
491 b->total = b->new = b->flagged = -1;
498 debug("Searching for mailboxes...\n");
499 last_scan_time = time(NULL);
500 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
502 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
504 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
505 if (err && err != GLOB_NOMATCH)
506 die("Failed to glob %s: %m", p->pattern);
507 for (uns i=0; i<g.gl_pathc; i++)
509 char *name = g.gl_pathv[i];
510 struct mbox *b = find_mbox(&mboxes, name);
513 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
514 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
524 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
530 debug("Lost mailbox %s\n", b->name);
537 debug("Scanning mailboxes...\n");
538 CLIST_FOR_EACH(struct mbox *, b, mboxes)
541 debug("%s: ", b->name);
542 if (!mbox_active_p(b))
548 b->force_refresh = 1;
549 if (stat(b->path, &st) < 0)
551 b->total = b->new = b->flagged = -1;
554 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
557 redraw_line(b->index);
561 b->last_time = st.st_mtime;
562 b->last_size = st.st_size;
563 debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
566 redraw_line(b->index);
570 debug("not changed\n");
571 b->force_refresh = 0;
575 debug("Scan finished\n");
576 last_scan_time = time(NULL);
582 #include <X11/Xlib.h>
584 static Display *leds_dpy;
585 static unsigned leds_care, leds_have, leds_want;
591 CLIST_FOR_EACH(struct option_node *, o, options)
593 leds_care |= (1 << o->o.led);
596 debug("LEDS: No mailbox wants them\n");
599 if (!getenv("DISPLAY"))
601 debug("LEDS: Do not have X display\n");
604 if (!(leds_dpy = XOpenDisplay(NULL)))
605 die("Cannot open X display, although the DISPLAY variable is set");
613 if (leds_want == leds_have)
616 debug("LEDS: have %02x, want %02x, care %02x\n", leds_have, leds_want, leds_care);
617 for (int i=1; i<10; i++)
618 if (leds_care & (leds_have ^ leds_want) & (1 << i))
622 cc.led_mode = (leds_want & (1 << i)) ? LedModeOn : LedModeOff;
623 XChangeKeyboardControl(leds_dpy, KBLed | KBLedMode, &cc);
626 leds_have = leds_want;
636 CLIST_FOR_EACH(struct mbox *, b, mboxes)
637 if (b->o.led > 0 && b->new)
638 leds_want |= (1 << b->o.led);
654 static void leds_init(void) { }
655 static void rethink_leds(void) { }
656 static void leds_cleanup(void) { }
660 static int cursor_at, cursor_max;
670 static int attrs[2][2][M_MAX]; // active, hilite, status
678 struct mbox *b = mbox_array[i];
679 int cc = (cursor_at == i);
680 int hi = b->o.highlight;
682 attrset(attrs[cc][hi][M_IDLE]);
684 printw("%c ", b->o.hotkey);
690 attrset(attrs[cc][hi][M_NEW]);
691 else if (b->flagged && b->o.show_flagged)
692 attrset(attrs[cc][hi][M_FLAG]);
693 printw("%-20s ", b->name);
696 else if (b->scanning)
698 attrset(attrs[cc][hi][M_SCAN]);
699 printw("[SCANNING]");
701 else if (b->total < 0)
703 attrset(attrs[cc][hi][M_BAD]);
708 attrset(attrs[cc][hi][M_IDLE]);
709 printw("%6d ", b->total);
713 attrset(attrs[cc][hi][M_NEW]);
714 printw("%6d ", b->new);
715 attrset(attrs[cc][hi][M_IDLE]);
716 int age = (last_scan_time - b->last_time);
720 printw("%2d min ", age/60);
721 else if (age < 86400)
722 printw("%2d hrs ", age/3600);
727 else if (b->flagged && b->o.show_flagged)
729 attrset(attrs[cc][hi][M_FLAG]);
730 printw("%6d ", b->flagged);
731 attrset(attrs[cc][hi][M_IDLE]);
733 attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */
736 if (snip && b->o.snippets && b->snippet[0])
739 getyx(stdscr, yy, xx);
740 int remains = COLS-1-xx;
743 #ifdef CONFIG_WIDE_CURSES
744 size_t len = strlen(b->snippet)+1;
746 mbstowcs(snip, b->snippet, len);
747 addnwstr(snip, remains);
749 printw("%-.*s", remains, b->snippet);
755 attrset(attrs[0][0][M_IDLE]);
762 cursor_max = num_mboxes;
763 if (cursor_max > LINES-1)
764 cursor_max = LINES-1;
765 if (cursor_at >= cursor_max)
766 cursor_at = cursor_max - 1;
770 for (int i=0; i<cursor_max; i++)
775 printw("(no mailboxes found)");
783 rethink_display(void)
788 CLIST_FOR_EACH(struct mbox *, b, mboxes)
789 if (mbox_visible_p(b))
792 if (i >= num_mboxes || mbox_array[i] != b)
795 if (i >= mbox_array_size)
797 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
798 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
802 if (b->o.beep && b->new > b->last_beep_new)
804 b->last_beep_new = b->new;
817 if (beeeep && allow_bells)
828 intrflush(stdscr, FALSE);
829 keypad(stdscr, TRUE);
832 static const int attrs_mono[2][M_MAX] = {
833 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
834 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
836 for (int i=0; i<2; i++)
837 for (int j=0; j<M_MAX; j++)
839 attrs[0][i][j] = attrs_mono[i][j];
840 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
846 if (COLOR_PAIRS >= 5)
848 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
849 init_pair(2, COLOR_RED, COLOR_BLACK);
850 init_pair(3, COLOR_WHITE, COLOR_BLUE);
851 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
852 init_pair(5, COLOR_RED, COLOR_BLUE);
853 init_pair(6, COLOR_GREEN, COLOR_BLACK);
854 init_pair(7, COLOR_GREEN, COLOR_BLUE);
855 static const int attrs_color[2][2][M_MAX] = {
856 [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) },
857 [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 },
858 [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) },
859 [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 },
861 memcpy(attrs, attrs_color, sizeof(attrs));
873 print_status(char *status)
877 printw("%s", status);
883 scan_and_redraw(void)
885 print_status("Busy...");
893 if (i >= 0 && i < cursor_max && i != cursor_at)
903 next_active(int since, int step)
907 since = (since+cursor_max) % cursor_max;
908 step = (step+cursor_max) % cursor_max;
914 struct mbox *b = mbox_array[i];
915 if (b->new && b->o.priority > bestp)
918 bestp = b->o.priority;
920 i = (i+step) % cursor_max;
928 mbox_run(struct mbox *b)
930 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
931 sprintf(cmd, run_cmd, b->path);
937 b->force_refresh = 1;
942 #define STR(c) STR2(c)
947 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
950 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
951 -d\t\t\tLog debug messages to stderr\n\
952 -i\t\t\tInclude user's INBOX\n\
953 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
954 -o <pattern>=<opts>\tSet mailbox options\n\
955 -o <opts>\t\tSet default options for all mailboxes\n\
956 -p <pri>\t\tSet minimum priority to show\n\
958 Mailbox options (set with `-o', use upper case to negate):\n\
959 0-9\t\t\tSet mailbox priority (0=default)\n\
960 b\t\t\tBeep when a message arrives\n\
961 e\t\t\tHide from display if empty\n\
962 f\t\t\tShow flagged messages if there are no new ones\n\
963 h\t\t\tHide from display\n\
964 l<led>\t\t\tLight a keyboard led (1-9) if running on X display\n\
965 m\t\t\tShow mailbox name of the sender\n\
966 p\t\t\tShow personal info (full name) of the sender\n\
967 s\t\t\tShow message snippets\n\
968 t\t\t\tHighlight the entry\n\
969 !<key>\t\t\tSet hot key\n\
971 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
972 It can be freely distributed and used according to the GNU GPL v2.\n\
978 parse_options(char *c)
982 if (sep = strchr(c, '='))
984 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
985 memcpy(n->pattern, c, sep-c);
986 n->pattern[sep-c] = 0;
987 clist_add_tail(&options, &n->n);
997 if (x >= '0' && x <= '9')
998 o->priority = x - '0';
999 else if (x == '!' && *c)
1001 else if (x == 'l' && *c >= '1' && *c <= '9')
1002 o->led = *c++ - '0';
1005 int value = !!islower(x);
1012 o->hide_if_empty = value;
1015 o->show_flagged = value;
1021 o->sender_mbox = value;
1024 o->sender_personal = value;
1027 o->snippets = value;
1030 o->highlight = value;
1033 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
1040 main(int argc, char **argv)
1042 clist_init(&mboxes);
1043 clist_init(&options);
1044 clist_init(&patterns);
1047 while ((c = getopt(argc, argv, "c:dim:o:p:")) >= 0)
1051 check_interval = atol(optarg);
1052 if (check_interval <= 0)
1065 parse_options(optarg);
1068 minimum_priority = atol(optarg);
1073 while (optind < argc)
1074 add_pattern(argv[optind++]);
1082 int should_exit = 0;
1084 while (!should_exit)
1086 time_t now = time(NULL);
1087 int remains = last_scan_time + check_interval - now;
1088 if (remains <= 0 || force_refresh)
1093 halfdelay((remains > 255) ? 255 : remains);
1095 for (int i=0; i<num_mboxes; i++)
1096 if (ch == mbox_array[i]->o.hotkey)
1100 mbox_run(mbox_array[i]);
1110 move_cursor(cursor_at+1);
1114 move_cursor(cursor_at-1);
1124 move_cursor(cursor_max-1);
1127 next_active(cursor_at+1, 1);
1130 next_active(cursor_at-1, -1);
1134 if (cursor_at < cursor_max)
1135 mbox_run(mbox_array[cursor_at]);
1138 clearok(stdscr, TRUE);
1147 print_status("Bells and whistles are now enabled. Toot!");
1151 print_status("Bells and whistles are now disabled. Pssst!");
1154 if (ch >= '0' && ch <= '9')
1156 minimum_priority = ch - '0';
1160 debug("Pressed unknown key %d\n", ch);