<<< Date Index >>>     <<< Thread Index >>>

[PATCH] For 1.5.12: attachment counting for index display



* On 2005.09.18, in <432CFB3D.3020409@xxxxxxxxxx>,
*       "Brendan Cully" <brendan@xxxxxxxxxx> wrote:
> >
> >My best proposal as yet is to cache that result, so that a parse is
> >required only once per message per session, and still killing the parse
> >tree once that datum is obtained.  If the attach-allow, etc. settings
> >were changed, the cached values would be invalidated.
> 
> I don't know for sure that it would go in, but how about a version that
> hard codes the attachment recognition logic (fewer variables for users,
> less to parse) and caches the attachment count in the envelope?

I'd really prefer to keep the configurability.  I've found myself using
it more than once.  But I've always been uncomfortable with having eight
configuration commands just for attachments, and it's definitely a good
target.

Other than that, I've done up a new version of the patch
(patch-1.5.11.dgc.attach.6) that partially addresses these concerns.
If you've been a user in the past, please give it a try and let me
know of any problems.  This patch is against current CVS HEAD.  The
documentation is extended beyond what was in attach.5, and comforms to
the new docbook format (I think -- I haven't gotten it installed yet).


Changes:

    For the new version of the patch I've reduced all the old
    configuration commands to "attachments" and "unattachments":

        attachments   {+|-}disposition mime-type
        unattachments {+|-}disposition mime-type

    Disposition is the attachment's Content-disposition type -- either
    "inline" or "attachment".  You can abbreviate this to I or A:

        attachments   +A */.*
        attachments   -A text/x-vcard application/pgp.*
        attachments   -A application/x-pkcs7-.*

    "Unattachments" is in the tradition of "unalternates", "unlists",
    etc.  It removes an existing entry.  So the analogues with attach.5
    are:

        attach-allow                attachments   +A
        attach-exclude              attachments   -A
        inline-allow                attachments   +I
        inline-exclude              attachments   -I
        unattach-allow              unattachments +A
        unattach-exclude            unattachments -A
        uninline-allow              unattachments +I
        uninline-exclude            unattachments -I

    The defaults in the global Muttrc are adjusted accordingly.

    While rearranging this code, I uncovered some bugs in how the old
    version did matching, and some disparity between %X's counting and
    ~X's.  These are fixed, so you might notice some changes if you've
    been using attach.5.  If in doubt, there are some new features in
    the attachments menu you might want to look at; see below.


    Attach.6 provides validated attachment-count caching, so it's a
    lot faster.  There are several elements here.  For one thing, I
    modified mutt_count_body_parts() to retrieve the value from cache
    if it's available and valid, or to recount if not, or if forced.
    I moved the call to mutt_count_body_parts() from the actual %X/~X
    evaluation to mutt_parse_mime_message().  Consequently, a message's
    cached attachment count (HEADER->attach_total) is refreshed and
    revalidated (HEADER->attach_valid) every time the message is parsed.
    This simplifies the implementation of %X and ~X, because they only
    need to decide whether to parse the message first, and then call
    mutt_count_body_parts() to get the actual value from cache.  That
    cached value remains valid until an "attachments" or "unattachments"
    command is run, so once a message is parsed it never needs to be
    recounted or reparsed until it changes.

    (If the %X or ~X parses the message, it still frees the parse tree,
    as in attach.5.  I've confirmed constant, low memory usage in this
    version as well.)


    I also put some modifications into recvattach.c, providing two new
    expandos for $attach_format.  There are two new elements to struct
    body: attach_count (an int) and attach_qualifies (a 1-bit bitfield).
    Along the course of counting attachments after a message parse, each
    MIME part is evaluated to see whether it should qualify for the
    final attachment count.  If it does, then BODY->attach_qualifies is
    set.  And as the MIME document is recursed, the number of qualifying
    attachments at each level and below is stored in BODY->attach_count.

    %Q expands to a "Q" if a MIME part qualifies.  It can be used as a
    %?Q?&? condition if you want to display something else.

    %X expands to the count value for a MIME part and its descendants
    (if any).  It also can be used conditionally.

    By way of example, here's an attachment menu with $attach_format
    set as follows.  This is the default layout, but with the %Q and %X
    inserted.

        set attach_format="%u%D%I %?Q?+& ?%?X?[%2X]&    ? %t%4n %T%.34d%> 
[%.7m/%.10M, %.6e%?C?, %C?, %s] "

  I           1 <no description>             [text/plain, 7bit, us-ascii, 0.1K] 
  A +[ 3]     2 2 atts forwarded                   [message/rfc822, 7bit, 1.5K]
  I  [ 2]     3 `-><no description>                 [multipa/mixed, 7bit, 0.6K]
  I           4   |-><no description>        [text/plain, 7bit, us-ascii, 0.1K]
  A +[ 1]     5   |->hosts                   [text/plain, 7bit, us-ascii, 0.1K]
  A +[ 1]     6   `->ntp.drift               [text/plain, 7bit, us-ascii, 0.1K]

    If I collapse the multipart/mixed part:

  I           1 <no description>             [text/plain, 7bit, us-ascii, 0.1K]
  A +[ 3]     2 2 atts forwarded                   [message/rfc822, 7bit, 1.5K]
  I  [ 2]     3 `-><no description>                 [multipa/mixed, 7bit, 0.6K] 

    In this example, the three MIME parts with a "+" are the ones that
    qualify toward the final attachment count (HEADER->attach_total).
    And each row shows how many qualifying attachments it contains, or
    just "1" if it qualifies and is not a container.


Where to go:

    I think this is a big improvement performance-wise, and is still
    quite conservative in resource usage.  You'd not even know it's
    there in terms of the memory footprint.

    I suspect we could get rid of $attach_recurse and
    $attach_ignore_fundamental.  I'd be surprised if anyone ever uses
    "no" for either of these.  Any objections?

    The MIME type defaults currently are in the global Muttrc template,
    and I think this has been workable.  But it would be handy if these
    defaults were already in the code, and I'll see what I can do
    without sacrificing "[un]attachments" altogether.

    %Q in $attach_format is really useful, but %X is perhaps not.

    Although running the attachment count whenever a message is parsed
    is, on the whole, a good move, it does mean that there's no way
    to inhibit the attachment count if you're not using the %X or ~X
    features.  The attachment count is pretty low overhead in the
    context of a MIME parse, but still it might be worthwhile to have a
    way to disable it.  How important is this?

And finally, the kicker: have I just lost all hope of ever getting
into CVS? :)

Your comments are appreciated.

-- 
 -D.    dgc@xxxxxxxxxxxx        NSIT    University of Chicago
Index: Muttrc.head.in
===================================================================
RCS file: /home/roessler/cvs/mutt/Muttrc.head.in,v
retrieving revision 3.1
diff -u -r3.1 Muttrc.head.in
--- Muttrc.head.in      17 Sep 2005 17:39:18 -0000      3.1
+++ Muttrc.head.in      20 Sep 2005 06:09:10 -0000
@@ -30,6 +30,68 @@
 # set use_8bitmime
 
 ##
+## *** DEFAULT SETTINGS FOR THE ATTACHMENTS PATCH ***
+##
+
+## Recurse through MIME parts that contain other MIME parts.
+set attach_recurse
+
+## Discount fundamental parts that are inlined and are not containers.
+## (E.g., don't count the basic text/plain part that a JPEG of your
+## summer vacation is attached to.)
+set attach_ignore_fundamental
+
+##
+## Please see the manual (section "attachments")  for detailed
+## documentation of the "attachments" command.
+##
+## Removing a pattern from a list removes that pattern literally. It
+## does not remove any type matching the pattern.
+##
+##  attachments   +A */.*
+##  attachments   +A image/jpeg
+##  unattachments +A */.*
+##
+## This leaves "attached" image/jpeg files on the allowed attachments
+## list. It does not remove all items, as you might expect, because the
+## second */.* is not a matching expression at this time.
+##
+## Remember: "unattachments" only undoes what "attachments" has done!
+## It does not trigger any matching on actual messages.
+
+## Qualify any MIME part with an "attachment" disposition, EXCEPT for
+## text/x-vcard and application/pgp parts. (PGP parts are already known
+## to mutt, and can be searched for with ~g, ~G, and ~k.)
+##
+## I've added x-pkcs7 to this, since it functions (for S/MIME)
+## analogously to PGP signature attachments. S/MIME isn't supported
+## in a stock mutt build, but we can still treat it specially here.
+##
+attachments   +A */.*
+attachments   -A text/x-vcard application/pgp.*
+attachments   -A application/x-pkcs7-.*
+
+## Discount all MIME parts with an "inline" disposition, unless they're
+## text/plain. (Why inline a text/plain part unless it's external to the
+## message flow?)
+##
+attachments   +I text/plain
+  
+## These two lines make Mutt qualify MIME containers.  (So, for example,
+## a message/rfc822 forward will count as an attachment.)  The first
+## line is unnecessary if you already have "attach-allow */.*", of
+## course.  These are off by default!  The MIME elements contained
+## within a message/* or multipart/* are still examined, even if the
+## containers themseves don't qualify.
+##
+#attachments  +A message/.* multipart/.*
+#attachments  +I message/.* multipart/.*
+
+## You probably don't really care to know about deleted attachments.
+attachments   -A message/external-body
+attachments   -I message/external-body
+
+##
 ## More settings
 ##
 
Index: PATCHES
===================================================================
RCS file: /home/roessler/cvs/mutt/PATCHES,v
retrieving revision 3.6
diff -u -r3.6 PATCHES
--- PATCHES     9 Dec 2002 17:44:54 -0000       3.6
+++ PATCHES     20 Sep 2005 06:09:10 -0000
@@ -0,0 +1 @@
+patch-1.5.11.dgc.attach.6
Index: globals.h
===================================================================
RCS file: /home/roessler/cvs/mutt/globals.h,v
retrieving revision 3.24
diff -u -r3.24 globals.h
--- globals.h   17 Sep 2005 20:46:10 -0000      3.24
+++ globals.h   20 Sep 2005 06:09:10 -0000
@@ -138,6 +138,10 @@
 
 WHERE LIST *AutoViewList INITVAL(0);
 WHERE LIST *AlternativeOrderList INITVAL(0);
+WHERE LIST *AttachAllow INITVAL(0);
+WHERE LIST *AttachExclude INITVAL(0);
+WHERE LIST *InlineAllow INITVAL(0);
+WHERE LIST *InlineExclude INITVAL(0);
 WHERE LIST *HeaderOrderList INITVAL(0);
 WHERE LIST *Ignore INITVAL(0);
 WHERE LIST *MimeLookupList INITVAL(0);
Index: hdrline.c
===================================================================
RCS file: /home/roessler/cvs/mutt/hdrline.c,v
retrieving revision 3.16
diff -u -r3.16 hdrline.c
--- hdrline.c   17 Sep 2005 20:46:10 -0000      3.16
+++ hdrline.c   20 Sep 2005 06:09:10 -0000
@@ -218,6 +218,7 @@
  * %T = $to_chars
  * %u = user (login) name of author
  * %v = first name of author, unless from self
+ * %X = number of MIME attachments
  * %y = `x-label:' field (if present)
  * %Y = `x-label:' field (if present, tree unfolded, and != parent's x-label)
  * %Z = status flags   */
@@ -654,6 +655,33 @@
       mutt_format_s (dest, destlen, prefix, buf2);
       break;
 
+    case 'X':
+      {
+       int count, flags;
+
+       flags = 0;
+
+       if (option(OPTATRECURSE))
+         flags |= M_PARTS_MSGTRANS;
+
+        if (hdr->content->parts)
+          count = mutt_count_body_parts(hdr, flags);
+        else
+        {
+         mutt_parse_mime_message(ctx, hdr);
+          count = mutt_count_body_parts(hdr, flags);
+         mutt_free_body(&hdr->content->parts);
+        }
+
+       /* The recursion allows messages without depth to return 0. */
+       if (optional)
+          optional = count != 0;
+
+        snprintf (fmt, sizeof (fmt), "%%%sd", prefix);
+        snprintf (dest, destlen, fmt, count);
+      }
+      break;
+
      case 'y':
        if (optional)
         optional = hdr->env->x_label ? 1 : 0;
Index: init.c
===================================================================
RCS file: /home/roessler/cvs/mutt/init.c,v
retrieving revision 3.41
diff -u -r3.41 init.c
--- init.c      17 Sep 2005 20:46:10 -0000      3.41
+++ init.c      20 Sep 2005 06:09:10 -0000
@@ -782,6 +782,237 @@
   return 0;
 }
 
+/* always wise to do what someone else did before */
+static void _attachments_clean (void)
+{
+  int i;
+  if (Context && Context->msgcount) 
+  {
+    for (i = 0; i < Context->msgcount; i++)
+      Context->hdrs[i]->attach_valid = 0;
+  }
+}
+
+static int parse_attach_list (BUFFER *buf, BUFFER *s, LIST **ldata, BUFFER 
*err)
+{
+  ATTACH_MATCH *a;
+  LIST *listp, *lastp;
+  char *p;
+  char *tmpminor;
+  int len;
+
+  /* Find the last item in the list that data points to. */
+  lastp = NULL;
+  dprint(5, (debugfile, "parse_attach_list: ldata = %08x, *ldata = %08x\n",
+             (unsigned int)ldata, (unsigned int)*ldata));
+  for (listp = *ldata; listp; listp = listp->next)
+  {
+    a = (ATTACH_MATCH *)listp->data;
+    dprint(5, (debugfile, "parse_attach_list: skipping %s/%s\n",
+               a->major, a->minor));
+    lastp = listp;
+  }
+
+  do
+  {
+    mutt_extract_token (buf, s, 0);
+
+    if (!buf->data || *buf->data == '\0')
+      continue;
+   
+    a = safe_malloc(sizeof(ATTACH_MATCH));
+
+    /* some cheap hacks that I expect to remove */
+    if (!mutt_strcasecmp(buf->data, "any"))
+      a->major = safe_strdup("*/.*");
+    else if (!mutt_strcasecmp(buf->data, "none"))
+      a->major = safe_strdup("cheap_hack/this_should_never_match");
+    else
+      a->major = safe_strdup(buf->data);
+
+    if ((p = strchr(a->major, '/')))
+    {
+      *p = '\0';
+      ++p;
+      a->minor = p;
+    }
+    else
+    {
+      a->minor = "unknown";
+    }
+
+    len = strlen(a->minor);
+    tmpminor = safe_malloc(len+3);
+    strcpy(&tmpminor[1], a->minor);
+    tmpminor[0] = '^';
+    tmpminor[len+1] = '$';
+    tmpminor[len+2] = '\0';
+
+    a->major_int = mutt_check_mime_type(a->major);
+    regcomp(&a->minor_rx, tmpminor, REG_ICASE|REG_EXTENDED);
+
+    safe_free(&tmpminor);
+
+    dprint(5, (debugfile, "parse_attach_list: added %s/%s [%d]\n",
+               a->major, a->minor, a->major_int));
+
+    listp = safe_malloc(sizeof(LIST));
+    listp->data = (char *)a;
+    listp->next = NULL;
+    if (lastp)
+    {
+      lastp->next = listp;
+    }
+    else
+    {
+      *ldata = listp;
+    }
+    lastp = listp;
+  }
+  while (MoreArgs (s));
+   
+  _attachments_clean();
+  return 0;
+}
+
+static int parse_unattach_list (BUFFER *buf, BUFFER *s, LIST **ldata, BUFFER 
*err)
+{
+  ATTACH_MATCH *a;
+  LIST *lp, *lastp;
+  char *tmp;
+  int major;
+  char *minor;
+
+  do
+  {
+    mutt_extract_token (buf, s, 0);
+
+    if (!mutt_strcasecmp(buf->data, "any"))
+      tmp = safe_strdup("*/.*");
+    else if (!mutt_strcasecmp(buf->data, "none"))
+      tmp = safe_strdup("cheap_hack/this_should_never_match");
+    else
+      tmp = safe_strdup(buf->data);
+
+    if ((minor = strchr(tmp, '/')))
+    {
+      *minor = '\0';
+      ++minor;
+    }
+    else
+    {
+      minor = "unknown";
+    }
+    major = mutt_check_mime_type(tmp);
+
+    lastp = NULL;
+    for(lp = *ldata; lp; lp = lastp->next)
+    {
+      a = (ATTACH_MATCH *)lp->data;
+      dprint(5, (debugfile, "parse_unattach_list: check %s/%s [%d] : %s/%s 
[%d]\n",
+                 a->major, a->minor, a->major_int, tmp, minor, major));
+      if (a->major_int == major && !mutt_strcasecmp(minor, a->minor))
+      {
+       dprint(5, (debugfile, "parse_unattach_list: removed %s/%s [%d]\n",
+                   a->major, a->minor, a->major_int));
+       regfree(&a->minor_rx);
+        free(a->major);
+        if (lastp)
+       {
+          lastp->next = lp->next;
+       }
+       lastp = lp;
+        free (lp->data);       /* same as a */
+        free (lp);
+      }
+
+      lastp = lp;
+      lp = lp->next;
+    }
+
+    remove_from_list (ldata, buf->data);
+  }
+  while (MoreArgs (s));
+   
+  _attachments_clean();
+  return 0;
+}
+
+
+static int parse_attachments (BUFFER *buf, BUFFER *s, unsigned long data, 
BUFFER *err)
+{
+  char op, *p;
+  LIST **listp;
+
+  mutt_extract_token(buf, s, 0);
+  if (!buf->data || *buf->data == '\0') {
+    strfcpy(err->data, _("attachments: no disposition"), err->dsize);
+    return -1;
+  }
+
+  p = buf->data;
+  op = *p++;
+  if (op != '+' && op != '-') {
+    op = '+';
+    p--;
+  }
+  if (!mutt_strncasecmp(p, "attachment", strlen(p))) {
+    if (op == '+')
+      listp = &AttachAllow;
+    else
+      listp = &AttachExclude;
+  }
+  else if (!mutt_strncasecmp(p, "inline", strlen(p))) {
+    if (op == '+')
+      listp = &InlineAllow;
+    else
+      listp = &InlineExclude;
+  }
+  else {
+    strfcpy(err->data, _("attachments: invalid disposition"), err->dsize);
+    return -1;
+  }
+
+  return parse_attach_list(buf, s, listp, err);
+}
+
+static int parse_unattachments (BUFFER *buf, BUFFER *s, unsigned long data, 
BUFFER *err)
+{
+  char op, *p;
+  LIST **listp;
+
+  mutt_extract_token(buf, s, 0);
+  if (!buf->data || *buf->data == '\0') {
+    strfcpy(err->data, _("unattachments: no disposition"), err->dsize);
+    return -1;
+  }
+
+  p = buf->data;
+  op = *p++;
+  if (op != '+' && op != '-') {
+    op = '+';
+    p--;
+  }
+  if (mutt_strncasecmp(p, "attachment", strlen(p))) {
+    if (op == '+')
+      listp = &AttachAllow;
+    else
+      listp = &AttachExclude;
+  }
+  else if (mutt_strncasecmp(p, "inline", strlen(p))) {
+    if (op == '+')
+      listp = &InlineAllow;
+    else
+      listp = &InlineExclude;
+  }
+  else {
+    strfcpy(err->data, _("unattachments: invalid disposition"), err->dsize);
+    return -1;
+  }
+
+  return parse_unattach_list(buf, s, listp, err);
+}
+
 static int parse_unlists (BUFFER *buf, BUFFER *s, unsigned long data, BUFFER 
*err)
 {
   do
Index: init.h
===================================================================
RCS file: /home/roessler/cvs/mutt/init.h,v
retrieving revision 3.87
diff -u -r3.87 init.h
--- init.h      17 Sep 2005 20:46:10 -0000      3.87
+++ init.h      20 Sep 2005 06:15:23 -0000
@@ -220,10 +220,12 @@
   ** .dt %m  .dd major MIME type
   ** .dt %M  .dd MIME subtype
   ** .dt %n  .dd attachment number
+  ** .dt %Q  .dd "Q", if MIME part qualifies for attachment counting
   ** .dt %s  .dd size
   ** .dt %t  .dd tagged flag
   ** .dt %T  .dd graphic tree characters
   ** .dt %u  .dd unlink (=to delete) flag
+  ** .dt %X  .dd number of qualifying MIME parts in this part and its children
   ** .dt %>X .dd right justify the rest of the string and pad with character 
"X"
   ** .dt %|X .dd pad to the end of the line with character "X"
   ** .de
@@ -243,6 +245,18 @@
   ** ``$$attach_sep'' separator is added after each attachment. When set,
   ** Mutt will operate on the attachments one by one.
   */
+  { "attach_ignore_fundamental", DT_BOOL, R_INDEX, OPTATIGNORE, 1 },
+  /*
+  ** .pp
+  ** If set, Mutt's attachment counter discounts the fundamental MIME
+  ** part if its disposition is inline.
+  */
+  { "attach_recurse", DT_BOOL, R_INDEX, OPTATRECURSE, 1 },
+  /*
+  ** .pp
+  ** If set, Mutt's attachment counter (%X/~X) will examine message/*
+  ** components for attachments.
+  */
   { "attribution",     DT_STR,  R_NONE, UL &Attribution, UL "On %d, %n wrote:" 
},
   /*
   ** .pp
@@ -990,6 +1004,7 @@
   ** .dt %T .dd the appropriate character from the $$to_chars string
   ** .dt %u .dd user (login) name of the author
   ** .dt %v .dd first name of the author, or the recipient if the message is 
from you
+  ** .dt %X .dd number of attachments
   ** .dt %y .dd `x-label:' field, if present
   ** .dt %Y .dd `x-label' field, if present, and (1) not at part of a thread 
tree,
   **            (2) at the top of a thread, or (3) `x-label' is different from
@@ -2971,6 +2986,9 @@
 static int parse_unmy_hdr (BUFFER *, BUFFER *, unsigned long, BUFFER *);
 static int parse_subscribe (BUFFER *, BUFFER *, unsigned long, BUFFER *);
 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_alternates (BUFFER *, BUFFER *, unsigned long, BUFFER *);
 static int parse_unalternates (BUFFER *, BUFFER *, unsigned long, BUFFER *);
@@ -2990,6 +3008,8 @@
   { "account-hook",     mutt_parse_hook,        M_ACCOUNTHOOK },
 #endif
   { "alias",           parse_alias,            0 },
+  { "attachments",     parse_attachments,      0 },
+  { "unattachments",parse_unattachments,0 },
   { "auto_view",       parse_list,             UL &AutoViewList },
   { "alternative_order",       parse_list,     UL &AlternativeOrderList},
   { "bind",            mutt_parse_bind,        0 },
Index: mime.h
===================================================================
RCS file: /home/roessler/cvs/mutt/mime.h,v
retrieving revision 3.3
diff -u -r3.3 mime.h
--- mime.h      17 Sep 2005 20:46:10 -0000      3.3
+++ mime.h      20 Sep 2005 06:09:11 -0000
@@ -27,7 +27,8 @@
   TYPEMODEL,
   TYPEMULTIPART,
   TYPETEXT,
-  TYPEVIDEO
+  TYPEVIDEO,
+  TYPEANY
 };
 
 /* Content-Transfer-Encoding */
