From: David Bremner Date: Mon, 24 Oct 2016 01:24:08 +0000 (-0300) Subject: Merge tag '0.23.1' X-Git-Tag: 0.24_rc0~108 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=9be349c20faea4b119c69ec63a39476ec9570d85;hp=ad517e9195a29b26955999c6e11fc37c73dbc01e Merge tag '0.23.1' notmuch 0.23.1 release --- diff --git a/devel/nmbug/nmbug b/devel/nmbug/nmbug index 1dd5f14f..6febf16f 100755 --- a/devel/nmbug/nmbug +++ b/devel/nmbug/nmbug @@ -475,7 +475,7 @@ def log(args=()): 'nmbug log HEAD..@{upstream}'. """ # we don't want output trapping here, because we want the pager. - args = ['log', '--name-status'] + list(args) + args = ['log', '--name-status', '--no-renames'] + list(args) with _git(args=args, expect=(0, 1, -13)) as p: p.wait() diff --git a/emacs/notmuch-address.el b/emacs/notmuch-address.el index 10eaab19..b2e1fba7 100644 --- a/emacs/notmuch-address.el +++ b/emacs/notmuch-address.el @@ -194,7 +194,14 @@ external commands." (t (funcall notmuch-address-selection-function (format "Address (%s matches): " num-options) - (cdr options) (car options)))))) + ;; We put the first match as the initial + ;; input; we put all the matches as + ;; possible completions, moving the + ;; first match to the end of the list + ;; makes cursor up/down in the list work + ;; better. + (append (cdr options) (list (car options))) + (car options)))))) (if chosen (progn (push chosen notmuch-address-history) diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index d582bff7..c858a20b 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -604,10 +604,11 @@ with `notmuch-hello-query-counts'." (defimage notmuch-hello-logo ((:type png :file "notmuch-logo.png"))) -(defun notmuch-hello-update (&optional no-display) - "Update the current notmuch view." +(defun notmuch-hello-update () + "Update the notmuch-hello buffer." ;; Lazy - rebuild everything. - (notmuch-hello no-display)) + (interactive) + (notmuch-hello t)) (defun notmuch-hello-window-configuration-change () "Hook function to update the hello buffer when it is switched to." diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el index 963253c9..3e20b8c7 100644 --- a/emacs/notmuch-jump.el +++ b/emacs/notmuch-jump.el @@ -104,7 +104,7 @@ not appear in the pop-up buffer. (copy-sequence minibuffer-prompt-properties) 'face)) ;; Build the keymap with our bindings - (minibuffer-map (notmuch-jump--make-keymap action-map)) + (minibuffer-map (notmuch-jump--make-keymap action-map prompt)) ;; The bindings save the the action in notmuch-jump--action (notmuch-jump--action nil)) ;; Read the action @@ -161,18 +161,47 @@ buffer." (set-keymap-parent map minibuffer-local-map) ;; Make this like a special-mode keymap, with no self-insert-command (suppress-keymap map) + (define-key map (kbd "DEL") 'exit-minibuffer) map) "Base keymap for notmuch-jump's minibuffer keymap.") -(defun notmuch-jump--make-keymap (action-map) +(defun notmuch-jump--make-keymap (action-map prompt) "Translate ACTION-MAP into a minibuffer keymap." (let ((map (make-sparse-keymap))) (set-keymap-parent map notmuch-jump-minibuffer-map) (dolist (action action-map) - (define-key map (first action) - `(lambda () (interactive) - (setq notmuch-jump--action ',(third action)) - (exit-minibuffer)))) + (if (= (length (first action)) 1) + (define-key map (first action) + `(lambda () (interactive) + (setq notmuch-jump--action ',(third action)) + (exit-minibuffer))))) + ;; By doing this in two passes (and checking if we already have a + ;; binding) we avoid problems if the user specifies a binding which + ;; is a prefix of another binding. + (dolist (action action-map) + (if (> (length (first action)) 1) + (let* ((key (elt (first action) 0)) + (keystr (string key)) + (new-prompt (concat prompt (format-kbd-macro keystr) " ")) + (action-submap nil)) + (unless (lookup-key map keystr) + (dolist (act action-map) + (when (= key (elt (first act) 0)) + (push (list (substring (first act) 1) + (second act) + (third act)) + action-submap))) + ;; We deal with backspace specially + (push (list (kbd "DEL") + "Backup" + (apply-partially #'notmuch-jump action-map prompt)) + action-submap) + (setq action-submap (nreverse action-submap)) + (define-key map keystr + `(lambda () (interactive) + (setq notmuch-jump--action + ',(apply-partially #'notmuch-jump action-submap new-prompt)) + (exit-minibuffer))))))) map)) ;; diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index 2f015b0d..1f0d1678 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -57,6 +57,10 @@ (custom-add-to-group 'notmuch-send 'message 'custom-group) +(defgroup notmuch-tag nil + "Tags and tagging in Notmuch." + :group 'notmuch) + (defgroup notmuch-crypto nil "Processing and display of cryptographic MIME parts." :group 'notmuch) @@ -147,6 +151,7 @@ For example, if you wanted to remove an \"inbox\" tag and add an (define-key map "z" 'notmuch-tree) (define-key map "m" 'notmuch-mua-new-mail) (define-key map "=" 'notmuch-refresh-this-buffer) + (define-key map (kbd "M-=") 'notmuch-refresh-all-buffers) (define-key map "G" 'notmuch-poll-and-refresh-this-buffer) (define-key map "j" 'notmuch-jump-search) map) @@ -413,10 +418,8 @@ of its command symbol." "Refresh the current buffer." (interactive) (when notmuch-buffer-refresh-function - (if (commandp notmuch-buffer-refresh-function) - ;; Pass prefix argument, etc. - (call-interactively notmuch-buffer-refresh-function) - (funcall notmuch-buffer-refresh-function)))) + ;; Pass prefix argument, etc. + (call-interactively notmuch-buffer-refresh-function))) (defun notmuch-poll-and-refresh-this-buffer () "Invoke `notmuch-poll' to import mail, then refresh the current buffer." @@ -424,6 +427,21 @@ of its command symbol." (notmuch-poll) (notmuch-refresh-this-buffer)) +(defun notmuch-refresh-all-buffers () + "Invoke `notmuch-refresh-this-buffer' on all notmuch major-mode buffers. + +The buffers are silently refreshed, i.e. they are not forced to +be displayed." + (interactive) + (dolist (buffer (buffer-list)) + (let ((buffer-mode (buffer-local-value 'major-mode buffer))) + (when (memq buffer-mode '(notmuch-show-mode + notmuch-tree-mode + notmuch-search-mode + notmuch-hello-mode)) + (with-current-buffer buffer + (notmuch-refresh-this-buffer)))))) + (defun notmuch-prettify-subject (subject) ;; This function is used by `notmuch-search-process-filter' which ;; requires that we not disrupt its' matching state. diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el index 55bc2672..f3336559 100644 --- a/emacs/notmuch-mua.el +++ b/emacs/notmuch-mua.el @@ -251,6 +251,10 @@ mutiple parts get a header." (notmuch-show-max-text-part-size 0) ;; Insert headers for parts as appropriate for replying. (notmuch-show-insert-header-p-function notmuch-mua-reply-insert-header-p-function) + ;; Ensure that any encrypted parts are + ;; decrypted during the generation of the reply + ;; text. + (notmuch-show-process-crypto process-crypto) ;; Don't indent multipart sub-parts. (notmuch-show-indent-multipart nil)) ;; We don't want sigstatus buttons (an information leak and usually wrong anyway). @@ -490,15 +494,64 @@ will be addressed to all recipients of the source message." (notmuch-mua-reply query-string sender reply-all) (deactivate-mark))) +(defun notmuch-mua-check-no-misplaced-secure-tag () + "Query user if there is a misplaced secure mml tag. + +Emacs message-send will (probably) ignore a secure mml tag unless +it is at the start of the body. Returns t if there is no such +tag, or the user confirms they mean it." + (save-excursion + (let ((body-start (progn (message-goto-body) (point)))) + (goto-char (point-max)) + (or + ;; We are always fine if there is no secure tag. + (not (search-backward "<#secure" nil 't)) + ;; There is a secure tag, so it must be at the start of the + ;; body, with no secure tag earlier (i.e., in the headers). + (and (= (point) body-start) + (not (search-backward "<#secure" nil 't))) + ;; The user confirms they means it. + (yes-or-no-p "\ +There is a <#secure> tag not at the start of the body. It is +likely that the message will be sent unsigned and unencrypted. +Really send? "))))) + +(defun notmuch-mua-check-secure-tag-has-newline () + "Query if the secure mml tag has a newline following it. + +Emacs message-send will (probably) ignore a correctly placed +secure mml tag unless it is followed by a newline. Returns t if +any secure tag is followed by a newline, or the user confirms +they mean it." + (save-excursion + (message-goto-body) + (or + ;; There is no (correctly placed) secure tag. + (not (looking-at "<#secure")) + ;; The secure tag is followed by a newline. + (looking-at "<#secure[^\n>]*>\n") + ;; The user confirms they means it. + (yes-or-no-p "\ +The <#secure> tag at the start of the body is not followed by a +newline. It is likely that the message will be sent unsigned and +unencrypted. Really send? ")))) + +(defun notmuch-mua-send-common (arg &optional exit) + (interactive "P") + (when (and (notmuch-mua-check-no-misplaced-secure-tag) + (notmuch-mua-check-secure-tag-has-newline)) + (letf (((symbol-function 'message-do-fcc) #'notmuch-maildir-message-do-fcc)) + (if exit + (message-send-and-exit arg) + (message-send arg))))) + (defun notmuch-mua-send-and-exit (&optional arg) (interactive "P") - (letf (((symbol-function 'message-do-fcc) #'notmuch-maildir-message-do-fcc)) - (message-send-and-exit arg))) + (notmuch-mua-send-common arg 't)) (defun notmuch-mua-send (&optional arg) (interactive "P") - (letf (((symbol-function 'message-do-fcc) #'notmuch-maildir-message-do-fcc)) - (message-send arg))) + (notmuch-mua-send-common arg)) (defun notmuch-mua-kill-buffer () (interactive) diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index e7d16f81..fcf7e6ee 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -1263,6 +1263,18 @@ matched." (message "No messages matched the query!") nil)))) +(defun notmuch-show--build-queries (thread context) + "Return a list of queries to try for this search. + +THREAD and CONTEXT are both strings, though CONTEXT may be nil. +When CONTEXT is not nil, the first query is the conjunction of it +and THREAD. The next query is THREAD alone, and serves as a +fallback if the prior matches no messages." + (let (queries) + (push (list thread) queries) + (if context (push (list thread "and (" context ")") queries)) + queries)) + (defun notmuch-show--build-buffer (&optional state) "Display messages matching the current buffer context. @@ -1270,25 +1282,20 @@ Apply the previously saved STATE if supplied, otherwise show the first relevant message. If no messages match the query return NIL." - (let* ((basic-args (list notmuch-show-thread-id)) - (args (if notmuch-show-query-context - (append (list "\'") basic-args - (list "and (" notmuch-show-query-context ")\'")) - (append (list "\'") basic-args (list "\'")))) - (cli-args (cons "--exclude=false" + (let* ((cli-args (cons "--exclude=false" (when notmuch-show-elide-non-matching-messages (list "--entire-thread=false")))) - - (forest (or (notmuch-query-get-threads (append cli-args args)) - ;; If a query context reduced the number of - ;; results to zero, try again without it. - (and notmuch-show-query-context - (notmuch-query-get-threads (append cli-args basic-args))))) - + (queries (notmuch-show--build-queries + notmuch-show-thread-id notmuch-show-query-context)) + (forest nil) ;; Must be reset every time we are going to start inserting ;; messages into the buffer. (notmuch-show-previous-subject "")) - + ;; Use results from the first query that returns some. + (while (and (not forest) queries) + (setq forest (notmuch-query-get-threads + (append cli-args (list "'") (car queries) (list "'")))) + (setq queries (cdr queries))) (when forest (notmuch-show-insert-forest forest) @@ -1319,8 +1326,13 @@ If no messages match the query return NIL." This includes: - the list of open messages, - - the current message." - (list (notmuch-show-get-message-id) (notmuch-show-get-message-ids-for-open-messages))) + - the combination of current message id with/for each visible window." + (let* ((win-list (get-buffer-window-list (current-buffer) nil t)) + (win-id-combo (mapcar (lambda (win) + (with-selected-window win + (list win (notmuch-show-get-message-id)))) + win-list))) + (list win-id-combo (notmuch-show-get-message-ids-for-open-messages)))) (defun notmuch-show-get-query () "Return the current query in this show buffer" @@ -1347,8 +1359,8 @@ This includes: This includes: - opening the messages previously opened, - closing all other messages, - - moving to the correct current message." - (let ((current (car state)) + - moving to the correct current message in every displayed window." + (let ((win-msg-alist (car state)) (open (cadr state))) ;; Open those that were open. @@ -1357,8 +1369,10 @@ This includes: (member (notmuch-show-get-message-id) open)) until (not (notmuch-show-goto-message-next))) - ;; Go to the previously open message. - (notmuch-show-goto-message current))) + (dolist (win-msg-pair win-msg-alist) + (with-selected-window (car win-msg-pair) + ;; Go to the previously open message in this window + (notmuch-show-goto-message (cadr win-msg-pair)))))) (defun notmuch-show-refresh-view (&optional reset-state) "Refresh the current view. @@ -1433,6 +1447,7 @@ reset based on the original query." (define-key map "V" 'notmuch-show-view-raw-message) (define-key map "c" 'notmuch-show-stash-map) (define-key map "h" 'notmuch-show-toggle-visibility-headers) + (define-key map "k" 'notmuch-tag-jump) (define-key map "*" 'notmuch-show-tag-all) (define-key map "-" 'notmuch-show-remove-tag) (define-key map "+" 'notmuch-show-add-tag) diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index 6c8b6a75..1b2ce5c2 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -28,6 +28,57 @@ (require 'crm) (require 'notmuch-lib) +(declare-function notmuch-search-tag "notmuch" tag-changes) +(declare-function notmuch-show-tag "notmuch-show" tag-changes) +(declare-function notmuch-tree-tag "notmuch-tree" tag-changes) + +(autoload 'notmuch-jump "notmuch-jump") + +(define-widget 'notmuch-tag-key-type 'list + "A single key tagging binding." + :format "%v" + :args '((list :inline t + :format "%v" + (key-sequence :tag "Key") + (radio :tag "Tag operations" (repeat :tag "Tag list" (string :format "%v" :tag "change")) + (variable :tag "Tag variable")) + (string :tag "Name")))) + +(defcustom notmuch-tagging-keys + `((,(kbd "a") notmuch-archive-tags "Archive") + (,(kbd "u") notmuch-show-mark-read-tags "Mark read") + (,(kbd "f") ("+flagged") "Flag") + (,(kbd "s") ("+spam" "-inbox") "Mark as spam") + (,(kbd "d") ("+deleted" "-inbox") "Delete")) + "A list of keys and corresponding tagging operations. + +For each key (or key sequence) you can specify a sequence of +tagging operations to apply, or a variable which contains a list +of tagging operations such as `notmuch-archive-tags'. The final +element is a name for this tagging operation. If the name is +omitted or empty then the list of tag changes, or the variable +name is used as the name. + +The key `notmuch-tag-jump-reverse-key' (k by default) should not +be used (either as a key, or as the start of a key sequence) as +it is already bound: it switches the menu to a menu of the +reverse tagging operations. The reverse of a tagging operation is +the same list of individual tag-ops but with `+tag` replaced by +`-tag` and vice versa. + +If setting this variable outside of customize then it should be a +list of triples (lists of three elements). Each triple should be +of the form (key-binding tagging-operations name). KEY-BINDING +can be a single character or a key sequence; TAGGING-OPERATIONS +should either be a list of individual tag operations each of the +form `+tag` or `-tag`, or the variable name of a variable that is +a list of tagging operations; NAME should be a name for the +tagging operation, if omitted or empty than then name is taken +from TAGGING-OPERATIONS." + :tag "List of tagging bindings" + :type '(repeat notmuch-tag-key-type) + :group 'notmuch-tag) + (define-widget 'notmuch-tag-format-type 'lazy "Customize widget for notmuch-tag-format and friends" :type '(alist :key-type (regexp :tag "Tag") @@ -437,6 +488,55 @@ begin with a \"+\" or a \"-\". If REVERSE is non-nil, replace all s))) tags)) +(defvar notmuch-tag-jump-reverse-key "k" + "The key in tag-jump to switch to the reverse tag changes.") + +(defun notmuch-tag-jump (reverse) + "Create a jump menu for tagging operations. + +Creates and displays a jump menu for the tagging operations +specified in `notmuch-tagging-keys'. If REVERSE is set then it +offers a menu of the reverses of the operations specified in +`notmuch-tagging-keys'; i.e. each `+tag` is replaced by `-tag` +and vice versa." + ;; In principle this function is simple, but it has to deal with + ;; lots of cases: different modes (search/show/tree), whether a name + ;; is specified, whether the tagging operations is a list of + ;; tag-ops, or a symbol that evaluates to such a list, and whether + ;; REVERSE is specified. + (interactive "P") + (let (action-map) + (dolist (binding notmuch-tagging-keys) + (let* ((tag-function (case major-mode + (notmuch-search-mode #'notmuch-search-tag) + (notmuch-show-mode #'notmuch-show-tag) + (notmuch-tree-mode #'notmuch-tree-tag))) + (key (first binding)) + (forward-tag-change (if (symbolp (second binding)) + (symbol-value (second binding)) + (second binding))) + (tag-change (if reverse + (notmuch-tag-change-list forward-tag-change 't) + forward-tag-change)) + (name (or (and (not (string= (third binding) "")) + (third binding)) + (and (symbolp (second binding)) + (symbol-name (second binding))))) + (name-string (if name + (if reverse (concat "Reverse " name) + name) + (mapconcat #'identity tag-change " ")))) + (push (list key name-string + `(lambda () (,tag-function ',tag-change))) + action-map))) + (push (list notmuch-tag-jump-reverse-key + (if reverse + "Forward tag changes " + "Reverse tag changes") + (apply-partially 'notmuch-tag-jump (not reverse))) + action-map) + (setq action-map (nreverse action-map)) + (notmuch-jump action-map "Tag: "))) ;; diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el index 658c4f90..d5587a9c 100644 --- a/emacs/notmuch-tree.el +++ b/emacs/notmuch-tree.el @@ -271,7 +271,6 @@ FUNC." (define-key map "x" 'notmuch-tree-quit) (define-key map "A" 'notmuch-tree-archive-thread) (define-key map "a" 'notmuch-tree-archive-message-then-next) - (define-key map "=" 'notmuch-tree-refresh-view) (define-key map "z" 'notmuch-tree-to-tree) (define-key map "n" 'notmuch-tree-next-matching-message) (define-key map "p" 'notmuch-tree-prev-matching-message) @@ -279,6 +278,7 @@ FUNC." (define-key map "P" 'notmuch-tree-prev-message) (define-key map (kbd "M-p") 'notmuch-tree-prev-thread) (define-key map (kbd "M-n") 'notmuch-tree-next-thread) + (define-key map "k" 'notmuch-tag-jump) (define-key map "-" 'notmuch-tree-remove-tag) (define-key map "+" 'notmuch-tree-add-tag) (define-key map "*" 'notmuch-tree-tag-thread) diff --git a/emacs/notmuch.el b/emacs/notmuch.el index 9c7f2020..bd08aa0c 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -169,6 +169,7 @@ there will be called at other points of notmuch execution." (define-key map "t" 'notmuch-search-filter-by-tag) (define-key map "l" 'notmuch-search-filter) (define-key map [mouse-1] 'notmuch-search-show-thread) + (define-key map "k" 'notmuch-tag-jump) (define-key map "*" 'notmuch-search-tag-all) (define-key map "a" 'notmuch-search-archive-thread) (define-key map "-" 'notmuch-search-remove-tag) @@ -900,9 +901,10 @@ PROMPT is the string to prompt with." (process-lines notmuch-command "search" "--output=tags" "*"))) (completions (append (list "folder:" "path:" "thread:" "id:" "date:" "from:" "to:" - "subject:" "attachment:" "mimetype:") + "subject:" "attachment:") (mapcar (lambda (tag) (concat "tag:" tag)) all-tags) - (mapcar (lambda (tag) (concat "is:" tag)) all-tags)))) + (mapcar (lambda (tag) (concat "is:" tag)) all-tags) + (mapcar (lambda (mimetype) (concat "mimetype:" mimetype)) (mailcap-mime-types))))) (let ((keymap (copy-keymap minibuffer-local-map)) (current-query (case major-mode (notmuch-search-mode (notmuch-search-get-query)) @@ -933,7 +935,7 @@ PROMPT is the string to prompt with." ;;;###autoload (put 'notmuch-search 'notmuch-doc "Search for messages.") -(defun notmuch-search (&optional query oldest-first target-thread target-line) +(defun notmuch-search (&optional query oldest-first target-thread target-line no-display) "Display threads matching QUERY in a notmuch-search buffer. If QUERY is nil, it is read interactively from the minibuffer. @@ -944,6 +946,9 @@ Other optional parameters are used as follows: current if it appears in the search results. TARGET-LINE: The line number to move to if the target thread does not appear in the search results. + NO-DISPLAY: Do not try to foreground the search results buffer. If it is + already foregrounded i.e. displayed in a window, this has no + effect, meaning the buffer will remain visible. When called interactively, this will prompt for a query and use the configured default sort order." @@ -957,7 +962,9 @@ the configured default sort order." (let* ((query (or query (notmuch-read-query "Notmuch search: "))) (buffer (get-buffer-create (notmuch-search-buffer-title query)))) - (switch-to-buffer buffer) + (if no-display + (set-buffer buffer) + (switch-to-buffer buffer)) (notmuch-search-mode) ;; Don't track undo information for this buffer (set 'buffer-undo-list t) @@ -993,17 +1000,18 @@ the configured default sort order." (defun notmuch-search-refresh-view () "Refresh the current view. -Kills the current buffer and runs a new search with the same +Erases the current buffer and runs a new search with the same query string as the current search. If the current thread is in the new search results, then point will be placed on the same thread. Otherwise, point will be moved to attempt to be in the same relative position within the new buffer." + (interactive) (let ((target-line (line-number-at-pos)) (oldest-first notmuch-search-oldest-first) (target-thread (notmuch-search-find-thread-id 'bare)) (query notmuch-search-query-string)) - (notmuch-bury-or-kill-this-buffer) - (notmuch-search query oldest-first target-thread target-line) + ;; notmuch-search erases the current buffer. + (notmuch-search query oldest-first target-thread target-line t) (goto-char (point-min)))) (defun notmuch-search-toggle-order () diff --git a/test/T000-basic.sh b/test/T000-basic.sh index d6811bd1..0a8d6cdf 100755 --- a/test/T000-basic.sh +++ b/test/T000-basic.sh @@ -92,7 +92,7 @@ test_expect_equal \ "$(echo $PATH|cut -f1 -d: | sed -e 's,/test/valgrind/bin$,,')" test_begin_subtest 'notmuch is compiled with debugging symbols' -readelf --sections $(which notmuch) | grep \.debug +readelf --sections $(command -v notmuch) | grep \.debug test_expect_equal 0 $? test_done diff --git a/test/T060-count.sh b/test/T060-count.sh index d6933a76..69ab591f 100755 --- a/test/T060-count.sh +++ b/test/T060-count.sh @@ -126,4 +126,32 @@ sed 's/^\(A Xapian exception [^:]*\):.*$/\1/' < OUTPUT > OUTPUT.clean test_expect_equal_file EXPECTED OUTPUT.clean restore_database +test_begin_subtest "count library function is non-destructive" +test_subtest_known_broken +cat< EXPECTED +1: 52 messages +2: 52 messages +Exclude 'spam' +3: 52 messages +4: 52 messages +EOF +test_python <