The attached patch allows the user to define a virtual folder using external search tools such as mairix, mu and notmuch.

It adds a $vfolder_command variable and a "vfolder" command to define virtual mailboxes (see the attached sample muttrc for examples). The user then visits vfolder:///name to open the virtual mailbox. Mutt executes $vfolder_command with the search query for "name" substituted, and reads the list of matching messages from its stdout.

Limitations:
- only works on maildirs
- deletion is supported only via marking messages with the 'T'rashed flag (this is for safety until the patch is more fully flushed out)
- new mail detection is not supported
- header caching is not enabled


me
# vim: ft=muttrc

set sort=mailbox-order # $vfolder_command sets the proper order mostly
set spoolfile=vfolder:///unread
set folder=vfolder:/// # allow use of =vfolder as shortcut

ignore *
unignore from to cc date subject

unhdr_order *
hdr_order from to cc date subject

# Shell command to execute the appropriate search tool
# - It can contain %s, otherwise the search will be appended to the command
# - quoting is not necessary, mutt quotes all tokens in the search string 
automatically
# - The command should return a list of maildir messages, separated by newlines.
#
# set vfolder_command='notmuch search --format=text --output=files'
# set vfolder_command='mu find --format=plain --fields=l'
# set vfolder_command='mairix --raw-output'  # mairix - note that mutt expects 
maildir-style mailboxes only

#
# Define virtual folders
#
# syntax: vfolder <NAME> <SEARCH-STRING>
# - enter vfolder:///NAME to open the mailbox

# notmuch

#vfolder mutt "to:mutt-users or to:mutt-dev or to:mutt-po"
#vfolder muttref "mutt and not (to:mutt-users or to:mutt-dev or to:mutt-po or 
folder:Sent)" # look for mutt mentions other than the usual places
#vfolder allmail "*" # notmuch uses * as a special case
#vfolder unread "tag:unread"
#vfolder flagged "tag:flagged"

# mu

#vfolder mutt 'to:mutt-*' # all mutt lists
#vfolder allmail 'not m:/spam' # mu has no facility to ignore directories, so 
can't use the empty search query
#vfolder unread 'not (flag:seen or m:/spam)'
#vfolder flagged 'flag:flagged'
#vfolder muttref 'mutt and not (to:mutt-* or m:/Sent)' # look for mutt mentions 
other than the usual places
# HG changeset patch
# Parent 90f7869decec16a03aee7826fa895625f519b939

diff --git a/Makefile.am b/Makefile.am
--- a/Makefile.am
+++ b/Makefile.am
@@ -33,7 +33,8 @@
        score.c send.c sendlib.c signal.c sort.c \
        status.c system.c thread.c charset.c history.c lib.c \
        muttlib.c editmsg.c mbyte.c \
-       url.c ascii.c crypt-mod.c crypt-mod.h safe_asprintf.c
+       url.c ascii.c crypt-mod.c crypt-mod.h safe_asprintf.c \
+       vfolder.c
 
 nodist_mutt_SOURCES = $(BUILT_SOURCES)
 
diff --git a/globals.h b/globals.h
--- a/globals.h
+++ b/globals.h
@@ -140,6 +140,7 @@
 WHERE char *Tempdir;
 WHERE char *Tochars;
 WHERE char *Username;
+WHERE char *VfolderCommand;
 WHERE char *Visual;
 
 WHERE char *CurrentFolder;
@@ -170,6 +171,7 @@
 WHERE RX_LIST *UnSubscribedLists INITVAL(0);
 WHERE SPAM_LIST *SpamList INITVAL(0);
 WHERE RX_LIST *NoSpamList INITVAL(0);
+WHERE VFOLDER_LIST *VfolderList INITVAL(0);
 
 
 /* bit vector for boolean variables */
diff --git a/init.c b/init.c
--- a/init.c
+++ b/init.c
@@ -1463,6 +1463,89 @@
   return 0;
 }
 
