2 * Incoming Mail Checker
4 * (c) 2005--2010 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 allow_osd = 1;
38 static int minimum_priority;
39 static time_t last_scan_time;
40 static char *run_cmd = "mutt -f %s";
69 static clist options, patterns;
70 static struct options global_options = {
83 int last_size, last_pos;
84 int total, new, flagged;
85 int last_total, last_new, last_flagged;
93 static struct mbox **mbox_array;
94 static int num_mboxes, mbox_array_size;
102 static clist osd_opts;
104 static void redraw_line(int i);
105 static void rethink_display(void);
108 add_pattern(char *patt)
110 struct pattern_node *n = xmalloc(sizeof(*n) + strlen(patt));
111 strcpy(n->pattern, patt);
112 if (patt = strchr(n->pattern, '='))
119 clist_add_tail(&patterns, &n->n);
125 struct passwd *p = getpwuid(getuid());
127 die("You don't exist, go away!");
128 char buf[sizeof("/var/mail/") + strlen(p->pw_name) + 7];
129 sprintf(buf, "/var/mail/%s=INBOX", p->pw_name);
134 init_options(struct options *o)
138 o->hide_if_empty = -1;
142 o->show_flagged = -1;
143 o->sender_personal = -1;
151 setup_options(struct mbox *b)
153 b->o = global_options;
154 CLIST_FOR_EACH(struct option_node *, n, options)
155 if (!fnmatch(n->pattern, b->name, 0))
157 debug("\tApplied options %s\n", n->pattern);
158 #define MERGE(f) if (n->o.f >= 0) b->o.f = n->o.f
161 MERGE(hide_if_empty);
166 MERGE(sender_personal);
175 add_osd_opt(char *arg)
177 struct osd_opt_node *n = xmalloc(sizeof(*n) + strlen(arg));
179 n->val = strchr(n->key, '=');
181 die("Malformed OSD option");
183 clist_add_tail(&osd_opts, &n->n);
187 mbox_name(char *path)
189 char *c = strrchr(path, '/');
190 return c ? (c+1) : path;
194 add_mbox(clist *l, char *path, char *name)
196 struct mbox *b = xmalloc(sizeof(*b));
197 bzero(b, sizeof(*b));
198 b->path = xstrdup(path);
199 b->name = xstrdup(name);
203 cnode *prev = l->head.prev;
204 while (prev != &l->head && strcmp(((struct mbox *)prev)->name, name) > 0)
206 clist_insert_after(&b->n, prev);
209 clist_add_tail(l, &b->n);
215 del_mbox(struct mbox *b)
224 find_mbox(clist *l, char *path)
226 CLIST_FOR_EACH(struct mbox *, b, *l)
227 if (!strcmp(b->path, path))
233 mbox_active_p(struct mbox *b)
235 if (b->o.priority < minimum_priority)
243 mbox_visible_p(struct mbox *b)
245 if (!mbox_active_p(b))
249 if (b->o.hide_if_empty && !b->total)
255 prepare_snippet(struct mbox *b, char *sender, char *subject)
257 while (*sender == ' ' || *sender == '\t')
259 while (*subject == ' ' || *subject == '\t')
262 char *pos = b->snippet;
263 char *term = b->snippet + sizeof(b->snippet) - 1;
264 if (sender[0] && (b->o.sender_mbox || b->o.sender_personal))
266 add_addr_snippet(&pos, term, sender, b->o.sender_mbox, b->o.sender_personal);
267 add_snippet(&pos, term, ": ");
270 add_subject_snippet(&pos, term, subject);
272 add_snippet(&pos, term, "No subject");
275 static int mb_fd, mb_pos;
276 static unsigned char mb_buf[4096], *mb_cc, *mb_end;
281 mb_cc = mb_end = mb_buf;
288 lseek(mb_fd, pos, SEEK_SET);
295 return mb_pos - (mb_end - mb_cc);
301 int len = read(mb_fd, mb_buf, sizeof(mb_buf));
310 mb_end = mb_buf + len;
319 return (mb_cc < mb_end) ? *mb_cc++ : mb_ll_get();
330 mb_check(const char *p, int len)
334 if (mb_get() != *p++)
341 scan_mbox(struct mbox *b, struct stat *st)
343 char buf[1024], sender[1024], subject[1024];
346 const char from[] = "\nFrom ";
350 b->total = b->new = b->flagged = 0;
355 /* FIXME: Should we do some locking? */
357 mb_fd = open(b->path, O_RDONLY);
360 debug("[open failed: %m] ");
361 b->total = b->new = b->flagged = -1;
366 c = read(mb_fd, signature, 2);
367 lseek(mb_fd, 0, SEEK_SET);
369 if (c == 2 && !memcmp(signature, "\037\213", 2)) //gzip
371 debug("[decompressing] ");
374 die("pipe failed: %m");
377 die("fork failed: %m");
380 if (dup2(mb_fd, 0) < 0 || dup2(fds[1], 1) < 0)
381 die("dup2 failed: %m");
385 execlp("gzip", "gzip", "-cd", NULL);
386 die("Cannot execute gzip: %m");
396 if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && !compressed)
398 mb_seek(b->last_pos);
399 if (mb_check(from, 6))
401 debug("[incremental] ");
406 debug("[incremental failed] ");
412 if (!mb_check(from+1, 5))
414 debug("[inconsistent] ");
415 b->total = b->new = b->flagged = -1;
418 b->total = b->new = b->flagged = 0;
419 b->last_total = b->last_new = b->last_flagged = 0;
420 b->snippet_is_new = 0;
424 b->total = b->last_total;
425 b->new = b->last_new;
426 b->flagged = b->last_flagged;
431 b->last_pos = mb_tell() - 5;
433 b->last_pos--; // last_pos should be the previous \n character
434 b->last_total = b->total;
435 b->last_new = b->new;
436 b->last_flagged = b->flagged;
437 while ((c = mb_get()) >= 0 && c != '\n')
452 debug("[truncated] ");
463 while (c == ' ' || c == '\t');
471 if (i < sizeof(buf) - 1)
477 if (!strncasecmp(buf, "Status:", 7))
479 else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
481 else if (!strncasecmp(buf, "From:", 5))
482 strcpy(sender, buf+5);
483 else if (!strncasecmp(buf, "Subject:", 8))
484 strcpy(subject, buf+8);
492 if (new || (flagged && !b->snippet_is_new))
494 b->snippet_is_new = new;
495 prepare_snippet(b, sender, subject);
514 if (wait(&status) < 0 || !WIFEXITED(status) || WEXITSTATUS(status))
515 b->total = b->new = b->flagged = -1;
522 debug("Searching for mailboxes...\n");
523 last_scan_time = time(NULL);
524 CLIST_FOR_EACH(struct pattern_node *, p, patterns)
526 debug("Trying pattern %s (name %s)\n", p->pattern, p->name);
528 int err = glob(p->pattern, GLOB_ERR | GLOB_NOSORT | GLOB_TILDE | GLOB_TILDE_CHECK, NULL, &g);
529 if (err && err != GLOB_NOMATCH)
530 die("Failed to glob %s: %m", p->pattern);
531 for (uns i=0; i<g.gl_pathc; i++)
533 char *name = g.gl_pathv[i];
534 struct mbox *b = find_mbox(&mboxes, name);
537 b = add_mbox(&mboxes, name, (p->name ? p->name : mbox_name(name)));
538 debug("Discovered mailbox %s (%s)\n", b->name, b->path);
548 CLIST_FOR_EACH_DELSAFE(struct mbox *, b, mboxes, tmp)
554 debug("Lost mailbox %s\n", b->name);
561 debug("Scanning mailboxes...\n");
562 CLIST_FOR_EACH(struct mbox *, b, mboxes)
565 debug("%s: ", b->name);
566 if (!mbox_active_p(b))
572 b->force_refresh = 1;
573 if (stat(b->path, &st) < 0)
575 b->total = b->new = b->flagged = -1;
578 else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
581 redraw_line(b->index);
585 b->last_time = st.st_mtime;
586 b->last_size = st.st_size;
587 debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
590 redraw_line(b->index);
594 debug("not changed\n");
595 b->force_refresh = 0;
599 debug("Scan finished\n");
600 last_scan_time = time(NULL);
606 #include <X11/Xlib.h>
607 #include <X11/Xatom.h>
609 static Display *x11_dpy;
610 static unsigned leds_care, leds_have, leds_want;
611 static unsigned osd_care, osd_have, osd_want;
617 leds_care = (global_options.led >= 0 ? (1 << global_options.led) : 0);
618 osd_care = (global_options.osd >= 0);
619 CLIST_FOR_EACH(struct option_node *, o, options)
622 leds_care |= (1 << o->o.led);
627 if (!leds_care && !osd_care)
629 debug("X11: No mailbox wants LEDs or OSD\n");
632 if (!getenv("DISPLAY"))
634 debug("X11: Do not have X display\n");
637 if (!(x11_dpy = XOpenDisplay(NULL)))
638 die("Cannot open X display, although the DISPLAY variable is set");
642 osd_pty = XInternAtom(x11_dpy, "OSD_QUEUE", False);
644 die("Cannot intern OSD_QUEUE atom");
646 // If OSD options contain no message, add one
648 CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
652 add_osd_opt("=You have new mail");
656 debug("X11: Initialized\n");
662 if (leds_want == leds_have)
665 debug("LEDS: have %02x, want %02x, care %02x\n", leds_have, leds_want, leds_care);
666 for (int i=1; i<10; i++)
667 if (leds_care & (leds_have ^ leds_want) & (1 << i))
671 cc.led_mode = (leds_want & (1 << i)) ? LedModeOn : LedModeOff;
672 XChangeKeyboardControl(x11_dpy, KBLed | KBLedMode, &cc);
675 leds_have = leds_want;
681 if (!osd_want || !allow_osd)
688 debug("OSD: Displaying\n");
693 CLIST_FOR_EACH(struct osd_opt_node *, n, osd_opts)
695 pos += snprintf(msg+pos, sizeof(msg)-pos-1, "%s:%s\n", n->key, n->val);
696 if (pos > sizeof(msg)-1)
698 pos = sprintf(msg, "OSD message too long!\n");
704 XChangeProperty(x11_dpy, DefaultRootWindow(x11_dpy), osd_pty, XA_STRING, 8, PropModeAppend, (unsigned char *) msg, pos);
716 CLIST_FOR_EACH(struct mbox *, b, mboxes)
718 if (b->o.led > 0 && b->new)
719 leds_want |= (1 << b->o.led);
720 if (b->o.osd > 0 && b->new)
739 static void x11_init(void) { }
740 static void rethink_leds(void) { }
741 static void x11_cleanup(void) { }
745 static int cursor_at, cursor_max;
755 static int attrs[2][2][M_MAX]; // active, hilite, status
763 struct mbox *b = mbox_array[i];
764 int cc = (cursor_at == i);
765 int hi = b->o.highlight;
767 attrset(attrs[cc][hi][M_IDLE]);
769 printw("%c ", b->o.hotkey);
775 attrset(attrs[cc][hi][M_NEW]);
776 else if (b->flagged && b->o.show_flagged)
777 attrset(attrs[cc][hi][M_FLAG]);
778 printw("%-20s ", b->name);
781 else if (b->scanning)
783 attrset(attrs[cc][hi][M_SCAN]);
784 printw("[SCANNING]");
786 else if (b->total < 0)
788 attrset(attrs[cc][hi][M_BAD]);
793 attrset(attrs[cc][hi][M_IDLE]);
794 printw("%6d ", b->total);
798 attrset(attrs[cc][hi][M_NEW]);
799 printw("%6d ", b->new);
800 attrset(attrs[cc][hi][M_IDLE]);
801 int age = (last_scan_time - b->last_time);
805 printw("%2d min ", age/60);
806 else if (age < 86400)
807 printw("%2d hrs ", age/3600);
812 else if (b->flagged && b->o.show_flagged)
814 attrset(attrs[cc][hi][M_FLAG]);
815 printw("%6d ", b->flagged);
816 attrset(attrs[cc][hi][M_IDLE]);
818 attrset(attrs[cc][0][M_FLAG]); /* We avoid the highlight intentionally */
821 if (snip && b->o.snippets && b->snippet[0])
824 getyx(stdscr, yy, xx);
825 int remains = COLS-1-xx;
828 #ifdef CONFIG_WIDE_CURSES
829 size_t len = strlen(b->snippet)+1;
831 mbstowcs(snip, b->snippet, len);
832 addnwstr(snip, remains);
834 printw("%-.*s", remains, b->snippet);
840 attrset(attrs[0][0][M_IDLE]);
847 cursor_max = num_mboxes;
848 if (cursor_max > LINES-1)
849 cursor_max = LINES-1;
850 if (cursor_at >= cursor_max)
851 cursor_at = cursor_max - 1;
855 for (int i=0; i<cursor_max; i++)
860 printw("(no mailboxes found)");
868 rethink_display(void)
873 CLIST_FOR_EACH(struct mbox *, b, mboxes)
874 if (mbox_visible_p(b))
877 if (i >= num_mboxes || mbox_array[i] != b)
880 if (i >= mbox_array_size)
882 mbox_array_size = (mbox_array_size ? 2*mbox_array_size : 16);
883 mbox_array = xrealloc(mbox_array, sizeof(struct mbox *) * mbox_array_size);
887 if (b->o.beep && b->new > b->last_beep_new)
889 b->last_beep_new = b->new;
902 if (beeeep && allow_bells)
913 intrflush(stdscr, FALSE);
914 keypad(stdscr, TRUE);
917 static const int attrs_mono[2][M_MAX] = {
918 [0] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_BOLD, [M_FLAG] = 0, [M_BAD] = A_DIM },
919 [1] = { [M_IDLE] = 0, [M_SCAN] = A_BOLD, [M_NEW] = A_REVERSE | A_BOLD, [M_FLAG] = A_REVERSE, [M_BAD] = A_DIM },
921 for (int i=0; i<2; i++)
922 for (int j=0; j<M_MAX; j++)
924 attrs[0][i][j] = attrs_mono[i][j];
925 attrs[1][i][j] = attrs_mono[i][j] | A_UNDERLINE;
931 if (COLOR_PAIRS >= 5)
933 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
934 init_pair(2, COLOR_RED, COLOR_BLACK);
935 init_pair(3, COLOR_WHITE, COLOR_BLUE);
936 init_pair(4, COLOR_YELLOW, COLOR_BLUE);
937 init_pair(5, COLOR_RED, COLOR_BLUE);
938 init_pair(6, COLOR_GREEN, COLOR_BLACK);
939 init_pair(7, COLOR_GREEN, COLOR_BLUE);
940 static const int attrs_color[2][2][M_MAX] = {
941 [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) },
942 [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 },
943 [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) },
944 [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 },
946 memcpy(attrs, attrs_color, sizeof(attrs));
958 print_status(char *status)
962 printw("%s", status);
968 scan_and_redraw(void)
970 print_status("Busy...");
978 if (i >= 0 && i < cursor_max && i != cursor_at)
988 next_active(int since, int step)
992 since = (since+cursor_max) % cursor_max;
993 step = (step+cursor_max) % cursor_max;
999 struct mbox *b = mbox_array[i];
1000 if (b->new && b->o.priority > bestp)
1003 bestp = b->o.priority;
1005 i = (i+step) % cursor_max;
1013 mbox_run(struct mbox *b)
1015 char cmd[strlen(run_cmd) + strlen(b->path) + 16];
1016 sprintf(cmd, run_cmd, b->path);
1022 b->force_refresh = 1;
1027 #define STR(c) STR2(c)
1032 fprintf(stderr, "Usage: cm [<options>] [<mbox-pattern> | <mbox>[=<name>]] ...\n\
1035 -c <interval>\t\tScan mailboxes every <interval> seconds (default is 30)\n\
1036 -d\t\t\tLog debug messages to stderr\n\
1037 -i\t\t\tInclude user's INBOX\n\
1038 -m <cmd>\t\tCommand to run on the selected mailbox, %%s gets replaced by mailbox path\n\
1039 -o <pattern>=<opts>\tSet mailbox options\n\
1040 -o <opts>\t\tSet default options for all mailboxes\n\
1041 -p <pri>\t\tSet minimum priority to show\n\
1042 -s <key>=<val>\t\tSet on-screen display options (consult OSDD docs)\n\
1044 Mailbox options (set with `-o', use upper case to negate):\n\
1045 0-9\t\t\tSet mailbox priority (0=default)\n\
1046 b\t\t\tBeep when a message arrives\n\
1047 d\t\t\tSend an on-screen-display message (requires OSDD)\n\
1048 e\t\t\tHide from display if empty\n\
1049 f\t\t\tShow flagged messages if there are no new ones\n\
1050 h\t\t\tHide from display\n\
1051 l<led>\t\t\tLight a keyboard led (1-9) if running on X display\n\
1052 m\t\t\tShow mailbox name of the sender\n\
1053 p\t\t\tShow personal info (full name) of the sender\n\
1054 s\t\t\tShow message snippets\n\
1055 t\t\t\tHighlight the entry\n\
1056 !<key>\t\t\tSet hot key\n\
1058 CheckMail " STR(VERSION) ", (c) " STR(YEAR) " Martin Mares <mj@ucw.cz>\n\
1059 It can be freely distributed and used according to the GNU GPL v2.\n\
1065 parse_options(char *c)
1069 if (sep = strchr(c, '='))
1071 struct option_node *n = xmalloc(sizeof(*n) + sep-c);
1072 memcpy(n->pattern, c, sep-c);
1073 n->pattern[sep-c] = 0;
1074 clist_add_tail(&options, &n->n);
1080 o = &global_options;
1084 if (x >= '0' && x <= '9')
1085 o->priority = x - '0';
1086 else if (x == '!' && *c)
1088 else if (x == 'l' && *c >= '1' && *c <= '9')
1089 o->led = *c++ - '0';
1092 int value = !!islower(x);
1102 o->hide_if_empty = value;
1105 o->show_flagged = value;
1111 o->sender_mbox = value;
1114 o->sender_personal = value;
1117 o->snippets = value;
1120 o->highlight = value;
1123 fprintf(stderr, "Invalid mailbox option `%c'\n", x);
1130 main(int argc, char **argv)
1132 clist_init(&mboxes);
1133 clist_init(&options);
1134 clist_init(&patterns);
1135 clist_init(&osd_opts);
1138 while ((c = getopt(argc, argv, "c:dim:o:p:s:")) >= 0)
1142 check_interval = atol(optarg);
1143 if (check_interval <= 0)
1156 parse_options(optarg);
1159 minimum_priority = atol(optarg);
1162 add_osd_opt(optarg);
1167 while (optind < argc)
1168 add_pattern(argv[optind++]);
1176 int should_exit = 0;
1178 while (!should_exit)
1180 time_t now = time(NULL);
1181 int remains = last_scan_time + check_interval - now;
1182 if (remains <= 0 || force_refresh)
1187 halfdelay((remains > 255) ? 255 : remains);
1189 for (int i=0; i<num_mboxes; i++)
1190 if (ch == mbox_array[i]->o.hotkey)
1194 mbox_run(mbox_array[i]);
1204 move_cursor(cursor_at+1);
1208 move_cursor(cursor_at-1);
1218 move_cursor(cursor_max-1);
1221 next_active(cursor_at+1, 1);
1224 next_active(cursor_at-1, -1);
1228 if (cursor_at < cursor_max)
1229 mbox_run(mbox_array[cursor_at]);
1232 clearok(stdscr, TRUE);
1241 print_status("Bells and whistles are now enabled. Toot!");
1245 print_status("Bells and whistles are now disabled. Pssst!");
1249 print_status("On-screen display is now enabled.");
1253 print_status("On-screen display is now disabled. Watch your step.");
1256 if (ch >= '0' && ch <= '9')
1258 minimum_priority = ch - '0';
1262 debug("Pressed unknown key %d\n", ch);