Index: mutt.h
===================================================================
RCS file: /home/roessler/cvs/mutt/mutt.h,v
retrieving revision 3.56
diff -u -r3.56 mutt.h
--- mutt.h      17 Sep 2005 20:46:10 -0000      3.56
+++ mutt.h      20 Sep 2005 06:09:11 -0000
@@ -238,6 +238,7 @@
   M_CRYPT_ENCRYPT,
   M_PGP_KEY,
   M_XLABEL,
+  M_MIMEATTACH,
   
   /* Options for Mailcap lookup */
   M_EDIT,
@@ -446,6 +447,9 @@
 
   OPTCRYPTUSEGPGME,
 
+  OPTATIGNORE,         /* Ignore fundamental inline parts? */
+  OPTATRECURSE,                /* Recurse message/\* types? */
+
   /* PGP options */
   
   OPTCRYPTAUTOSIGN,
@@ -643,7 +647,9 @@
   struct header *hdr;          /* header information for message/rfc822 */
 
   struct attachptr *aptr;      /* Menu information, used in recvattach.c */
-  
+
+  signed short attach_count;
+
   time_t stamp;                        /* time stamp of last
                                 * encoding update.
                                 */
@@ -681,6 +687,7 @@
   unsigned int badsig : 1;     /* bad cryptographic signature (needed to check 
encrypted s/mime-signatures) */
 
   unsigned int collapsed : 1;  /* used by recvattach */
