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;
+}