+#define new_vfolder() safe_calloc(1, sizeof(VFOLDER_LIST))
+
+void mutt_free_vfolder (VFOLDER_LIST **p)
+{
+  if (*p)
+  {
+    FREE (&(*p)->name);
+    FREE (&(*p)->query);
+    (*p)->next = 0;
+    FREE (p);
+  }
+}
+
+void mutt_free_vfolder_list (VFOLDER_LIST **p)
+{
+  VFOLDER_LIST *tmp;
+
+  while (*p)
+  {
+    tmp = *p;
+    p = &(*p)->next;
+    mutt_free_vfolder (&tmp);
+  }
+}
+
+/* Syntax: vfolder <NAME> <QUERY-STRING> */
+static int parse_vfolder (BUFFER *buf, BUFFER *s, unsigned long data, BUFFER 
*err)
+{
+  VFOLDER_LIST **end = &VfolderList;
+
+  if (!MoreArgs(s))
+  {
+    strfcpy (err->data, _("missing virtual folder name"), err->dsize);
+    return -1;
+  }
+  mutt_extract_token (buf, s, 0);
+  while (*end)
+    end = &(*end)->next;
+  *end = new_vfolder();
+  (*end)->name = safe_strdup(buf->data);
+  if (!MoreArgs(s))
+  {
+    strfcpy (err->data, _("missing query string"), err->dsize);
+    goto error_out;
+  }
+  mutt_extract_token (buf, s, 0);
+  (*end)->query = safe_strdup(buf->data);
+  return 0;
+error_out:
+  mutt_free_vfolder (end);
+  return -1;
+}
+
+/* Syntax: unvfolder *|<NAME> ... */
+static int parse_unvfolder (BUFFER *buf, BUFFER *s, unsigned long data, BUFFER 
*err)
+{
+  VFOLDER_LIST **tmp;
+
+  while (MoreArgs(s))
+  {
+    mutt_extract_token (buf, s, 0);
+    if (mutt_strcmp (buf->data, "*") == 0)
+    {
+      /* remove all defined vfolders */
+      mutt_free_vfolder_list (&VfolderList);
+    }
+    else
+    {
+      for (tmp = &VfolderList; *tmp; tmp = &(*tmp)->next)
+      {
+       if (mutt_strcmp ((*tmp)->name, buf->data) == 0)
+       {
+         VFOLDER_LIST *p = *tmp;
+         *tmp = (*tmp)->next;
+         mutt_free_vfolder (&p);
+         break;
+       }
+      }
+    }
+  }
+  return 0;
+}
+
 static int
 parse_sort (short *val, const char *s, const struct mapping_t *map, BUFFER 
*err)
 {
diff --git a/init.h b/init.h
--- a/init.h
+++ b/init.h
@@ -3332,6 +3332,17 @@
   ** messages, indicating which version of mutt was used for composing
   ** them.
   */
+  { "vfolder_command", DT_STR,  R_NONE, UL &VfolderCommand, 0 },
+  /*
+  ** .pp
+  ** Specifies a shell command to be invoked when opening a virtual folder.
+  ** If the string contains a \fC%s\fP escape, it will be replaced with the
+  ** search query.  Otherwise, a space and the search query is appended to
+  ** the string.
+  ** .pp
+  ** The shell command should return a list of matching messages, each
+  ** separated by a newline character.
+  */ 
   { "visual",          DT_PATH, R_NONE, UL &Visual, 0 },
   /*
   ** .pp
@@ -3497,6 +3508,8 @@
 static int parse_unsubscribe (BUFFER *, BUFFER *, unsigned long, BUFFER *);
 static int parse_attachments (BUFFER *, BUFFER *, unsigned long, BUFFER *);
 static int parse_unattachments (BUFFER *, BUFFER *, unsigned long, BUFFER *);
+static int parse_vfolder (BUFFER *, BUFFER *, unsigned long, BUFFER *);
+static int parse_unvfolder (BUFFER *, BUFFER *, unsigned long, BUFFER *);
 
 
 static int parse_alternates (BUFFER *, BUFFER *, unsigned long, BUFFER *);
@@ -3578,5 +3591,7 @@
   { "unscore",         mutt_parse_unscore,     0 },
   { "unset",           parse_set,              M_SET_UNSET },
   { "unsubscribe",     parse_unsubscribe,      0 },
+  { "unvfolder",        parse_unvfolder,       0 },
+  { "vfolder",          parse_vfolder,         0 },
   { NULL,              NULL,                   0 }
 };
diff --git a/main.c b/main.c
--- a/main.c
+++ b/main.c
@@ -825,18 +825,21 @@
 
     strfcpy (fpath, Maildir, sizeof (fpath));
     mutt_expand_path (fpath, sizeof (fpath));
+    if (!mx_is_vfolder (fpath))
+    {
 #ifdef USE_IMAP
-    /* we're not connected yet - skip mail folder creation */
-    if (!mx_is_imap (fpath))
+      /* we're not connected yet - skip mail folder creation */
+      if (!mx_is_imap (fpath))
 #endif
-    if (stat (fpath, &sb) == -1 && errno == ENOENT)
-    {
-      snprintf (msg, sizeof (msg), _("%s does not exist. Create it?"), 
Maildir);
-      if (mutt_yesorno (msg, M_YES) == M_YES)
-      {
-       if (mkdir (fpath, 0700) == -1 && errno != EEXIST)
-         mutt_error ( _("Can't create %s: %s."), Maildir, strerror (errno));
-      }
+       if (stat (fpath, &sb) == -1 && errno == ENOENT)
+       {
+         snprintf (msg, sizeof (msg), _("%s does not exist. Create it?"), 
Maildir);
+         if (mutt_yesorno (msg, M_YES) == M_YES)
+         {
+           if (mkdir (fpath, 0700) == -1 && errno != EEXIST)
+             mutt_error ( _("Can't create %s: %s."), Maildir, strerror 
(errno));
+         }
+       }
     }
   }
 