+  unsigned int attach_qualifies : 1;
 
 } BODY;
 
@@ -719,6 +726,9 @@
   unsigned int searched : 1;
   unsigned int matched : 1;
 
+  /* tells whether the attachment count is valid */
+  unsigned int attach_valid : 1;
+
   /* the following are used to support collapsing threads  */
   unsigned int collapsed : 1;  /* is this message part of a collapsed thread? 
*/
   unsigned int limited : 1;    /* is this message in a limited view?  */
@@ -743,6 +753,9 @@
   char *tree;                  /* character string to print thread tree */
   struct thread *thread;
 
+  /* Number of qualifying attachments in message, if attach_valid */
+  short attach_total;
+
 #ifdef MIXMASTER
   LIST *chain;
 #endif
@@ -880,6 +893,20 @@
 void state_prefix_putc (char, STATE *);
 int  state_printf(STATE *, const char *, ...);
 
+/* for attachment counter */
+typedef struct
+{
+  char   *major;
+  int     major_int;
+  char   *minor;
+  regex_t minor_rx;
+} ATTACH_MATCH;
+
+/* Flags for mutt_count_body_parts() */
+#define M_PARTS_MSGTRANS       (1<<0)  /* message/rfc822 is transparent */
+#define M_PARTS_TOPLEVEL       (1<<1)  /* is the top-level part */
+#define M_PARTS_RECOUNT                (1<<2)  /* force recount */
+
 #include "ascii.h"
 #include "protos.h"
 #include "lib.h"
