]> git.notmuchmail.org Git - notmuch/commitdiff
Merge tag '0.12'
authorDavid Bremner <bremner@debian.org>
Tue, 20 Mar 2012 11:08:17 +0000 (08:08 -0300)
committerDavid Bremner <bremner@debian.org>
Tue, 20 Mar 2012 11:08:17 +0000 (08:08 -0300)
notmuch 0.12 release

46 files changed:
NEWS
command-line-arguments.c
devel/TODO
devel/schemata
emacs/notmuch-hello.el
emacs/notmuch-lib.el
emacs/notmuch-mua.el
emacs/notmuch-show.el
emacs/notmuch.el
lib/notmuch-private.h
lib/notmuch.h
lib/query.cc
lib/thread.cc
man/man1/notmuch-config.1
man/man1/notmuch-count.1
man/man1/notmuch-reply.1
man/man1/notmuch-search.1
man/man1/notmuch-show.1
notmuch-client.h
notmuch-config.c
notmuch-count.c
notmuch-reply.c
notmuch-search.c
notmuch-setup.c
notmuch-show.c
test/count
test/crypto
test/emacs
test/emacs-hello [new file with mode: 0755]
test/emacs.expected-output/notmuch-hello
test/emacs.expected-output/notmuch-hello-new-section [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-no-saved-searches
test/emacs.expected-output/notmuch-hello-section-counts [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-section-hidden-tag [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-section-with-empty [new file with mode: 0644]
test/emacs.expected-output/notmuch-hello-with-empty
test/encoding
test/json
test/maildir-sync
test/multipart
test/notmuch-test
test/raw
test/search
test/search-folder-coherence
test/test-lib.sh
test/thread-naming

diff --git a/NEWS b/NEWS
index 2e393c4b5332045e965cd2d6987993f06d4ea908..ed5e3c5a6e4af5e8fd70999a7e970b47f8b1bea0 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,57 @@
+Notmuch 0.13 (2012-xx-xx)
+=========================
+
+Command-Line Interface
+----------------------
+
+Reply to sender
+
+  "notmuch reply" has gained the ability to create a reply template
+  for replying just to the sender of the message, in addition to reply
+  to all. The feature is available through the new command line option
+  --reply-to=(all|sender).
+
+JSON reply format
+
+  "notmuch reply" can now produce JSON output that contains the headers
+  for a reply message and full information about the original message
+  begin replied to. This allows MUAs to create replies intelligtently.
+  For example, an MUA that can parse HTML might quote HTML parts.
+
+  Calling notmuch reply with --format=json imposes the restriction that
+  only a single message is returned by the search, as replying to
+  multiple messages does not have a well-defined behavior. The default
+  retains its current behavior for multiple message replies.
+
+Tag exclusion
+
+  Tags can be automatically excluded from search results by adding them
+  to the new 'search.exclude_tags' option in the Notmuch config file.
+
+  This behaviour can be overridden by explicitly including an excluded
+  tag in your query, for example:
+
+    notmuch search $your_query and tag:$excluded_tag
+
+  Existing users will probably want to run "notmuch setup" again to add
+  the new well-commented [search] section to the configuration file.
+
+  For new configurations, accepting the default setting will cause the
+  tags "deleted" and "spam" to be excluded, equivalent to running:
+
+    notmuch config set search.exclude_tags deleted spam
+
+Emacs Interface
+---------------
+
+Reply improvement using the JSON format
+
+  Emacs now uses the JSON reply format to create replies. It obeys
+  the customization variables message-citation-line-format and
+  message-citation-line-function when creating the first line of the
+  reply body, and it will quote HTML parts if no text/plain parts are
+  available.
+
 Notmuch 0.12 (2012-03-20)
 =========================
 
index e7114143259c6115c3c79b0d2c59a276704debbf..76b185f85be6d04487f6a107be03d820bf6cd234 100644 (file)
@@ -28,6 +28,24 @@ _process_keyword_arg (const notmuch_opt_desc_t *arg_desc, const char *arg_str) {
     return FALSE;
 }
 
+static notmuch_bool_t
+_process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str) {
+
+    if (next == 0) {
+       *((notmuch_bool_t *)arg_desc->output_var) = TRUE;
+       return TRUE;
+    }
+    if (strcmp (arg_str, "false") == 0) {
+       *((notmuch_bool_t *)arg_desc->output_var) = FALSE;
+       return TRUE;
+    }
+    if (strcmp (arg_str, "true") == 0) {
+       *((notmuch_bool_t *)arg_desc->output_var) = TRUE;
+       return TRUE;
+    }
+    return FALSE;
+}
+
 /*
    Search for the {pos_arg_index}th position argument, return FALSE if
    that does not exist.
@@ -76,14 +94,15 @@ parse_option (const char *arg,
            char *endptr;
 
            /* Everything but boolean arguments (switches) needs a
-            * delimiter, and a non-zero length value
+            * delimiter, and a non-zero length value. Boolean
+            * arguments may take an optional =true or =false value.
             */
-
-           if (try->opt_type != NOTMUCH_OPT_BOOLEAN) {
-               if (next != '=' && next != ':') return FALSE;
-               if (value[0] == 0) return FALSE;
+           if (next != '=' && next != ':' && next != 0) return FALSE;
+           if (next == 0) {
+               if (try->opt_type != NOTMUCH_OPT_BOOLEAN)
+                   return FALSE;
            } else {
-               if (next != 0) return FALSE;
+               if (value[0] == 0) return FALSE;
            }
 
            if (try->output_var == NULL)
@@ -94,8 +113,7 @@ parse_option (const char *arg,
                return _process_keyword_arg (try, value);
                break;
            case NOTMUCH_OPT_BOOLEAN:
-               *((notmuch_bool_t *)try->output_var) = TRUE;
-               return TRUE;
+               return _process_boolean_arg (try, next, value);
                break;
            case NOTMUCH_OPT_INT:
                *((int *)try->output_var) = strtol (value, &endptr, 10);
index 4dda6f4654895d5b1aa7edf0b4915bbd20d17156..7b750afa5cbde78b868f6ec111630aca8ac8da29 100644 (file)
@@ -141,6 +141,14 @@ Simplify notmuch-reply to simply print the headers (we have the
 original values) rather than calling GMime (which encodes) and adding
 the confusing gmime-filter-headers.c code (which decodes).
 
+Properly handle replying to multiple messages. Currently, the JSON
+reply format only supports a single message, but the default reply
+format accepts searches returning multiple messages. The expected
+behavior of replying to multiple messages is not obvious, and there
+are multiple ideas that might make sense. Some consensus needs to be
+reached on this issue, and then both reply formats should be updated
+to be consistent.
+
 notmuch library
 ---------------
 Add support for custom flag<->tag mappings. In the notmuch
index 24ad77571bc84685bd337e2e6fa8eb663c03bf0c..728a46f214ac5a1597c48a62f6d00efbd41ffcd6 100644 (file)
@@ -77,8 +77,9 @@ part = {
     content?:       string
 }
 
-# The headers of a message (format_headers_json with raw headers) or
-# a part (format_headers_message_part_json with pretty-printed headers)
+# The headers of a message (format_headers_json with raw headers
+# and reply = FALSE) or a part (format_headers_message_part_json
+# with pretty-printed headers)
 headers = {
     Subject:        string,
     From:           string,
@@ -136,3 +137,25 @@ thread = {
                               # matched and unmatched
     subject:        string
 }
+
+notmuch reply schema
+--------------------
+
+reply = {
+    # The headers of the constructed reply (format_headers_json with
+    # raw headers and reply = TRUE)
+    reply-headers: reply_headers,
+
+    # As in the show format (format_part_json)
+    original: message
+}
+
+reply_headers = {
+    Subject:        string,
+    From:           string,
+    To?:            string,
+    Cc?:            string,
+    Bcc?:           string,
+    In-reply-to:    string,
+    References:     string
+}
index d17a30f91e0c830dc3e394c97ff48b666401936d..e9caade5d4c1e6ad5fe2f6d76c63182d527c9212 100644 (file)
@@ -154,6 +154,108 @@ International Bureau of Weights and Measures."
 (defvar notmuch-hello-url "http://notmuchmail.org"
   "The `notmuch' web site.")
 
+(defvar notmuch-hello-search-pos nil
+  "Position of search widget, if any.
+
+This should only be set by `notmuch-hello-insert-search'.")
+
+(defvar notmuch-hello-custom-section-options
+  '((:filter (string :tag "Filter for each tag"))
+    (:filter-count (string :tag "Different filter to generate message counts"))
+    (:initially-hidden (const :tag "Hide this section on startup" t))
+    (:show-empty-searches (const :tag "Show queries with no matching messages" t))
+    (:hide-if-empty (const :tag "Hide this section if all queries are empty
+\(and not shown by show-empty-searches)" t)))
+  "Various customization-options for notmuch-hello-tags/query-section.")
+
+(define-widget 'notmuch-hello-tags-section 'lazy
+  "Customize-type for notmuch-hello tag-list sections."
+  :tag "Customized tag-list section (see docstring for details)"
+  :type
+  `(list :tag ""
+        (const :tag "" notmuch-hello-insert-tags-section)
+        (string :tag "Title for this section")
+        (plist
+         :inline t
+         :options
+         ,(append notmuch-hello-custom-section-options
+                  '((:hide-tags (repeat :tag "Tags that will be hidden"
+                                        string)))))))
+
+(define-widget 'notmuch-hello-query-section 'lazy
+  "Customize-type for custom saved-search-like sections"
+  :tag "Customized queries section (see docstring for details)"
+  :type
+  `(list :tag ""
+        (const :tag "" notmuch-hello-insert-query-list)
+        (string :tag "Title for this section")
+        (repeat :tag "Queries"
+                (cons (string :tag "Name") (string :tag "Query")))
+        (plist :inline t :options ,notmuch-hello-custom-section-options)))
+
+(defcustom notmuch-hello-sections
+  (list #'notmuch-hello-insert-header
+       #'notmuch-hello-insert-saved-searches
+       #'notmuch-hello-insert-search
+       #'notmuch-hello-insert-recent-searches
+       #'notmuch-hello-insert-alltags
+       #'notmuch-hello-insert-footer)
+  "Sections for notmuch-hello.
+
+The list contains functions which are used to construct sections in
+notmuch-hello buffer.  When notmuch-hello buffer is constructed,
+these functions are run in the order they appear in this list.  Each
+function produces a section simply by adding content to the current
+buffer.  A section should not end with an empty line, because a
+newline will be inserted after each section by `notmuch-hello'.
+
+Each function should take no arguments.  If the produced section
+includes `notmuch-hello-target' (i.e. cursor should be positioned
+inside this section), the function should return this element's
+position.
+Otherwise, it should return nil.
+
+For convenience an element can also be a list of the form (FUNC ARG1
+ARG2 .. ARGN) in which case FUNC will be applied to the rest of the
+list.
+
+A \"Customized tag-list section\" item in the customize-interface
+displays a list of all tags, optionally hiding some of them. It
+is also possible to filter the list of messages matching each tag
+by an additional filter query. Similarly, the count of messages
+displayed next to the buttons can be generated by applying a
+different filter to the tag query. These filters are also
+supported for \"Customized queries section\" items."
+  :group 'notmuch
+  :type
+  '(repeat
+    (choice (function-item notmuch-hello-insert-header)
+           (function-item notmuch-hello-insert-saved-searches)
+           (function-item notmuch-hello-insert-search)
+           (function-item notmuch-hello-insert-recent-searches)
+           (function-item notmuch-hello-insert-alltags)
+           (function-item notmuch-hello-insert-footer)
+           (function-item notmuch-hello-insert-inbox)
+           notmuch-hello-tags-section
+           notmuch-hello-query-section
+           (function :tag "Custom section"))))
+
+(defvar notmuch-hello-target nil
+  "Button text at position of point before rebuilding the notmuch-buffer.
+
+This variable contains the text of the button, if any, the
+point was positioned at before the notmuch-hello buffer was
+rebuilt. This should never actually be global and is defined as a
+defvar only for documentation purposes and to avoid a compiler
+warning about it occurring as a free variable.")
+
+(defvar notmuch-hello-hidden-sections nil
+  "List of sections titles whose contents are hidden")
+
+(defvar notmuch-hello-first-run t
+  "True if `notmuch-hello' is run for the first time, set to nil
+afterwards.")
+
 (defun notmuch-hello-nice-number (n)
   (let (result)
     (while (> n 0)
@@ -201,8 +303,8 @@ International Bureau of Weights and Measures."
     (message "Saved '%s' as '%s'." search name)
     (notmuch-hello-update)))
 
-(defun notmuch-hello-longest-label (tag-alist)
-  (or (loop for elem in tag-alist
+(defun notmuch-hello-longest-label (searches-alist)
+  (or (loop for elem in searches-alist
            maximize (length (car elem)))
       0))
 
@@ -266,12 +368,70 @@ should be. Returns a cons cell `(tags-per-line width)'."
                                   (* tags-per-line (+ 9 1))))
                           tags-per-line))))
 
-(defun notmuch-hello-insert-tags (tag-alist widest target)
-  (let* ((tags-and-width (notmuch-hello-tags-per-line widest))
+(defun notmuch-hello-filtered-query (query filter)
+  "Constructs a query to search all messages matching QUERY and FILTER.
+
+If FILTER is a string, it is directly used in the returned query.
+
+If FILTER is a function, it is called with QUERY as a parameter and
+the string it returns is used as the query. If nil is returned,
+the entry is hidden.
+
+Otherwise, FILTER is ignored.
+"
+  (cond
+   ((functionp filter) (funcall filter query))
+   ((stringp filter)
+    (concat "(" query ") and (" filter ")"))
+   (t query)))
+
+(defun notmuch-hello-query-counts (query-alist &rest options)
+  "Compute list of counts of matched messages from QUERY-ALIST.
+
+QUERY-ALIST must be a list containing elements of the form (NAME . QUERY)
+or (NAME QUERY COUNT-QUERY). If the latter form is used,
+COUNT-QUERY specifies an alternate query to be used to generate
+the count for the associated query.
+
+The result is the list of elements of the form (NAME QUERY COUNT).
+
+The values :show-empty-searches, :filter and :filter-count from
+options will be handled as specified for
+`notmuch-hello-insert-searches'."
+  (notmuch-remove-if-not
+   #'identity
+   (mapcar
+    (lambda (elem)
+      (let* ((name (car elem))
+            (query-and-count (if (consp (cdr elem))
+                                 ;; do we have a different query for the message count?
+                                 (cons (second elem) (third elem))
+                               (cons (cdr elem) (cdr elem))))
+            (message-count
+             (string-to-number
+              (notmuch-saved-search-count
+               (notmuch-hello-filtered-query (cdr query-and-count)
+                                             (or (plist-get options :filter-count)
+                                                (plist-get options :filter)))))))
+       (and (or (plist-get options :show-empty-searches) (> message-count 0))
+            (list name (notmuch-hello-filtered-query
+                        (car query-and-count) (plist-get options :filter))
+                  message-count))))
+    query-alist)))
+
+(defun notmuch-hello-insert-buttons (searches)
+  "Insert buttons for SEARCHES.
+
+SEARCHES must be a list containing lists of the form (NAME QUERY COUNT), where
+QUERY is the query to start when the button for the corresponding entry is
+activated. COUNT should be the number of messages matching the query.
+Such a list can be computed with `notmuch-hello-query-counts'."
+  (let* ((widest (notmuch-hello-longest-label searches))
+        (tags-and-width (notmuch-hello-tags-per-line widest))
         (tags-per-line (car tags-and-width))
         (widest (cdr tags-and-width))
         (count 0)
-        (reordered-list (notmuch-hello-reflect tag-alist tags-per-line))
+        (reordered-list (notmuch-hello-reflect searches tags-per-line))
         ;; Hack the display of the buttons used.
         (widget-push-button-prefix "")
         (widget-push-button-suffix "")
@@ -281,13 +441,13 @@ should be. Returns a cons cell `(tags-per-line width)'."
     (mapc (lambda (elem)
            ;; (not elem) indicates an empty slot in the matrix.
            (when elem
-             (let* ((name (car elem))
-                    (query (cdr elem))
+             (let* ((name (first elem))
+                    (query (second elem))
+                    (msg-count (third elem))
                     (formatted-name (format "%s " name)))
                (widget-insert (format "%8s "
-                                      (notmuch-hello-nice-number
-                                       (string-to-number (notmuch-saved-search-count query)))))
-               (if (string= formatted-name target)
+                                      (notmuch-hello-nice-number msg-count)))
+               (if (string= formatted-name notmuch-hello-target)
                    (setq found-target-pos (point-marker)))
                (widget-create 'push-button
                               :notify #'notmuch-hello-widget-search
@@ -359,29 +519,241 @@ Complete list of currently available key bindings:
  (kill-all-local-variables)
  (use-local-map notmuch-hello-mode-map)
  (setq major-mode 'notmuch-hello-mode
-       mode-name "notmuch-hello")
+       mode-name "notmuch-hello")
  (run-mode-hooks 'notmuch-hello-mode-hook)
  ;;(setq buffer-read-only t)
 )
 
-(defun notmuch-hello-generate-tag-alist ()
+(defun notmuch-hello-generate-tag-alist (&optional hide-tags)
   "Return an alist from tags to queries to display in the all-tags section."
-  (notmuch-remove-if-not
-   #'cdr
-   (mapcar (lambda (tag)
-            (cons tag
-                  (cond
-                   ((functionp notmuch-hello-tag-list-make-query)
-                    (concat "tag:" tag " and ("
-                            (funcall notmuch-hello-tag-list-make-query tag) ")"))
-                   ((stringp notmuch-hello-tag-list-make-query)
-                    (concat "tag:" tag " and ("
-                            notmuch-hello-tag-list-make-query ")"))
-                   (t (concat "tag:" tag)))))
-          (notmuch-remove-if-not
-           (lambda (tag)
-             (not (member tag notmuch-hello-hide-tags)))
-           (process-lines notmuch-command "search-tags")))))
+  (mapcar (lambda (tag)
+           (cons tag (format "tag:%s" tag)))
+         (notmuch-remove-if-not
+          (lambda (tag)
+            (not (member tag hide-tags)))
+          (process-lines notmuch-command "search-tags"))))
+
+(defun notmuch-hello-insert-header ()
+  "Insert the default notmuch-hello header."
+  (when notmuch-show-logo
+    (let ((image notmuch-hello-logo))
+      ;; The notmuch logo uses transparency. That can display poorly
+      ;; when inserting the image into an emacs buffer (black logo on
+      ;; a black background), so force the background colour of the
+      ;; image. We use a face to represent the colour so that
+      ;; `defface' can be used to declare the different possible
+      ;; colours, which depend on whether the frame has a light or
+      ;; dark background.
+      (setq image (cons 'image
+                       (append (cdr image)
+                               (list :background (face-background 'notmuch-hello-logo-background)))))
+      (insert-image image))
+    (widget-insert "  "))
+
+  (widget-insert "Welcome to ")
+  ;; Hack the display of the links used.
+  (let ((widget-link-prefix "")
+       (widget-link-suffix ""))
+    (widget-create 'link
+                  :notify (lambda (&rest ignore)
+                            (browse-url notmuch-hello-url))
+                  :help-echo "Visit the notmuch website."
+                  "notmuch")
+    (widget-insert ". ")
+    (widget-insert "You have ")
+    (widget-create 'link
+                  :notify (lambda (&rest ignore)
+                            (notmuch-hello-update))
+                  :help-echo "Refresh"
+                  (notmuch-hello-nice-number
+                   (string-to-number (car (process-lines notmuch-command "count")))))
+    (widget-insert " messages.\n")))
+
+
+(defun notmuch-hello-insert-saved-searches ()
+  "Insert the saved-searches section."
+  (let ((searches (notmuch-hello-query-counts
+                  (if notmuch-saved-search-sort-function
+                      (funcall notmuch-saved-search-sort-function
+                               notmuch-saved-searches)
+                    notmuch-saved-searches)
+                  :show-empty-searches notmuch-show-empty-saved-searches))
+       found-target-pos)
+    (when searches
+      (widget-insert "Saved searches: ")
+      (widget-create 'push-button
+                    :notify (lambda (&rest ignore)
+                              (customize-variable 'notmuch-saved-searches))
+                    "edit")
+      (widget-insert "\n\n")
+      (let ((start (point)))
+       (setq found-target-pos
+             (notmuch-hello-insert-buttons searches))
+       (indent-rigidly start (point) notmuch-hello-indent)
+       found-target-pos))))
+
+(defun notmuch-hello-insert-search ()
+  "Insert a search widget."
+  (widget-insert "Search: ")
+  (setq notmuch-hello-search-pos (point-marker))
+  (widget-create 'editable-field
+                ;; Leave some space at the start and end of the
+                ;; search boxes.
+                :size (max 8 (- (window-width) notmuch-hello-indent
+                                (length "Search: ")))
+                :action (lambda (widget &rest ignore)
+                          (notmuch-hello-search (widget-value widget))))
+  ;; Add an invisible dot to make `widget-end-of-line' ignore
+  ;; trailing spaces in the search widget field.  A dot is used
+  ;; instead of a space to make `show-trailing-whitespace'
+  ;; happy, i.e. avoid it marking the whole line as trailing
+  ;; spaces.
+  (widget-insert ".")
+  (put-text-property (1- (point)) (point) 'invisible t)
+  (widget-insert "\n"))
+
+(defun notmuch-hello-insert-recent-searches ()
+  "Insert recent searches."
+  (when notmuch-search-history
+    (widget-insert "Recent searches: ")
+    (widget-create 'push-button
+                  :notify (lambda (&rest ignore)
+                            (setq notmuch-search-history nil)
+                            (notmuch-hello-update))
+                  "clear")
+    (widget-insert "\n\n")
+    (let ((start (point)))
+      (loop for i from 1 to notmuch-hello-recent-searches-max
+           for search in notmuch-search-history do
+           (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i))))
+             (set widget-symbol
+                  (widget-create 'editable-field
+                                 ;; Don't let the search boxes be
+                                 ;; less than 8 characters wide.
+                                 :size (max 8
+                                            (- (window-width)
+                                               ;; Leave some space
+                                               ;; at the start and
+                                               ;; end of the
+                                               ;; boxes.
+                                               (* 2 notmuch-hello-indent)
+                                               ;; 1 for the space
+                                               ;; before the
+                                               ;; `[save]' button. 6
+                                               ;; for the `[save]'
+                                               ;; button.
+                                               1 6))
+                                 :action (lambda (widget &rest ignore)
+                                           (notmuch-hello-search (widget-value widget)))
+                                 search))
+             (widget-insert " ")
+             (widget-create 'push-button
+                            :notify (lambda (widget &rest ignore)
+                                      (notmuch-hello-add-saved-search widget))
+                            :notmuch-saved-search-widget widget-symbol
+                            "save"))
+           (widget-insert "\n"))
+      (indent-rigidly start (point) notmuch-hello-indent))
+    nil))
+
+(defun notmuch-hello-insert-searches (title query-alist &rest options)
+  "Insert a section with TITLE showing a list of buttons made from QUERY-ALIST.
+
+QUERY-ALIST must be a list containing elements of the form (NAME . QUERY)
+or (NAME QUERY COUNT-QUERY). If the latter form is used,
+COUNT-QUERY specifies an alternate query to be used to generate
+the count for the associated item.
+
+Supports the following entries in OPTIONS as a plist:
+:initially-hidden - if non-nil, section will be hidden on startup
+:show-empty-searches - show buttons with no matching messages
+:hide-if-empty - hide if no buttons would be shown
+   (only makes sense without :show-empty-searches)
+:filter - This can be a function that takes the search query as its argument and
+   returns a filter to be used in conjuction with the query for that search or nil
+   to hide the element. This can also be a string that is used as a combined with
+   each query using \"and\".
+:filter-count - Separate filter to generate the count displayed each search. Accepts
+   the same values as :filter. If :filter and :filter-count are specified, this
+   will be used instead of :filter, not in conjunction with it."
+  (widget-insert title ": ")
+  (if (and notmuch-hello-first-run (plist-get options :initially-hidden))
+      (add-to-list 'notmuch-hello-hidden-sections title))
+  (let ((is-hidden (member title notmuch-hello-hidden-sections))
+       (start (point)))
+    (if is-hidden
+       (widget-create 'push-button
+                      :notify `(lambda (widget &rest ignore)
+                                 (setq notmuch-hello-hidden-sections
+                                       (delete ,title notmuch-hello-hidden-sections))
+                                 (notmuch-hello-update))
+                      "show")
+      (widget-create 'push-button
+                    :notify `(lambda (widget &rest ignore)
+                               (add-to-list 'notmuch-hello-hidden-sections
+                                            ,title)
+                               (notmuch-hello-update))
+                    "hide"))
+    (widget-insert "\n")
+    (let (target-pos)
+      (when (not is-hidden)
+       (let ((searches (apply 'notmuch-hello-query-counts query-alist options)))
+         (when (or (not (plist-get options :hide-if-empty))
+                   searches)
+           (widget-insert "\n")
+           (setq target-pos
+                 (notmuch-hello-insert-buttons searches))
+           (indent-rigidly start (point) notmuch-hello-indent))))
+      target-pos)))
+
+(defun notmuch-hello-insert-tags-section (&optional title &rest options)
+  "Insert a section displaying all tags with message counts.
+
+TITLE defaults to \"All tags\".
+Allowed options are those accepted by `notmuch-hello-insert-searches' and the
+following:
+
+:hide-tags - List of tags that should be excluded."
+  (apply 'notmuch-hello-insert-searches
+        (or title "All tags")
+        (notmuch-hello-generate-tag-alist (plist-get options :hide-tags))
+        options))
+
+(defun notmuch-hello-insert-inbox ()
+  "Show an entry for each saved search and inboxed messages for each tag"
+  (notmuch-hello-insert-searches "What's in your inbox"
+                                (append
+                                 (notmuch-saved-searches)
+                                 (notmuch-hello-generate-tag-alist))
+                                :filter "tag:inbox"))
+
+(defun notmuch-hello-insert-alltags ()
+  "Insert a section displaying all tags and associated message counts"
+  (notmuch-hello-insert-tags-section
+   nil
+   :initially-hidden (not notmuch-show-all-tags-list)
+   :hide-tags notmuch-hello-hide-tags
+   :filter notmuch-hello-tag-list-make-query))
+
+(defun notmuch-hello-insert-footer ()
+  "Insert the notmuch-hello footer."
+  (let ((start (point)))
+    (widget-insert "Type a search query and hit RET to view matching threads.\n")
+    (when notmuch-search-history
+      (widget-insert "Hit RET to re-submit a previous search. Edit it first if you like.\n")
+      (widget-insert "Save recent searches with the `save' button.\n"))
+    (when notmuch-saved-searches
+      (widget-insert "Edit saved searches with the `edit' button.\n"))
+    (widget-insert "Hit RET or click on a saved search or tag name to view matching threads.\n")
+    (widget-insert "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
+    (widget-create 'link
+                  :notify (lambda (&rest ignore)
+                            (customize-variable 'notmuch-hello-sections))
+                  :button-prefix "" :button-suffix ""
+                  "Customize")
+    (widget-insert " this page.")
+    (let ((fill-column (- (window-width) notmuch-hello-indent)))
+      (center-region start (point)))))
 
 ;;;###autoload
 (defun notmuch-hello (&optional no-display)
@@ -397,13 +769,13 @@ Complete list of currently available key bindings:
       (set-buffer "*notmuch-hello*")
     (switch-to-buffer "*notmuch-hello*"))
 
-  (let ((target (if (widget-at)
-                  (widget-value (widget-at))
-                (condition-case nil
-                    (progn
-                      (widget-forward 1)
-                      (widget-value (widget-at)))
-                  (error nil))))
+  (let ((notmuch-hello-target (if (widget-at)
+                                 (widget-value (widget-at))
+                               (condition-case nil
+                                   (progn
+                                     (widget-forward 1)
+                                     (widget-value (widget-at)))
+                                 (error nil))))
        (inhibit-read-only t))
 
     ;; Delete all editable widget fields.  Editable widget fields are
@@ -422,168 +794,20 @@ Complete list of currently available key bindings:
       (mapc 'delete-overlay (car all))
       (mapc 'delete-overlay (cdr all)))
 
-    (when notmuch-show-logo
-      (let ((image notmuch-hello-logo))
-       ;; The notmuch logo uses transparency. That can display poorly
-       ;; when inserting the image into an emacs buffer (black logo on
-       ;; a black background), so force the background colour of the
-       ;; image. We use a face to represent the colour so that
-       ;; `defface' can be used to declare the different possible
-       ;; colours, which depend on whether the frame has a light or
-       ;; dark background.
-       (setq image (cons 'image
-                         (append (cdr image)
-                                 (list :background (face-background 'notmuch-hello-logo-background)))))
-       (insert-image image))
-      (widget-insert "  "))
-
-    (widget-insert "Welcome to ")
-    ;; Hack the display of the links used.
-    (let ((widget-link-prefix "")
-         (widget-link-suffix ""))
-      (widget-create 'link
-                    :notify (lambda (&rest ignore)
-                              (browse-url notmuch-hello-url))
-                    :help-echo "Visit the notmuch website."
-                    "notmuch")
-      (widget-insert ". ")
-      (widget-insert "You have ")
-      (widget-create 'link
-                    :notify (lambda (&rest ignore)
-                              (notmuch-hello-update))
-                    :help-echo "Refresh"
-                    (notmuch-hello-nice-number
-                     (string-to-number (car (process-lines notmuch-command "count")))))
-      (widget-insert " messages.\n"))
-
-    (let ((found-target-pos nil)
-         (final-target-pos nil)
-         (default-pos))
-      (let* ((saved-alist
-             ;; Filter out empty saved searches if required.
-             (if notmuch-show-empty-saved-searches
-                 notmuch-saved-searches
-               (loop for elem in notmuch-saved-searches
-                     if (> (string-to-number (notmuch-saved-search-count (cdr elem))) 0)
-                     collect elem)))
-            (saved-widest (notmuch-hello-longest-label saved-alist))
-            (alltags-alist (if notmuch-show-all-tags-list (notmuch-hello-generate-tag-alist)))
-            (alltags-widest (notmuch-hello-longest-label alltags-alist))
-            (widest (max saved-widest alltags-widest)))
-
-       (when saved-alist
-         ;; Sort saved searches if required.
-         (when notmuch-saved-search-sort-function
-           (setq saved-alist
-                 (funcall notmuch-saved-search-sort-function saved-alist)))
-         (widget-insert "\nSaved searches: ")
-         (widget-create 'push-button
-                        :notify (lambda (&rest ignore)
-                                  (customize-variable 'notmuch-saved-searches))
-                        "edit")
-         (widget-insert "\n\n")
-         (setq final-target-pos (point-marker))
-         (let ((start (point)))
-           (setq found-target-pos (notmuch-hello-insert-tags saved-alist widest target))
-           (if found-target-pos
-               (setq final-target-pos found-target-pos))
-           (indent-rigidly start (point) notmuch-hello-indent)))
-
-       (widget-insert "\nSearch: ")
-       (setq default-pos (point-marker))
-       (widget-create 'editable-field
-                      ;; Leave some space at the start and end of the
-                      ;; search boxes.
-                      :size (max 8 (- (window-width) notmuch-hello-indent
-                                      (length "Search: ")))
-                      :action (lambda (widget &rest ignore)
-                                (notmuch-hello-search (widget-value widget))))
-       ;; Add an invisible dot to make `widget-end-of-line' ignore
-       ;; trailing spaces in the search widget field.  A dot is used
-       ;; instead of a space to make `show-trailing-whitespace'
-       ;; happy, i.e. avoid it marking the whole line as trailing
-       ;; spaces.
-       (widget-insert ".")
-       (put-text-property (1- (point)) (point) 'invisible t)
-       (widget-insert "\n")
-
-       (when notmuch-search-history
-         (widget-insert "\nRecent searches: ")
-         (widget-create 'push-button
-                        :notify (lambda (&rest ignore)
-                                  (setq notmuch-search-history nil)
-                                  (notmuch-hello-update))
-                        "clear")
-         (widget-insert "\n\n")
-         (let ((start (point)))
-           (loop for i from 1 to notmuch-hello-recent-searches-max
-                 for search in notmuch-search-history do
-                   (let ((widget-symbol (intern (format "notmuch-hello-search-%d" i))))
-                     (set widget-symbol
-                          (widget-create 'editable-field
-                                         ;; Don't let the search boxes be
-                                         ;; less than 8 characters wide.
-                                         :size (max 8
-                                                    (- (window-width)
-                                                       ;; Leave some space
-                                                       ;; at the start and
-                                                       ;; end of the
-                                                       ;; boxes.
-                                                       (* 2 notmuch-hello-indent)
-                                                       ;; 1 for the space
-                                                       ;; before the
-                                                       ;; `[save]' button. 6
-                                                       ;; for the `[save]'
-                                                       ;; button.
-                                                       1 6))
-                                         :action (lambda (widget &rest ignore)
-                                                   (notmuch-hello-search (widget-value widget)))
-                                         search))
-                     (widget-insert " ")
-                     (widget-create 'push-button
-                                    :notify (lambda (widget &rest ignore)
-                                              (notmuch-hello-add-saved-search widget))
-                                    :notmuch-saved-search-widget widget-symbol
-                                    "save"))
-                   (widget-insert "\n"))
-           (indent-rigidly start (point) notmuch-hello-indent)))
-
-       (when alltags-alist
-         (widget-insert "\nAll tags: ")
-         (widget-create 'push-button
-                        :notify (lambda (widget &rest ignore)
-                                  (setq notmuch-show-all-tags-list nil)
-                                  (notmuch-hello-update))
-                        "hide")
-         (widget-insert "\n\n")
-         (let ((start (point)))
-           (setq found-target-pos (notmuch-hello-insert-tags alltags-alist widest target))
-           (unless final-target-pos
-             (setq final-target-pos found-target-pos))
-           (indent-rigidly start (point) notmuch-hello-indent)))
-
-       (widget-insert "\n")
-
-       (unless notmuch-show-all-tags-list
-         (widget-create 'push-button
-                        :notify (lambda (widget &rest ignore)
-                                  (setq notmuch-show-all-tags-list t)
-                                  (notmuch-hello-update))
-                        "Show all tags")))
-
-      (let ((start (point)))
-       (widget-insert "\n\n")
-       (widget-insert "Type a search query and hit RET to view matching threads.\n")
-       (when notmuch-search-history
-         (widget-insert "Hit RET to re-submit a previous search. Edit it first if you like.\n")
-         (widget-insert "Save recent searches with the `save' button.\n"))
-       (when notmuch-saved-searches
-         (widget-insert "Edit saved searches with the `edit' button.\n"))
-       (widget-insert "Hit RET or click on a saved search or tag name to view matching threads.\n")
-       (widget-insert "`=' to refresh this screen. `s' to search messages. `q' to quit.\n")
-       (let ((fill-column (- (window-width) notmuch-hello-indent)))
-         (center-region start (point))))
-
+    (let (final-target-pos)
+      (mapc
+       (lambda (section)
+        (let ((point-before (point))
+              (result (if (functionp section)
+                          (funcall section)
+                        (apply (car section) (cdr section)))))
+          (if (and (not final-target-pos) (integer-or-marker-p result))
+              (setq final-target-pos result))
+          ;; don't insert a newline when the previous section didn't show
+          ;; anything.
+          (unless (eq (point) point-before)
+            (widget-insert "\n"))))
+       notmuch-hello-sections)
       (widget-setup)
 
       (when final-target-pos
@@ -592,9 +816,10 @@ Complete list of currently available key bindings:
          (widget-forward 1)))
 
       (unless (widget-at)
-       (goto-char default-pos))))
-
-  (run-hooks 'notmuch-hello-refresh-hook))
+       (when notmuch-hello-search-pos
+         (goto-char notmuch-hello-search-pos)))))
+  (run-hooks 'notmuch-hello-refresh-hook)
+  (setq notmuch-hello-first-run nil))
 
 (defun notmuch-folder ()
   "Deprecated function for invoking notmuch---calling `notmuch' is preferred now."
index d315f7656e9bc4a004575faa14be386a858cdb83..c146748ac2935206377a8418bfc6a5cf5b105fd2 100644 (file)
@@ -21,6 +21,8 @@
 
 ;; This is an part of an emacs-based interface to the notmuch mail system.
 
+(eval-when-compile (require 'cl))
+
 (defvar notmuch-command "notmuch"
   "Command to run the notmuch binary.")
 
@@ -173,6 +175,67 @@ the user hasn't set this variable with the old or new value."
   (list 'when (< emacs-major-version 23)
        form))
 
+(defun notmuch-split-content-type (content-type)
+  "Split content/type into 'content' and 'type'"
+  (split-string content-type "/"))
+
+(defun notmuch-match-content-type (t1 t2)
+  "Return t if t1 and t2 are matching content types, taking wildcards into account"
+  (let ((st1 (notmuch-split-content-type t1))
+       (st2 (notmuch-split-content-type t2)))
+    (if (or (string= (cadr st1) "*")
+           (string= (cadr st2) "*"))
+       (string= (car st1) (car st2))
+      (string= t1 t2))))
+
+(defvar notmuch-multipart/alternative-discouraged
+  '(
+    ;; Avoid HTML parts.
+    "text/html"
+    ;; multipart/related usually contain a text/html part and some associated graphics.
+    "multipart/related"
+    ))
+
+(defun notmuch-multipart/alternative-choose (types)
+  "Return a list of preferred types from the given list of types"
+  ;; Based on `mm-preferred-alternative-precedence'.
+  (let ((seq types))
+    (dolist (pref (reverse notmuch-multipart/alternative-discouraged))
+      (dolist (elem (copy-sequence seq))
+       (when (string-match pref elem)
+         (setq seq (nconc (delete elem seq) (list elem))))))
+    seq))
+
+(defun notmuch-parts-filter-by-type (parts type)
+  "Given a list of message parts, return a list containing the ones matching
+the given type."
+  (remove-if-not
+   (lambda (part) (notmuch-match-content-type (plist-get part :content-type) type))
+   parts))
+
+;; Helper for parts which are generally not included in the default
+;; JSON output.
+(defun notmuch-get-bodypart-internal (message-id part-number process-crypto)
+  (let ((args '("show" "--format=raw"))
+       (part-arg (format "--part=%s" part-number)))
+    (setq args (append args (list part-arg)))
+    (if process-crypto
+       (setq args (append args '("--decrypt"))))
+    (setq args (append args (list message-id)))
+    (with-temp-buffer
+      (let ((coding-system-for-read 'no-conversion))
+       (progn
+         (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
+         (buffer-string))))))
+
+(defun notmuch-get-bodypart-content (msg part nth process-crypto)
+  (or (plist-get part :content)
+      (notmuch-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth process-crypto)))
+
+(defun notmuch-plist-to-alist (plist)
+  (loop for (key value . rest) on plist by #'cddr
+       collect (cons (substring (symbol-name key) 1) value)))
+
 ;; Compatibility functions for versions of emacs before emacs 23.
 ;;
 ;; Both functions here were copied from emacs 23 with the following copyright:
index 13244eb893f60ceceafc9953d9ac697b229ec702..6aae3a0578f6651a54758cc459476782f0b0a9fd 100644 (file)
 ;;
 ;; Authors: David Edmondson <dme@dme.org>
 
+(require 'json)
 (require 'message)
+(require 'format-spec)
 
 (require 'notmuch-lib)
 (require 'notmuch-address)
 
+(eval-when-compile (require 'cl))
+
 ;;
 
 (defcustom notmuch-mua-send-hook '(notmuch-mua-message-send-hook)
@@ -72,54 +76,92 @@ list."
            (push header message-hidden-headers)))
        notmuch-mua-hidden-headers))
 
+(defun notmuch-mua-get-quotable-parts (parts)
+  (loop for part in parts
+       if (notmuch-match-content-type (plist-get part :content-type) "multipart/alternative")
+         collect (let* ((subparts (plist-get part :content))
+                       (types (mapcar (lambda (part) (plist-get part :content-type)) subparts))
+                       (chosen-type (car (notmuch-multipart/alternative-choose types))))
+                  (loop for part in (reverse subparts)
+                        if (notmuch-match-content-type (plist-get part :content-type) chosen-type)
+                        return part))
+       else if (notmuch-match-content-type (plist-get part :content-type) "multipart/*")
+         append (notmuch-mua-get-quotable-parts (plist-get part :content))
+       else if (notmuch-match-content-type (plist-get part :content-type) "text/*")
+         collect part))
+
 (defun notmuch-mua-reply (query-string &optional sender reply-all)
-  (let (headers
-       body
-       (args '("reply")))
-    (if notmuch-show-process-crypto
-       (setq args (append args '("--decrypt"))))
+  (let ((args '("reply" "--format=json"))
+       reply
+       original)
+    (when notmuch-show-process-crypto
+      (setq args (append args '("--decrypt"))))
+
     (if reply-all
        (setq args (append args '("--reply-to=all")))
       (setq args (append args '("--reply-to=sender"))))
     (setq args (append args (list query-string)))
-    ;; This make assumptions about the output of `notmuch reply', but
-    ;; really only that the headers come first followed by a blank
-    ;; line and then the body.
+
+    ;; Get the reply object as JSON, and parse it into an elisp object.
     (with-temp-buffer
       (apply 'call-process (append (list notmuch-command nil (list t t) nil) args))
       (goto-char (point-min))
-      (if (re-search-forward "^$" nil t)
-         (save-excursion
-           (save-restriction
-             (narrow-to-region (point-min) (point))
-             (goto-char (point-min))
-             (setq headers (mail-header-extract)))))
-      (forward-line 1)
-      ;; Original message may contain (malicious) MML tags. We must
-      ;; properly quote them in the reply.
-      (mml-quote-region (point) (point-max))
-      (setq body (buffer-substring (point) (point-max))))
-    ;; If sender is non-nil, set the From: header to its value.
-    (when sender
-      (mail-header-set 'from sender headers))
-    (let
-       ;; Overlay the composition window on that being used to read
-       ;; the original message.
-       ((same-window-regexps '("\\*mail .*")))
-      (notmuch-mua-mail (mail-header 'to headers)
-                       (mail-header 'subject headers)
-                       (message-headers-to-generate headers t '(to subject))))
-    ;; insert the message body - but put it in front of the signature
-    ;; if one is present
-    (goto-char (point-max))
-    (if (re-search-backward message-signature-separator nil t)
+      (let ((json-object-type 'plist)
+           (json-array-type 'list)
+           (json-false 'nil))
+       (setq reply (json-read))))
+
+    ;; Extract the original message to simplify the following code.
+    (setq original (plist-get reply :original))
+
+    ;; Extract the headers of both the reply and the original message.
+    (let* ((original-headers (plist-get original :headers))
+          (reply-headers (plist-get reply :reply-headers)))
+
+      ;; If sender is non-nil, set the From: header to its value.
+      (when sender
+       (plist-put reply-headers :From sender))
+      (let
+         ;; Overlay the composition window on that being used to read
+         ;; the original message.
+         ((same-window-regexps '("\\*mail .*")))
+       (notmuch-mua-mail (plist-get reply-headers :To)
+                         (plist-get reply-headers :Subject)
+                         (notmuch-plist-to-alist reply-headers)))
+      ;; Insert the message body - but put it in front of the signature
+      ;; if one is present
+      (goto-char (point-max))
+      (if (re-search-backward message-signature-separator nil t)
          (forward-line -1)
-      (goto-char (point-max)))
-    (insert body)
-    (push-mark))
-  (set-buffer-modified-p nil)
-
-  (message-goto-body))
+       (goto-char (point-max)))
+
+      (let ((from (plist-get original-headers :From))
+           (date (plist-get original-headers :Date))
+           (start (point)))
+
+       ;; message-cite-original constructs a citation line based on the From and Date
+       ;; headers of the original message, which are assumed to be in the buffer.
+       (insert "From: " from "\n")
+       (insert "Date: " date "\n\n")
+
+       ;; Get the parts of the original message that should be quoted; this includes
+       ;; all the text parts, except the non-preferred ones in a multipart/alternative.
+       (let ((quotable-parts (notmuch-mua-get-quotable-parts (plist-get original :body))))
+         (mapc (lambda (part)
+                 (insert (notmuch-get-bodypart-content original part
+                                                       (plist-get part :id)
+                                                       notmuch-show-process-crypto)))
+               quotable-parts))
+
+       (set-mark (point))
+       (goto-char start)
+       ;; Quote the original message according to the user's configured style.
+       (message-cite-original))))
+
+  (goto-char (point-max))
+  (push-mark)
+  (message-goto-body)
+  (set-buffer-modified-p nil))
 
 (defun notmuch-mua-forward-message ()
   (message-forward)
@@ -145,7 +187,7 @@ OTHER-ARGS are passed through to `message-mail'."
       (when (not (string= "" user-agent))
        (push (cons "User-Agent" user-agent) other-headers))))
 
-  (unless (mail-header 'from other-headers)
+  (unless (mail-header 'From other-headers)
     (push (cons "From" (concat
                        (notmuch-user-name) " <" (notmuch-user-primary-email) ">")) other-headers))
 
@@ -208,7 +250,7 @@ the From: address first."
   (interactive "P")
   (let ((other-headers
         (when (or prompt-for-sender notmuch-always-prompt-for-sender)
-          (list (cons 'from (notmuch-mua-prompt-for-sender))))))
+          (list (cons 'From (notmuch-mua-prompt-for-sender))))))
     (notmuch-mua-mail nil nil other-headers)))
 
 (defun notmuch-mua-new-forward-message (&optional prompt-for-sender)
index 7c4c0beadecf4b842284bf93121b1764c146ad72..0cd7d82676723bcff3feaa52afd3678bc7c51465 100644 (file)
@@ -488,7 +488,7 @@ message at DEPTH in the current thread."
         (setq notmuch-show-process-crypto ,process-crypto)
         ;; Always acquires the part via `notmuch part', even if it is
         ;; available in the JSON output.
-        (insert (notmuch-show-get-bodypart-internal ,message-id ,nth))
+        (insert (notmuch-get-bodypart-internal ,message-id ,nth notmuch-show-process-crypto))
         ,@body))))
 
 (defun notmuch-show-save-part (message-id nth &optional filename content-type)
@@ -536,36 +536,19 @@ current buffer, if possible."
        ;; test whether we are able to inline it (which includes both
        ;; capability and suitability tests).
        (when (mm-inlined-p handle)
-         (insert (notmuch-show-get-bodypart-content msg part nth))
+         (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
          (when (mm-inlinable-p handle)
            (set-buffer display-buffer)
            (mm-display-part handle)
            t))))))
 
-(defvar notmuch-show-multipart/alternative-discouraged
-  '(
-    ;; Avoid HTML parts.
-    "text/html"
-    ;; multipart/related usually contain a text/html part and some associated graphics.
-    "multipart/related"
-    ))
-
 (defun notmuch-show-multipart/*-to-list (part)
   (mapcar (lambda (inner-part) (plist-get inner-part :content-type))
          (plist-get part :content)))
 
-(defun notmuch-show-multipart/alternative-choose (types)
-  ;; Based on `mm-preferred-alternative-precedence'.
-  (let ((seq types))
-    (dolist (pref (reverse notmuch-show-multipart/alternative-discouraged))
-      (dolist (elem (copy-sequence seq))
-       (when (string-match pref elem)
-         (setq seq (nconc (delete elem seq) (list elem))))))
-    seq))
-
 (defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth declared-type)
   (notmuch-show-insert-part-header nth declared-type content-type nil)
-  (let ((chosen-type (car (notmuch-show-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
+  (let ((chosen-type (car (notmuch-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
        (inner-parts (plist-get part :content))
        (start (point)))
     ;; This inserts all parts of the chosen type rather than just one,
@@ -630,8 +613,8 @@ current buffer, if possible."
          ;; times (hundreds!), which results in many calls to
          ;; `notmuch part'.
          (unless content
-           (setq content (notmuch-show-get-bodypart-internal (concat "id:" message-id)
-                                                             part-number))
+           (setq content (notmuch-get-bodypart-internal (concat "id:" message-id)
+                                                             part-number notmuch-show-process-crypto))
            (with-current-buffer w3m-current-buffer
              (notmuch-show-w3m-cid-store-internal url
                                                   message-id
@@ -751,7 +734,7 @@ current buffer, if possible."
     ;; insert a header to make this clear.
     (if (> nth 1)
        (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename)))
-    (insert (notmuch-show-get-bodypart-content msg part nth))
+    (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
     (save-excursion
       (save-restriction
        (narrow-to-region start (point-max))
@@ -761,7 +744,7 @@ current buffer, if possible."
 (defun notmuch-show-insert-part-text/calendar (msg part content-type nth depth declared-type)
   (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename))
   (insert (with-temp-buffer
-           (insert (notmuch-show-get-bodypart-content msg part nth))
+           (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
            (goto-char (point-min))
            (let ((file (make-temp-file "notmuch-ical"))
                  result)
@@ -808,9 +791,6 @@ current buffer, if possible."
 
 ;; Functions for determining how to handle MIME parts.
 
-(defun notmuch-show-split-content-type (content-type)
-  (split-string content-type "/"))
-
 (defun notmuch-show-handlers-for (content-type)
   "Return a list of content handlers for a part of type CONTENT-TYPE."
   (let (result)
@@ -821,30 +801,11 @@ current buffer, if possible."
          (list (intern (concat "notmuch-show-insert-part-*/*"))
                (intern (concat
                         "notmuch-show-insert-part-"
-                        (car (notmuch-show-split-content-type content-type))
+                        (car (notmuch-split-content-type content-type))
                         "/*"))
                (intern (concat "notmuch-show-insert-part-" content-type))))
     result))
 
-;; Helper for parts which are generally not included in the default
-;; JSON output.
-(defun notmuch-show-get-bodypart-internal (message-id part-number)
-  (let ((args '("show" "--format=raw"))
-       (part-arg (format "--part=%s" part-number)))
-    (setq args (append args (list part-arg)))
-    (if notmuch-show-process-crypto
-       (setq args (append args '("--decrypt"))))
-    (setq args (append args (list message-id)))
-    (with-temp-buffer
-      (let ((coding-system-for-read 'no-conversion))
-       (progn
-         (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
-         (buffer-string))))))
-
-(defun notmuch-show-get-bodypart-content (msg part nth)
-  (or (plist-get part :content)
-      (notmuch-show-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth)))
-
 ;; \f
 
 (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth declared-type)
@@ -981,7 +942,8 @@ current buffer, if possible."
 
     ;; Message visibility depends on whether it matched the search
     ;; criteria.
-    (notmuch-show-message-visible msg (plist-get msg :match))))
+    (notmuch-show-message-visible msg (and (plist-get msg :match)
+                                          (not (plist-get msg :excluded))))))
 
 (defun notmuch-show-toggle-process-crypto ()
   "Toggle the processing of cryptographic MIME parts."
@@ -1081,11 +1043,7 @@ function is used."
          notmuch-show-parent-buffer parent-buffer
          notmuch-show-query-context query-context)
     (notmuch-show-build-buffer)
-
-    ;; Move to the first open message and mark it read
-    (if (notmuch-show-message-visible-p)
-       (notmuch-show-mark-read)
-      (notmuch-show-next-open-message))))
+    (notmuch-show-goto-first-wanted-message)))
 
 (defun notmuch-show-build-buffer ()
   (let ((inhibit-read-only t))
@@ -1167,9 +1125,7 @@ reset based on the original query."
        (notmuch-show-apply-state state)
       ;; We're resetting state, so navigate to the first open message
       ;; and mark it read, just like opening a new show buffer.
-      (if (notmuch-show-message-visible-p)
-         (notmuch-show-mark-read)
-       (notmuch-show-next-open-message)))))
+      (notmuch-show-goto-first-wanted-message))))
 
 (defvar notmuch-show-stash-map
   (let ((map (make-sparse-keymap)))
@@ -1601,6 +1557,29 @@ to show, nil otherwise."
        (goto-char (point-max))))
     r))
 
+(defun notmuch-show-next-matching-message ()
+  "Show the next matching message."
+  (interactive)
+  (let (r)
+    (while (and (setq r (notmuch-show-goto-message-next))
+               (not (notmuch-show-get-prop :match))))
+    (if r
+       (progn
+         (notmuch-show-mark-read)
+         (notmuch-show-message-adjust))
+      (goto-char (point-max)))))
+
+(defun notmuch-show-goto-first-wanted-message ()
+  "Move to the first open message and mark it read"
+  (goto-char (point-min))
+  (if (notmuch-show-message-visible-p)
+      (notmuch-show-mark-read)
+    (notmuch-show-next-open-message))
+  (when (eobp)
+    (goto-char (point-min))
+    (unless (notmuch-show-get-prop :match)
+      (notmuch-show-next-matching-message))))
+
 (defun notmuch-show-previous-open-message ()
   "Show the previous open message."
   (interactive)
index f851c6f70476fcf91dff581ca94034bdc0f7997b..f0afa0721628c8d53e7abfe3b8d4e603260d2892 100644 (file)
@@ -872,16 +872,18 @@ non-authors is found, assume that all of the authors match."
                      (goto-char (point-max))
                      (if (/= (match-beginning 1) line)
                          (insert (concat "Error: Unexpected output from notmuch search:\n" (substring string line (match-beginning 1)) "\n")))
-                     (let ((beg (point)))
-                       (notmuch-search-show-result date count authors
-                                                   (notmuch-prettify-subject subject) tags)
-                       (notmuch-search-color-line beg (point) tag-list)
-                       (put-text-property beg (point) 'notmuch-search-thread-id thread-id)
-                       (put-text-property beg (point) 'notmuch-search-authors authors)
-                       (put-text-property beg (point) 'notmuch-search-subject subject)
-                       (when (string= thread-id notmuch-search-target-thread)
-                         (set 'found-target beg)
-                         (set 'notmuch-search-target-thread "found")))
+                     ;; We currently just throw away excluded matches.
+                     (unless (eq (aref count 1) ?0)
+                       (let ((beg (point)))
+                         (notmuch-search-show-result date count authors
+                                                     (notmuch-prettify-subject subject) tags)
+                         (notmuch-search-color-line beg (point) tag-list)
+                         (put-text-property beg (point) 'notmuch-search-thread-id thread-id)
+                         (put-text-property beg (point) 'notmuch-search-authors authors)
+                         (put-text-property beg (point) 'notmuch-search-subject subject)
+                         (when (string= thread-id notmuch-search-target-thread)
+                           (set 'found-target beg)
+                           (set 'notmuch-search-target-thread "found"))))
                      (set 'line (match-end 0)))
                  (set 'more nil)
                  (while (and (< line (length string)) (= (elt string line) ?\n))
@@ -960,7 +962,7 @@ PROMPT is the string to prompt with."
                                         completions)))
               (t (list string)))))))
       ;; this was simpler than convincing completing-read to accept spaces:
-      (define-key keymap (kbd "<tab>") 'minibuffer-complete)
+      (define-key keymap (kbd "TAB") 'minibuffer-complete)
       (let ((history-delete-duplicates t))
        (read-from-minibuffer prompt nil keymap nil
                              'notmuch-search-history nil nil)))))
index 7bf153e03fcd1b35bcc697f505e9d6441ecef9c4..ea836f721291c3d7d84bfb9edb3f70b3ea717155 100644 (file)
@@ -148,6 +148,8 @@ typedef enum _notmuch_private_status {
 
 typedef struct _notmuch_doc_id_set notmuch_doc_id_set_t;
 
+typedef struct _notmuch_string_list notmuch_string_list_t;
+
 /* database.cc */
 
 /* Lookup a prefix value by name.
@@ -216,6 +218,7 @@ _notmuch_thread_create (void *ctx,
                        notmuch_database_t *notmuch,
                        unsigned int seed_doc_id,
                        notmuch_doc_id_set_t *match_set,
+                       notmuch_string_list_t *excluded_terms,
                        notmuch_sort_t sort);
 
 /* message.cc */
@@ -401,6 +404,7 @@ typedef struct _notmuch_message_list {
  */
 struct visible _notmuch_messages {
     notmuch_bool_t is_of_list_type;
+    notmuch_doc_id_set_t *excluded_doc_ids;
     notmuch_message_node_t *iterator;
 };
 
@@ -458,11 +462,11 @@ typedef struct _notmuch_string_node {
     struct _notmuch_string_node *next;
 } notmuch_string_node_t;
 
-typedef struct visible _notmuch_string_list {
+struct visible _notmuch_string_list {
     int length;
     notmuch_string_node_t *head;
     notmuch_string_node_t **tail;
-} notmuch_string_list_t;
+};
 
 notmuch_string_list_t *
 _notmuch_string_list_create (const void *ctx);
index 7929fe7222ea0bc92d0842a2444d92373a0fc4c9..babd2086faf4cdaca964dc11638128542b8504d9 100644 (file)
@@ -449,6 +449,13 @@ typedef enum {
 const char *
 notmuch_query_get_query_string (notmuch_query_t *query);
 
+/* Specify whether to results should omit the excluded results rather
+ * than just marking them excluded. This is useful for passing a
+ * notmuch_messages_t not containing the excluded messages to other
+ * functions. */
+void
+notmuch_query_set_omit_excluded_messages (notmuch_query_t *query, notmuch_bool_t omit);
+
 /* Specify the sorting desired for this query. */
 void
 notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
@@ -665,8 +672,10 @@ notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
 /* Get the number of messages in 'thread' that matched the search.
  *
  * This count includes only the messages in this thread that were
- * matched by the search from which the thread was created. Contrast
- * with notmuch_thread_get_total_messages() .
+ * matched by the search from which the thread was created and were
+ * not excluded by any exclude tags passed in with the query (see
+ * notmuch_query_add_tag_exclude). Contrast with
+ * notmuch_thread_get_total_messages() .
  */
 int
 notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
@@ -895,7 +904,8 @@ notmuch_message_get_filenames (notmuch_message_t *message);
 
 /* Message flags */
 typedef enum _notmuch_message_flag {
-    NOTMUCH_MESSAGE_FLAG_MATCH
+    NOTMUCH_MESSAGE_FLAG_MATCH,
+    NOTMUCH_MESSAGE_FLAG_EXCLUDED
 } notmuch_message_flag_t;
 
 /* Get a value of a flag for the email corresponding to 'message'. */
index 0b366025ba847314605157e23b193410a792e801..68ac1e40178548f53dae91a263972eb210fdccf1 100644 (file)
@@ -28,6 +28,7 @@ struct _notmuch_query {
     const char *query_string;
     notmuch_sort_t sort;
     notmuch_string_list_t *exclude_terms;
+    notmuch_bool_t omit_excluded_messages;
 };
 
 typedef struct _notmuch_mset_messages {
@@ -57,15 +58,27 @@ struct visible _notmuch_threads {
     notmuch_doc_id_set_t match_set;
 };
 
+/* We need this in the message functions so forward declare. */
+static notmuch_bool_t
+_notmuch_doc_id_set_init (void *ctx,
+                         notmuch_doc_id_set_t *doc_ids,
+                         GArray *arr);
+
+static notmuch_bool_t
+_debug_query (void)
+{
+    char *env = getenv ("NOTMUCH_DEBUG_QUERY");
+    return (env && strcmp (env, "") != 0);
+}
+
 notmuch_query_t *
 notmuch_query_create (notmuch_database_t *notmuch,
                      const char *query_string)
 {
     notmuch_query_t *query;
 
-#ifdef DEBUG_QUERY
-    fprintf (stderr, "Query string is:\n%s\n", query_string);
-#endif
+    if (_debug_query ())
+       fprintf (stderr, "Query string is:\n%s\n", query_string);
 
     query = talloc (NULL, notmuch_query_t);
     if (unlikely (query == NULL))
@@ -79,6 +92,8 @@ notmuch_query_create (notmuch_database_t *notmuch,
 
     query->exclude_terms = _notmuch_string_list_create (query);
 
+    query->omit_excluded_messages = FALSE;
+
     return query;
 }
 
@@ -88,6 +103,12 @@ notmuch_query_get_query_string (notmuch_query_t *query)
     return query->query_string;
 }
 
+void
+notmuch_query_set_omit_excluded_messages (notmuch_query_t *query, notmuch_bool_t omit)
+{
+    query->omit_excluded_messages = omit;
+}
+
 void
 notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
 {
@@ -122,12 +143,16 @@ _notmuch_messages_destructor (notmuch_mset_messages_t *messages)
     return 0;
 }
 
-/* Return a query that does not match messages with the excluded tags
- * registered with the query.  Any tags that explicitly appear in
- * xquery will not be excluded. */
+/* Return a query that matches messages with the excluded tags
+ * registered with query.  Any tags that explicitly appear in xquery
+ * will not be excluded, and will be removed from the list of exclude
+ * tags.  The caller of this function has to combine the returned
+ * query appropriately.*/
 static Xapian::Query
 _notmuch_exclude_tags (notmuch_query_t *query, Xapian::Query xquery)
 {
+    Xapian::Query exclude_query = Xapian::Query::MatchNothing;
+
     for (notmuch_string_node_t *term = query->exclude_terms->head; term;
         term = term->next) {
        Xapian::TermIterator it = xquery.get_terms_begin ();
@@ -137,10 +162,12 @@ _notmuch_exclude_tags (notmuch_query_t *query, Xapian::Query xquery)
                break;
        }
        if (it == end)
-           xquery = Xapian::Query (Xapian::Query::OP_AND_NOT,
-                                   xquery, Xapian::Query (term->string));
+           exclude_query = Xapian::Query (Xapian::Query::OP_OR,
+                                   exclude_query, Xapian::Query (term->string));
+       else
+           term->string = talloc_strdup (query, "");
     }
-    return xquery;
+    return exclude_query;
 }
 
 notmuch_messages_t *
@@ -168,8 +195,9 @@ notmuch_query_search_messages (notmuch_query_t *query)
        Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
                                                   _find_prefix ("type"),
                                                   "mail"));
-       Xapian::Query string_query, final_query;
+       Xapian::Query string_query, final_query, exclude_query;
        Xapian::MSet mset;
+       Xapian::MSetIterator iterator;
        unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
                              Xapian::QueryParser::FLAG_PHRASE |
                              Xapian::QueryParser::FLAG_LOVEHATE |
@@ -187,8 +215,36 @@ notmuch_query_search_messages (notmuch_query_t *query)
            final_query = Xapian::Query (Xapian::Query::OP_AND,
                                         mail_query, string_query);
        }
+       messages->base.excluded_doc_ids = NULL;
+
+       if (query->exclude_terms) {
+           exclude_query = _notmuch_exclude_tags (query, final_query);
+
+           if (query->omit_excluded_messages)
+               final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
+                                            final_query, exclude_query);
+           else {
+               exclude_query = Xapian::Query (Xapian::Query::OP_AND,
+                                          exclude_query, final_query);
+
+               enquire.set_weighting_scheme (Xapian::BoolWeight());
+               enquire.set_query (exclude_query);
+
+               mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
+
+               GArray *excluded_doc_ids = g_array_new (FALSE, FALSE, sizeof (unsigned int));
+
+               for (iterator = mset.begin (); iterator != mset.end (); iterator++) {
+                   unsigned int doc_id = *iterator;
+                   g_array_append_val (excluded_doc_ids, doc_id);
+               }
+               messages->base.excluded_doc_ids = talloc (messages, _notmuch_doc_id_set);
+               _notmuch_doc_id_set_init (query, messages->base.excluded_doc_ids,
+                                         excluded_doc_ids);
+               g_array_unref (excluded_doc_ids);
+           }
+       }
 
-       final_query = _notmuch_exclude_tags (query, final_query);
 
        enquire.set_weighting_scheme (Xapian::BoolWeight());
 
@@ -206,9 +262,12 @@ notmuch_query_search_messages (notmuch_query_t *query)
            break;
        }
 
-#if DEBUG_QUERY
-       fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
-#endif
+       if (_debug_query ()) {
+           fprintf (stderr, "Exclude query is:\n%s\n",
+                    exclude_query.get_description ().c_str ());
+           fprintf (stderr, "Final query is:\n%s\n",
+                    final_query.get_description ().c_str ());
+       }
 
        enquire.set_query (final_query);
 
@@ -277,6 +336,10 @@ _notmuch_mset_messages_get (notmuch_messages_t *messages)
        INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
     }
 
+    if (messages->excluded_doc_ids &&
+       _notmuch_doc_id_set_contains (messages->excluded_doc_ids, doc_id))
+       notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, TRUE);
+
     return message;
 }
 
@@ -422,6 +485,7 @@ notmuch_threads_get (notmuch_threads_t *threads)
                                   threads->query->notmuch,
                                   doc_id,
                                   &threads->match_set,
+                                  threads->query->exclude_terms,
                                   threads->query->sort);
 }
 
@@ -449,7 +513,7 @@ notmuch_query_count_messages (notmuch_query_t *query)
        Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
                                                   _find_prefix ("type"),
                                                   "mail"));
-       Xapian::Query string_query, final_query;
+       Xapian::Query string_query, final_query, exclude_query;
        Xapian::MSet mset;
        unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
                              Xapian::QueryParser::FLAG_PHRASE |
@@ -469,14 +533,20 @@ notmuch_query_count_messages (notmuch_query_t *query)
                                         mail_query, string_query);
        }
 
-       final_query = _notmuch_exclude_tags (query, final_query);
+       exclude_query = _notmuch_exclude_tags (query, final_query);
+
+       final_query = Xapian::Query (Xapian::Query::OP_AND_NOT,
+                                        final_query, exclude_query);
 
        enquire.set_weighting_scheme(Xapian::BoolWeight());
        enquire.set_docid_order(Xapian::Enquire::ASCENDING);
 
-#if DEBUG_QUERY
-       fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
-#endif
+       if (_debug_query ()) {
+           fprintf (stderr, "Exclude query is:\n%s\n",
+                    exclude_query.get_description ().c_str ());
+           fprintf (stderr, "Final query is:\n%s\n",
+                    final_query.get_description ().c_str ());
+       }
 
        enquire.set_query (final_query);
 
index 0435ee6d1f3a62d24098b8f100bd6356ec2562ea..e976d643db3584f7ce057a7e20dc4adca2bcbdb7 100644 (file)
@@ -214,7 +214,8 @@ _thread_cleanup_author (notmuch_thread_t *thread,
  */
 static void
 _thread_add_message (notmuch_thread_t *thread,
-                    notmuch_message_t *message)
+                    notmuch_message_t *message,
+                    notmuch_string_list_t *exclude_terms)
 {
     notmuch_tags_t *tags;
     const char *tag;
@@ -262,6 +263,15 @@ _thread_add_message (notmuch_thread_t *thread,
         notmuch_tags_move_to_next (tags))
     {
        tag = notmuch_tags_get (tags);
+       /* Mark excluded messages. */
+       for (notmuch_string_node_t *term = exclude_terms->head; term;
+            term = term->next) {
+           /* We ignore initial 'K'. */
+           if (strcmp(tag, (term->string + 1)) == 0) {
+               notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED, TRUE);
+               break;
+           }
+       }
        g_hash_table_insert (thread->tags, xstrdup (tag), NULL);
     }
 }
@@ -321,7 +331,8 @@ _thread_add_matched_message (notmuch_thread_t *thread,
            _thread_set_subject_from_message (thread, message);
     }
 
-    thread->matched_messages++;
+    if (!notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED))
+       thread->matched_messages++;
 
     if (g_hash_table_lookup_extended (thread->message_hash,
                            notmuch_message_get_message_id (message), NULL,
@@ -392,6 +403,7 @@ _notmuch_thread_create (void *ctx,
                        notmuch_database_t *notmuch,
                        unsigned int seed_doc_id,
                        notmuch_doc_id_set_t *match_set,
+                       notmuch_string_list_t *exclude_terms,
                        notmuch_sort_t sort)
 {
     notmuch_thread_t *thread;
@@ -467,7 +479,7 @@ _notmuch_thread_create (void *ctx,
        if (doc_id == seed_doc_id)
            message = seed_message;
 
-       _thread_add_message (thread, message);
+       _thread_add_message (thread, message, exclude_terms);
 
        if ( _notmuch_doc_id_set_contains (match_set, doc_id)) {
            _notmuch_doc_id_set_remove (match_set, doc_id);
index a7468950141a73f3799e5bd6cd4947e7b9d7ca10..395cb9c4e1d320ae0bb9dd99e4a6b60c7362d38d 100644 (file)
@@ -83,6 +83,14 @@ will be ignored, regardless of the location in the mail store
 directory hierarchy.
 .RE
 
+.RS 4
+.TP 4
+.B search.exclude_tags
+A list of tags that will be excluded from search results by
+default. Using an excluded tag in a query will override that
+exclusion.
+.RE
+
 .RS 4
 .TP 4
 .B maildir.synchronize_flags
index 8de43453d89f439c81e32552c468a6bad6a0328b..35ecc5323497306eb60c6491941188260f757924 100644 (file)
@@ -38,6 +38,13 @@ Output the number of matching messages. This is the default.
 Output the number of matching threads.
 .RE
 .RE
+
+.RS 4
+.TP 4
+.BR \-\-no\-exclude
+
+Do not exclude the messages matching search.exclude_tags in the config file.
+.RE
 .RE
 .RE
 
index bd95b5f8c23ed1cb94da2058b59749ef0b56d7f8..8666549b5aec14b7965f79baf5559d428375a779 100644 (file)
@@ -37,12 +37,17 @@ Supported options for
 include
 .RS
 .TP 4
-.BR \-\-format= ( default | headers\-only )
+.BR \-\-format= ( default | json | headers\-only )
 .RS
 .TP 4
 .BR default
 Includes subject and quoted message body.
 .TP
+.BR json
+Produces JSON output containing headers for a reply message and the
+contents of the original message. This output can be used by a client
+to create a reply message intelligently.
+.TP
 .BR headers\-only
 Only produces In\-Reply\-To, References, To, Cc, and Bcc headers.
 .RE
@@ -63,6 +68,16 @@ values from the first that contains something other than only the
 user's addresses.
 .RE
 .RE
+.RS
+.TP 4
+.B \-\-decrypt
+
+Decrypt any MIME encrypted parts found in the selected content
+(ie. "multipart/encrypted" parts). Status of the decryption will be
+reported (currently only supported with --format=json) and the
+multipart/encrypted part will be replaced by the decrypted
+content.
+.RE
 
 See \fBnotmuch-search-terms\fR(7)
 for details of the supported syntax for <search-terms>.
@@ -73,7 +88,8 @@ with a search string matching a single message, (such as
 id:<message-id>), but it can be useful to reply to several messages at
 once. For example, when a series of patches are sent in a single
 thread, replying to the entire thread allows for the reply to comment
-on issue found in multiple patches.
+on issues found in multiple patches. The default format supports
+replying to multiple messages at once, but the JSON format does not.
 .RE
 .RE
 
index bf172207205254426c3f92678f3922b563104631..06d81a6f27889bd7b687c77734dc3eb9258aa239 100644 (file)
@@ -112,6 +112,13 @@ result from the end.
 Limit the number of displayed results to N.
 .RE
 
+.RS 4
+.TP 4
+.BR \-\-no\-exclude
+
+Do not exclude the messages matching search.exclude_tags in the config file.
+.RE
+
 .SH SEE ALSO
 
 \fBnotmuch\fR(1), \fBnotmuch-config\fR(1), \fBnotmuch-count\fR(1),
index d69834a1b6cf9dc2b2a0134bb3b35e73cc5ceb01..b81cce698379981b82b36623f98a6af856232b2b 100644 (file)
@@ -84,12 +84,17 @@ http://homepage.ntlworld.com/jonathan.deboynepollard/FGA/mail-mbox-formats.html
 .TP 4
 .BR raw " (default for a single part, see \-\-part)"
 
-For a message, the original, raw content of the email message is
-output. Consumers of this format should expect to implement MIME
-decoding and similar functions.
+For a message or an attached message part, the original, raw content
+of the email message is output. Consumers of this format should expect
+to implement MIME decoding and similar functions.
 
 For a single part (\-\-part) the raw part content is output after
-performing any necessary MIME decoding.
+performing any necessary MIME decoding.  Note that messages with a
+simple body still have two parts: part 0 is the whole message and part
+1 is the body.
+
+For a multipart part, the part headers and body (including all child
+parts) is output.
 
 The raw format must only be used with search terms matching single
 message.
@@ -128,6 +133,13 @@ multipart/encrypted part will be replaced by the decrypted
 content.
 .RE
 
+.RS 4
+.TP 4
+.B \-\-no-exclude
+
+Do not exclude the messages matching search.exclude_tags in the config file.
+.RE
+
 A common use of
 .B notmuch show
 is to display a single thread of email messages. For this, use a
index f4a62ccbaff6f69e65011c47bc8df823add9255f..fa04fa2e48d68761fe6fab97b12856db75fe4529 100644 (file)
 #define STRINGIFY(s) STRINGIFY_(s)
 #define STRINGIFY_(s) #s
 
-struct mime_node;
+typedef struct mime_node mime_node_t;
 struct notmuch_show_params;
 
 typedef struct notmuch_show_format {
     const char *message_set_start;
-    void (*part) (const void *ctx,
-                 struct mime_node *node, int indent,
-                 const struct notmuch_show_params *params);
+    notmuch_status_t (*part) (const void *ctx,
+                             struct mime_node *node, int indent,
+                             const struct notmuch_show_params *params);
     const char *message_start;
     void (*message) (const void *ctx,
                     notmuch_message_t *message,
@@ -191,6 +191,12 @@ show_message_body (notmuch_message_t *message,
 notmuch_status_t
 show_one_part (const char *filename, int part);
 
+void
+format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first);
+
+void
+format_headers_json (const void *ctx, GMimeMessage *message, notmuch_bool_t reply);
+
 char *
 json_quote_chararray (const void *ctx, const char *str, const size_t len);
 
@@ -288,7 +294,7 @@ debugger_is_active (void);
  * parts.  Message-type parts have one child, multipart-type parts
  * have multiple children, and leaf parts have zero children.
  */
-typedef struct mime_node {
+struct mime_node {
     /* The MIME object of this part.  This will be a GMimeMessage,
      * GMimePart, GMimeMultipart, or a subclass of one of these.
      *
@@ -351,7 +357,7 @@ typedef struct mime_node {
      * number to assign it (or -1 if unknown). */
     int next_child;
     int next_part_num;
-} mime_node_t;
+};
 
 /* Construct a new MIME node pointing to the root message part of
  * message.  If cryptoctx is non-NULL, it will be used to verify
index 61fda3ea4bafd49ef14d075c2cef70c8d5f2a20c..e9b275096acb999103db4b5a68a74f27f0a5dcf1 100644 (file)
@@ -377,7 +377,8 @@ notmuch_config_open (void *ctx,
 
     if (notmuch_config_get_search_exclude_tags (config, &tmp) == NULL) {
        if (is_new) {
-           /* We do not set default search_exclude_tags for 0.12 */
+           const char *tags[] = { "deleted", "spam" };
+           notmuch_config_set_search_exclude_tags (config, tags, 2);
        } else {
            notmuch_config_set_search_exclude_tags (config, NULL, 0);
        }
index 63459fb611f98d9a69507df7afdb919e61272563..46b76ae1d13b9340da505ff2b8471112f50d6c87 100644 (file)
@@ -35,8 +35,7 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
     char *query_str;
     int opt_index;
     int output = OUTPUT_MESSAGES;
-    const char **search_exclude_tags;
-    size_t search_exclude_tags_length;
+    notmuch_bool_t no_exclude = FALSE;
     unsigned int i;
 
     notmuch_opt_desc_t options[] = {
@@ -44,6 +43,7 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
          (notmuch_keyword_t []){ { "threads", OUTPUT_THREADS },
                                  { "messages", OUTPUT_MESSAGES },
                                  { 0, 0 } } },
+       { NOTMUCH_OPT_BOOLEAN, &no_exclude, "no-exclude", 'd', 0 },
        { 0, 0, 0, 0, 0 }
     };
 
@@ -78,10 +78,17 @@ notmuch_count_command (void *ctx, int argc, char *argv[])
        return 1;
     }
 
-    search_exclude_tags = notmuch_config_get_search_exclude_tags
-       (config, &search_exclude_tags_length);
-    for (i = 0; i < search_exclude_tags_length; i++)
-       notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+    if (!no_exclude) {
+       const char **search_exclude_tags;
+       size_t search_exclude_tags_length;
+
+       search_exclude_tags = notmuch_config_get_search_exclude_tags
+           (config, &search_exclude_tags_length);
+       for (i = 0; i < search_exclude_tags_length; i++)
+           notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+    }
+
+    notmuch_query_set_omit_excluded_messages (query, TRUE);
 
     switch (output) {
     case OUTPUT_MESSAGES:
index 6b244e6dfa752c329363f0cbf7374d2582282f28..e2b6c253a1ef505a8648ec87a874e43aaab9881b 100644 (file)
@@ -505,6 +505,61 @@ guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message
     return NULL;
 }
 
+static GMimeMessage *
+create_reply_message(void *ctx,
+                    notmuch_config_t *config,
+                    notmuch_message_t *message,
+                    notmuch_bool_t reply_all)
+{
+    const char *subject, *from_addr = NULL;
+    const char *in_reply_to, *orig_references, *references;
+
+    /* The 1 means we want headers in a "pretty" order. */
+    GMimeMessage *reply = g_mime_message_new (1);
+    if (reply == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return NULL;
+    }
+
+    subject = notmuch_message_get_header (message, "subject");
+    if (subject) {
+       if (strncasecmp (subject, "Re:", 3))
+           subject = talloc_asprintf (ctx, "Re: %s", subject);
+       g_mime_message_set_subject (reply, subject);
+    }
+
+    from_addr = add_recipients_from_message (reply, config,
+                                            message, reply_all);
+
+    if (from_addr == NULL)
+       from_addr = guess_from_received_header (config, message);
+
+    if (from_addr == NULL)
+       from_addr = notmuch_config_get_user_primary_email (config);
+
+    from_addr = talloc_asprintf (ctx, "%s <%s>",
+                                notmuch_config_get_user_name (config),
+                                from_addr);
+    g_mime_object_set_header (GMIME_OBJECT (reply),
+                             "From", from_addr);
+
+    in_reply_to = talloc_asprintf (ctx, "<%s>",
+                                  notmuch_message_get_message_id (message));
+
+    g_mime_object_set_header (GMIME_OBJECT (reply),
+                             "In-Reply-To", in_reply_to);
+
+    orig_references = notmuch_message_get_header (message, "references");
+    references = talloc_asprintf (ctx, "%s%s%s",
+                                 orig_references ? orig_references : "",
+                                 orig_references ? " " : "",
+                                 in_reply_to);
+    g_mime_object_set_header (GMIME_OBJECT (reply),
+                             "References", references);
+
+    return reply;
+}
+
 static int
 notmuch_reply_format_default(void *ctx,
                             notmuch_config_t *config,
@@ -515,8 +570,6 @@ notmuch_reply_format_default(void *ctx,
     GMimeMessage *reply;
     notmuch_messages_t *messages;
     notmuch_message_t *message;
-    const char *subject, *from_addr = NULL;
-    const char *in_reply_to, *orig_references, *references;
     const notmuch_show_format_t *format = &format_reply;
 
     for (messages = notmuch_query_search_messages (query);
@@ -525,49 +578,16 @@ notmuch_reply_format_default(void *ctx,
     {
        message = notmuch_messages_get (messages);
 
-       /* The 1 means we want headers in a "pretty" order. */
-       reply = g_mime_message_new (1);
-       if (reply == NULL) {
-           fprintf (stderr, "Out of memory\n");
-           return 1;
-       }
+       reply = create_reply_message (ctx, config, message, reply_all);
 
-       subject = notmuch_message_get_header (message, "subject");
-       if (subject) {
-           if (strncasecmp (subject, "Re:", 3))
-               subject = talloc_asprintf (ctx, "Re: %s", subject);
-           g_mime_message_set_subject (reply, subject);
+       /* If reply creation failed, we're out of memory, so don't
+        * bother trying any more messages.
+        */
+       if (!reply) {
+           notmuch_message_destroy (message);
+           return 1;
        }
 
-       from_addr = add_recipients_from_message (reply, config, message,
-                                                reply_all);
-
-       if (from_addr == NULL)
-           from_addr = guess_from_received_header (config, message);
-
-       if (from_addr == NULL)
-           from_addr = notmuch_config_get_user_primary_email (config);
-
-       from_addr = talloc_asprintf (ctx, "%s <%s>",
-                                    notmuch_config_get_user_name (config),
-                                    from_addr);
-       g_mime_object_set_header (GMIME_OBJECT (reply),
-                                 "From", from_addr);
-
-       in_reply_to = talloc_asprintf (ctx, "<%s>",
-                            notmuch_message_get_message_id (message));
-
-       g_mime_object_set_header (GMIME_OBJECT (reply),
-                                 "In-Reply-To", in_reply_to);
-
-       orig_references = notmuch_message_get_header (message, "references");
-       references = talloc_asprintf (ctx, "%s%s%s",
-                                     orig_references ? orig_references : "",
-                                     orig_references ? " " : "",
-                                     in_reply_to);
-       g_mime_object_set_header (GMIME_OBJECT (reply),
-                                 "References", references);
-
        show_reply_headers (reply);
 
        g_object_unref (G_OBJECT (reply));
@@ -584,6 +604,51 @@ notmuch_reply_format_default(void *ctx,
     return 0;
 }
 
+static int
+notmuch_reply_format_json(void *ctx,
+                         notmuch_config_t *config,
+                         notmuch_query_t *query,
+                         notmuch_show_params_t *params,
+                         notmuch_bool_t reply_all)
+{
+    GMimeMessage *reply;
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    mime_node_t *node;
+
+    if (notmuch_query_count_messages (query) != 1) {
+       fprintf (stderr, "Error: search term did not match precisely one message.\n");
+       return 1;
+    }
+
+    messages = notmuch_query_search_messages (query);
+    message = notmuch_messages_get (messages);
+    if (mime_node_open (ctx, message, params->cryptoctx, params->decrypt,
+                       &node) != NOTMUCH_STATUS_SUCCESS)
+       return 1;
+
+    reply = create_reply_message (ctx, config, message, reply_all);
+    if (!reply)
+       return 1;
+
+    /* The headers of the reply message we've created */
+    printf ("{\"reply-headers\": ");
+    format_headers_json (ctx, reply, TRUE);
+    g_object_unref (G_OBJECT (reply));
+    reply = NULL;
+
+    /* Start the original */
+    printf (", \"original\": ");
+
+    format_part_json (ctx, node, TRUE);
+
+    /* End */
+    printf ("}\n");
+    notmuch_message_destroy (message);
+
+    return 0;
+}
+
 /* This format is currently tuned for a git send-email --notmuch hook */
 static int
 notmuch_reply_format_headers_only(void *ctx,
@@ -646,6 +711,7 @@ notmuch_reply_format_headers_only(void *ctx,
 
 enum {
     FORMAT_DEFAULT,
+    FORMAT_JSON,
     FORMAT_HEADERS_ONLY,
 };
 
@@ -665,6 +731,7 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
     notmuch_opt_desc_t options[] = {
        { NOTMUCH_OPT_KEYWORD, &format, "format", 'f',
          (notmuch_keyword_t []){ { "default", FORMAT_DEFAULT },
+                                 { "json", FORMAT_JSON },
                                  { "headers-only", FORMAT_HEADERS_ONLY },
                                  { 0, 0 } } },
        { NOTMUCH_OPT_KEYWORD, &reply_all, "reply-to", 'r',
@@ -683,6 +750,8 @@ notmuch_reply_command (void *ctx, int argc, char *argv[])
 
     if (format == FORMAT_HEADERS_ONLY)
        reply_format_func = notmuch_reply_format_headers_only;
+    else if (format == FORMAT_JSON)
+       reply_format_func = notmuch_reply_format_json;
     else
        reply_format_func = notmuch_reply_format_default;
 
index 92ce38a1a5fb37454237c09313e099aa6f8f9b52..f6061e4ec08920273450bfba954473812110504f 100644 (file)
@@ -210,6 +210,9 @@ do_search_threads (const search_format_t *format,
     int first_thread = 1;
     int i;
 
+    if (output == OUTPUT_THREADS)
+       notmuch_query_set_omit_excluded_messages (query, TRUE);
+
     if (offset < 0) {
        offset += notmuch_query_count_threads (query);
        if (offset < 0)
@@ -300,6 +303,8 @@ do_search_messages (const search_format_t *format,
     int first_message = 1;
     int i;
 
+    notmuch_query_set_omit_excluded_messages (query, TRUE);
+
     if (offset < 0) {
        offset += notmuch_query_count_messages (query);
        if (offset < 0)
@@ -371,6 +376,10 @@ do_search_tags (notmuch_database_t *notmuch,
     const char *tag;
     int first_tag = 1;
 
+    notmuch_query_set_omit_excluded_messages (query, TRUE);
+    /* should the following only special case if no excluded terms
+     * specified? */
+
     /* Special-case query of "*" for better performance. */
     if (strcmp (notmuch_query_get_query_string (query), "*") == 0) {
        tags = notmuch_database_get_all_tags (notmuch);
@@ -426,8 +435,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
     output_t output = OUTPUT_SUMMARY;
     int offset = 0;
     int limit = -1; /* unlimited */
-    const char **search_exclude_tags;
-    size_t search_exclude_tags_length;
+    notmuch_bool_t no_exclude = FALSE;
     unsigned int i;
 
     enum { NOTMUCH_FORMAT_JSON, NOTMUCH_FORMAT_TEXT }
@@ -449,6 +457,7 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
                                  { "files", OUTPUT_FILES },
                                  { "tags", OUTPUT_TAGS },
                                  { 0, 0 } } },
+       { NOTMUCH_OPT_BOOLEAN, &no_exclude, "no-exclude", 'd', 0 },
        { NOTMUCH_OPT_INT, &offset, "offset", 'O', 0 },
        { NOTMUCH_OPT_INT, &limit, "limit", 'L', 0  },
        { 0, 0, 0, 0, 0 }
@@ -496,10 +505,15 @@ notmuch_search_command (void *ctx, int argc, char *argv[])
 
     notmuch_query_set_sort (query, sort);
 
-    search_exclude_tags = notmuch_config_get_search_exclude_tags
-       (config, &search_exclude_tags_length);
-    for (i = 0; i < search_exclude_tags_length; i++)
-       notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+    if (!no_exclude) {
+       const char **search_exclude_tags;
+       size_t search_exclude_tags_length;
+
+       search_exclude_tags = notmuch_config_get_search_exclude_tags
+           (config, &search_exclude_tags_length);
+       for (i = 0; i < search_exclude_tags_length; i++)
+           notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+    }
 
     switch (output) {
     default:
index 307231d51b55f8ea13c7358839db86cc3f7df4dd..94d0aa7bace6e96a8eb417ace7a71a8a8ed800d5 100644 (file)
@@ -133,6 +133,8 @@ notmuch_setup_command (unused (void *ctx),
     int is_new;
     const char **new_tags;
     size_t new_tags_len;
+    const char **search_exclude_tags;
+    size_t search_exclude_tags_len;
 
 #define prompt(format, ...)                                    \
     do {                                                       \
@@ -209,7 +211,22 @@ notmuch_setup_command (unused (void *ctx),
     }
 
 
-    /* Temporarily remove exclude tag support for 0.12 */
+    search_exclude_tags = notmuch_config_get_search_exclude_tags (config, &search_exclude_tags_len);
+
+    printf ("Tags to exclude when searching messages (separated by spaces) [");
+    print_tag_list (search_exclude_tags, search_exclude_tags_len);
+    prompt ("]: ");
+
+    if (strlen (response)) {
+       GPtrArray *tags = parse_tag_list (ctx, response);
+
+       notmuch_config_set_search_exclude_tags (config,
+                                               (const char **) tags->pdata,
+                                               tags->len);
+
+       g_ptr_array_free (tags, TRUE);
+    }
+
 
     if (! notmuch_config_save (config)) {
        if (is_new)
index 93fb16f32f9a897309657dac170b433360d6b28b..ff9d4278b04e1261afa8a33c5c2d50cfb8aacd50 100644 (file)
 
 #include "notmuch-client.h"
 
-static void
-format_headers_message_part_text (GMimeMessage *message);
-
-static void
+static notmuch_status_t
 format_part_text (const void *ctx, mime_node_t *node,
                  int indent, const notmuch_show_params_t *params);
 
@@ -34,93 +31,38 @@ static const notmuch_show_format_t format_text = {
     .message_set_end = ""
 };
 
-static void
-format_message_json (const void *ctx,
-                    notmuch_message_t *message,
-                    unused (int indent));
-static void
-format_headers_json (const void *ctx,
-                    notmuch_message_t *message);
-
-static void
-format_headers_message_part_json (GMimeMessage *message);
-
-static void
-format_part_start_json (unused (GMimeObject *part),
-                       int *part_count);
-
-static void
-format_part_encstatus_json (int status);
-
-static void
-#ifdef GMIME_ATLEAST_26
-format_part_sigstatus_json (GMimeSignatureList* siglist);
-#else
-format_part_sigstatus_json (const GMimeSignatureValidity* validity);
-#endif
-
-static void
-format_part_content_json (GMimeObject *part);
-
-static void
-format_part_end_json (GMimeObject *part);
+static notmuch_status_t
+format_part_json_entry (const void *ctx, mime_node_t *node,
+                       int indent, const notmuch_show_params_t *params);
 
-/* Any changes to the JSON format should be reflected in the file
- * devel/schemata. */
 static const notmuch_show_format_t format_json = {
-    "[", NULL,
-       "{", format_message_json,
-           "\"headers\": {", format_headers_json, format_headers_message_part_json, "}",
-           ", \"body\": [",
-               format_part_start_json,
-               format_part_encstatus_json,
-               format_part_sigstatus_json,
-               format_part_content_json,
-               format_part_end_json,
-               ", ",
-           "]",
-       "}", ", ",
-    "]"
+    .message_set_start = "[",
+    .part = format_part_json_entry,
+    .message_set_sep = ", ",
+    .message_set_end = "]"
 };
 
-static void
-format_message_mbox (const void *ctx,
-                    notmuch_message_t *message,
-                    unused (int indent));
+static notmuch_status_t
+format_part_mbox (const void *ctx, mime_node_t *node,
+                 int indent, const notmuch_show_params_t *params);
 
 static const notmuch_show_format_t format_mbox = {
-    "", NULL,
-        "", format_message_mbox,
-            "", NULL, NULL, "",
-            "",
-                NULL,
-                NULL,
-                NULL,
-                NULL,
-                NULL,
-                "",
-            "",
-        "", "",
-    ""
+    .message_set_start = "",
+    .part = format_part_mbox,
+    .message_set_sep = "",
+    .message_set_end = ""
 };
 
-static void
-format_part_content_raw (GMimeObject *part);
+static notmuch_status_t
+format_part_raw (unused (const void *ctx), mime_node_t *node,
+                unused (int indent),
+                unused (const notmuch_show_params_t *params));
 
 static const notmuch_show_format_t format_raw = {
-    "", NULL,
-       "", NULL,
-           "", NULL, format_headers_message_part_text, "\n",
-            "",
-                NULL,
-                NULL,
-                NULL,
-                format_part_content_raw,
-                NULL,
-                "",
-            "",
-       "", "",
-    ""
+    .message_set_start = "",
+    .part = format_part_raw,
+    .message_set_sep = "",
+    .message_set_end = ""
 };
 
 static const char *
@@ -170,7 +112,7 @@ _get_one_line_summary (const void *ctx, notmuch_message_t *message)
 }
 
 static void
-format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))
+format_message_json (const void *ctx, notmuch_message_t *message)
 {
     notmuch_tags_t *tags;
     int first = 1;
@@ -181,9 +123,10 @@ format_message_json (const void *ctx, notmuch_message_t *message, unused (int in
     date = notmuch_message_get_date (message);
     relative_date = notmuch_time_relative_date (ctx, date);
 
-    printf ("\"id\": %s, \"match\": %s, \"filename\": %s, \"timestamp\": %ld, \"date_relative\": \"%s\", \"tags\": [",
+    printf ("\"id\": %s, \"match\": %s, \"excluded\": %s, \"filename\": %s, \"timestamp\": %ld, \"date_relative\": \"%s\", \"tags\": [",
            json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),
            notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",
+           notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? "true" : "false",
            json_quote_str (ctx_quote, notmuch_message_get_filename (message)),
            date, relative_date);
 
@@ -257,138 +200,49 @@ _is_from_line (const char *line)
        return 0;
 }
 
-/* Print a message in "mboxrd" format as documented, for example,
- * here:
- *
- * http://qmail.org/qmail-manual-html/man5/mbox.html
- */
-static void
-format_message_mbox (const void *ctx,
-                    notmuch_message_t *message,
-                    unused (int indent))
-{
-    const char *filename;
-    FILE *file;
-    const char *from;
-
-    time_t date;
-    struct tm date_gmtime;
-    char date_asctime[26];
-
-    char *line = NULL;
-    size_t line_size;
-    ssize_t line_len;
-
-    filename = notmuch_message_get_filename (message);
-    file = fopen (filename, "r");
-    if (file == NULL) {
-       fprintf (stderr, "Failed to open %s: %s\n",
-                filename, strerror (errno));
-       return;
-    }
-
-    from = notmuch_message_get_header (message, "from");
-    from = _extract_email_address (ctx, from);
-
-    date = notmuch_message_get_date (message);
-    gmtime_r (&date, &date_gmtime);
-    asctime_r (&date_gmtime, date_asctime);
-
-    printf ("From %s %s", from, date_asctime);
-
-    while ((line_len = getline (&line, &line_size, file)) != -1 ) {
-       if (_is_from_line (line))
-           putchar ('>');
-       printf ("%s", line);
-    }
-
-    printf ("\n");
-
-    fclose (file);
-}
-
-static void
-format_headers_message_part_text (GMimeMessage *message)
+void
+format_headers_json (const void *ctx, GMimeMessage *message, notmuch_bool_t reply)
 {
+    void *local = talloc_new (ctx);
     InternetAddressList *recipients;
     const char *recipients_string;
 
-    printf ("Subject: %s\n", g_mime_message_get_subject (message));
-    printf ("From: %s\n", g_mime_message_get_sender (message));
+    printf ("{%s: %s",
+           json_quote_str (local, "Subject"),
+           json_quote_str (local, g_mime_message_get_subject (message)));
+    printf (", %s: %s",
+           json_quote_str (local, "From"),
+           json_quote_str (local, g_mime_message_get_sender (message)));
     recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
     recipients_string = internet_address_list_to_string (recipients, 0);
     if (recipients_string)
-       printf ("To: %s\n",
-               recipients_string);
+       printf (", %s: %s",
+               json_quote_str (local, "To"),
+               json_quote_str (local, recipients_string));
     recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
     recipients_string = internet_address_list_to_string (recipients, 0);
     if (recipients_string)
-       printf ("Cc: %s\n",
-               recipients_string);
-    printf ("Date: %s\n", g_mime_message_get_date_as_string (message));
-}
-
-static void
-format_headers_json (const void *ctx, notmuch_message_t *message)
-{
-    const char *headers[] = {
-       "Subject", "From", "To", "Cc", "Bcc", "Date"
-    };
-    const char *name, *value;
-    unsigned int i;
-    int first_header = 1;
-    void *ctx_quote = talloc_new (ctx);
-
-    for (i = 0; i < ARRAY_SIZE (headers); i++) {
-       name = headers[i];
-       value = notmuch_message_get_header (message, name);
-       if (value)
-       {
-           if (!first_header)
-               fputs (", ", stdout);
-           first_header = 0;
-
-           printf ("%s: %s",
-                   json_quote_str (ctx_quote, name),
-                   json_quote_str (ctx_quote, value));
-       }
-    }
-
-    talloc_free (ctx_quote);
-}
+       printf (", %s: %s",
+               json_quote_str (local, "Cc"),
+               json_quote_str (local, recipients_string));
 
-static void
-format_headers_message_part_json (GMimeMessage *message)
-{
-    void *ctx = talloc_new (NULL);
-    void *ctx_quote = talloc_new (ctx);
-    InternetAddressList *recipients;
-    const char *recipients_string;
+    if (reply) {
+       printf (", %s: %s",
+               json_quote_str (local, "In-reply-to"),
+               json_quote_str (local, g_mime_object_get_header (GMIME_OBJECT (message), "In-reply-to")));
 
-    printf ("%s: %s",
-           json_quote_str (ctx_quote, "From"),
-           json_quote_str (ctx_quote, g_mime_message_get_sender (message)));
-    recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_TO);
-    recipients_string = internet_address_list_to_string (recipients, 0);
-    if (recipients_string)
        printf (", %s: %s",
-               json_quote_str (ctx_quote, "To"),
-               json_quote_str (ctx_quote, recipients_string));
-    recipients = g_mime_message_get_recipients (message, GMIME_RECIPIENT_TYPE_CC);
-    recipients_string = internet_address_list_to_string (recipients, 0);
-    if (recipients_string)
+               json_quote_str (local, "References"),
+               json_quote_str (local, g_mime_object_get_header (GMIME_OBJECT (message), "References")));
+    } else {
        printf (", %s: %s",
-               json_quote_str (ctx_quote, "Cc"),
-               json_quote_str (ctx_quote, recipients_string));
-    printf (", %s: %s",
-           json_quote_str (ctx_quote, "Subject"),
-           json_quote_str (ctx_quote, g_mime_message_get_subject (message)));
-    printf (", %s: %s",
-           json_quote_str (ctx_quote, "Date"),
-           json_quote_str (ctx_quote, g_mime_message_get_date_as_string (message)));
+               json_quote_str (local, "Date"),
+               json_quote_str (local, g_mime_message_get_date_as_string (message)));
+    }
 
-    talloc_free (ctx_quote);
-    talloc_free (ctx);
+    printf ("}");
+
+    talloc_free (local);
 }
 
 /* Write a MIME text part out to the given stream.
@@ -471,29 +325,13 @@ signer_status_to_string (GMimeSignerStatus x)
 }
 #endif
 
-static void
-format_part_start_json (unused (GMimeObject *part), int *part_count)
-{
-    printf ("{\"id\": %d", *part_count);
-}
-
-static void
-format_part_encstatus_json (int status)
-{
-    printf (", \"encstatus\": [{\"status\": ");
-    if (status) {
-       printf ("\"good\"");
-    } else {
-       printf ("\"bad\"");
-    }
-    printf ("}]");
-}
-
 #ifdef GMIME_ATLEAST_26
 static void
-format_part_sigstatus_json (GMimeSignatureList *siglist)
+format_part_sigstatus_json (mime_node_t *node)
 {
-    printf (", \"sigstatus\": [");
+    GMimeSignatureList *siglist = node->sig_list;
+
+    printf ("[");
 
     if (!siglist) {
        printf ("]");
@@ -557,9 +395,11 @@ format_part_sigstatus_json (GMimeSignatureList *siglist)
 }
 #else
 static void
-format_part_sigstatus_json (const GMimeSignatureValidity* validity)
+format_part_sigstatus_json (mime_node_t *node)
 {
-    printf (", \"sigstatus\": [");
+    const GMimeSignatureValidity* validity = node->sig_validity;
+
+    printf ("[");
 
     if (!validity) {
        printf ("]");
@@ -618,109 +458,7 @@ format_part_sigstatus_json (const GMimeSignatureValidity* validity)
 }
 #endif
 
-static void
-format_part_content_json (GMimeObject *part)
-{
-    GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
-    GMimeStream *stream_memory = g_mime_stream_mem_new ();
-    const char *cid = g_mime_object_get_content_id (part);
-    void *ctx = talloc_new (NULL);
-    GByteArray *part_content;
-
-    printf (", \"content-type\": %s",
-           json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
-
-    if (cid != NULL)
-           printf(", \"content-id\": %s", json_quote_str (ctx, cid));
-
-    if (GMIME_IS_PART (part))
-    {
-       const char *filename = g_mime_part_get_filename (GMIME_PART (part));
-       if (filename)
-           printf (", \"filename\": %s", json_quote_str (ctx, filename));
-    }
-
-    if (g_mime_content_type_is_type (content_type, "text", "*"))
-    {
-       /* For non-HTML text parts, we include the content in the
-        * JSON. Since JSON must be Unicode, we handle charset
-        * decoding here and do not report a charset to the caller.
-        * For text/html parts, we do not include the content. If a
-        * caller is interested in text/html parts, it should retrieve
-        * them separately and they will not be decoded. Since this
-        * makes charset decoding the responsibility on the caller, we
-        * report the charset for text/html parts.
-        */
-       if (g_mime_content_type_is_type (content_type, "text", "html"))
-       {
-           const char *content_charset = g_mime_object_get_content_type_parameter (GMIME_OBJECT (part), "charset");
-
-           if (content_charset != NULL)
-               printf (", \"content-charset\": %s", json_quote_str (ctx, content_charset));
-       }
-       else
-       {
-           show_text_part_content (part, stream_memory);
-           part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
-
-           printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
-       }
-    }
-    else if (g_mime_content_type_is_type (content_type, "multipart", "*"))
-    {
-       printf (", \"content\": [");
-    }
-    else if (g_mime_content_type_is_type (content_type, "message", "rfc822"))
-    {
-       printf (", \"content\": [{");
-    }
-
-    talloc_free (ctx);
-    if (stream_memory)
-       g_object_unref (stream_memory);
-}
-
-static void
-format_part_end_json (GMimeObject *part)
-{
-    GMimeContentType *content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
-
-    if (g_mime_content_type_is_type (content_type, "multipart", "*"))
-       printf ("]");
-    else if (g_mime_content_type_is_type (content_type, "message", "rfc822"))
-       printf ("}]");
-
-    printf ("}");
-}
-
-static void
-format_part_content_raw (GMimeObject *part)
-{
-    if (! GMIME_IS_PART (part))
-       return;
-
-    GMimeStream *stream_stdout;
-    GMimeStream *stream_filter = NULL;
-    GMimeDataWrapper *wrapper;
-
-    stream_stdout = g_mime_stream_file_new (stdout);
-    g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
-
-    stream_filter = g_mime_stream_filter_new (stream_stdout);
-
-    wrapper = g_mime_part_get_content_object (GMIME_PART (part));
-
-    if (wrapper && stream_filter)
-       g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
-
-    if (stream_filter)
-       g_object_unref (stream_filter);
-
-    if (stream_stdout)
-       g_object_unref(stream_stdout);
-}
-
-static void
+static notmuch_status_t
 format_part_text (const void *ctx, mime_node_t *node,
                  int indent, const notmuch_show_params_t *params)
 {
@@ -737,11 +475,12 @@ format_part_text (const void *ctx, mime_node_t *node,
        notmuch_message_t *message = node->envelope_file;
 
        part_type = "message";
-       printf ("\f%s{ id:%s depth:%d match:%d filename:%s\n",
+       printf ("\f%s{ id:%s depth:%d match:%d excluded:%d filename:%s\n",
                part_type,
                notmuch_message_get_message_id (message),
                indent,
-               notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
+               notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? 1 : 0,
+               notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_EXCLUDED) ? 1 : 0,
                notmuch_message_get_filename (message));
     } else {
        GMimeContentDisposition *disposition = g_mime_object_get_content_disposition (meta);
@@ -808,9 +547,253 @@ format_part_text (const void *ctx, mime_node_t *node,
        printf ("\fbody}\n");
 
     printf ("\f%s}\n", part_type);
+
+    return NOTMUCH_STATUS_SUCCESS;
 }
 
-static void
+void
+format_part_json (const void *ctx, mime_node_t *node, notmuch_bool_t first)
+{
+    /* Any changes to the JSON format should be reflected in the file
+     * devel/schemata. */
+
+    if (node->envelope_file) {
+       printf ("{");
+       format_message_json (ctx, node->envelope_file);
+
+       printf ("\"headers\": ");
+       format_headers_json (ctx, GMIME_MESSAGE (node->part), FALSE);
+
+       printf (", \"body\": [");
+       format_part_json (ctx, mime_node_child (node, 0), first);
+
+       printf ("]}");
+       return;
+    }
+
+    void *local = talloc_new (ctx);
+    /* The disposition and content-type metadata are associated with
+     * the envelope for message parts */
+    GMimeObject *meta = node->envelope_part ?
+       GMIME_OBJECT (node->envelope_part) : node->part;
+    GMimeContentType *content_type = g_mime_object_get_content_type (meta);
+    const char *cid = g_mime_object_get_content_id (meta);
+    const char *filename = GMIME_IS_PART (node->part) ?
+       g_mime_part_get_filename (GMIME_PART (node->part)) : NULL;
+    const char *terminator = "";
+    int i;
+
+    if (!first)
+       printf (", ");
+
+    printf ("{\"id\": %d", node->part_num);
+
+    if (node->decrypt_attempted)
+       printf (", \"encstatus\": [{\"status\": \"%s\"}]",
+               node->decrypt_success ? "good" : "bad");
+
+    if (node->verify_attempted) {
+       printf (", \"sigstatus\": ");
+       format_part_sigstatus_json (node);
+    }
+
+    printf (", \"content-type\": %s",
+           json_quote_str (local, g_mime_content_type_to_string (content_type)));
+
+    if (cid)
+       printf (", \"content-id\": %s", json_quote_str (local, cid));
+
+    if (filename)
+       printf (", \"filename\": %s", json_quote_str (local, filename));
+
+    if (GMIME_IS_PART (node->part)) {
+       /* For non-HTML text parts, we include the content in the
+        * JSON. Since JSON must be Unicode, we handle charset
+        * decoding here and do not report a charset to the caller.
+        * For text/html parts, we do not include the content. If a
+        * caller is interested in text/html parts, it should retrieve
+        * them separately and they will not be decoded. Since this
+        * makes charset decoding the responsibility on the caller, we
+        * report the charset for text/html parts.
+        */
+       if (g_mime_content_type_is_type (content_type, "text", "html")) {
+           const char *content_charset = g_mime_object_get_content_type_parameter (meta, "charset");
+
+           if (content_charset != NULL)
+               printf (", \"content-charset\": %s", json_quote_str (local, content_charset));
+       } else if (g_mime_content_type_is_type (content_type, "text", "*")) {
+           GMimeStream *stream_memory = g_mime_stream_mem_new ();
+           GByteArray *part_content;
+           show_text_part_content (node->part, stream_memory);
+           part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
+
+           printf (", \"content\": %s", json_quote_chararray (local, (char *) part_content->data, part_content->len));
+           g_object_unref (stream_memory);
+       }
+    } else if (GMIME_IS_MULTIPART (node->part)) {
+       printf (", \"content\": [");
+       terminator = "]";
+    } else if (GMIME_IS_MESSAGE (node->part)) {
+       printf (", \"content\": [{");
+       printf ("\"headers\": ");
+       format_headers_json (local, GMIME_MESSAGE (node->part), FALSE);
+
+       printf (", \"body\": [");
+       terminator = "]}]";
+    }
+
+    talloc_free (local);
+
+    for (i = 0; i < node->nchildren; i++)
+       format_part_json (ctx, mime_node_child (node, i), i == 0);
+
+    printf ("%s}", terminator);
+}
+
+static notmuch_status_t
+format_part_json_entry (const void *ctx, mime_node_t *node, unused (int indent),
+                       unused (const notmuch_show_params_t *params))
+{
+    format_part_json (ctx, node, TRUE);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Print a message in "mboxrd" format as documented, for example,
+ * here:
+ *
+ * http://qmail.org/qmail-manual-html/man5/mbox.html
+ */
+static notmuch_status_t
+format_part_mbox (const void *ctx, mime_node_t *node, unused (int indent),
+                 unused (const notmuch_show_params_t *params))
+{
+    notmuch_message_t *message = node->envelope_file;
+
+    const char *filename;
+    FILE *file;
+    const char *from;
+
+    time_t date;
+    struct tm date_gmtime;
+    char date_asctime[26];
+
+    char *line = NULL;
+    size_t line_size;
+    ssize_t line_len;
+
+    if (!message)
+       INTERNAL_ERROR ("format_part_mbox requires a root part");
+
+    filename = notmuch_message_get_filename (message);
+    file = fopen (filename, "r");
+    if (file == NULL) {
+       fprintf (stderr, "Failed to open %s: %s\n",
+                filename, strerror (errno));
+       return NOTMUCH_STATUS_FILE_ERROR;
+    }
+
+    from = notmuch_message_get_header (message, "from");
+    from = _extract_email_address (ctx, from);
+
+    date = notmuch_message_get_date (message);
+    gmtime_r (&date, &date_gmtime);
+    asctime_r (&date_gmtime, date_asctime);
+
+    printf ("From %s %s", from, date_asctime);
+
+    while ((line_len = getline (&line, &line_size, file)) != -1 ) {
+       if (_is_from_line (line))
+           putchar ('>');
+       printf ("%s", line);
+    }
+
+    printf ("\n");
+
+    fclose (file);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+format_part_raw (unused (const void *ctx), mime_node_t *node,
+                unused (int indent),
+                unused (const notmuch_show_params_t *params))
+{
+    if (node->envelope_file) {
+       /* Special case the entire message to avoid MIME parsing. */
+       const char *filename;
+       FILE *file;
+       size_t size;
+       char buf[4096];
+
+       filename = notmuch_message_get_filename (node->envelope_file);
+       if (filename == NULL) {
+           fprintf (stderr, "Error: Cannot get message filename.\n");
+           return NOTMUCH_STATUS_FILE_ERROR;
+       }
+
+       file = fopen (filename, "r");
+       if (file == NULL) {
+           fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
+           return NOTMUCH_STATUS_FILE_ERROR;
+       }
+
+       while (!feof (file)) {
+           size = fread (buf, 1, sizeof (buf), file);
+           if (ferror (file)) {
+               fprintf (stderr, "Error: Read failed from %s\n", filename);
+               fclose (file);
+               return NOTMUCH_STATUS_FILE_ERROR;
+           }
+
+           if (fwrite (buf, size, 1, stdout) != 1) {
+               fprintf (stderr, "Error: Write failed\n");
+               fclose (file);
+               return NOTMUCH_STATUS_FILE_ERROR;
+           }
+       }
+
+       fclose (file);
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    GMimeStream *stream_stdout;
+    GMimeStream *stream_filter = NULL;
+
+    stream_stdout = g_mime_stream_file_new (stdout);
+    g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+
+    stream_filter = g_mime_stream_filter_new (stream_stdout);
+
+    if (GMIME_IS_PART (node->part)) {
+       /* For leaf parts, we emit only the transfer-decoded
+        * body. */
+       GMimeDataWrapper *wrapper;
+       wrapper = g_mime_part_get_content_object (GMIME_PART (node->part));
+
+       if (wrapper && stream_filter)
+           g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+    } else {
+       /* Write out the whole part.  For message parts (the root
+        * part and embedded message parts), this will be the
+        * message including its headers (but not the
+        * encapsulating part's headers).  For multipart parts,
+        * this will include the headers. */
+       if (stream_filter)
+           g_mime_object_write_to_stream (node->part, stream_filter);
+    }
+
+    if (stream_filter)
+       g_object_unref (stream_filter);
+
+    if (stream_stdout)
+       g_object_unref(stream_stdout);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
 show_message (void *ctx,
              const notmuch_show_format_t *format,
              notmuch_message_t *message,
@@ -820,14 +803,18 @@ show_message (void *ctx,
     if (format->part) {
        void *local = talloc_new (ctx);
        mime_node_t *root, *part;
-
-       if (mime_node_open (local, message, params->cryptoctx, params->decrypt,
-                           &root) == NOTMUCH_STATUS_SUCCESS &&
-           (part = mime_node_seek_dfs (root, (params->part < 0 ?
-                                              0 : params->part))))
-           format->part (local, part, indent, params);
+       notmuch_status_t status;
+
+       status = mime_node_open (local, message, params->cryptoctx,
+                                params->decrypt, &root);
+       if (status)
+           goto DONE;
+       part = mime_node_seek_dfs (root, (params->part < 0 ? 0 : params->part));
+       if (part)
+           status = format->part (local, part, indent, params);
+      DONE:
        talloc_free (local);
-       return;
+       return status;
     }
 
     if (params->part <= 0) {
@@ -851,9 +838,11 @@ show_message (void *ctx,
 
        fputs (format->message_end, stdout);
     }
+
+    return NOTMUCH_STATUS_SUCCESS;
 }
 
-static void
+static notmuch_status_t
 show_messages (void *ctx,
               const notmuch_show_format_t *format,
               notmuch_messages_t *messages,
@@ -864,6 +853,7 @@ show_messages (void *ctx,
     notmuch_bool_t match;
     int first_set = 1;
     int next_indent;
+    notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
 
     fputs (format->message_set_start, stdout);
 
@@ -884,17 +874,22 @@ show_messages (void *ctx,
        next_indent = indent;
 
        if (match || params->entire_thread) {
-           show_message (ctx, format, message, indent, params);
+           status = show_message (ctx, format, message, indent, params);
+           if (status && !res)
+               res = status;
            next_indent = indent + 1;
 
-           fputs (format->message_set_sep, stdout);
+           if (!status)
+               fputs (format->message_set_sep, stdout);
        }
 
-       show_messages (ctx,
-                      format,
-                      notmuch_message_get_replies (message),
-                      next_indent,
-                      params);
+       status = show_messages (ctx,
+                               format,
+                               notmuch_message_get_replies (message),
+                               next_indent,
+                               params);
+       if (status && !res)
+           res = status;
 
        notmuch_message_destroy (message);
 
@@ -902,6 +897,8 @@ show_messages (void *ctx,
     }
 
     fputs (format->message_set_end, stdout);
+
+    return res;
 }
 
 /* Formatted output of single message */
@@ -929,50 +926,7 @@ do_show_single (void *ctx,
 
     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH, 1);
 
-    /* Special case for --format=raw of full single message, just cat out file */
-    if (params->raw && 0 == params->part) {
-
-       const char *filename;
-       FILE *file;
-       size_t size;
-       char buf[4096];
-
-       filename = notmuch_message_get_filename (message);
-       if (filename == NULL) {
-           fprintf (stderr, "Error: Cannot message filename.\n");
-           return 1;
-       }
-
-       file = fopen (filename, "r");
-       if (file == NULL) {
-           fprintf (stderr, "Error: Cannot open file %s: %s\n", filename, strerror (errno));
-           return 1;
-       }
-
-       while (!feof (file)) {
-           size = fread (buf, 1, sizeof (buf), file);
-           if (ferror (file)) {
-               fprintf (stderr, "Error: Read failed from %s\n", filename);
-               fclose (file);
-               return 1;
-           }
-
-           if (fwrite (buf, size, 1, stdout) != 1) {
-               fprintf (stderr, "Error: Write failed\n");
-               fclose (file);
-               return 1;
-           }
-       }
-
-       fclose (file);
-
-    } else {
-
-       show_message (ctx, format, message, 0, params);
-
-    }
-
-    return 0;
+    return show_message (ctx, format, message, 0, params) != NOTMUCH_STATUS_SUCCESS;
 }
 
 /* Formatted output of threads */
@@ -986,6 +940,7 @@ do_show (void *ctx,
     notmuch_thread_t *thread;
     notmuch_messages_t *messages;
     int first_toplevel = 1;
+    notmuch_status_t status, res = NOTMUCH_STATUS_SUCCESS;
 
     fputs (format->message_set_start, stdout);
 
@@ -1005,7 +960,9 @@ do_show (void *ctx,
            fputs (format->message_set_sep, stdout);
        first_toplevel = 0;
 
-       show_messages (ctx, format, messages, 0, params);
+       status = show_messages (ctx, format, messages, 0, params);
+       if (status && !res)
+           res = status;
 
        notmuch_thread_destroy (thread);
 
@@ -1013,7 +970,7 @@ do_show (void *ctx,
 
     fputs (format->message_set_end, stdout);
 
-    return 0;
+    return res != NOTMUCH_STATUS_SUCCESS;
 }
 
 enum {
@@ -1036,6 +993,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
     notmuch_show_params_t params = { .part = -1 };
     int format_sel = NOTMUCH_FORMAT_NOT_SPECIFIED;
     notmuch_bool_t verify = FALSE;
+    notmuch_bool_t no_exclude = FALSE;
 
     notmuch_opt_desc_t options[] = {
        { NOTMUCH_OPT_KEYWORD, &format_sel, "format", 'f',
@@ -1048,6 +1006,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        { NOTMUCH_OPT_BOOLEAN, &params.entire_thread, "entire-thread", 't', 0 },
        { NOTMUCH_OPT_BOOLEAN, &params.decrypt, "decrypt", 'd', 0 },
        { NOTMUCH_OPT_BOOLEAN, &verify, "verify", 'v', 0 },
+       { NOTMUCH_OPT_BOOLEAN, &no_exclude, "no-exclude", 'n', 0 },
        { 0, 0, 0, 0, 0 }
     };
 
@@ -1078,6 +1037,7 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
            fprintf (stderr, "Error: specifying parts is incompatible with mbox output format.\n");
            return 1;
        }
+
        format = &format_mbox;
        break;
     case NOTMUCH_FORMAT_RAW:
@@ -1135,10 +1095,28 @@ notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
        return 1;
     }
 
+    /* if format=mbox then we can not output excluded messages as
+     * there is no way to make the exclude flag available */
+    if (format_sel == NOTMUCH_FORMAT_MBOX)
+       notmuch_query_set_omit_excluded_messages (query, TRUE);
+
+    /* If a single message is requested we do not use search_excludes. */
     if (params.part >= 0)
        ret = do_show_single (ctx, query, format, &params);
-    else
+    else {
+       if (!no_exclude) {
+           const char **search_exclude_tags;
+           size_t search_exclude_tags_length;
+           unsigned int i;
+
+           search_exclude_tags = notmuch_config_get_search_exclude_tags
+               (config, &search_exclude_tags_length);
+           for (i = 0; i < search_exclude_tags_length; i++)
+               notmuch_query_add_tag_exclude (query, search_exclude_tags[i]);
+       }
        ret = do_show (ctx, query, format, &params);
+    }
+
 
     notmuch_query_destroy (query);
     notmuch_database_close (notmuch);
index 300b171419734d649a41e2a8a0a567dd879f8d26..b97fc06652f33f87fcf3a4d6016b1b28db536695 100755 (executable)
@@ -37,4 +37,25 @@ test_expect_equal \
     "0" \
     "`notmuch count --output=threads ${SEARCH}`"
 
+test_begin_subtest "count excluding \"deleted\" messages"
+notmuch config set search.exclude_tags deleted
+generate_message '[subject]="Not deleted"'
+generate_message '[subject]="Another not deleted"'
+generate_message '[subject]="Deleted"'
+notmuch new > /dev/null
+notmuch tag +deleted id:$gen_msg_id
+test_expect_equal \
+    "2" \
+    "`notmuch count subject:deleted`"
+
+test_begin_subtest "count \"deleted\" messages, exclude overridden"
+test_expect_equal \
+    "1" \
+    "`notmuch count subject:deleted and tag:deleted`"
+
+test_begin_subtest "count \"deleted\" messages, with --no-exclude"
+test_expect_equal \
+    "3" \
+    "`notmuch count --no-exclude subject:deleted`"
+
 test_done
index 6723ef8755f6dcbf6a11aefce900c6cb4b29b6e0..be752b19674cdd174e27cee6ead6a53bc52cc172 100755 (executable)
@@ -43,6 +43,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
 expected='[[[{"id": "XXXXX",
  "match": true,
+ "excluded": false,
  "filename": "YYYYY",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
@@ -50,9 +51,8 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "good",
  "fingerprint": "'$FINGERPRINT'",
@@ -77,6 +77,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
 expected='[[[{"id": "XXXXX",
  "match": true,
+ "excluded": false,
  "filename": "YYYYY",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
@@ -84,9 +85,8 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "good",
  "fingerprint": "'$FINGERPRINT'",
@@ -111,6 +111,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
 expected='[[[{"id": "XXXXX",
  "match": true,
+ "excluded": false,
  "filename": "YYYYY",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
@@ -118,9 +119,8 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "error",
  "keyid": "'$(echo $FINGERPRINT | cut -c 25-)'",
@@ -151,7 +151,7 @@ test_begin_subtest "decryption, --format=text"
 output=$(notmuch show --format=text --decrypt subject:"test encrypted message 001" \
     | notmuch_show_sanitize_all \
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
-expected='\fmessage{ id:XXXXX depth:0 match:1 filename:XXXXX
+expected='\fmessage{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2000-01-01) (encrypted inbox)
 Subject: test encrypted message 001
@@ -185,6 +185,7 @@ output=$(notmuch show --format=json --decrypt subject:"test encrypted message 00
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
 expected='[[[{"id": "XXXXX",
  "match": true,
+ "excluded": false,
  "filename": "YYYYY",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
@@ -192,9 +193,8 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test encrypted message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "encstatus": [{"status": "good"}],
  "sigstatus": [],
@@ -240,6 +240,7 @@ output=$(notmuch show --format=json --decrypt subject:"test encrypted message 00
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
 expected='[[[{"id": "XXXXX",
  "match": true,
+ "excluded": false,
  "filename": "YYYYY",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
@@ -247,9 +248,8 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test encrypted message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "encstatus": [{"status": "bad"}],
  "content-type": "multipart/encrypted",
@@ -275,6 +275,7 @@ output=$(notmuch show --format=json --decrypt subject:"test encrypted message 00
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
 expected='[[[{"id": "XXXXX",
  "match": true,
+ "excluded": false,
  "filename": "YYYYY",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
@@ -282,9 +283,8 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test encrypted message 002",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "encstatus": [{"status": "good"}],
  "sigstatus": [{"status": "good",
@@ -330,6 +330,7 @@ output=$(notmuch show --format=json --verify subject:"test signed message 001" \
     | sed -e 's|"created": [1234567890]*|"created": 946728000|')
 expected='[[[{"id": "XXXXX",
  "match": true,
+ "excluded": false,
  "filename": "YYYYY",
  "timestamp": 946728000,
  "date_relative": "2000-01-01",
@@ -337,9 +338,8 @@ expected='[[[{"id": "XXXXX",
  "headers": {"Subject": "test signed message 001",
  "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
  "To": "test_suite@notmuchmail.org",
- "Cc": "",
- "Bcc": "",
- "Date": "01 Jan 2000 12:00:00 -0000"},
+ "Date": "Sat,
+ 01 Jan 2000 12:00:00 +0000"},
  "body": [{"id": 1,
  "sigstatus": [{"status": "error",
  "keyid": "6D92612D94E46381",
index 7549892805510210689a18744ea7d7f9bad2a7b2..8a28705873c5299c099399a9d0e4b79414901eed 100755 (executable)
@@ -78,7 +78,7 @@ thread=$(notmuch search --output=threads subject:message-with-invalid-from)
 test_emacs "(notmuch-show \"$thread\")
            (test-output)"
 cat <<EOF >EXPECTED
-Invalid " From <test_suite@notmuchmail.org> (2001-01-05) (inbox)
+"Invalid " (2001-01-05) (inbox)
 Subject: message-with-invalid-from
 To: Notmuch Test Suite <test_suite@notmuchmail.org>
 Date: Fri, 05 Jan 2001 15:43:57 +0000
@@ -268,11 +268,107 @@ Subject: Re: Testing message sent via SMTP
 In-Reply-To: <XXX>
 Fcc: ${MAIL_DIR}/sent
 --text follows this line--
-On 01 Jan 2000 12:00:00 -0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
 > This is a test that messages are sent via SMTP
 EOF
 test_expect_equal_file OUTPUT EXPECTED
 
+test_begin_subtest "Reply within emacs to a multipart/mixed message"
+test_emacs '(notmuch-show "id:20091118002059.067214ed@hikari")
+               (notmuch-show-reply)
+               (test-output)'
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Adrian Perez de Castro <aperez@igalia.com>, notmuch@notmuchmail.org
+Subject: Re: [notmuch] Introducing myself
+In-Reply-To: <20091118002059.067214ed@hikari>
+Fcc: ${MAIL_DIR}/sent
+--text follows this line--
+Adrian Perez de Castro <aperez@igalia.com> writes:
+
+> Hello to all,
+>
+> I have just heard about Not Much today in some random Linux-related news
+> site (LWN?), my name is Adrian Perez and I work as systems administrator
+> (although I can do some code as well :P). I have always thought that the
+> ideas behind Sup were great, but after some time using it, I got tired of
+> the oddities that it has. I also do not like doing things like having to
+> install Ruby just for reading and sorting mails. Some time ago I thought
+> about doing something like Not Much and in fact I played a bit with the
+> Python+Xapian and the Python+Whoosh combinations, because I find relaxing
+> to code things in Python when I am not working and also it is installed
+> by default on most distribution. I got to have some mailboxes indexed and
+> basic searching working a couple of months ago. Lately I have been very
+> busy and had no time for coding, and them... boom! Not Much appears -- and
+> it is almost exactly what I was trying to do, but faster. I have been
+> playing a bit with Not Much today, and I think it has potential.
+>
+> Also, I would like to share one idea I had in mind, that you might find
+> interesting: One thing I have found very annoying is having to re-tag my
+> mail when the indexes get b0rked (it happened a couple of times to me while
+> using Sup), so I was planning to mails as read/unread and adding the tags
+> not just to the index, but to the mail text itself, e.g. by adding a
+> "X-Tags" header field or by reusing the "Keywords" one. This way, the index
+> could be totally recreated by re-reading the mail directories, and this
+> would also allow to a tools like OfflineIMAP [1] to get the mails into a
+> local maildir, tagging and indexing the mails with the e-mail reader and
+> then syncing back the messages with the "X-Tags" header to the IMAP server.
+> This would allow to use the mail reader from a different computer and still
+> have everything tagged finely.
+>
+> Best regards,
+>
+>
+> ---
+> [1] http://software.complete.org/software/projects/show/offlineimap
+>
+> -- 
+> Adrian Perez de Castro <aperez@igalia.com>
+> Igalia - Free Software Engineering
+> _______________________________________________
+> notmuch mailing list
+> notmuch@notmuchmail.org
+> http://notmuchmail.org/mailman/listinfo/notmuch
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
+test_begin_subtest "Reply within emacs to a multipart/alternative message"
+test_emacs '(notmuch-show "id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com")
+               (notmuch-show-reply)
+               (test-output)'
+cat <<EOF >EXPECTED
+From: Notmuch Test Suite <test_suite@notmuchmail.org>
+To: Alex Botero-Lowry <alex.boterolowry@gmail.com>, notmuch@notmuchmail.org
+Subject: Re: [notmuch] preliminary FreeBSD support
+In-Reply-To: <cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com>
+Fcc: ${MAIL_DIR}/sent
+--text follows this line--
+Alex Botero-Lowry <alex.boterolowry@gmail.com> writes:
+
+> I saw the announcement this morning, and was very excited, as I had been
+> hoping sup would be turned into a library,
+> since I like the concept more than the UI (I'd rather an emacs interface).
+>
+> I did a preliminary compile which worked out fine, but
+> sysconf(_SC_SC_GETPW_R_SIZE_MAX) returns -1 on
+> FreeBSD, so notmuch_config_open segfaulted.
+>
+> Attached is a patch that supplies a default buffer size of 64 in cases where
+> -1 is returned.
+>
+> http://www.opengroup.org/austin/docs/austin_328.txt - seems to indicate this
+> is acceptable behavior,
+> and http://mail-index.netbsd.org/pkgsrc-bugs/2006/06/07/msg016808.htmlspecifically
+> uses 64 as the
+> buffer size.
+> _______________________________________________
+> notmuch mailing list
+> notmuch@notmuchmail.org
+> http://notmuchmail.org/mailman/listinfo/notmuch
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
 test_begin_subtest "Quote MML tags in reply"
 message_id='test-emacs-mml-quoting@message.id'
 add_message [id]="$message_id" \
@@ -288,7 +384,8 @@ Subject: Re: Quote MML tags in reply
 In-Reply-To: <test-emacs-mml-quoting@message.id>
 Fcc: ${MAIL_DIR}/sent
 --text follows this line--
-On Fri, 05 Jan 2001 15:43:57 +0000, Notmuch Test Suite <test_suite@notmuchmail.org> wrote:
+Notmuch Test Suite <test_suite@notmuchmail.org> writes:
+
 > <#!part disposition=inline>
 EOF
 test_expect_equal_file OUTPUT EXPECTED
@@ -414,7 +511,7 @@ test_emacs '(notmuch-show "id:\"bought\"")
        (reverse-region (point-min) (point-max))
            (test-output)'
 cat <<EOF >EXPECTED
-Sat, 01 Jan 2000 12:00:00 -0000
+Sat, 01 Jan 2000 12:00:00 +0000
 Some One <someone@somewhere.org>
 Some One Else <notsomeone@somewhere.org>
 Notmuch <notmuch@notmuchmail.org>
diff --git a/test/emacs-hello b/test/emacs-hello
new file mode 100755 (executable)
index 0000000..b235e3a
--- /dev/null
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+
+test_description="Testing emacs notmuch-hello view"
+. test-lib.sh
+
+EXPECTED=$TEST_DIRECTORY/emacs.expected-output
+
+add_email_corpus
+
+test_begin_subtest "User-defined section with inbox tag"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-searches
+                                     \"Test\" '((\"inbox\" . \"tag:inbox\")))))))
+           (notmuch-hello)
+           (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-new-section
+
+test_begin_subtest "User-defined section with empty, hidden entry"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-searches
+                                     \"Test-with-empty\"
+                                     '((\"inbox\" . \"tag:inbox\")
+                                       (\"doesnotexist\" . \"tag:doesnotexist\"))
+                                     :hide-empty-searches t)))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-section-with-empty
+
+test_begin_subtest "User-defined section, unread tag filtered out"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-tags-section
+                                     \"Test-with-filtered\"
+                                     :hide-tags '(\"unread\"))))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-section-hidden-tag
+
+test_begin_subtest "User-defined section, different query for counts"
+test_emacs "(let ((notmuch-hello-sections
+                   (list (lambda () (notmuch-hello-insert-tags-section
+                                     \"Test-with-counts\"
+                                     :filter-count \"tag:signed\")))))
+             (notmuch-hello)
+             (test-output))"
+test_expect_equal_file OUTPUT $EXPECTED/notmuch-hello-section-counts
+
+test_done
index 3e59595f91416e03093466c09e184f29c01ca9c3..147079064ec94e20ffd43e4bd18c7c6283594bae 100644 (file)
@@ -6,9 +6,10 @@ Saved searches: [edit]
 
 Search:                                                                     .
 
-[Show all tags]
+All tags: [show]
 
         Type a search query and hit RET to view matching threads.
                Edit saved searches with the `edit' button.
   Hit RET or click on a saved search or tag name to view matching threads.
       `=' to refresh this screen. `s' to search messages. `q' to quit.
+                           Customize this page.
diff --git a/test/emacs.expected-output/notmuch-hello-new-section b/test/emacs.expected-output/notmuch-hello-new-section
new file mode 100644 (file)
index 0000000..c64d712
--- /dev/null
@@ -0,0 +1,4 @@
+Test: [hide]
+
+         52 inbox  
+
index ef0e5d050511b9e28024008fb56a63200b6f1f78..05475b15abee9c81a4fc30907438c83c7fd73e23 100644 (file)
@@ -2,9 +2,10 @@
 
 Search:                                                                     .
 
-[Show all tags]
+All tags: [show]
 
         Type a search query and hit RET to view matching threads.
                Edit saved searches with the `edit' button.
   Hit RET or click on a saved search or tag name to view matching threads.
       `=' to refresh this screen. `s' to search messages. `q' to quit.
+                           Customize this page.
diff --git a/test/emacs.expected-output/notmuch-hello-section-counts b/test/emacs.expected-output/notmuch-hello-section-counts
new file mode 100644 (file)
index 0000000..9d79659
--- /dev/null
@@ -0,0 +1,5 @@
+Test-with-counts: [hide]
+
+          2 attachment             7 signed          
+          7 inbox                  7 unread          
+
diff --git a/test/emacs.expected-output/notmuch-hello-section-hidden-tag b/test/emacs.expected-output/notmuch-hello-section-hidden-tag
new file mode 100644 (file)
index 0000000..3688e7c
--- /dev/null
@@ -0,0 +1,4 @@
+Test-with-filtered: [hide]
+
+          4 attachment            52 inbox                  7 signed 
+
diff --git a/test/emacs.expected-output/notmuch-hello-section-with-empty b/test/emacs.expected-output/notmuch-hello-section-with-empty
new file mode 100644 (file)
index 0000000..8209fed
--- /dev/null
@@ -0,0 +1,4 @@
+Test-with-empty: [hide]
+
+         52 inbox  
+
index 71edba73ba8672839efe181b9b0aac1ea05fee67..5e5322210dbdfb96db71adedd84a6d8100a0a80f 100644 (file)
@@ -6,9 +6,10 @@ Saved searches: [edit]
 
 Search:                                                                     .
 
-[Show all tags]
+All tags: [show]
 
         Type a search query and hit RET to view matching threads.
                Edit saved searches with the `edit' button.
   Hit RET or click on a saved search or tag name to view matching threads.
       `=' to refresh this screen. `s' to search messages. `q' to quit.
+                           Customize this page.
index f0d073c586973dea5ff84a58e1f088012e14fd9c..2e1326ebe1bca735c461704d8147dd180a1a3aa9 100755 (executable)
@@ -6,10 +6,10 @@ test_begin_subtest "Message with text of unknown charset"
 add_message '[content-type]="text/plain; charset=unknown-8bit"' \
            "[body]=irrelevant"
 output=$(notmuch show id:${gen_msg_id} 2>&1 | notmuch_show_sanitize)
-test_expect_equal "$output" "\fmessage{ id:msg-001@notmuch-test-suite depth:0 match:1 filename:/XXX/mail/msg-001
+test_expect_equal "$output" "\fmessage{ id:msg-001@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-001
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (inbox unread)
-Subject: Test message #1
+Subject: Message with text of unknown charset
 From: Notmuch Test Suite <test_suite@notmuchmail.org>
 To: Notmuch Test Suite <test_suite@notmuchmail.org>
 Date: Fri, 05 Jan 2001 15:43:57 +0000
index 7df438036583b7b487632731083e11c58165d8dd..643978869d25b95ef5a4ea6d23f1fc493de9f09c 100755 (executable)
--- a/test/json
+++ b/test/json
@@ -5,7 +5,7 @@ test_description="--format=json output"
 test_begin_subtest "Show message: json"
 add_message "[subject]=\"json-show-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-show-message\""
 output=$(notmuch show --format=json "json-show-message")
-test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
+test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"json-show-message\n\"}]}, []]]]"
 
 test_begin_subtest "Search message: json"
 add_message "[subject]=\"json-search-subject\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"json-search-message\""
@@ -22,7 +22,7 @@ test_expect_equal "$output" "[{\"thread\": \"XXX\",
 test_begin_subtest "Show message: json, utf-8"
 add_message "[subject]=\"json-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-show-méssage\""
 output=$(notmuch show --format=json "jsön-show-méssage")
-test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]"
+test_expect_equal "$output" "[[[{\"id\": \"${gen_msg_id}\", \"match\": true, \"excluded\": false, \"filename\": \"${gen_msg_filename}\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\",\"unread\"], \"headers\": {\"Subject\": \"json-show-utf8-body-sübjéct\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"text/plain\", \"content\": \"jsön-show-méssage\n\"}]}, []]]]"
 
 test_begin_subtest "Show message: json, inline attachment filename"
 subject='json-show-inline-attachment-filename'
@@ -35,7 +35,7 @@ emacs_deliver_message \
      (insert \"Message-ID: <$id>\n\")"
 output=$(notmuch show --format=json "id:$id")
 filename=$(notmuch search --output=files "id:$id")
-test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"filename\": \"$filename\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"test_suite@notmuchmail.org\", \"Cc\": \"\", \"Bcc\": \"\", \"Date\": \"01 Jan 2000 12:00:00 -0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"filename\": \"README\"}]}]}, []]]]"
+test_expect_equal "$output" "[[[{\"id\": \"$id\", \"match\": true, \"excluded\": false, \"filename\": \"$filename\", \"timestamp\": 946728000, \"date_relative\": \"2000-01-01\", \"tags\": [\"inbox\"], \"headers\": {\"Subject\": \"$subject\", \"From\": \"Notmuch Test Suite <test_suite@notmuchmail.org>\", \"To\": \"test_suite@notmuchmail.org\", \"Date\": \"Sat, 01 Jan 2000 12:00:00 +0000\"}, \"body\": [{\"id\": 1, \"content-type\": \"multipart/mixed\", \"content\": [{\"id\": 2, \"content-type\": \"text/plain\", \"content\": \"This is a test message with inline attachment with a filename\"}, {\"id\": 3, \"content-type\": \"application/octet-stream\", \"filename\": \"README\"}]}]}, []]]]"
 
 test_begin_subtest "Search message: json, utf-8"
 add_message "[subject]=\"json-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
index d5872a53589ccd106420190af66b71f8e5be4979..d72ec07e9cb9eaaeaf08f8f2cdbbbb0df750e336 100755 (executable)
@@ -46,6 +46,7 @@ test_begin_subtest "notmuch show works with renamed file (without notmuch new)"
 output=$(notmuch show --format=json id:${gen_msg_id} | filter_show_json)
 test_expect_equal "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
 "match": true,
+"excluded": false,
 "filename": "MAIL_DIR/cur/adding-replied-tag:2,RS",
 "timestamp": 978709437,
 "date_relative": "2001-01-05",
@@ -53,8 +54,6 @@ test_expect_equal "$output" '[[[{"id": "adding-replied-tag@notmuch-test-suite",
 "headers": {"Subject": "Adding replied tag",
 "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
 "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
-"Cc": "",
-"Bcc": "",
 "Date": "Fri,
 05 Jan 2001 15:43:57 +0000"},
 "body": [{"id": 1,
index 2dd73f59926f9f08f34c964232c01ba4d9df08c8..72d3927651a1e12f6ada517f6936ab56b59ebe16 100755 (executable)
@@ -46,6 +46,7 @@ Content-Disposition: inline
 EOF
 cat embedded_message >> ${MAIL_DIR}/multipart
 cat <<EOF >> ${MAIL_DIR}/multipart
+
 --=-=-=
 Content-Disposition: attachment; filename=attachment
 
@@ -108,7 +109,7 @@ notmuch new > /dev/null
 test_begin_subtest "--format=text --part=0, full message"
 notmuch show --format=text --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-\fmessage{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 filename:${MAIL_DIR}/multipart
+\fmessage{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 excluded:0 filename:${MAIL_DIR}/multipart
 \fheader{
 Carl Worth <cworth@cworth.org> (2001-01-05) (attachment inbox signed unread)
 Subject: Multipart message
@@ -322,10 +323,10 @@ notmuch show --format=json --part=0 'id:87liy5ap00.fsf@yoom.home.cworth.org' | s
 echo >>OUTPUT # expect *no* newline at end of output
 cat <<EOF >EXPECTED
 
-{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "match": true, "filename": "${MAIL_DIR}/multipart", "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Cc": "", "Bcc": "", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [
+{"id": "87liy5ap00.fsf@yoom.home.cworth.org", "match": true, "excluded": false, "filename": "${MAIL_DIR}/multipart", "timestamp": 978709437, "date_relative": "2001-01-05", "tags": ["attachment","inbox","signed","unread"], "headers": {"Subject": "Multipart message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [
 {"id": 1, "content-type": "multipart/signed", "content": [
 {"id": 2, "content-type": "multipart/mixed", "content": [
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
 {"id": 5, "content-type": "text/html"}, 
 {"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, 
@@ -342,7 +343,7 @@ cat <<EOF >EXPECTED
 
 {"id": 1, "content-type": "multipart/signed", "content": [
 {"id": 2, "content-type": "multipart/mixed", "content": [
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
 {"id": 5, "content-type": "text/html"}, 
 {"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, 
@@ -358,7 +359,7 @@ echo >>OUTPUT # expect *no* newline at end of output
 cat <<EOF >EXPECTED
 
 {"id": 2, "content-type": "multipart/mixed", "content": [
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
 {"id": 5, "content-type": "text/html"}, 
 {"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}, 
@@ -372,7 +373,7 @@ notmuch show --format=json --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' | s
 echo >>OUTPUT # expect *no* newline at end of output
 cat <<EOF >EXPECTED
 
-{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Subject": "html message", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
+{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"Subject": "html message", "From": "Carl Worth <cworth@cworth.org>", "To": "cworth@cworth.org", "Date": "Fri, 05 Jan 2001 15:42:57 +0000"}, "body": [
 {"id": 4, "content-type": "multipart/alternative", "content": [
 {"id": 5, "content-type": "text/html"}, 
 {"id": 6, "content-type": "text/plain", "content": "This is an embedded message, with a multipart/alternative part.\n"}]}]}]}
@@ -449,58 +450,80 @@ test_expect_equal_file OUTPUT "${MAIL_DIR}"/multipart
 
 test_begin_subtest "--format=raw --part=1, message body"
 notmuch show --format=raw --part=1 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
-# output should *not* include newline
-echo >>OUTPUT
-cat <<EOF >EXPECTED
-Subject: html message
-From: Carl Worth <cworth@cworth.org>
-To: cworth@cworth.org
-Date: Fri, 05 Jan 2001 15:42:57 +0000
-
-<p>This is an embedded message, with a multipart/alternative part.</p>
-This is an embedded message, with a multipart/alternative part.
-This is a text attachment.
-And this message is signed.
-
--Carl
------BEGIN PGP SIGNATURE-----
-Version: GnuPG v1.4.11 (GNU/Linux)
-
-iEYEARECAAYFAk3SA/gACgkQ6JDdNq8qSWj0sACghqVJEQJUs3yV8zbTzhgnSIcD
-W6cAmQE4dcYrx/LPLtYLZm1jsGauE5hE
-=zkga
------END PGP SIGNATURE-----
-EOF
-test_expect_equal_file OUTPUT EXPECTED
+test_expect_equal_file OUTPUT "${MAIL_DIR}"/multipart
 
 test_begin_subtest "--format=raw --part=2, multipart/mixed"
 notmuch show --format=raw --part=2 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
-Subject: html message
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: message/rfc822
+Content-Disposition: inline
+
 From: Carl Worth <cworth@cworth.org>
 To: cworth@cworth.org
+Subject: html message
 Date: Fri, 05 Jan 2001 15:42:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=-=="
+
+--==-=-==
+Content-Type: text/html
 
 <p>This is an embedded message, with a multipart/alternative part.</p>
+
+--==-=-==
+Content-Type: text/plain
+
 This is an embedded message, with a multipart/alternative part.
+
+--==-=-==--
+
+--=-=-=
+Content-Disposition: attachment; filename=attachment
+
 This is a text attachment.
+
+--=-=-=
+
 And this message is signed.
 
 -Carl
+
+--=-=-=--
 EOF
 test_expect_equal_file OUTPUT EXPECTED
 
 test_begin_subtest "--format=raw --part=3, rfc822 part"
-test_subtest_known_broken
-
 notmuch show --format=raw --part=3 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 test_expect_equal_file OUTPUT embedded_message
 
-test_begin_subtest "--format=raw --part=4, rfc822's html part"
+test_begin_subtest "--format=raw --part=4, rfc822's multipart"
 notmuch show --format=raw --part=4 'id:87liy5ap00.fsf@yoom.home.cworth.org' >OUTPUT
 cat <<EOF >EXPECTED
+From: Carl Worth <cworth@cworth.org>
+To: cworth@cworth.org
+Subject: html message
+Date: Fri, 05 Jan 2001 15:42:57 +0000
+User-Agent: Notmuch/0.5 (http://notmuchmail.org) Emacs/23.3.1 (i486-pc-linux-gnu)
+Message-ID: <87liy5ap01.fsf@yoom.home.cworth.org>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="==-=-=="
+
+--==-=-==
+Content-Type: text/html
+
 <p>This is an embedded message, with a multipart/alternative part.</p>
+
+--==-=-==
+Content-Type: text/plain
+
 This is an embedded message, with a multipart/alternative part.
+
+--==-=-==--
 EOF
 test_expect_equal_file OUTPUT EXPECTED
 
@@ -589,9 +612,61 @@ Non-text part: text/html
 EOF
 test_expect_equal_file OUTPUT EXPECTED
 
+test_begin_subtest "'notmuch reply' to a multipart message with json format"
+notmuch reply --format=json 'id:87liy5ap00.fsf@yoom.home.cworth.org' | notmuch_json_show_sanitize >OUTPUT
+cat <<EOF >EXPECTED
+{"reply-headers": {"Subject": "Re: Multipart message",
+ "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+ "To": "Carl Worth <cworth@cworth.org>,
+ cworth@cworth.org",
+ "In-reply-to": "<87liy5ap00.fsf@yoom.home.cworth.org>",
+ "References": " <87liy5ap00.fsf@yoom.home.cworth.org>"},
+ "original": {"id": "XXXXX",
+ "match": false,
+ "excluded": false,
+ "filename": "YYYYY",
+ "timestamp": 978709437,
+ "date_relative": "2001-01-05",
+ "tags": ["attachment","inbox","signed","unread"],
+ "headers": {"Subject": "Multipart message",
+ "From": "Carl Worth <cworth@cworth.org>",
+ "To": "cworth@cworth.org",
+ "Date": "Fri,
+ 05 Jan 2001 15:43:57 +0000"},
+ "body": [{"id": 1,
+ "content-type": "multipart/signed",
+ "content": [{"id": 2,
+ "content-type": "multipart/mixed",
+ "content": [{"id": 3,
+ "content-type": "message/rfc822",
+ "content": [{"headers": {"Subject": "html message",
+ "From": "Carl Worth <cworth@cworth.org>",
+ "To": "cworth@cworth.org",
+ "Date": "Fri,
+ 05 Jan 2001 15:42:57 +0000"},
+ "body": [{"id": 4,
+ "content-type": "multipart/alternative",
+ "content": [{"id": 5,
+ "content-type": "text/html"},
+ {"id": 6,
+ "content-type": "text/plain",
+ "content": "This is an embedded message,
+ with a multipart/alternative part.\n"}]}]}]},
+ {"id": 7,
+ "content-type": "text/plain",
+ "filename": "YYYYY",
+ "content": "This is a text attachment.\n"},
+ {"id": 8,
+ "content-type": "text/plain",
+ "content": "And this message is signed.\n\n-Carl\n"}]},
+ {"id": 9,
+ "content-type": "application/pgp-signature"}]}]}}
+EOF
+test_expect_equal_file OUTPUT EXPECTED
+
 test_begin_subtest "'notmuch show --part' does not corrupt a part with CRLF pair"
 notmuch show --format=raw --part=3 id:base64-part-with-crlf > crlf.out
 echo -n -e "\xEF\x0D\x0A" > crlf.expected
 test_expect_equal_file crlf.out crlf.expected
 
-test_done
+test_done
\ No newline at end of file
index e14d34e424705851951aad5c4c650ab555ace7fc..f03b594d0b753a3518520cc84163cdeb71b2f5ca 100755 (executable)
@@ -54,6 +54,7 @@ TESTS="
   argument-parsing
   emacs-test-functions
   emacs-address-cleaning
+  emacs-hello
   emacs-show
 "
 TESTS=${NOTMUCH_TESTS:=$TESTS}
index 0171e641efd624917a4f55fed4c9d39581384bfa..de0b8677943f325afe2601e16b9458b416794d4f 100755 (executable)
--- a/test/raw
+++ b/test/raw
@@ -3,11 +3,8 @@
 test_description='notmuch show --format=raw'
 . ./test-lib.sh
 
-test_begin_subtest "Generate some messages"
-generate_message
-generate_message
-output=$(NOTMUCH_NEW)
-test_expect_equal "$output" "Added 2 new messages to the database."
+add_message
+add_message
 
 test_begin_subtest "Attempt to show multiple raw messages"
 output=$(notmuch show --format=raw "*" 2>&1)
index 414be35610ef8812e398e8b85b988230881a12d6..17af6a267135017d194d478bc6dcb0701cd42f80 100755 (executable)
@@ -130,13 +130,31 @@ output=$(notmuch search "bödý" | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2000-01-01 [1/1] Notmuch Test Suite; utf8-message-body-subject (inbox unread)"
 
 test_begin_subtest "Exclude \"deleted\" messages from search"
-notmuch config set search.exclude_tags deleted
+notmuch config set search.exclude_tags deleted
 generate_message '[subject]="Not deleted"'
+not_deleted_id=$gen_msg_id
 generate_message '[subject]="Deleted"'
 notmuch new > /dev/null
 notmuch tag +deleted id:$gen_msg_id
+deleted_id=$gen_msg_id
 output=$(notmuch search subject:deleted | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)"
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX   2001-01-05 [0/1] Notmuch Test Suite; Deleted (deleted inbox unread)"
+
+test_begin_subtest "Exclude \"deleted\" messages from message search"
+output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id"
+
+test_begin_subtest "Exclude \"deleted\" messages from message search (no-exclude)"
+output=$(notmuch search --no-exclude --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id
+id:$deleted_id"
+
+test_begin_subtest "Exclude \"deleted\" messages from message search (non-existent exclude-tag)"
+notmuch config set search.exclude_tags deleted non_existent_tag
+output=$(notmuch search --output=messages subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "id:$not_deleted_id"
+notmuch config set search.exclude_tags deleted
 
 test_begin_subtest "Exclude \"deleted\" messages from search, overridden"
 output=$(notmuch search subject:deleted and tag:deleted | notmuch_search_sanitize)
@@ -148,6 +166,11 @@ output=$(notmuch search subject:deleted | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
 thread:XXX   2001-01-05 [1/2] Notmuch Test Suite; Not deleted reply (deleted inbox unread)"
 
+test_begin_subtest "Don't exclude \"deleted\" messages when --no-exclude specified"
+output=$(notmuch search --no-exclude subject:deleted | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)
+thread:XXX   2001-01-05 [2/2] Notmuch Test Suite; Deleted (deleted inbox unread)"
+
 test_begin_subtest "Don't exclude \"deleted\" messages from search if not configured"
 notmuch config set search.exclude_tags
 output=$(notmuch search subject:deleted | notmuch_search_sanitize)
index f8119cbb4d99be909045e1ddfab500e6813c1940..3f6ec76348c07de88d99def9003475083fa05f5e 100755 (executable)
@@ -32,7 +32,7 @@ test_expect_equal_file OUTPUT EXPECTED
 
 test_begin_subtest "Test matches folder:spam"
 output=$(notmuch search folder:spam)
-test_expect_equal "$output" "thread:0000000000000001   2001-01-05 [1/1] Notmuch Test Suite; Test message #1 (inbox unread)"
+test_expect_equal "$output" "thread:0000000000000001   2001-01-05 [1/1] Notmuch Test Suite; Single new message (inbox unread)"
 
 test_begin_subtest "Remove folder:spam copy of email"
 rm $dir/spam/$(basename $file_x)
index 278150677c4a92e8df4a723be693fae108213576..06aaea270e946cb6811ed6e18258781b5d267b8d 100644 (file)
@@ -318,7 +318,11 @@ generate_message ()
     fi
 
     if [ -z "${template[subject]}" ]; then
-       template[subject]="Test message #${gen_msg_cnt}"
+       if [ -n "$test_subtest_name" ]; then
+           template[subject]="$test_subtest_name"
+       else
+           template[subject]="Test message #${gen_msg_cnt}"
+       fi
     fi
 
     if [ -z "${template[date]}" ]; then
index 942e5939764d4d611f385b33384530cdd7793a70..1a1a48f6f5b92c88b479f0b7825b79ee22c6879a 100755 (executable)
@@ -65,7 +65,7 @@ test_expect_equal "$output" "thread:XXX   2001-01-12 [6/8] Notmuch Test Suite; t
 
 test_begin_subtest 'Test order of messages in "notmuch show"'
 output=$(notmuch show thread-naming | notmuch_show_sanitize)
-test_expect_equal "$output" "\fmessage{ id:msg-$(printf "%03d" $first)@notmuch-test-suite depth:0 match:1 filename:/XXX/mail/msg-$(printf "%03d" $first)
+test_expect_equal "$output" "\fmessage{ id:msg-$(printf "%03d" $first)@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $first)
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-05) (unread)
 Subject: thread-naming: Initial thread subject
@@ -79,7 +79,7 @@ This is just a test message (#$first)
 \fpart}
 \fbody}
 \fmessage}
-\fmessage{ id:msg-$(printf "%03d" $((first + 1)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 1)))
+\fmessage{ id:msg-$(printf "%03d" $((first + 1)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 1)))
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-06) (inbox unread)
 Subject: thread-naming: Older changed subject
@@ -93,7 +93,7 @@ This is just a test message (#$((first + 1)))
 \fpart}
 \fbody}
 \fmessage}
-\fmessage{ id:msg-$(printf "%03d" $((first + 2)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 2)))
+\fmessage{ id:msg-$(printf "%03d" $((first + 2)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 2)))
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-07) (inbox unread)
 Subject: thread-naming: Newer changed subject
@@ -107,7 +107,7 @@ This is just a test message (#$((first + 2)))
 \fpart}
 \fbody}
 \fmessage}
-\fmessage{ id:msg-$(printf "%03d" $((first + 3)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 3)))
+\fmessage{ id:msg-$(printf "%03d" $((first + 3)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 3)))
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-08) (unread)
 Subject: thread-naming: Final thread subject
@@ -121,7 +121,7 @@ This is just a test message (#$((first + 3)))
 \fpart}
 \fbody}
 \fmessage}
-\fmessage{ id:msg-$(printf "%03d" $((first + 4)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 4)))
+\fmessage{ id:msg-$(printf "%03d" $((first + 4)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 4)))
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-09) (inbox unread)
 Subject: Re: thread-naming: Initial thread subject
@@ -135,7 +135,7 @@ This is just a test message (#$((first + 4)))
 \fpart}
 \fbody}
 \fmessage}
-\fmessage{ id:msg-$(printf "%03d" $((first + 5)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 5)))
+\fmessage{ id:msg-$(printf "%03d" $((first + 5)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 5)))
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-10) (inbox unread)
 Subject: Aw: thread-naming: Initial thread subject
@@ -149,7 +149,7 @@ This is just a test message (#$((first + 5)))
 \fpart}
 \fbody}
 \fmessage}
-\fmessage{ id:msg-$(printf "%03d" $((first + 6)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 6)))
+\fmessage{ id:msg-$(printf "%03d" $((first + 6)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 6)))
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-11) (inbox unread)
 Subject: Vs: thread-naming: Initial thread subject
@@ -163,7 +163,7 @@ This is just a test message (#$((first + 6)))
 \fpart}
 \fbody}
 \fmessage}
-\fmessage{ id:msg-$(printf "%03d" $((first + 7)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 7)))
+\fmessage{ id:msg-$(printf "%03d" $((first + 7)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 7)))
 \fheader{
 Notmuch Test Suite <test_suite@notmuchmail.org> (2001-01-12) (inbox unread)
 Subject: Sv: thread-naming: Initial thread subject