diff --git a/mh.c b/mh.c
--- a/mh.c
+++ b/mh.c
@@ -557,7 +557,7 @@
   }
 }
 
-static void maildir_parse_flags (HEADER * h, const char *path)
+void maildir_parse_flags (HEADER * h, const char *path)
 {
   char *p, *q = NULL;
 
@@ -1196,7 +1196,7 @@
   return (int)( *((const char *) a) - *((const char *) b));
 }
 
-static void maildir_flags (char *dest, size_t destlen, HEADER * hdr)
+void maildir_flags (char *dest, size_t destlen, HEADER * hdr)
 {
   *dest = '\0';
 
diff --git a/mutt.h b/mutt.h
--- a/mutt.h
+++ b/mutt.h
@@ -543,6 +543,13 @@
   struct spam_list_t *next;
 } SPAM_LIST;
 
+typedef struct vfolder_list
+{
+  char *name; /* how the user will refer to this virtual mailbox */
+  char *query; /* the query string to be passed to $vfolder_command when 
opening this vfolder */
+  struct vfolder_list *next;
+} VFOLDER_LIST;
+
 #define mutt_new_list() safe_calloc (1, sizeof (LIST))
 #define mutt_new_rx_list() safe_calloc (1, sizeof (RX_LIST))
 #define mutt_new_spam_list() safe_calloc (1, sizeof (SPAM_LIST))
diff --git a/mx.c b/mx.c
--- a/mx.c
+++ b/mx.c
@@ -343,6 +343,16 @@
 }
 #endif
 
+int mx_is_vfolder (const char *p)
+{
+  url_scheme_t scheme;
+
+  if (!p)
+    return 0;
+  scheme = url_check_scheme (p);
+  return (scheme == U_VFOLDER);
+}
+
 int mx_get_magic (const char *path)
 {
   struct stat st;
@@ -360,6 +370,9 @@
     return M_POP;
 #endif /* USE_POP */
 
+  if (mx_is_vfolder (path))
+    return M_VFOLDER;
+
   if (stat (path, &st) == -1)
   {
     dprint (1, (debugfile, "mx_get_magic(): unable to stat %s: %s (errno 
%d).\n",
@@ -445,6 +458,9 @@
   if (mx_is_imap (path))
     return imap_access (path, flags);
 #endif
+  /* virtual folders are not writable */
+  if (mx_is_vfolder (path))
+    return -1;
 
   return access (path, flags);
 }
@@ -461,6 +477,12 @@
     return imap_open_mailbox_append (ctx);
 
 #endif
+  /* virtual folders are not writable */
+  if (mx_is_vfolder (ctx->path))
+  {
+    mutt_error(_("Virtual folder is not writable"));
+    return -1;
+  }
   
   if(stat(ctx->path, &sb) == 0)
   {
@@ -668,6 +690,10 @@
       break;
 #endif /* USE_POP */
 
+    case M_VFOLDER:
+      rc = vfolder_open_mailbox (ctx);
+      break;
+
     default:
       rc = -1;
       break;
@@ -764,6 +790,10 @@
       rc = pop_sync_mailbox (ctx, index_hint);
       break;
 #endif /* USE_POP */
+
+    case M_VFOLDER:
+      rc = vfolder_sync_mailbox (ctx, index_hint);
+      break;
   }
 
 #if 0
@@ -1341,6 +1371,9 @@
       case M_POP:
        return (pop_check_mailbox (ctx, index_hint));
 #endif /* USE_POP */
+
+      case M_VFOLDER:
+       return 0; /* new mail checking is not supported */
     }
   }
 