Index: parse.c
===================================================================
RCS file: /home/roessler/cvs/mutt/parse.c,v
retrieving revision 3.18
diff -u -r3.18 parse.c
--- parse.c     17 Sep 2005 20:46:10 -0000      3.18
+++ parse.c     20 Sep 2005 06:09:11 -0000
@@ -300,6 +300,10 @@
     return TYPEVIDEO;
   else if (ascii_strcasecmp ("model", s) == 0)
     return TYPEMODEL;
+  else if (ascii_strcasecmp ("*", s) == 0)
+    return TYPEANY;
+  else if (ascii_strcasecmp (".*", s) == 0)
+    return TYPEANY;
   else
     return TYPEOTHER;
 }
@@ -924,22 +928,31 @@
 void mutt_parse_mime_message (CONTEXT *ctx, HEADER *cur)
 {
   MESSAGE *msg;
+  int flags = 0;
 
-  if (cur->content->type != TYPEMESSAGE && cur->content->type != TYPEMULTIPART)
-    return; /* nothing to do */
+  do {
+    if (cur->content->type != TYPEMESSAGE &&
+        cur->content->type != TYPEMULTIPART)
+      break; /* nothing to do */
 
-  if (cur->content->parts)
-    return; /* The message was parsed earlier. */
+    if (cur->content->parts)
+      break; /* The message was parsed earlier. */
 
-  if ((msg = mx_open_message (ctx, cur->msgno)))
-  {
-    mutt_parse_part (msg->fp, cur->content);
+    if ((msg = mx_open_message (ctx, cur->msgno)))
+    {
+      mutt_parse_part (msg->fp, cur->content);
 
-    if (WithCrypto)
-      cur->security = crypt_query (cur->content);
+      if (WithCrypto)
+        cur->security = crypt_query (cur->content);
 
-    mx_close_message (&msg);
-  }
+      mx_close_message (&msg);
+    }
+  } while (0);
+
+  if (option(OPTATRECURSE))
+    flags |= M_PARTS_MSGTRANS;
+
+  mutt_count_body_parts(cur, flags|M_PARTS_RECOUNT);
 }
 
 int mutt_parse_rfc822_line (ENVELOPE *e, HEADER *hdr, char *line, char *p, 
short user_hdrs, short weed,
@@ -1457,3 +1470,165 @@
   
   return p;
 }
