]> mj.ucw.cz Git - checkmail.git/blobdiff - cm.c
Maildir: Skip Dovecot extended flags properly
[checkmail.git] / cm.c
diff --git a/cm.c b/cm.c
index 49d0f6508e40b50ac7f0dfb51de2789fcb04b7e5..9bc036672c4139ee175a48b34239bf0b0698bc52 100644 (file)
--- a/cm.c
+++ b/cm.c
@@ -1,25 +1,26 @@
 /*
  *     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>
@@ -38,6 +39,7 @@ static int allow_osd = 1;
 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;
@@ -52,6 +54,7 @@ struct options {
   int hotkey;
   int led;
   int osd;
+  int unread_is_new;
 };
 
 struct option_node {
@@ -71,6 +74,8 @@ static struct options global_options = {
   .sender_personal = 1
 };
 
+#define MDIR_MAX_NAME_LEN 128
+
 struct mbox {
   cnode n;
   struct options o;
@@ -80,15 +85,17 @@ struct mbox {
   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;
@@ -147,6 +154,7 @@ init_options(struct options *o)
   o->hotkey = -1;
   o->led = -1;
   o->osd = -1;
+  o->unread_is_new = -1;
 }
 
 static void
@@ -170,6 +178,7 @@ setup_options(struct mbox *b)
        MERGE(hotkey);
        MERGE(led);
        MERGE(osd);
+       MERGE(unread_is_new);
       }
 }
 
@@ -289,7 +298,7 @@ build_snippet(char *buf, char *term, struct mbox *b)
   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
@@ -354,14 +363,84 @@ mb_check(const char *p, int len)
   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;
@@ -369,8 +448,6 @@ scan_mbox(struct mbox *b, struct stat *st)
       return;
     }
 
-  /* FIXME: Should we do some locking? */
-
   mb_fd = open(b->path, O_RDONLY);
   if (mb_fd < 0)
     {
@@ -378,6 +455,7 @@ scan_mbox(struct mbox *b, struct stat *st)
       b->total = b->new = b->flagged = -1;
       return;
     }
+  mb_seekable = 1;
 
   char signature[2];
   c = read(mb_fd, signature, 2);
@@ -406,11 +484,12 @@ scan_mbox(struct mbox *b, struct stat *st)
       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))
@@ -434,7 +513,7 @@ scan_mbox(struct mbox *b, struct stat *st)
        }
       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
     {
@@ -454,51 +533,31 @@ scan_mbox(struct mbox *b, struct stat *st)
       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++;
@@ -507,13 +566,16 @@ scan_mbox(struct mbox *b, struct stat *st)
       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])
        {
@@ -535,6 +597,138 @@ scan_mbox(struct mbox *b, struct stat *st)
     }
 }
 
+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)
 {
@@ -587,27 +781,57 @@ 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;
+       }
+
+      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 if (!b->last_time || st.st_mtime != b->last_time || st.st_size != b->last_size || b->force_refresh)
+      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)
        {
@@ -616,7 +840,6 @@ scan(int notify)
        }
       else
        debug("not changed\n");
-      b->force_refresh = 0;
     }
   force_refresh = 0;
 
@@ -637,6 +860,7 @@ static Atom osd_pty;
 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)
@@ -844,7 +1068,7 @@ rethink_osd(int notify)
     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;
       }
 
@@ -862,6 +1086,7 @@ rethink_osd(int notify)
        }
       else
        debug("OSD: No changes\n");
+      osd_last_time = time(NULL);
     }
 }
 
@@ -958,7 +1183,7 @@ redraw_line(int i)
              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)
@@ -1211,10 +1436,21 @@ next_active(int since, int step)
   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;
     }
@@ -1301,6 +1537,7 @@ Options:\n\
 -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\
@@ -1311,6 +1548,7 @@ f\t\t\tShow flagged messages if there are no new ones\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\
@@ -1371,6 +1609,9 @@ parse_options(char *c)
          case 'm':
            o->sender_mbox = value;
            break;
+         case 'o':
+           o->unread_is_new = value;
+           break;
          case 'p':
            o->sender_personal = value;
            break;
@@ -1396,7 +1637,7 @@ main(int argc, char **argv)
   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':
@@ -1422,6 +1663,9 @@ main(int argc, char **argv)
       case 's':
        add_osd_opt(optarg);
        break;
+      case 't':
+       simple_tab = 1;
+       break;
       default:
        usage();
       }