From: David Bremner Date: Tue, 20 Mar 2012 11:08:17 +0000 (-0300) Subject: Merge tag '0.12' X-Git-Tag: 0.13_rc1~126 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=596a2076dcc1ebec2dc217f6d967397ef125aac4;hp=0dcdc2ae8a352a164e052188cfb6224f24dad1ae Merge tag '0.12' notmuch 0.12 release --- diff --git a/NEWS b/NEWS index 2e393c4b..ed5e3c5a 100644 --- 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) ========================= diff --git a/command-line-arguments.c b/command-line-arguments.c index e7114143..76b185f8 100644 --- a/command-line-arguments.c +++ b/command-line-arguments.c @@ -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); diff --git a/devel/TODO b/devel/TODO index 4dda6f46..7b750afa 100644 --- a/devel/TODO +++ b/devel/TODO @@ -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 diff --git a/devel/schemata b/devel/schemata index 24ad7757..728a46f2 100644 --- a/devel/schemata +++ b/devel/schemata @@ -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 +} diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index d17a30f9..e9caade5 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -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." diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index d315f765..c146748a 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -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: diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index 13244eb8..6aae3a05 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -19,11 +19,15 @@ ;; ;; Authors: David Edmondson +(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) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index 7c4c0bea..0cd7d826 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -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))) - ;; (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) diff --git a/emacs/notmuch.el b/emacs/notmuch.el index f851c6f7..f0afa072 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -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 "") '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))))) diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h index 7bf153e0..ea836f72 100644 --- a/lib/notmuch-private.h +++ b/lib/notmuch-private.h @@ -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); diff --git a/lib/notmuch.h b/lib/notmuch.h index 7929fe72..babd2086 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -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'. */ diff --git a/lib/query.cc b/lib/query.cc index 0b366025..68ac1e40 100644 --- a/lib/query.cc +++ b/lib/query.cc @@ -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); diff --git a/lib/thread.cc b/lib/thread.cc index 0435ee6d..e976d643 100644 --- a/lib/thread.cc +++ b/lib/thread.cc @@ -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); diff --git a/man/man1/notmuch-config.1 b/man/man1/notmuch-config.1 index a7468950..395cb9c4 100644 --- a/man/man1/notmuch-config.1 +++ b/man/man1/notmuch-config.1 @@ -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 diff --git a/man/man1/notmuch-count.1 b/man/man1/notmuch-count.1 index 8de43453..35ecc532 100644 --- a/man/man1/notmuch-count.1 +++ b/man/man1/notmuch-count.1 @@ -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 diff --git a/man/man1/notmuch-reply.1 b/man/man1/notmuch-reply.1 index bd95b5f8..8666549b 100644 --- a/man/man1/notmuch-reply.1 +++ b/man/man1/notmuch-reply.1 @@ -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 . @@ -73,7 +88,8 @@ with a search string matching a single message, (such as 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 diff --git a/man/man1/notmuch-search.1 b/man/man1/notmuch-search.1 index bf172207..06d81a6f 100644 --- a/man/man1/notmuch-search.1 +++ b/man/man1/notmuch-search.1 @@ -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), diff --git a/man/man1/notmuch-show.1 b/man/man1/notmuch-show.1 index d69834a1..b81cce69 100644 --- a/man/man1/notmuch-show.1 +++ b/man/man1/notmuch-show.1 @@ -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 diff --git a/notmuch-client.h b/notmuch-client.h index f4a62ccb..fa04fa2e 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -62,14 +62,14 @@ #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 diff --git a/notmuch-config.c b/notmuch-config.c index 61fda3ea..e9b27509 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -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); } diff --git a/notmuch-count.c b/notmuch-count.c index 63459fb6..46b76ae1 100644 --- a/notmuch-count.c +++ b/notmuch-count.c @@ -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: diff --git a/notmuch-reply.c b/notmuch-reply.c index 6b244e6d..e2b6c253 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -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; diff --git a/notmuch-search.c b/notmuch-search.c index 92ce38a1..f6061e4e 100644 --- a/notmuch-search.c +++ b/notmuch-search.c @@ -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: diff --git a/notmuch-setup.c b/notmuch-setup.c index 307231d5..94d0aa7b 100644 --- a/notmuch-setup.c +++ b/notmuch-setup.c @@ -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) diff --git a/notmuch-show.c b/notmuch-show.c index 93fb16f3..ff9d4278 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -20,10 +20,7 @@ #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, ¶ms.entire_thread, "entire-thread", 't', 0 }, { NOTMUCH_OPT_BOOLEAN, ¶ms.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, ¶ms); - 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, ¶ms); + } + notmuch_query_destroy (query); notmuch_database_close (notmuch); diff --git a/test/count b/test/count index 300b1714..b97fc066 100755 --- a/test/count +++ b/test/count @@ -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 diff --git a/test/crypto b/test/crypto index 6723ef87..be752b19 100755 --- a/test/crypto +++ b/test/crypto @@ -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 ", "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 ", "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 ", "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=' message{ id:XXXXX depth:0 match:1 filename:XXXXX +expected=' message{ id:XXXXX depth:0 match:1 excluded:0 filename:XXXXX header{ Notmuch Test Suite (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 ", "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 ", "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 ", "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 ", "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", diff --git a/test/emacs b/test/emacs index 75498928..8a287058 100755 --- a/test/emacs +++ b/test/emacs @@ -78,7 +78,7 @@ thread=$(notmuch search --output=threads subject:message-with-invalid-from) test_emacs "(notmuch-show \"$thread\") (test-output)" cat <EXPECTED -Invalid " From (2001-01-05) (inbox) +"Invalid " (2001-01-05) (inbox) Subject: message-with-invalid-from To: Notmuch Test Suite Date: Fri, 05 Jan 2001 15:43:57 +0000 @@ -268,11 +268,107 @@ Subject: Re: Testing message sent via SMTP In-Reply-To: Fcc: ${MAIL_DIR}/sent --text follows this line-- -On 01 Jan 2000 12:00:00 -0000, Notmuch Test Suite wrote: +Notmuch Test Suite 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 <EXPECTED +From: Notmuch Test Suite +To: Adrian Perez de Castro , 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 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 +> 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 <EXPECTED +From: Notmuch Test Suite +To: Alex Botero-Lowry , notmuch@notmuchmail.org +Subject: Re: [notmuch] preliminary FreeBSD support +In-Reply-To: +Fcc: ${MAIL_DIR}/sent +--text follows this line-- +Alex Botero-Lowry 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: Fcc: ${MAIL_DIR}/sent --text follows this line-- -On Fri, 05 Jan 2001 15:43:57 +0000, Notmuch Test Suite wrote: +Notmuch Test Suite 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 <EXPECTED -Sat, 01 Jan 2000 12:00:00 -0000 +Sat, 01 Jan 2000 12:00:00 +0000 Some One Some One Else Notmuch diff --git a/test/emacs-hello b/test/emacs-hello new file mode 100755 index 00000000..b235e3ab --- /dev/null +++ b/test/emacs-hello @@ -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 diff --git a/test/emacs.expected-output/notmuch-hello b/test/emacs.expected-output/notmuch-hello index 3e59595f..14707906 100644 --- a/test/emacs.expected-output/notmuch-hello +++ b/test/emacs.expected-output/notmuch-hello @@ -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 index 00000000..c64d7128 --- /dev/null +++ b/test/emacs.expected-output/notmuch-hello-new-section @@ -0,0 +1,4 @@ +Test: [hide] + + 52 inbox + diff --git a/test/emacs.expected-output/notmuch-hello-no-saved-searches b/test/emacs.expected-output/notmuch-hello-no-saved-searches index ef0e5d05..05475b15 100644 --- a/test/emacs.expected-output/notmuch-hello-no-saved-searches +++ b/test/emacs.expected-output/notmuch-hello-no-saved-searches @@ -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 index 00000000..9d796590 --- /dev/null +++ b/test/emacs.expected-output/notmuch-hello-section-counts @@ -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 index 00000000..3688e7cd --- /dev/null +++ b/test/emacs.expected-output/notmuch-hello-section-hidden-tag @@ -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 index 00000000..8209feda --- /dev/null +++ b/test/emacs.expected-output/notmuch-hello-section-with-empty @@ -0,0 +1,4 @@ +Test-with-empty: [hide] + + 52 inbox + diff --git a/test/emacs.expected-output/notmuch-hello-with-empty b/test/emacs.expected-output/notmuch-hello-with-empty index 71edba73..5e532221 100644 --- a/test/emacs.expected-output/notmuch-hello-with-empty +++ b/test/emacs.expected-output/notmuch-hello-with-empty @@ -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/encoding b/test/encoding index f0d073c5..2e1326eb 100755 --- a/test/encoding +++ b/test/encoding @@ -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" " message{ id:msg-001@notmuch-test-suite depth:0 match:1 filename:/XXX/mail/msg-001 +test_expect_equal "$output" " message{ id:msg-001@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-001 header{ Notmuch Test Suite (2001-01-05) (inbox unread) -Subject: Test message #1 +Subject: Message with text of unknown charset From: Notmuch Test Suite To: Notmuch Test Suite Date: Fri, 05 Jan 2001 15:43:57 +0000 diff --git a/test/json b/test/json index 7df43803..64397886 100755 --- 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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"To\": \"Notmuch Test Suite \", \"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 \", \"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 \", \"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\"" diff --git a/test/maildir-sync b/test/maildir-sync index d5872a53..d72ec07e 100755 --- a/test/maildir-sync +++ b/test/maildir-sync @@ -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 ", "To": "Notmuch Test Suite ", -"Cc": "", -"Bcc": "", "Date": "Fri, 05 Jan 2001 15:43:57 +0000"}, "body": [{"id": 1, diff --git a/test/multipart b/test/multipart index 2dd73f59..72d39276 100755 --- a/test/multipart +++ b/test/multipart @@ -46,6 +46,7 @@ Content-Disposition: inline EOF cat embedded_message >> ${MAIL_DIR}/multipart cat <> ${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 <EXPECTED - message{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 filename:${MAIL_DIR}/multipart + message{ id:87liy5ap00.fsf@yoom.home.cworth.org depth:0 match:1 excluded:0 filename:${MAIL_DIR}/multipart header{ Carl Worth (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 <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 ", "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 ", "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 ", "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 ", "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 <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 ", "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 ", "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 <EXPECTED {"id": 2, "content-type": "multipart/mixed", "content": [ -{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth ", "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 ", "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 <EXPECTED -{"id": 3, "content-type": "message/rfc822", "content": [{"headers": {"From": "Carl Worth ", "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 ", "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 <EXPECTED -Subject: html message -From: Carl Worth -To: cworth@cworth.org -Date: Fri, 05 Jan 2001 15:42:57 +0000 - -

This is an embedded message, with a multipart/alternative part.

-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 <EXPECTED -Subject: html message +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: message/rfc822 +Content-Disposition: inline + From: Carl Worth 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

This is an embedded message, with a multipart/alternative part.

+ +--==-=-== +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 <EXPECTED +From: Carl Worth +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 +

This is an embedded message, with a multipart/alternative part.

+ +--==-=-== +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 <EXPECTED +{"reply-headers": {"Subject": "Re: Multipart message", + "From": "Notmuch Test Suite ", + "To": "Carl Worth , + 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 ", + "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 ", + "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 diff --git a/test/notmuch-test b/test/notmuch-test index e14d34e4..f03b594d 100755 --- a/test/notmuch-test +++ b/test/notmuch-test @@ -54,6 +54,7 @@ TESTS=" argument-parsing emacs-test-functions emacs-address-cleaning + emacs-hello emacs-show " TESTS=${NOTMUCH_TESTS:=$TESTS} diff --git a/test/raw b/test/raw index 0171e641..de0b8677 100755 --- 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) diff --git a/test/search b/test/search index 414be356..17af6a26 100755 --- a/test/search +++ b/test/search @@ -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) diff --git a/test/search-folder-coherence b/test/search-folder-coherence index f8119cbb..3f6ec763 100755 --- a/test/search-folder-coherence +++ b/test/search-folder-coherence @@ -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) diff --git a/test/test-lib.sh b/test/test-lib.sh index 27815067..06aaea27 100644 --- a/test/test-lib.sh +++ b/test/test-lib.sh @@ -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 diff --git a/test/thread-naming b/test/thread-naming index 942e5939..1a1a48f6 100755 --- a/test/thread-naming +++ b/test/thread-naming @@ -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" " message{ id:msg-$(printf "%03d" $first)@notmuch-test-suite depth:0 match:1 filename:/XXX/mail/msg-$(printf "%03d" $first) +test_expect_equal "$output" " message{ id:msg-$(printf "%03d" $first)@notmuch-test-suite depth:0 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $first) header{ Notmuch Test Suite (2001-01-05) (unread) Subject: thread-naming: Initial thread subject @@ -79,7 +79,7 @@ This is just a test message (#$first) part} body} message} - message{ id:msg-$(printf "%03d" $((first + 1)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 1))) + message{ id:msg-$(printf "%03d" $((first + 1)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 1))) header{ Notmuch Test Suite (2001-01-06) (inbox unread) Subject: thread-naming: Older changed subject @@ -93,7 +93,7 @@ This is just a test message (#$((first + 1))) part} body} message} - message{ id:msg-$(printf "%03d" $((first + 2)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 2))) + message{ id:msg-$(printf "%03d" $((first + 2)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 2))) header{ Notmuch Test Suite (2001-01-07) (inbox unread) Subject: thread-naming: Newer changed subject @@ -107,7 +107,7 @@ This is just a test message (#$((first + 2))) part} body} message} - message{ id:msg-$(printf "%03d" $((first + 3)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 3))) + message{ id:msg-$(printf "%03d" $((first + 3)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 3))) header{ Notmuch Test Suite (2001-01-08) (unread) Subject: thread-naming: Final thread subject @@ -121,7 +121,7 @@ This is just a test message (#$((first + 3))) part} body} message} - message{ id:msg-$(printf "%03d" $((first + 4)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 4))) + message{ id:msg-$(printf "%03d" $((first + 4)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 4))) header{ Notmuch Test Suite (2001-01-09) (inbox unread) Subject: Re: thread-naming: Initial thread subject @@ -135,7 +135,7 @@ This is just a test message (#$((first + 4))) part} body} message} - message{ id:msg-$(printf "%03d" $((first + 5)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 5))) + message{ id:msg-$(printf "%03d" $((first + 5)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 5))) header{ Notmuch Test Suite (2001-01-10) (inbox unread) Subject: Aw: thread-naming: Initial thread subject @@ -149,7 +149,7 @@ This is just a test message (#$((first + 5))) part} body} message} - message{ id:msg-$(printf "%03d" $((first + 6)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 6))) + message{ id:msg-$(printf "%03d" $((first + 6)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 6))) header{ Notmuch Test Suite (2001-01-11) (inbox unread) Subject: Vs: thread-naming: Initial thread subject @@ -163,7 +163,7 @@ This is just a test message (#$((first + 6))) part} body} message} - message{ id:msg-$(printf "%03d" $((first + 7)))@notmuch-test-suite depth:1 match:1 filename:/XXX/mail/msg-$(printf "%03d" $((first + 7))) + message{ id:msg-$(printf "%03d" $((first + 7)))@notmuch-test-suite depth:1 match:1 excluded:0 filename:/XXX/mail/msg-$(printf "%03d" $((first + 7))) header{ Notmuch Test Suite (2001-01-12) (inbox unread) Subject: Sv: thread-naming: Initial thread subject