+
+/* Compares mime types to the ok and except lists */
+int count_body_parts_check(LIST **checklist, BODY *b, int dflt)
+{
+  LIST *type;
+  ATTACH_MATCH *a;
+
+  /* If list is null, use default behavior. */
+  if (! *checklist)
+  {
+    /*return dflt;*/
+    return 0;
+  }
+
+  for (type = *checklist; type; type = type->next)
+  {
+    a = (ATTACH_MATCH *)type->data;
+    dprint(5, (debugfile, "cbpc: %s %d/%s ?? %s/%s [%d]... ",
+               dflt ? "[OK]   " : "[EXCL] ",
+               b->type, b->subtype, a->major, a->minor, a->major_int));
+    if ((a->major_int == TYPEANY || a->major_int == b->type) &&
+       !regexec(&a->minor_rx, b->subtype, 0, NULL, 0))
+    {
+      dprint(5, (debugfile, "yes\n"));
+      return 1;
+    }
+    else
+    {
+      dprint(5, (debugfile, "no\n"));
+    }
+  }
+
+  return 0;
+}
+
+/*
+ * Define CBP_DEBUG to rewrite content-descs indicating why a body part
+ * counts or does not count. This is bad code, but it's only for debugging.
+ */
+#undef CBP_DEBUG
+#ifdef CBP_DEBUG
+# define AT_COUNT(why)   { shallcount = 1; \
+                          bp->description = strdup("yes: " ## why); }
+# define AT_NOCOUNT(why) { shallcount = 0; \
+                          bp->description = strdup(" no: " ## why); }
+#else
+# define AT_COUNT(why)   { shallcount = 1; }
+# define AT_NOCOUNT(why) { shallcount = 0; }
+#endif
+
+int count_body_parts (BODY *body, int flags)
+{
+  int count = 0;
+  int shallcount, shallrecurse;
+  BODY *bp;
+
+  if (body == NULL)
+    return 0;
+
+  for (bp = body; bp != NULL; bp = bp->next)
+  {
+    /* Initial disposition is to count and not to recurse this part. */
+    AT_COUNT("default");
+    shallrecurse = 0;
+
+    dprint(5, (debugfile, "bp: desc=\"%s\"; fn=\"%s\", type=\"%d/%s\"\n",
+          bp->description ? bp->description : ("none"),
+          bp->filename ? bp->filename :
+                       bp->d_filename ? bp->d_filename : "(none)",
+          bp->type, bp->subtype ? bp->subtype : "*"));
+
+    if (bp->type == TYPEMESSAGE)
+    {
+      /* If messages are "transparent", recursively examine their parts. */
+      if (flags & M_PARTS_MSGTRANS)
+       shallrecurse = 1;
+
+      /* If it's an external body pointer, don't recurse it. */
+      if (!ascii_strcasecmp (bp->subtype, "external-body"))
+       shallrecurse = 0;
+
+      /* Don't count containers if they're top-level. */
+      if (flags & M_PARTS_TOPLEVEL)
+       AT_NOCOUNT("top-level message/*");
+    }
+    else if (bp->type == TYPEMULTIPART)
+    {
+      /* Always recurse multiparts, except multipart/alternative. */
+      shallrecurse = 1;
+      if (!mutt_strcasecmp(bp->subtype, "alternative"))
+        shallrecurse = 0;
+
+      /* Don't count containers if they're top-level. */
+      if (flags & M_PARTS_TOPLEVEL)
+       AT_NOCOUNT("top-level multipart");
+    }
+
+    /* Do not count the fundamental part if it is inlined and
+     * attach_ignore_fundamental is set.
+     */
+    if (option(OPTATIGNORE) && (bp->disposition == DISPINLINE) &&
+        bp->type != TYPEMULTIPART && bp->type != TYPEMESSAGE && bp == body)
+      AT_NOCOUNT("ignore fundamental inlines");
+
+    /* If this body isn't scheduled for enumeration already, don't bother
+     * profiling it further.
+     */
+    if (shallcount)
+    {
+      /* Turn off shallcount if message type is not in ok list,
+       * or if it is in except list. Check is done separately for
+       * inlines vs. attachments.
+       */
+
+      if (bp->disposition == DISPATTACH)
+      {
+        if (!count_body_parts_check(&AttachAllow, bp, 1))
+         AT_NOCOUNT("attach not allowed");
+        if (count_body_parts_check(&AttachExclude, bp, 0))
+         AT_NOCOUNT("attach excluded");
+      }
+      else
+      {
+        if (!count_body_parts_check(&InlineAllow, bp, 1))
+         AT_NOCOUNT("inline not allowed");
+        if (count_body_parts_check(&InlineExclude, bp, 0))
+         AT_NOCOUNT("excluded");
+      }
+    }
+
+    if (shallcount)
+      count++;
+    bp->attach_qualifies = shallcount ? 1 : 0;
+
+    dprint(5, (debugfile, "cbp: %08x shallcount = %d\n", (unsigned int)bp, 
shallcount));
+
+    if (shallrecurse)
+    {
+      dprint(5, (debugfile, "cbp: %08x pre count = %d\n", (unsigned int)bp, 
count));
+      bp->attach_count = count_body_parts(bp->parts, flags & 
~M_PARTS_TOPLEVEL);
+      count += bp->attach_count;
+      dprint(5, (debugfile, "cbp: %08x post count = %d\n", (unsigned int)bp, 
count));
+    }
+  }
+
+  dprint(5, (debugfile, "bp: return %d\n", count < 0 ? 0 : count));
+  return count < 0 ? 0 : count;
+}
+
+int mutt_count_body_parts (HEADER *hdr, int flags)
+{
+  if (hdr->attach_valid && !(flags & M_PARTS_RECOUNT))
+    return hdr->attach_total;
+
+  if (AttachAllow || AttachExclude || InlineAllow || InlineExclude)
+    hdr->attach_total = count_body_parts(hdr->content, flags | 
M_PARTS_TOPLEVEL);
+  else
+    hdr->attach_total = 0;
+
+  hdr->attach_valid = 1;
+  return hdr->attach_total;
+}
Index: pattern.c
===================================================================
RCS file: /home/roessler/cvs/mutt/pattern.c,v
retrieving revision 3.22
diff -u -r3.22 pattern.c
--- pattern.c   17 Sep 2005 20:46:10 -0000      3.22
+++ pattern.c   20 Sep 2005 06:09:11 -0000
@@ -91,6 +91,7 @@
   { 'v', M_COLLAPSED,          0,              NULL },
   { 'V', M_CRYPT_VERIFIED,     0,              NULL },
   { 'x', M_REFERENCE,          0,              eat_regexp },
+  { 'X', M_MIMEATTACH,         0,              eat_range },
   { 'y', M_XLABEL,             0,              eat_regexp },
   { 'z', M_SIZE,               0,              eat_range },
   { '=', M_DUPLICATED,         0,              NULL },
@@ -1111,6 +1112,25 @@
       return (pat->not ^ (h->env->spam && h->env->spam->data && patmatch (pat, 
h->env->spam->data) == 0));
     case M_DUPLICATED:
       return (pat->not ^ (h->thread && h->thread->duplicate_thread));
+    case M_MIMEATTACH:
+      {
+      int count, flags = 0;
+
+      if (option(OPTATRECURSE))
+        flags |= M_PARTS_MSGTRANS;
+
+      if (h->content->parts)
+        count = mutt_count_body_parts(h, flags);
+      else
+      {
+        mutt_parse_mime_message(ctx, h);
+        count = mutt_count_body_parts(h, flags);
+        mutt_free_body(&h->content->parts);
+      }
+
+      return (pat->not ^ (count >= pat->min && (pat->max == M_MAXRANGE ||
+                                                count <= pat->max)));
+      }
     case M_UNREFERENCED:
       return (pat->not ^ (h->thread && !h->thread->child));
   }
Index: protos.h
===================================================================
RCS file: /home/roessler/cvs/mutt/protos.h,v
retrieving revision 3.38
diff -u -r3.38 protos.h
--- protos.h    17 Sep 2005 20:46:11 -0000      3.38
+++ protos.h    20 Sep 2005 06:09:11 -0000
@@ -169,6 +169,7 @@
 void mutt_buffy (char *, size_t);
 int  mutt_buffy_list (void);
 void mutt_canonical_charset (char *, size_t, const char *);
+int mutt_count_body_parts (HEADER *hdr, int flags);
 void mutt_check_rescore (CONTEXT *);
 void mutt_clear_error (void);
 void mutt_create_alias (ENVELOPE *, ADDRESS *);
Index: recvattach.c
===================================================================
RCS file: /home/roessler/cvs/mutt/recvattach.c,v
retrieving revision 3.19
diff -u -r3.19 recvattach.c
--- recvattach.c        17 Sep 2005 20:46:11 -0000      3.19
+++ recvattach.c        20 Sep 2005 06:09:11 -0000
@@ -297,6 +297,14 @@
        snprintf (dest, destlen, fmt, aptr->num + 1);
       }
       break;