@@ -1401,6 +1434,18 @@
     }
 #endif /* USE_POP */
 
+    case M_VFOLDER:
+    {
+      msg->fp = fopen (ctx->hdrs[msgno]->path, "r");
+      if (!msg->fp)
+      {
+       mutt_perror (ctx->hdrs[msgno]->path);
+       dprint (1, (debugfile, "mx_open_message: fopen: %s: %s (errno %d).\n",
+                   ctx->hdrs[msgno]->path, strerror (errno), errno));
+       FREE (&msg);
+      }
+    } break;
+
     default:
       dprint (1, (debugfile, "mx_open_message(): function not implemented for 
mailbox type %d.\n", ctx->magic));
       FREE (&msg);
diff --git a/mx.h b/mx.h
--- a/mx.h
+++ b/mx.h
@@ -35,7 +35,8 @@
   M_MH,
   M_MAILDIR,
   M_IMAP,
-  M_POP
+  M_POP,
+  M_VFOLDER
 };
 
 WHERE short DefaultMagic INITVAL (M_MBOX);
@@ -63,6 +64,8 @@
 int maildir_read_dir (CONTEXT *);
 int maildir_check_mailbox (CONTEXT *, int *);
 int maildir_check_empty (const char *);
+void maildir_parse_flags (HEADER *, const char *);
+void maildir_flags (char *, size_t, HEADER *);
 
 int maildir_commit_message (CONTEXT *, MESSAGE *, HEADER *);
 int mh_commit_message (CONTEXT *, MESSAGE *, HEADER *);
@@ -72,6 +75,9 @@
 
 FILE *maildir_open_find_message (const char *, const char *);
 
+int vfolder_open_mailbox (CONTEXT *);
+int vfolder_sync_mailbox (CONTEXT *, int *);
+
 int mbox_strict_cmp_headers (const HEADER *, const HEADER *);
 int mutt_reopen_mailbox (CONTEXT *, int *);
 
diff --git a/url.c b/url.c
--- a/url.c
+++ b/url.c
@@ -42,6 +42,7 @@
   { "mailto",  U_MAILTO },
   { "smtp",     U_SMTP },
   { "smtps",    U_SMTPS },
+  { "vfolder",  U_VFOLDER },
   { NULL,      U_UNKNOWN }
 };
 
diff --git a/url.h b/url.h
--- a/url.h
+++ b/url.h
@@ -11,6 +11,7 @@
   U_SMTP,
   U_SMTPS,
   U_MAILTO,
+  U_VFOLDER,
   U_UNKNOWN
 }
 url_scheme_t;
