/*
* Incoming Mail Checker
*
- * (c) 2005--2010 Martin Mares <mj@ucw.cz>
+ * (c) 2005--2015 Martin Mares <mj@ucw.cz>
*/
#define _GNU_SOURCE
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
#include <ctype.h>
-#include <getopt.h>
+#include <dirent.h>
#include <fcntl.h>
-#include <glob.h>
#include <fnmatch.h>
-#include <sys/stat.h>
-#include <unistd.h>
+#include <getopt.h>
+#include <glob.h>
#include <pwd.h>
-#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
#ifdef CONFIG_WIDE_CURSES
#include <ncursesw/ncurses.h>
static int minimum_priority;
static time_t last_scan_time;
static char *run_cmd = "mutt -f %s";
+static int simple_tab;
struct options {
int priority;
int hotkey;
int led;
int osd;
+ int unread_is_new;
};
struct option_node {
.sender_personal = 1
};
+#define MDIR_MAX_NAME_LEN 128
+
struct mbox {
cnode n;
struct options o;
int scanning;
int seen;
time_t last_time;
+ time_t best_time;
+ time_t display_valid_until;
int last_size, last_pos;
int total, new, flagged;
int last_total, last_new, last_flagged;
int last_beep_new;
int force_refresh;
- int snippet_is_new;
+ int best_is_new;
char sender_snippet[128];
char subject_snippet[128];
+ char mdir_best[MDIR_MAX_NAME_LEN];
};
static clist mboxes;
o->hotkey = -1;
o->led = -1;
o->osd = -1;
+ o->unread_is_new = -1;
}
static void
MERGE(hotkey);
MERGE(led);
MERGE(osd);
+ MERGE(unread_is_new);
}
}
add_snippet(&buf, term, b->subject_snippet);
}
-static int mb_fd, mb_pos;
+static int mb_fd, mb_pos, mb_seekable;
static unsigned char mb_buf[4096], *mb_cc, *mb_end;
static void
return 1;
}
+static void
+mb_skip(int len)
+{
+ while (len)
+ {
+ int avail = mb_end - mb_cc;
+ if (!avail)
+ {
+ if (mb_seekable && len >= (int) sizeof(mb_buf))
+ {
+ int next = len - len % sizeof(mb_buf);
+ mb_seek(mb_tell() + next);
+ len -= next;
+ continue;
+ }
+ if (mb_ll_get() < 0)
+ return;
+ len--;
+ }
+ else
+ {
+ int next = (avail < len) ? avail : len;
+ len -= next;
+ mb_cc += next;
+ }
+ }
+}
+
+#define HDR_BUF_SIZE 1024
+
+static int
+read_hdr_field(char *buf)
+{
+ uns i = 0;
+ for (;;)
+ {
+ int c = mb_get();
+ if (c < 0)
+ {
+ debug("[truncated] ");
+ return -1;
+ }
+ if (c == '\n')
+ {
+ if (!i)
+ break;
+ int fold = -1;
+ do
+ {
+ fold++;
+ c = mb_get();
+ }
+ while (c == ' ' || c == '\t');
+ mb_unget(c);
+ if (!fold)
+ break;
+ c = ' ';
+ }
+ if (c == '\r')
+ continue;
+ if (i < HDR_BUF_SIZE - 1)
+ buf[i++] = c;
+ }
+ buf[i] = 0;
+ if (debug_mode > 2)
+ debug("Header: <%s>\n", buf);
+ return buf[0] ? 1 : 0;
+}
+
static void
scan_mbox(struct mbox *b, struct stat *st)
{
- char buf[1024], sender[1024], subject[1024];
+ char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE];
int c;
int compressed = 0;
const char from[] = "\nFrom ";
+ b->best_time = st->st_mtime;
if (!st->st_size)
{
b->total = b->new = b->flagged = 0;
return;
}
- /* FIXME: Should we do some locking? */
-
mb_fd = open(b->path, O_RDONLY);
if (mb_fd < 0)
{
b->total = b->new = b->flagged = -1;
return;
}
+ mb_seekable = 1;
char signature[2];
c = read(mb_fd, signature, 2);
close(mb_fd);
mb_fd = fds[0];
compressed = 1;
+ mb_seekable = 0;
}
mb_reset(0);
int incremental = 0;
- if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && !compressed)
+ if (b->last_size && b->last_pos && st->st_size > b->last_size && !b->force_refresh && mb_seekable)
{
mb_seek(b->last_pos);
if (mb_check(from, 6))
}
b->total = b->new = b->flagged = 0;
b->last_total = b->last_new = b->last_flagged = 0;
- b->snippet_is_new = 0;
+ b->best_is_new = 0;
}
else
{
while ((c = mb_get()) >= 0 && c != '\n')
;
+ int content_length = -1;
int new = 1;
int flagged = 0;
sender[0] = 0;
subject[0] = 0;
for (;;)
{
- uns i = 0;
- for (;;)
- {
- c = mb_get();
- if (c < 0)
- {
- debug("[truncated] ");
- goto done;
- }
- if (c == '\n')
- {
- int fold = -1;
- do
- {
- fold++;
- c = mb_get();
- }
- while (c == ' ' || c == '\t');
- mb_unget(c);
- if (!fold)
- break;
- c = ' ';
- }
- if (c == '\r')
- continue;
- if (i < sizeof(buf) - 1)
- buf[i++] = c;
- }
- buf[i] = 0;
- if (!buf[0])
+ int c = read_hdr_field(buf);
+ if (c < 0)
+ goto done;
+ if (!c)
break;
if (!strncasecmp(buf, "Status:", 7))
- new = 0;
+ {
+ if (!b->o.unread_is_new || strchr(buf + 7, 'R'))
+ new = 0;
+ }
else if (!strncasecmp(buf, "X-Status:", 9) && strchr(buf+9, 'F'))
flagged = 1;
else if (!strncasecmp(buf, "From:", 5))
strcpy(sender, buf+5);
else if (!strncasecmp(buf, "Subject:", 8))
strcpy(subject, buf+8);
+ else if (!strncasecmp(buf, "Content-Length:", 15))
+ content_length = atoi(buf + 15);
}
b->total++;
if (flagged)
b->flagged++;
if (debug_mode > 1)
- debug("new=%d flagged=%d sender=<%s> subject=<%s>\n", new, flagged, sender, subject);
- if (new || (flagged && !b->snippet_is_new))
+ debug("new=%d flagged=%d len=%d sender=<%s> subject=<%s>\n", new, flagged, content_length, sender, subject);
+ if (new || (flagged && !b->best_is_new))
{
- b->snippet_is_new = new;
+ b->best_is_new = new;
prepare_snippets(b, sender, subject);
}
+ if (content_length >= 0)
+ mb_skip(content_length);
+
int ct = 1;
while (from[ct])
{
}
}
+static time_t
+mdir_mtime(struct mbox *b)
+{
+ time_t mtime = 0;
+ char path[strlen(b->path) + 5];
+
+ for (int new=0; new<2; new++)
+ {
+ sprintf(path, "%s/%s", b->path, (new ? "new" : "cur"));
+ struct stat st;
+ if (stat(path, &st) < 0)
+ debug("[cannot stat %s] ", path);
+ else if (st.st_mtime > mtime)
+ mtime = st.st_mtime;
+ }
+ return mtime;
+}
+
+static void
+mdir_get_snippet(struct mbox *b)
+{
+ char buf[HDR_BUF_SIZE], sender[HDR_BUF_SIZE], subject[HDR_BUF_SIZE];
+ sender[0] = 0;
+ subject[0] = 0;
+
+ char path[strlen(b->path) + MDIR_MAX_NAME_LEN];
+ sprintf(path, "%s/%s", b->path, b->mdir_best);
+ mb_fd = open(path, O_RDONLY);
+ if (mb_fd < 0)
+ {
+ debug("[open failed: %m] ");
+ prepare_snippets(b, sender, subject);
+ return;
+ }
+ mb_seekable = 1;
+ mb_reset(0);
+
+ while (read_hdr_field(buf) > 0)
+ {
+ if (!strncasecmp(buf, "From:", 5))
+ strcpy(sender, buf+5);
+ else if (!strncasecmp(buf, "Subject:", 8))
+ strcpy(subject, buf+8);
+ }
+
+ close(mb_fd);
+ prepare_snippets(b, sender, subject);
+}
+
+static void
+scan_mdir(struct mbox *b)
+{
+ int dir_len = strlen(b->path);
+ char path[dir_len + MDIR_MAX_NAME_LEN];
+ strcpy(path, b->path);
+
+ b->total = b->new = b->flagged = 0;
+ b->best_time = 0;
+ b->best_is_new = 0;
+
+ for (int new=0; new<2; new++)
+ {
+ strcpy(path + dir_len, (new ? "/new" : "/cur"));
+ DIR *d = opendir(path);
+ if (!d)
+ {
+ debug("[cannot open %s: %m] ");
+ continue;
+ }
+ struct dirent *de;
+ while (de = readdir(d))
+ {
+ char *name = de->d_name;
+ if (name[0] == '.' || strlen(name) + 10 > MDIR_MAX_NAME_LEN)
+ continue;
+ path[dir_len + 4] = '/';
+ strcpy(path + dir_len + 5, name);
+
+ char *colon = strchr(name, ':');
+ int seen = 0;
+ int flagged = 0;
+ if (colon && colon[1] == '2' && colon[2] == ',')
+ {
+ // Another comma can separate extended flags (Dovecot extension)
+ for (int i=3; colon[i] && colon[i] != ','; i++)
+ switch (colon[i])
+ {
+ case 'S':
+ seen = 1;
+ break;
+ case 'F':
+ flagged = 1;
+ break;
+ }
+ }
+
+ int is_new = new;
+ if (b->o.unread_is_new && !seen)
+ is_new = 1;
+
+ b->total++;
+ if (is_new)
+ b->new++;
+ if (flagged)
+ b->flagged++;
+ if (debug_mode > 1)
+ debug("%s: new=%d flagged=%d seen=%d\n", path, is_new, flagged, seen);
+
+ if (is_new || flagged)
+ {
+ // Need to pick the best message to display
+ struct stat st;
+ if (stat(path, &st) < 0)
+ debug("[cannot stat %s: %m] ", path);
+ else if (is_new > b->best_is_new || is_new == b->best_is_new && st.st_mtime > b->best_time)
+ {
+ b->best_is_new = is_new;
+ b->best_time = st.st_mtime;
+ strcpy(b->mdir_best, path + dir_len + 1);
+ }
+ }
+ }
+ closedir(d);
+ }
+
+ if (b->best_time && b->o.snippets)
+ {
+ debug("best <%s> ", b->mdir_best);
+ mdir_get_snippet(b);
+ }
+}
+
static void
scan(int notify)
{
debug("inactive\n");
continue;
}
- if (force_refresh)
- b->force_refresh = 1;
+ b->force_refresh = force_refresh;
if (stat(b->path, &st) < 0)
{
b->total = b->new = b->flagged = -1;
debug("%m\n");
+ continue;
}
- else if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
+
+ time_t current_mtime;
+ int current_size;
+ int is_mdir;
+ if (S_ISREG(st.st_mode))
+ {
+ // Regular mailbox
+ is_mdir = 0;
+ current_mtime = st.st_mtime;
+ current_size = st.st_size;
+ debug("[mbox] ");
+ }
+ else if (S_ISDIR(st.st_mode))
+ {
+ // Maildir
+ is_mdir = 1;
+ current_mtime = mdir_mtime(b);
+ current_size = 0;
+ debug("[mdir] ");
+ }
+ else
+ {
+ debug("neither file nor directory\n");
+ continue;
+ }
+
+ if (!b->last_time || current_mtime != b->last_time || current_size != b->last_size || b->force_refresh)
{
b->scanning = 1;
redraw_line(b->index);
refresh();
- scan_mbox(b, &st);
- b->last_time = st.st_mtime;
- b->last_size = st.st_size;
+ if (is_mdir)
+ scan_mdir(b);
+ else
+ scan_mbox(b, &st);
+ b->last_time = current_mtime;
+ b->last_size = current_size;
debug("%d %d %d (stopped at %d of %d)\n", b->total, b->new, b->flagged, b->last_pos, b->last_size);
b->scanning = 0;
redraw_line(b->index);
refresh();
+ b->force_refresh = 0;
+ }
+ else if (b->display_valid_until <= last_scan_time)
+ {
+ debug("not changed, but needs redraw\n");
+ redraw_line(b->index);
}
else
debug("not changed\n");
- b->force_refresh = 0;
}
force_refresh = 0;
static unsigned osd_care;
#define OSD_MSG_SIZE 1024
static char osd_last_msg[OSD_MSG_SIZE];
+static time_t osd_last_time;
static void
x11_init(void)
if (b->o.osd > 0)
{
p.total_new += b->new;
- if (b->new && (!p.mbox || p.mbox->o.priority < b->o.priority))
+ if (b->new && b->best_time > osd_last_time && (!p.mbox || p.mbox->o.priority < b->o.priority))
p.mbox = b;
}
}
else
debug("OSD: No changes\n");
+ osd_last_time = time(NULL);
}
}
int hi = b->o.highlight;
unsigned namepos = 0;
unsigned namelen = strlen(b->name);
+ int valid = 3600;
attrset(attrs[cc][hi][M_IDLE]);
if (b->o.hotkey)
attrset(attrs[cc][hi][M_NEW]);
printw("%6d ", b->new);
attrset(attrs[cc][hi][M_IDLE]);
- int age = (last_scan_time - b->last_time);
+ int age = (last_scan_time - b->best_time);
if (age < 0)
age = 0;
if (age < 3600)
- printw("%2d min ", age/60);
+ {
+ printw("%2d min ", age/60);
+ valid = 60;
+ }
else if (age < 86400)
printw("%2d hr%c ", age/3600, (age >= 7200 ? 's' : ' '));
else
}
}
}
+ b->display_valid_until = last_scan_time + valid;
}
attrset(attrs[0][0][M_IDLE]);
clrtoeol();
do
{
struct mbox *b = mbox_array[i];
- if (b->new && b->o.priority > bestp)
+ if (simple_tab)
{
- besti = i;
- bestp = b->o.priority;
+ if (b->new)
+ {
+ besti = i;
+ break;
+ }
+ }
+ else
+ {
+ if (b->new && b->o.priority > bestp)
+ {
+ besti = i;
+ bestp = b->o.priority;
+ }
}
i = (i+step) % cursor_max;
}
-o <opts>\t\tSet default options for all mailboxes\n\
-p <pri>\t\tSet minimum priority to show\n\
-s <key>=<val>\t\tSet on-screen display options (consult OSDD docs)\n\
+-t\t\t\tLet TAB select the next mailbox with new mail, no matter what priority it has\n\
\n\
Mailbox options (set with `-o', use upper case to negate):\n\
0-9\t\t\tSet mailbox priority (0=default)\n\
h\t\t\tHide from display\n\
l<led>\t\t\tLight a keyboard led (1-9) if running on X display\n\
m\t\t\tShow mailbox name of the sender\n\
+o\t\t\tCount old, but unread messages as new\n\
p\t\t\tShow personal info (full name) of the sender\n\
s\t\t\tShow message snippets\n\
t\t\t\tHighlight the entry\n\
case 'm':
o->sender_mbox = value;
break;
+ case 'o':
+ o->unread_is_new = value;
+ break;
case 'p':
o->sender_personal = value;
break;
clist_init(&osd_opts);
int c;
- while ((c = getopt(argc, argv, "c:dim:o:p:s:")) >= 0)
+ while ((c = getopt(argc, argv, "c:dim:o:p:s:t")) >= 0)
switch (c)
{
case 'c':
case 's':
add_osd_opt(optarg);
break;
+ case 't':
+ simple_tab = 1;
+ break;
default:
usage();
}