+    case 'Q':
+      if (optional)
+        optional = aptr->content->attach_qualifies;
+      else {
+           snprintf (fmt, sizeof (fmt), "%%%sc", prefix);
+        mutt_format_s (dest, destlen, fmt, "Q");
+      }
+      break;
     case 's':
       if (flags & M_FORMAT_STAT_FILE)
       {
@@ -334,6 +342,15 @@
       else if (!aptr->content->unlink)
         optional = 0;
       break;
+    case 'X':
+      if (optional)
+        optional = (aptr->content->attach_count + 
aptr->content->attach_qualifies) != 0;
+      else
+      {
+        snprintf (fmt, sizeof (fmt), "%%%sd", prefix);
+        snprintf (dest, destlen, fmt, aptr->content->attach_count + 
aptr->content->attach_qualifies);
+      }
+      break;
     default:
       *dest = 0;
   }
Index: doc/manual.xml.head
===================================================================
RCS file: /home/roessler/cvs/mutt/doc/manual.xml.head,v
retrieving revision 3.11
diff -u -r3.11 manual.xml.head
--- doc/manual.xml.head 19 Sep 2005 05:12:29 -0000      3.11
+++ doc/manual.xml.head 20 Sep 2005 06:09:11 -0000
@@ -3307,6 +3307,7 @@
 ~v             message is part of a collapsed thread.
 ~V             cryptographically verified messages
 ~x EXPR         messages which contain EXPR in the `References' field
+~X [MIN]-[MAX]  messages with MIN to MAX attachments *)
 ~y EXPR         messages which contain EXPR in the `X-Label' field
 ~z [MIN]-[MAX]  messages with a size in the range MIN to MAX *)
 ~=             duplicated messages (see $duplicate_threads)