diff --git a/vfolder.c b/vfolder.c
new file mode 100644
--- /dev/null
+++ b/vfolder.c
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2013 Michael R. Elkins <m...@mutt.org>
+ * 
+ *     This program is free software; you can redistribute it and/or modify
+ *     it under the terms of the GNU General Public License as published by
+ *     the Free Software Foundation; either version 2 of the License, or
+ *     (at your option) any later version.
+ * 
+ *     This program is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *     GNU General Public License for more details.
+ * 
+ *     You should have received a copy of the GNU General Public License
+ *     along with this program; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  
02110-1301, USA.
+ */ 
+
+#include <errno.h>
+#include "config.h"
+#include "mutt.h"
+#include "mx.h"
+#include "url.h"
+#include "mutt_curses.h" /* for mutt_progress_* */
+
+/* split arguments by space and quote each argument in order to protect it 
during a shell command */
+static char *split_and_quote (const char *s)
+{
+  char *r = strdup (""); /* empty string is a valid search in mu, so do not 
return NULL like we normally do */
+  size_t rsize = 0;
+  const char *p = s;
+  size_t plen;
+
+  while (*s)
+  {
+    p = strchr (s, ' ');
+    if (!p)
+    {
+      plen = strlen (s);
+      p = s + plen;
+    }
+    else
+    {
+      plen = p - s;
+      while (*p == ' ')
+       p++;
+    }
+    safe_realloc (&r, rsize + plen + 3 + 1);
+    r[rsize] = ' ';
+    r[rsize + 1] = '\'';
+    memcpy (r + rsize + 2, s, plen);
+    r[rsize + plen + 2] = '\'';
+    rsize += plen + 3;
+
+    s = p;
+  }
+  r[rsize] = 0;
+
+  return r;
+}
+
+int vfolder_open_mailbox (CONTEXT *ctx)
+{
+  ciss_url_t url;
+  VFOLDER_LIST *vf;
+  char cmd[LONG_STRING];
+  FILE *fpout, *fperr;
+  pid_t pid;
+  char fname[_POSIX_PATH_MAX];
+  int rc;
+  char path[_POSIX_PATH_MAX];
+  char msgbuf[STRING];
+  progress_t progress;
+  char buf[LONG_STRING];
+  char *qs;
+#ifdef DEBUG
+  int has_err = 0;
+#endif
+
+  if (!VfolderCommand)
+  {
+    mutt_error (_("$vfolder_command is not defined"));
+    return -1;
+  }
+
+  /* copy the mailbox path since url_parse_ciss() is destructive */
+  strfcpy (path, ctx->path, sizeof (path));
+  if (url_parse_ciss (&url, path) != 0 ||
+      url.scheme != U_VFOLDER)
+  {
+    mutt_error (_("Invalid vfolder URL: %s"), ctx->path);
+    return -1;
+  }
+
+  for (vf = VfolderList; vf; vf = vf->next)
+  {
+    if (strcmp (vf->name, url.path) == 0)
+      break;
+  }
+  if (!vf)
+  {
+    mutt_error (_("vfolder %s is not defined"), url.path);
+    return -1;
+  }
+
+  /* both mu and mairix can't cope with a single quoted argument, so we have to
+   * split and quote each token in the search string.
+   */
+  qs = split_and_quote (vf->query);
+  mutt_expand_fmt (cmd, sizeof(cmd), VfolderCommand, qs);
+  FREE (&qs);
+
+  dprint (1, (debugfile, "vfolder_open_mailbox: executing: %s\n", cmd));
+
+  pid = mutt_create_filter (cmd, NULL, &fpout, &fperr);
+  if (pid == -1)
+  {
+    mutt_error (_("Error invoking $vfolder_command: %s"), cmd);
+    return -1;
+  }
+
+  /* progress retains a pointer to msgbuf so it must live at least as long */
+  snprintf (msgbuf, sizeof (msgbuf), _("Reading %s..."), ctx->path);
+  mutt_progress_init (&progress, msgbuf, M_PROGRESS_MSG, ReadInc, 0);
+
+  /*
+   * The $vfolder_command will output a list of pathnames separated by newlines
+   */
+  while (fgets (fname, sizeof(fname), fpout) != NULL)
+  {
+    HEADER *hdr;
+    FILE *fpmsg;
+    struct stat st;
+    char *base_fname;
+    int old;
+
+    if (fname[0])
+      fname[strlen(fname) - 1] = 0; /* remove newline */
+
+    /* if this message appears to be part of a maildir folder, parse out its 
flags */
+    base_fname = strstr (fname, "/cur/");
+    if (base_fname)
+      old = option (OPTMARKOLD);
+    else
+    {
+      base_fname = strstr (fname, "/new/");
+      if (!base_fname)
+      {
+       dprint (2, (debugfile, "vfolder_open_mailbox: skipping pathname that 
does not appear to be part of a maildir: %s\n", fname));
+       continue;
+      }
+      old = 0;
+    }
+    base_fname += 5; /* skip over the subdir name */
+
+    fpmsg = fopen (fname, "r");
+    if (!fpmsg)
+    {
+      dprint (1, (debugfile, "vfolder_open_mailbox: %s (errno %d)\n", fname, 
strerror(errno), errno));
+      continue;
+    }
+
+    mutt_progress_update (&progress, ctx->msgcount + 1, -1);
+
+    hdr = mutt_new_header ();
+    hdr->old = old;
+    hdr->index = ctx->msgcount;
+    hdr->path = safe_strdup (fname); /* note: full path name */
+    hdr->env = mutt_read_rfc822_header (fpmsg, hdr, 0, 0);
+
+    fstat (fileno (fpmsg), &st);
+    safe_fclose (&fpmsg);
+
+    hdr->content->length = st.st_size - hdr->content->offset;
+
+    /* override the status and x-status fields based on the flags in the 
filename */
+    maildir_parse_flags (hdr, base_fname);
+
+    if (ctx->msgcount == ctx->hdrmax)
+      mx_alloc_memory (ctx);
+    ctx->hdrs[ctx->msgcount] = hdr;
+    ctx->size += st.st_size;
+    ctx->msgcount++;
+  }
+  safe_fclose (&fpout);
+  /* read stderr from child and dump into debug log */
+  while (fgets (buf, sizeof (buf), fperr) != NULL)
+  {
+#ifdef DEBUG
+    if (!has_err)
+    {
+      dprint (1, (debugfile, "vfolder_open_mailbox: '%s' output to stderr:\n", 
cmd));
+      has_err = 1;
+    }
+    dprint (1, (debugfile, "%s", buf));
+#endif
+  }
+  safe_fclose (&fperr);
+  rc = mutt_wait_filter (pid);
+  if (rc == -1)
+    dprint(1, (debugfile, "vfolder_open_mailbox: oops, the shell command has 
not completed\n"));
+  else if (rc)
+    dprint(1, (debugfile, "vfolder_open_mailbox: shell exited with non-zero 
status %d\n", rc));
+
+  mx_update_context (ctx, ctx->msgcount);
+
+  return 0;
+}
+
+int vfolder_sync_mailbox (CONTEXT *ctx, int *index_hint)
+{
+  int i;
+  char *p;
+  char newpath[_POSIX_PATH_MAX];
+
+  for (i = 0; i < ctx->msgcount; i++)
+  {
+    HEADER *h = ctx->hdrs[i];
+
+    if (h->changed || (h->deleted != h->trash))
+    {
+      strfcpy (newpath, h->path, sizeof (newpath));
+      p = strstr (newpath, ":2,");
+      if (!p)
+      {
+       /* must be a message in /new/ */
+       p = strstr (newpath, "/new/");
+       if (!p)
+       {
+         mutt_error (_("Unable to parse flags from %s"), newpath);
+         dprint (1, (debugfile, "vfolder_sync_mailbox: unable to find maildir 
flags in pathname: %s\n", newpath));
+         return -1;
+       }
+       /* overwrite /new/ with /cur/ so there will be enough buffer space */
+       memcpy (p, "/cur", 4);
+       p = newpath + strlen (newpath);
+      }
+      /* kill the previous flags */
+      else
+       *p = 0;
+      maildir_flags (p, sizeof (newpath) - strlen (newpath), h);
+
+      /* ensure the filename actually changed */
+      if (strcmp (newpath, h->path) != 0)
+      {
+       /* record that the message is possibly marked as trashed on disk */
+       h->trash = h->deleted;
+
+       dprint (2, (debugfile, "vfolder_sync_mailbox: renaming %s to %s\n", 
h->path, newpath));
+       if (rename (h->path, newpath) != 0)
+       {
+         mutt_perror ("rename");
+         return (-1);
+       }
+       mutt_str_replace (&h->path, newpath);
+      }
+    }
+  }
+  return 0;
+}

Reply via email to