@@ -5146,6 +5147,114 @@
 
 </sect2>
 
+<sect2 id="attachments">
+<title>Attachment Searching and Counting</title>
+
+<para>
+If you ever lose track of attachments in your mailboxes, Mutt's
+attachment-counting and -searching support might be for you.  You can
+make your message index display the number of qualifying attachments in
+each message, or search for messages by attachment count.  You also can
+configure what kinds of attachments qualify for this feature with the
+attachments and unattachments commands.
+</para>
+
+<para>
+The syntax is:
+</para>
+<screen>
+attachments   {+|-}disposition mime-type
+unattachments {+|-}disposition mime-type
+</screen>
+
+<para>
+Disposition is the attachment's Content-disposition type -- either
+"inline" or "attachment".  You can abbreviate this to I or A.
+</para>
+
+<para>
+Disposition is prefixed by either a + symbolor a - symbol.  If it's
+a +, you're saying that you want to allow this disposition and MIME
+type to qualify.  If it's a -, you're saying that this disposition
+and MIME type is an exception to previous + rules.  There are examples
+below of how this is useful.
+</para>
+
+<para>
+Mime-type is, unsurprisingly, the MIME type of the attachment you want
+to affect.  A MIME type is always of the format "major/minor", where
+"major" describes the broad category of document you're looking at, and
+"minor" describes the specific type within that category.  The major
+part of mim-type must be literal text (or the special token "*"), but
+the minor part may be a regular expression.  (Therefore, "*/.*" matches
+any MIME type.)
+</para>
+
+<para>
+The MIME types you give to the attachments directive are a kind of
+pattern.  When you use the attachments directive, the patterns you
+specify are added to a list.  When you use unattachments, the pattern
+is removed from the list.  The patterns are not expanded and matched
+to specific MIME types at this time -- they're just text in a list.
+They're only matched when actually evaluating a message.
+</para>
+
+<para>
+Some examples might help to illustrate.  The examples that are not
+commented out define the default configuration of the lists.
+</para>
+
+<screen>
+## Removing a pattern from a list removes that pattern literally. It
+## does not remove any type matching the pattern.
+##
+##  attachments   +A */.*
+##  attachments   +A image/jpeg
+##  unattachments +A */.*
+##
+## This leaves "attached" image/jpeg files on the allowed attachments
+## list. It does not remove all items, as you might expect, because the
+## second */.* is not a matching expression at this time.
+##
+## Remember: "unattachments" only undoes what "attachments" has done!
+## It does not trigger any matching on actual messages.
+
+
+## Qualify any MIME part with an "attachment" disposition, EXCEPT for
+## text/x-vcard and application/pgp parts. (PGP parts are already known
+## to mutt, and can be searched for with ~g, ~G, and ~k.)
+##
+## I've added x-pkcs7 to this, since it functions (for S/MIME)
+## analogously to PGP signature attachments. S/MIME isn't supported
+## in a stock mutt build, but we can still treat it specially here.
+##
+attachments   +A */.*
+attachments   -A text/x-vcard application/pgp.*
+attachments   -A application/x-pkcs7-.*
+
+## Discount all MIME parts with an "inline" disposition, unless they're
+## text/plain. (Why inline a text/plain part unless it's external to the
+## message flow?)
+##
+attachments   +I text/plain
+
+## These two lines make Mutt qualify MIME containers.  (So, for example,
+## a message/rfc822 forward will count as an attachment.)  The first
+## line is unnecessary if you already have "attach-allow */.*", of
+## course.  These are off by default!  The MIME elements contained
+## within a message/* or multipart/* are still examined, even if the
+## containers themseves don't qualify.
+##
+#attachments  +A message/.* multipart/.*
+#attachments  +I message/.* multipart/.*
+
+## You probably don't really care to know about deleted attachments.
+attachments   -A message/external-body
+attachments   -I message/external-body
+</screen>
+
+</sect2>
+
 <sect2 id="mime-lookup">
 <title>MIME Lookup</title>
 
Index: doc/muttrc.man.head
===================================================================
RCS file: /home/roessler/cvs/mutt/doc/muttrc.man.head,v
retrieving revision 3.18
diff -u -r3.18 muttrc.man.head
--- doc/muttrc.man.head 17 Sep 2005 20:46:11 -0000      3.18
+++ doc/muttrc.man.head 20 Sep 2005 06:09:11 -0000
@@ -437,6 +437,7 @@
 ~v     message is part of a collapsed thread.
 ~V     cryptographically verified messages
 ~x \fIEXPR\fP  messages which contain \fIEXPR\fP in the \(lqReferences\(rq 
field
+~X \fIMIN\fP-\fIMAX\fP  messages with MIN - MAX attachments
 ~y \fIEXPR\fP  messages which contain \fIEXPR\fP in the \(lqX-Label\(rq field
 ~z \fIMIN\fP-\fIMAX\fP messages with a size in the range \fIMIN\fP to \fIMAX\fP
 ~=     duplicated messages (see $duplicate_threads)
@@ -445,7 +446,7 @@
 .PP
 In the above, \fIEXPR\fP is a regular expression.
 .PP
-With the \fB~m\fP, \fB~n\fP, and \fB~z\fP operators, you can also
+With the \fB~m\fP, \fB~n\fP, \fB~X\fP, and \fB~z\fP operators, you can also
 specify ranges in the forms \fB<\fP\fIMAX\fP, \fB>\fP\fIMIN\fP,
 \fIMIN\fP\fB-\fP, and \fB-\fP\fIMAX\fP.
 .SS Matching dates