From: David Bremner Date: Sat, 11 Sep 2021 00:02:10 +0000 (-0300) Subject: Merge tag '0.33.1' X-Git-Tag: archive/debian/0.34_rc0-1~22 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=7556bb7da27621895327b84d22abba2519c24ba7;hp=8e59438025c88ebd83a78cf12c06ff954d979e01 Merge tag '0.33.1' notmuch 0.33.1 release --- diff --git a/NEWS b/NEWS index fe8b3cad..ec28a769 100644 --- a/NEWS +++ b/NEWS @@ -14,7 +14,7 @@ Notmuch 0.33 (2021-09-03) Library ------- -Correct documentatation about transactions. +Correct documentation about transactions. Add a configurable automatic commit of transactions. See `database.autocommit` in notmuch-config(1). @@ -183,7 +183,7 @@ notmuch_database_remove_message or notmuch_message_delete in one session has been fixed. As always, the canonical source of API documentation is -`lib/notmuch.h`, or the doxygen formatted documentation in `notmuch(3)` +`lib/notmuch.h`, or the doxygen formatted documentation in `notmuch(3)`. CLI --- @@ -249,12 +249,12 @@ Fix for exclude tags in notmuch2 bindings. Build ----- -Portability update for T360-symbol-hiding +Portability update for T360-symbol-hiding. Library ------- -Fix for memory error in notmuch_database_get_config_list +Fix for memory error in notmuch_database_get_config_list. Notmuch 0.31.2 (2020-11-08) =========================== @@ -469,7 +469,7 @@ Command Line Interface ---------------------- `notmuch show` now supports --body=false and --include-html with ---format=text +--format=text. Fix several performance problems with `notmuch reindex`. diff --git a/bindings/python-cffi/notmuch2/_build.py b/bindings/python-cffi/notmuch2/_build.py index f712b6c5..24df939e 100644 --- a/bindings/python-cffi/notmuch2/_build.py +++ b/bindings/python-cffi/notmuch2/_build.py @@ -53,6 +53,7 @@ ffibuilder.cdef( NOTMUCH_STATUS_NO_CONFIG, NOTMUCH_STATUS_NO_DATABASE, NOTMUCH_STATUS_DATABASE_EXISTS, + NOTMUCH_STATUS_BAD_QUERY_SYNTAX, NOTMUCH_STATUS_LAST_STATUS } notmuch_status_t; typedef enum { diff --git a/bindings/python-cffi/notmuch2/_errors.py b/bindings/python-cffi/notmuch2/_errors.py index 9301073e..f55cc96b 100644 --- a/bindings/python-cffi/notmuch2/_errors.py +++ b/bindings/python-cffi/notmuch2/_errors.py @@ -56,6 +56,8 @@ class NotmuchError(Exception): NoDatabaseError, capi.lib.NOTMUCH_STATUS_DATABASE_EXISTS: DatabaseExistsError, + capi.lib.NOTMUCH_STATUS_BAD_QUERY_SYNTAX: + QuerySyntaxError, } return types[status] @@ -103,6 +105,7 @@ class IllegalArgumentError(NotmuchError): pass class NoConfigError(NotmuchError): pass class NoDatabaseError(NotmuchError): pass class DatabaseExistsError(NotmuchError): pass +class QuerySyntaxError(NotmuchError): pass class ObjectDestroyedError(NotmuchError): """The object has already been destroyed and it's memory freed. diff --git a/configure b/configure index cfa9c09b..4262d122 100755 --- a/configure +++ b/configure @@ -820,6 +820,19 @@ else WITH_BASH=0 fi +printf "Checking for sfsexp... " +if pkg-config --exists sfsexp; then + printf "Yes.\n" + have_sfsexp=1 + sfsexp_cflags=$(pkg-config --cflags sfsexp) + sfsexp_ldflags=$(pkg-config --libs sfsexp) +else + printf "No (will not enable s-expression queries).\n" + have_sfsexp=0 + sfsexp_cflags= + sfsexp_ldflags= +fi + if [ -z "${EMACSLISPDIR-}" ]; then EMACSLISPDIR="\$(prefix)/share/emacs/site-lisp" fi @@ -1443,6 +1456,13 @@ HAVE_VALGRIND = ${have_valgrind} # And if so, flags needed at compile time for valgrind macros VALGRIND_CFLAGS = ${valgrind_cflags} +# Whether the sfsexp library is available +HAVE_SFSEXP = ${have_sfsexp} + +# And if so, flags needed at compile/link time for sfsexp +SFSEXP_CFLAGS = ${sfsexp_cflags} +SFSEXP_LDFLAGS = ${sfsexp_ldflags} + # Support for emacs WITH_EMACS = ${WITH_EMACS} @@ -1459,6 +1479,7 @@ WITH_ZSH = ${WITH_ZSH} COMMON_CONFIGURE_CFLAGS = \\ \$(GMIME_CFLAGS) \$(TALLOC_CFLAGS) \$(ZLIB_CFLAGS) \\ -DHAVE_VALGRIND=\$(HAVE_VALGRIND) \$(VALGRIND_CFLAGS) \\ + -DHAVE_SFSEXP=\$(HAVE_SFSEXP) \$(SFSEXP_CFLAGS) \\ -DHAVE_GETLINE=\$(HAVE_GETLINE) \\ -DWITH_EMACS=\$(WITH_EMACS) \\ -DHAVE_CANONICALIZE_FILE_NAME=\$(HAVE_CANONICALIZE_FILE_NAME) \\ @@ -1475,7 +1496,7 @@ CONFIGURE_CFLAGS = \$(COMMON_CONFIGURE_CFLAGS) CONFIGURE_CXXFLAGS = \$(COMMON_CONFIGURE_CFLAGS) \$(XAPIAN_CXXFLAGS) -CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS) +CONFIGURE_LDFLAGS = \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(ZLIB_LDFLAGS) \$(XAPIAN_LDFLAGS) \$(SFSEXP_LDFLAGS) EOF # construct the sh.config @@ -1524,6 +1545,9 @@ NOTMUCH_HAVE_PYTHON3_CFFI=${have_python3_cffi} # Is the python pytest package available? NOTMUCH_HAVE_PYTHON3_PYTEST=${have_python3_pytest} +# Is the sfsexp library available? +NOTMUCH_HAVE_SFSEXP=${have_sfsexp} + # Platform we are run on PLATFORM=${platform} EOF diff --git a/doc/conf.py b/doc/conf.py index 3ec55a61..1fbd102b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -159,6 +159,10 @@ man_pages = [ u'syntax for notmuch queries', [notmuch_authors], 7), + ('man7/notmuch-sexp-queries', 'notmuch-sexp-queries', + u's-expression syntax for notmuch queries', + [notmuch_authors], 7), + ('man1/notmuch-show', 'notmuch-show', u'show messages matching the given search terms', [notmuch_authors], 1), diff --git a/doc/index.rst b/doc/index.rst index a3bf3480..fbdcf779 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -24,6 +24,7 @@ Contents: man1/notmuch-restore man1/notmuch-search man7/notmuch-search-terms + man7/notmuch-sexp-queries man1/notmuch-show man1/notmuch-tag python-bindings diff --git a/doc/man1/notmuch-config.rst b/doc/man1/notmuch-config.rst index 07a9eaf0..7d901758 100644 --- a/doc/man1/notmuch-config.rst +++ b/doc/man1/notmuch-config.rst @@ -251,6 +251,11 @@ paths are presumed relative to `$HOME` for items in section :any:`notmuch-search-terms(7)` for more information about named queries. +**squery.** + Expansion for named query called , using s-expression syntax. See + :any:`notmuch-sexp-queries(7)` for more information about s-expression + queries. + FILES ===== diff --git a/doc/man7/notmuch-sexp-queries.rst b/doc/man7/notmuch-sexp-queries.rst new file mode 100644 index 00000000..019d15f0 --- /dev/null +++ b/doc/man7/notmuch-sexp-queries.rst @@ -0,0 +1,315 @@ +.. _notmuch-sexp-queries(7): + +==================== +notmuch-sexp-queries +==================== + +SYNOPSIS +======== + +**notmuch** *subcommand* ``--query=sexp`` [option ...] ``--`` '(and (to santa) (date december))' + +DESCRIPTION +=========== + +Notmuch supports an alternative query syntax based on `S-expressions +`_ . It can be selected +with the command line ``--query=sexp`` or with the appropriate option +to the library function :c:func:`notmuch_query_create_with_syntax`. +Support for this syntax is currently optional, you can test if your +build of notmuch supports it with + +:: + + $ notmuch config get built_with.sexpr_query + + +S-EXPRESSIONS +------------- + +An *s-expression* is either an atom, or list of whitespace delimited +s-expressions inside parentheses. Atoms are either + +*basic value* + A basic value is an unquoted string containing no whitespace, double quotes, or + parentheses. + +*quoted string* + Double quotes (") delimit strings possibly containing whitespace + or parentheses. These can contain double quote characters by + escaping with backslash. E.g. ``"this is a quote \""``. + +S-EXPRESSION QUERIES +-------------------- + +An s-expression query is either an atom, the empty list, or a +*compound query* consisting of a prefix atom (first element) defining +a *field*, *logical operation*, or *modifier*, and 0 or more +subqueries. + +``*`` + "*" matches any non-empty string in the current field. + +``()`` + The empty list matches all messages + +*term* + + Match all messages containing *term*, possibly after stemming or + phrase splitting. For discussion of stemming in notmuch see + :any:`notmuch-search-terms(7)`. Stemming only applies to unquoted + terms (basic values) in s-expression queries. For information on + phrase splitting see :any:`fields`. + +``(`` *field* |q1| |q2| ... |qn| ``)`` + Restrict the queries |q1| to |qn| to *field*, and combine with *and* + (for most fields) or *or*. See :any:`fields` for more information. + +``(`` *operator* |q1| |q2| ... |qn| ``)`` + Combine queries |q1| to |qn|. Currently supported operators are + ``and``, ``or``, and ``not``. ``(not`` |q1| ... |qn| ``)`` is equivalent + to ``(and (not`` |q1| ``) ... (not`` |qn| ``))``. + +``(`` *modifier* |q1| |q2| ... |qn| ``)`` + Combine queries |q1| to |qn|, and reinterpret the result (e.g. as a regular expression). + See :any:`modifiers` for more information. + +``(macro (`` |p1| ... |pn| ``) body)`` + Define saved query with parameter substitution. The syntax is + recognized only in saved s-expression queries (see ``squery.*`` in + :any:`notmuch-config(1)`). Parameter names in ``body`` must be + prefixed with ``,`` to be expanded (see :any:`macro_examples`). + Macros may refer to other macros, but only to their own + parameters [#macro-details]_. + +.. _fields: + +FIELDS +`````` + +*Fields* [#aka-pref]_ +correspond to attributes of mail messages. Some are inherent (and +immutable) like ``subject``, while others ``tag`` and ``property`` are +settable by the user. Each concrete field in +:any:`the table below ` +is discussed further under "Search prefixes" in +:any:`notmuch-search-terms(7)`. The row *user* refers to user defined +fields, described in :any:`notmuch-config(1)`. + +Most fields are either *phrase fields* [#aka-prob]_ (which match +sequences of words), or *term fields* [#aka-bool]_ (which match exact +strings). *Phrase splitting* breaks the term (basic value or quoted +string) into words, ignore punctuation. Phrase splitting is applied to +terms in phrase (probabilistic) fields. Both phrase splitting and +stemming apply only in phrase fields. + +Each term or phrase field has an associated combining operator +(``and`` or ``or``) used to combine the queries from each element of +the tail of the list. This is generally ``or`` for those fields where +a message has one such attribute, and ``and`` otherwise. + +Term or phrase fields can contain arbitrarily complex queries made up +from terms, operators, and modifiers, but not other fields. + +.. _field-table: + +.. table:: Fields with supported modifiers + + +------------+-----------+-----------+-----------+-----------+----------+ + | field | combine | type | expand | wildcard | regex | + +============+===========+===========+===========+===========+==========+ + | *none* | and | | no | yes | no | + +------------+-----------+-----------+-----------+-----------+----------+ + | *user* | and | phrase | no | yes | no | + +------------+-----------+-----------+-----------+-----------+----------+ + | attachment | and | phrase | yes | yes | no | + +------------+-----------+-----------+-----------+-----------+----------+ + | body | and | phrase | no | no | no | + +------------+-----------+-----------+-----------+-----------+----------+ + | date | | range | no | no | no | + +------------+-----------+-----------+-----------+-----------+----------+ + | folder | or | phrase | yes | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | from | and | phrase | yes | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | id | or | term | no | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | is | and | term | yes | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | lastmod | | range | no | no | no | + +------------+-----------+-----------+-----------+-----------+----------+ + | mid | or | term | no | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | mimetype | or | phrase | yes | yes | no | + +------------+-----------+-----------+-----------+-----------+----------+ + | path | or | term | no | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | property | and | term | yes | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | subject | and | phrase | yes | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | tag | and | term | yes | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | thread | or | term | yes | yes | yes | + +------------+-----------+-----------+-----------+-----------+----------+ + | to | and | phrase | yes | yes | no | + +------------+-----------+-----------+-----------+-----------+----------+ + +.. _modifiers: + +MODIFIERS +````````` + +*Modifiers* refer to any prefixes (first elements of compound queries) +that are neither operators nor fields. + +``(infix`` *atom* ``)`` + Interpret *atom* as an infix notmuch query (see + :any:`notmuch-search-terms(7)`). Not supported inside fields. + +``(matching`` |q1| |q2| ... |qn| ``)`` ``(of`` |q1| |q2| ... |qn| ``)`` + Match all messages have the same values of the current field as + those matching all of |q1| ... |qn|. Supported in most term [#not-path]_ or + phrase fields. Most commonly used in the ``thread`` field. + +``(query`` *atom* ``)`` + Expand to the saved query named by *atom*. See + :any:`notmuch-config(1)` for more. Note that the saved query must + be in infix syntax (:any:`notmuch-search-terms(7)`). Not supported + inside fields. + +``(regex`` *atom* ``)`` ``(rx`` *atom* ``)`` + Interpret *atom* as a POSIX.2 regular expression (see + :manpage:`regex(7)`). This applies in term fields and a subset [#not-phrase]_ of + phrase fields (see :any:`field-table`). + +``(starts-with`` *subword* ``)`` + Matches any term starting with *subword*. This applies in either + phrase or term :any:`fields `, or outside of fields [#not-body]_. Note that + a ``starts-with`` query cannot be part of a phrase. The + atom ``*`` is a synonym for ``(starts-with "")``. + +EXAMPLES +======== + +``Wizard`` + Match all messages containing the word "wizard", ignoring case. + +``added`` + Match all messages containing "added", but also those containing "add", "additional", + "Additional", "adds", etc... via stemming. + +``(and Bob Marley)`` + Match messages containing words "Bob" and "Marley", or their stems + The words need not be adjacent. + +``(not Bob Marley)`` + Match messages containing neither "Bob" nor "Marley", nor their stems, + +``"quick fox"`` ``quick-fox`` ``quick@fox`` + Match the *phrase* "quick" followed by "fox" in phrase fields (or + outside a field). Match the literal string in a term field. + +``(folder (of (id 1234@invalid)))`` + Match any message in the same folder as the one with Message-Id "1234@invalid" + +``(id 1234@invalid blah@test)`` + Matches Message-Id "1234@invalid" *or* Message-Id "blah@test" + +``(and (infix "date:2009-11-18..2009-11-18") (tag unread))`` + Match messages in the given date range with tag unread. + +``(starts-with prelim)`` + Match any words starting with "prelim". + +``(subject quick "brown fox")`` + Match messages whose subject contains "quick" (anywhere, stemmed) and + the phrase "brown fox". + +``(subject (starts-with prelim))`` + Matches any word starting with "prelim", inside a message subject. + +``(subject (starts-wih quick) "brown fox")`` + Match messages whose subject contains "quick brown fox", but also + "brown fox quicksand". + +``(thread (of (id 1234@invalid)))`` + Match any message in the same thread as the one with Message-Id "1234@invalid" + +``(thread (matching (from bob@example.com) (to bob@example.com)))`` + Match any (messages in) a thread containing a message from + "bob@example.com" and a (possibly distinct) message to "bob at + example.com") + +``(to (or bob@example.com mallory@example.org))`` ``(or (to bob@example.com) (to mallory@example.org))`` + Match in the "To" or "Cc" headers, "bob@example.com", + "mallory@example.org", and also "bob@example.com.au" since it + contains the adjacent triple "bob", "example", "com". + +``(not (to *))`` + Match messages with an empty or invalid 'To' and 'Cc' field. + +``(List *)`` + Match messages with a non-empty List-Id header, assuming + configuration ``index.header.List=List-Id`` + +.. _macro_examples: + +MACRO EXAMPLES +-------------- + +A macro that takes two parameters and applies different fields to them. + +:: + + $ notmuch config set squery.TagSubject '(macro (tagname subj) (and (tag ,tagname) (subject ,subj)))' + $ notmuch search --query=sexp '(TagSubject inbox maildir)' + +Nested macros are allowed. + +:: + + $ notmuch config set squery.Inner '(macro (x) (subject ,x))' + $ notmuch config set squery.Outer '(macro (x y) (and (tag ,x) (Inner ,y)))' + $ notmuch search --query=sexp '(Outer inbox maildir)' + +Parameters can be re-used to reduce boilerplate. Any field, including +user defined fields is permitted within a macro. + +:: + + $ notmuch config set squery.About '(macro (name) (or (subject ,name) (List ,name)))' + $ notmuch search --query=sexp '(About notmuch)' + + +NOTES +===== + +.. [#macro-details] Technically macros impliment lazy evaluation and + lexical scope. There is one top level scope + containing all macro definitions, but all + parameter definitions are local to a given macro. + +.. [#aka-pref] a.k.a. prefixes + +.. [#aka-prob] a.k.a. probabilistic prefixes + +.. [#aka-bool] a.k.a. boolean prefixes + +.. [#not-phrase] Due to the implemention of phrase fields in Xapian, + regex queries could only match individual words. + +.. [#not-body] Due the the way ``body`` is implemented in notmuch, + this modifier is not supported in the ``body`` field. + +.. [#not-path] Due to the way recursive ``path`` queries are implemented + in notmuch, this modifier is not supported in the + ``path`` field. + +.. |q1| replace:: :math:`q_1` +.. |q2| replace:: :math:`q_2` +.. |qn| replace:: :math:`q_n` + +.. |p1| replace:: :math:`p_1` +.. |p2| replace:: :math:`p_2` +.. |pn| replace:: :math:`p_n` diff --git a/emacs/notmuch-draft.el b/emacs/notmuch-draft.el index a68b7d8d..aeb6c588 100644 --- a/emacs/notmuch-draft.el +++ b/emacs/notmuch-draft.el @@ -42,7 +42,7 @@ :group 'notmuch) (defcustom notmuch-draft-tags '("+draft") - "List of tags changes to apply to a draft message when it is saved in the database. + "List of tag changes to apply when saving a draft message in the database. Tags starting with \"+\" (or not starting with either \"+\" or \"-\") in the list will be added, and tags starting with \"-\" diff --git a/emacs/notmuch-hello.el b/emacs/notmuch-hello.el index 1e66555b..21855b70 100644 --- a/emacs/notmuch-hello.el +++ b/emacs/notmuch-hello.el @@ -144,9 +144,11 @@ a plist. Supported properties are Possible values are `oldest-first', `newest-first' or nil. Nil means use the default sort order. :search-type Specify whether to run the search in search-mode, - tree mode or unthreaded mode. Set to 'tree to specify tree - mode, 'unthreaded to specify unthreaded mode, and set to nil - (or anything except tree and unthreaded) to specify search mode. + tree mode or unthreaded mode. Set to `tree' to + specify tree mode, 'unthreaded to specify + unthreaded mode, and set to nil (or anything + except tree and unthreaded) to specify search + mode. Other accepted forms are a cons cell of the form (NAME . QUERY) or a list of the form (NAME QUERY COUNT-QUERY)." @@ -869,16 +871,16 @@ Supports the following entries in OPTIONS as a plist: (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)) + :notify (lambda (&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)) + :notify (lambda (&rest _ignore) + (add-to-list 'notmuch-hello-hidden-sections + title) + (notmuch-hello-update)) "hide")) (widget-insert "\n") (unless is-hidden diff --git a/emacs/notmuch-jump.el b/emacs/notmuch-jump.el index 5ec8eb9c..6a276928 100644 --- a/emacs/notmuch-jump.el +++ b/emacs/notmuch-jump.el @@ -25,6 +25,10 @@ (require 'notmuch-lib) (require 'notmuch-hello) +(declare-function notmuch-search "notmuch") +(declare-function notmuch-tree "notmuch-tree") +(declare-function notmuch-unthreaded "notmuch-tree") + ;;;###autoload (defun notmuch-jump-search () "Jump to a saved search by shortcut key. @@ -50,11 +54,11 @@ fast way to jump to a saved search from anywhere in Notmuch." (push (list key name (cond ((eq (plist-get saved-search :search-type) 'tree) - `(lambda () (notmuch-tree ',query))) + (lambda () (notmuch-tree query))) ((eq (plist-get saved-search :search-type) 'unthreaded) - `(lambda () (notmuch-unthreaded ',query))) + (lambda () (notmuch-unthreaded query))) (t - `(lambda () (notmuch-search ',query ',oldest-first))))) + (lambda () (notmuch-search query oldest-first))))) action-map))))) (setq action-map (nreverse action-map)) (if action-map @@ -168,9 +172,10 @@ buffer." (pcase-dolist (`(,key ,_name ,fn) action-map) (when (= (length key) 1) (define-key map key - `(lambda () (interactive) - (setq notmuch-jump--action ',fn) - (exit-minibuffer))))) + (lambda () + (interactive) + (setq notmuch-jump--action fn) + (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. @@ -191,12 +196,13 @@ buffer." 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))))))) + (lambda () + (interactive) + (setq notmuch-jump--action + (apply-partially #'notmuch-jump + action-submap + new-prompt)) + (exit-minibuffer))))))) map)) (provide 'notmuch-jump) diff --git a/emacs/notmuch-maildir-fcc.el b/emacs/notmuch-maildir-fcc.el index c715532b..7e177bf7 100644 --- a/emacs/notmuch-maildir-fcc.el +++ b/emacs/notmuch-maildir-fcc.el @@ -41,16 +41,17 @@ Three types of values are permitted: - a string: the value of `notmuch-fcc-dirs' is the Fcc header to be used. -- a list: the folder is chosen based on the From address of the - current message using a list of regular expressions and - corresponding folders: +- an alist: the folder is chosen based on the From address of + the current message according to an alist mapping regular + expressions to folders or nil: ((\"Sebastian@SSpaeth.de\" . \"privat\") (\"spaetz@sspaeth.de\" . \"OUTBOX.OSS\") (\".*\" . \"defaultinbox\")) - If none of the regular expressions match the From address, no - Fcc header will be added. + If none of the regular expressions match the From address, or + if the cdr of the matching entry is nil, then no Fcc header + will be added. If `notmuch-maildir-use-notmuch-insert' is set (the default) then the header should be of the form \"folder +tag1 -tag2\" where @@ -74,7 +75,8 @@ directory if it does not exist yet when sending a mail." (const :tag "No FCC header" nil) (string :tag "A single folder") (repeat :tag "A folder based on the From header" - (cons regexp (string :tag "Folder")))) + (cons regexp (choice (const :tag "No FCC header" nil) + (string :tag "Folder"))))) :require 'notmuch-fcc-initialization :group 'notmuch-send) @@ -105,13 +107,14 @@ Otherwise set it according to `notmuch-fcc-dirs'." ;; Old style - no longer works. (error "Invalid `notmuch-fcc-dirs' setting (old style)")) ((listp notmuch-fcc-dirs) - (or (seq-some (let ((from (message-field-value "From"))) - (pcase-lambda (`(,regexp . ,folder)) - (and (string-match-p regexp from) - folder))) - notmuch-fcc-dirs) - (progn (message "No Fcc header added.") - nil))) + (if-let ((match (seq-some (let ((from (message-field-value "From"))) + (pcase-lambda (`(,regexp . ,folder)) + (and (string-match-p regexp from) + (cons t folder)))) + notmuch-fcc-dirs))) + (cdr match) + (message "No Fcc header added.") + nil)) (t (error "Invalid `notmuch-fcc-dirs' setting (neither string nor list)"))))) (when subdir diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el index 0f96c4fe..eeb0c54b 100644 --- a/emacs/notmuch-show.el +++ b/emacs/notmuch-show.el @@ -719,21 +719,23 @@ will return nil if the CID is unknown or cannot be retrieved." t) (defun notmuch-show-insert-part-message/rfc822 (msg part _content-type _nth depth _button) - (let* ((message (car (plist-get part :content))) - (body (car (plist-get message :body))) - (start (point))) - ;; Override `notmuch-message-headers' to force `From' to be - ;; displayed. - (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date"))) - (notmuch-show-insert-headers (plist-get message :headers))) - ;; Blank line after headers to be compatible with the normal - ;; message display. - (insert "\n") - ;; Show the body - (notmuch-show-insert-bodypart msg body depth) - (when notmuch-show-indent-multipart - (indent-rigidly start (point) 1))) - t) + (let ((message (car (plist-get part :content)))) + (and + message + (let ((body (car (plist-get message :body))) + (start (point))) + ;; Override `notmuch-message-headers' to force `From' to be + ;; displayed. + (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date"))) + (notmuch-show-insert-headers (plist-get message :headers))) + ;; Blank line after headers to be compatible with the normal + ;; message display. + (insert "\n") + ;; Show the body + (notmuch-show-insert-bodypart msg body depth) + (when notmuch-show-indent-multipart + (indent-rigidly start (point) 1)) + t)))) (defun notmuch-show-insert-part-text/plain (msg part _content-type _nth depth button) ;; For backward compatibility we want to apply the text/plain hook @@ -2078,19 +2080,19 @@ message." (let ((cwd default-directory) (buf (get-buffer-create (concat "*notmuch-pipe*")))) (with-current-buffer buf - (setq buffer-read-only nil) - (erase-buffer) - ;; Use the originating buffer's working directory instead of - ;; that of the pipe buffer. - (cd cwd) - (let ((exit-code (call-process-shell-command shell-command nil buf))) - (goto-char (point-max)) - (set-buffer-modified-p nil) - (setq buffer-read-only t) - (unless (zerop exit-code) - (pop-to-buffer buf) - (message (format "Command '%s' exited abnormally with code %d" - shell-command exit-code)))))))) + (setq buffer-read-only t) + (let ((inhibit-read-only t)) + (erase-buffer) + ;; Use the originating buffer's working directory instead of + ;; that of the pipe buffer. + (cd cwd) + (let ((exit-code (call-process-shell-command shell-command nil buf))) + (goto-char (point-max)) + (set-buffer-modified-p nil) + (unless (zerop exit-code) + (pop-to-buffer buf) + (message (format "Command '%s' exited abnormally with code %d" + shell-command exit-code))))))))) (defun notmuch-show-tag-message (&rest tag-changes) "Change tags for the current message. diff --git a/emacs/notmuch-tag.el b/emacs/notmuch-tag.el index ebccb5a0..e3a60441 100644 --- a/emacs/notmuch-tag.el +++ b/emacs/notmuch-tag.el @@ -553,7 +553,7 @@ and vice versa." name) (mapconcat #'identity tag-change " ")))) (push (list key name-string - `(lambda () (,tag-function ',tag-change))) + (lambda () (funcall tag-function tag-change))) action-map))) (push (list notmuch-tag-jump-reverse-key (if reverse diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el index 2f508128..b3f1183d 100644 --- a/emacs/notmuch-tree.el +++ b/emacs/notmuch-tree.el @@ -77,21 +77,28 @@ (defcustom notmuch-tree-result-format `(("date" . "%12s ") ("authors" . "%-20s") - ((("tree" . "%s")("subject" . "%s")) ." %-54s ") + ((("tree" . "%s") + ("subject" . "%s")) + . " %-54s ") ("tags" . "(%s)")) - "Result formatting for tree view. Supported fields are: date, -authors, subject, tree, tags. Tree means the thread tree -box graphics. The field may also be a list in which case -the formatting rules are applied recursively and then the -output of all the fields in the list is inserted -according to format-string. - -Note the author string should not contain -whitespace (put it in the neighbouring fields instead). -For example: - (setq notmuch-tree-result-format \(\(\"authors\" . \"%-40s\"\) - \(\"subject\" . \"%s\"\)\)\)" - :type '(alist :key-type (string) :value-type (string)) + "Result formatting for tree view. + +Supported fields are: date, authors, subject, tree, tags. + +Tree means the thread tree box graphics. The field may +also be a list in which case the formatting rules are +applied recursively and then the output of all the fields +in the list is inserted according to format-string. + +Note that the author string should not contain whitespace +\(put it in the neighbouring fields instead). For example: + (setq notmuch-tree-result-format + '((\"authors\" . \"%-40s\") + (\"subject\" . \"%s\")))" + :type '(alist :key-type (choice string + (alist :key-type string + :value-type string)) + :value-type string) :group 'notmuch-tree) (defcustom notmuch-unthreaded-result-format @@ -99,19 +106,24 @@ For example: ("authors" . "%-20s") ((("subject" . "%s")) ." %-54s ") ("tags" . "(%s)")) - "Result formatting for unthreaded tree view. Supported fields are: date, -authors, subject, tree, tags. Tree means the thread tree -box graphics. The field may also be a list in which case -the formatting rules are applied recursively and then the -output of all the fields in the list is inserted -according to format-string. - -Note the author string should not contain -whitespace (put it in the neighbouring fields instead). -For example: - (setq notmuch-tree-result-format \(\(\"authors\" . \"%-40s\"\) - \(\"subject\" . \"%s\"\)\)\)" - :type '(alist :key-type (string) :value-type (string)) + "Result formatting for unthreaded tree view. + +Supported fields are: date, authors, subject, tree, tags. + +Tree means the thread tree box graphics. The field may +also be a list in which case the formatting rules are +applied recursively and then the output of all the fields +in the list is inserted according to format-string. + +Note that the author string should not contain whitespace +\(put it in the neighbouring fields instead). For example: + (setq notmuch-unthreaded-result-format + '((\"authors\" . \"%-40s\") + (\"subject\" . \"%s\")))" + :type '(alist :key-type (choice string + (alist :key-type string + :value-type string)) + :value-type string) :group 'notmuch-tree) (defun notmuch-tree-result-format () @@ -873,6 +885,9 @@ unchanged ADDRESS if parsing fails." ((listp field) (format format-string (notmuch-tree-format-field-list field msg))) + ((functionp field) + (funcall field format-string msg)) + ((string-equal field "date") (let ((face (if match 'notmuch-tree-match-date-face diff --git a/emacs/notmuch.el b/emacs/notmuch.el index 739cb93b..479b39f1 100644 --- a/emacs/notmuch.el +++ b/emacs/notmuch.el @@ -88,18 +88,21 @@ ("authors" . "%-20s ") ("subject" . "%s ") ("tags" . "(%s)")) - "Search result formatting. Supported fields are: - date, count, authors, subject, tags + "Search result formatting. + +Supported fields are: date, count, authors, subject, tags. For example: - (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\) - \(\"subject\" . \"%s\"\)\)\) + (setq notmuch-search-result-format + '((\"authors\" . \"%-40s\") + (\"subject\" . \"%s\"))) + Line breaks are permitted in format strings (though this is currently experimental). Note that a line break at the end of an \"authors\" field will get elided if the authors list is long; place it instead at the beginning of the following field. To enter a line break when setting this variable with setq, use \\n. To enter a line break in customize, press \\[quoted-insert] C-j." - :type '(alist :key-type (string) :value-type (string)) + :type '(alist :key-type string :value-type string) :group 'notmuch-search) ;; The name of this variable `notmuch-init-file' is consistent with the @@ -830,26 +833,28 @@ non-authors is found, assume that all of the authors match." (insert padding)))) (defun notmuch-search-insert-field (field format-string result) - (cond - ((string-equal field "date") - (insert (propertize (format format-string (plist-get result :date_relative)) - 'face 'notmuch-search-date))) - ((string-equal field "count") - (insert (propertize (format format-string - (format "[%s/%s]" (plist-get result :matched) - (plist-get result :total))) - 'face 'notmuch-search-count))) - ((string-equal field "subject") - (insert (propertize (format format-string - (notmuch-sanitize (plist-get result :subject))) - 'face 'notmuch-search-subject))) - ((string-equal field "authors") - (notmuch-search-insert-authors - format-string (notmuch-sanitize (plist-get result :authors)))) - ((string-equal field "tags") - (let ((tags (plist-get result :tags)) - (orig-tags (plist-get result :orig-tags))) - (insert (format format-string (notmuch-tag-format-tags tags orig-tags))))))) + (pcase field + ((pred functionp) + (insert (funcall field format-string result))) + ("date" + (insert (propertize (format format-string (plist-get result :date_relative)) + 'face 'notmuch-search-date))) + ("count" + (insert (propertize (format format-string + (format "[%s/%s]" (plist-get result :matched) + (plist-get result :total))) + 'face 'notmuch-search-count))) + ("subject" + (insert (propertize (format format-string + (notmuch-sanitize (plist-get result :subject))) + 'face 'notmuch-search-subject))) + ("authors" + (notmuch-search-insert-authors format-string + (notmuch-sanitize (plist-get result :authors)))) + ("tags" + (let ((tags (plist-get result :tags)) + (orig-tags (plist-get result :orig-tags))) + (insert (format format-string (notmuch-tag-format-tags tags orig-tags))))))) (defun notmuch-search-show-result (result pos) "Insert RESULT at POS." diff --git a/lib/Makefile.local b/lib/Makefile.local index e2d4b91d..1378a74b 100644 --- a/lib/Makefile.local +++ b/lib/Makefile.local @@ -63,7 +63,8 @@ libnotmuch_cxx_srcs = \ $(dir)/features.cc \ $(dir)/prefix.cc \ $(dir)/open.cc \ - $(dir)/init.cc + $(dir)/init.cc \ + $(dir)/parse-sexp.cc libnotmuch_modules := $(libnotmuch_c_srcs:.c=.o) $(libnotmuch_cxx_srcs:.cc=.o) diff --git a/lib/built-with.c b/lib/built-with.c index 0c70010b..89958e12 100644 --- a/lib/built-with.c +++ b/lib/built-with.c @@ -32,6 +32,8 @@ notmuch_built_with (const char *name) return HAVE_XAPIAN_DB_RETRY_LOCK; } else if (STRNCMP_LITERAL (name, "session_key") == 0) { return true; + } else if (STRNCMP_LITERAL (name, "sexpr_query") == 0) { + return HAVE_SFSEXP; } else { return false; } diff --git a/lib/database-private.h b/lib/database-private.h index 9706c17e..8b9d67fe 100644 --- a/lib/database-private.h +++ b/lib/database-private.h @@ -40,6 +40,10 @@ #include +#if HAVE_SFSEXP +#include +#endif + /* Bit masks for _notmuch_database::features. Features are named, * independent aspects of the database schema. * @@ -232,6 +236,7 @@ struct _notmuch_database { */ unsigned long view; Xapian::QueryParser *query_parser; + Xapian::Stem *stemmer; Xapian::TermGenerator *term_gen; Xapian::RangeProcessor *value_range_processor; Xapian::RangeProcessor *date_range_processor; @@ -300,4 +305,38 @@ _notmuch_database_setup_standard_query_fields (notmuch_database_t *notmuch); notmuch_status_t _notmuch_database_setup_user_query_fields (notmuch_database_t *notmuch); +#if __cplusplus +/* query.cc */ +notmuch_status_t +_notmuch_query_string_to_xapian_query (notmuch_database_t *notmuch, + std::string query_string, + Xapian::Query &output, + std::string &msg); +/* parse-sexp.cc */ +notmuch_status_t +_notmuch_sexp_string_to_xapian_query (notmuch_database_t *notmuch, const char *querystr, + Xapian::Query &output); + +notmuch_status_t +_notmuch_query_expand (notmuch_database_t *notmuch, const char *field, Xapian::Query subquery, + Xapian::Query &output, std::string &msg); + +/* regexp-fields.cc */ +notmuch_status_t +_notmuch_regexp_to_query (notmuch_database_t *notmuch, Xapian::valueno slot, std::string field, + std::string regexp_str, + Xapian::Query &output, std::string &msg); + +/* thread-fp.cc */ +notmuch_status_t +_notmuch_query_name_to_query (notmuch_database_t *notmuch, const std::string name, + Xapian::Query &output); + +#if HAVE_SFSEXP +/* parse-sexp.cc */ +notmuch_status_t +_notmuch_sexp_string_to_xapian_query (notmuch_database_t *notmuch, const char *querystr, + Xapian::Query &output); +#endif +#endif #endif diff --git a/lib/database.cc b/lib/database.cc index 31794900..7eb0de79 100644 --- a/lib/database.cc +++ b/lib/database.cc @@ -309,6 +309,8 @@ notmuch_status_to_string (notmuch_status_t status) return "No database found"; case NOTMUCH_STATUS_DATABASE_EXISTS: return "Database exists, not recreated"; + case NOTMUCH_STATUS_BAD_QUERY_SYNTAX: + return "Syntax error in query"; default: case NOTMUCH_STATUS_LAST_STATUS: return "Unknown error status value"; diff --git a/lib/message-file.c b/lib/message-file.c index 647ccf3a..68f646a4 100644 --- a/lib/message-file.c +++ b/lib/message-file.c @@ -291,11 +291,16 @@ _notmuch_message_file_get_header (notmuch_message_file_t *message, if (value) return value; - if (strcasecmp (header, "received") == 0) { + if (strcasecmp (header, "received") == 0 || + strcasecmp (header, "delivered-to") == 0) { /* - * The Received: header is special. We concatenate all - * instances of the header as we use this when analyzing the - * path the mail has taken from sender to recipient. + * The Received: header is special. We concatenate all instances of the + * header as we use this when analyzing the path the mail has taken + * from sender to recipient. + * + * Similarly, multiple instances of Delivered-To may be present. We + * concatenate them so the one with highest priority may be picked (eg. + * primary_email before other_email). */ decoded = _notmuch_message_file_get_combined_header (message, header); } else { diff --git a/lib/notmuch.h b/lib/notmuch.h index ef11ed1b..546643e8 100644 --- a/lib/notmuch.h +++ b/lib/notmuch.h @@ -220,6 +220,10 @@ typedef enum _notmuch_status { * Database exists, so not (re)-created */ NOTMUCH_STATUS_DATABASE_EXISTS, + /** + * Syntax error in query + */ + NOTMUCH_STATUS_BAD_QUERY_SYNTAX, /** * Not an actual status value. Just a way to find out how many * valid status values there are. @@ -961,6 +965,16 @@ notmuch_query_t * notmuch_query_create (notmuch_database_t *database, const char *query_string); +typedef enum { + NOTMUCH_QUERY_SYNTAX_XAPIAN, + NOTMUCH_QUERY_SYNTAX_SEXP +} notmuch_query_syntax_t; + +notmuch_status_t +notmuch_query_create_with_syntax (notmuch_database_t *database, + const char *query_string, + notmuch_query_syntax_t syntax, + notmuch_query_t **output); /** * Sort values for notmuch_query_set_sort. */ diff --git a/lib/open.cc b/lib/open.cc index 280ffee3..8a835e98 100644 --- a/lib/open.cc +++ b/lib/open.cc @@ -432,7 +432,8 @@ _finish_open (notmuch_database_t *notmuch, "lastmod:"); notmuch->query_parser->set_default_op (Xapian::Query::OP_AND); notmuch->query_parser->set_database (*notmuch->xapian_db); - notmuch->query_parser->set_stemmer (Xapian::Stem ("english")); + notmuch->stemmer = new Xapian::Stem ("english"); + notmuch->query_parser->set_stemmer (*notmuch->stemmer); notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME); notmuch->query_parser->add_rangeprocessor (notmuch->value_range_processor); notmuch->query_parser->add_rangeprocessor (notmuch->date_range_processor); diff --git a/lib/parse-sexp.cc b/lib/parse-sexp.cc new file mode 100644 index 00000000..356c32ea --- /dev/null +++ b/lib/parse-sexp.cc @@ -0,0 +1,587 @@ +#include "database-private.h" + +#if HAVE_SFSEXP +#include "sexp.h" +#include "unicode-util.h" + +/* _sexp is used for file scope symbols to avoid clashing with + * definitions from sexp.h */ + +/* sexp_binding structs attach name to a sexp and a defining + * context. The latter allows lazy evaluation of parameters whose + * definition contains other parameters. Lazy evaluation is needed + * because a primary goal of macros is to change the parent field for + * a sexp. + */ + +typedef struct sexp_binding { + const char *name; + const sexp_t *sx; + const struct sexp_binding *context; + const struct sexp_binding *next; +} _sexp_binding_t; + +typedef enum { + SEXP_FLAG_NONE = 0, + SEXP_FLAG_FIELD = 1 << 0, + SEXP_FLAG_BOOLEAN = 1 << 1, + SEXP_FLAG_SINGLE = 1 << 2, + SEXP_FLAG_WILDCARD = 1 << 3, + SEXP_FLAG_REGEX = 1 << 4, + SEXP_FLAG_DO_REGEX = 1 << 5, + SEXP_FLAG_EXPAND = 1 << 6, + SEXP_FLAG_DO_EXPAND = 1 << 7, + SEXP_FLAG_ORPHAN = 1 << 8, +} _sexp_flag_t; + +/* + * define bitwise operators to hide casts */ + +inline _sexp_flag_t +operator| (_sexp_flag_t a, _sexp_flag_t b) +{ + return static_cast<_sexp_flag_t>( + static_cast(a) | static_cast(b)); +} + +inline _sexp_flag_t +operator& (_sexp_flag_t a, _sexp_flag_t b) +{ + return static_cast<_sexp_flag_t>( + static_cast(a) & static_cast(b)); +} + +typedef struct { + const char *name; + Xapian::Query::op xapian_op; + Xapian::Query initial; + _sexp_flag_t flags; +} _sexp_prefix_t; + +static _sexp_prefix_t prefixes[] = +{ + { "and", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_NONE }, + { "attachment", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND }, + { "body", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD }, + { "from", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, + { "folder", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, + { "id", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX }, + { "infix", Xapian::Query::OP_INVALID, Xapian::Query::MatchAll, + SEXP_FLAG_SINGLE | SEXP_FLAG_ORPHAN }, + { "is", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, + { "matching", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_DO_EXPAND }, + { "mid", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX }, + { "mimetype", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND }, + { "not", Xapian::Query::OP_AND_NOT, Xapian::Query::MatchAll, + SEXP_FLAG_NONE }, + { "of", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_DO_EXPAND }, + { "or", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + SEXP_FLAG_NONE }, + { "path", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX }, + { "property", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, + { "query", Xapian::Query::OP_INVALID, Xapian::Query::MatchNothing, + SEXP_FLAG_SINGLE | SEXP_FLAG_ORPHAN }, + { "regex", Xapian::Query::OP_INVALID, Xapian::Query::MatchAll, + SEXP_FLAG_SINGLE | SEXP_FLAG_DO_REGEX }, + { "rx", Xapian::Query::OP_INVALID, Xapian::Query::MatchAll, + SEXP_FLAG_SINGLE | SEXP_FLAG_DO_REGEX }, + { "starts-with", Xapian::Query::OP_WILDCARD, Xapian::Query::MatchAll, + SEXP_FLAG_SINGLE }, + { "subject", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, + { "tag", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, + { "thread", Xapian::Query::OP_OR, Xapian::Query::MatchNothing, + SEXP_FLAG_FIELD | SEXP_FLAG_BOOLEAN | SEXP_FLAG_WILDCARD | SEXP_FLAG_REGEX | SEXP_FLAG_EXPAND }, + { "to", Xapian::Query::OP_AND, Xapian::Query::MatchAll, + SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD | SEXP_FLAG_EXPAND }, + { } +}; + +static notmuch_status_t _sexp_to_xapian_query (notmuch_database_t *notmuch, + const _sexp_prefix_t *parent, + const _sexp_binding_t *env, + const sexp_t *sx, + Xapian::Query &output); + +static notmuch_status_t +_sexp_combine_query (notmuch_database_t *notmuch, + const _sexp_prefix_t *parent, + const _sexp_binding_t *env, + Xapian::Query::op operation, + Xapian::Query left, + const sexp_t *sx, + Xapian::Query &output) +{ + Xapian::Query subquery; + + notmuch_status_t status; + + /* if we run out elements, return accumulator */ + + if (! sx) { + output = left; + return NOTMUCH_STATUS_SUCCESS; + } + + status = _sexp_to_xapian_query (notmuch, parent, env, sx, subquery); + if (status) + return status; + + return _sexp_combine_query (notmuch, + parent, + env, + operation, + Xapian::Query (operation, left, subquery), + sx->next, output); +} + +static notmuch_status_t +_sexp_parse_phrase (std::string term_prefix, const char *phrase, Xapian::Query &output) +{ + Xapian::Utf8Iterator p (phrase); + Xapian::Utf8Iterator end; + std::vector terms; + + while (p != end) { + Xapian::Utf8Iterator start; + while (p != end && ! Xapian::Unicode::is_wordchar (*p)) + p++; + + if (p == end) + break; + + start = p; + + while (p != end && Xapian::Unicode::is_wordchar (*p)) + p++; + + if (p != start) { + std::string word (start, p); + word = Xapian::Unicode::tolower (word); + terms.push_back (term_prefix + word); + } + } + output = Xapian::Query (Xapian::Query::OP_PHRASE, terms.begin (), terms.end ()); + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_sexp_parse_wildcard (notmuch_database_t *notmuch, + const _sexp_prefix_t *parent, + unused(const _sexp_binding_t *env), + std::string match, + Xapian::Query &output) +{ + + std::string term_prefix = parent ? _notmuch_database_prefix (notmuch, parent->name) : ""; + + if (parent && ! (parent->flags & SEXP_FLAG_WILDCARD)) { + _notmuch_database_log (notmuch, "'%s' does not support wildcard queries\n", parent->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + output = Xapian::Query (Xapian::Query::OP_WILDCARD, + term_prefix + Xapian::Unicode::tolower (match)); + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_sexp_parse_one_term (notmuch_database_t *notmuch, std::string term_prefix, const sexp_t *sx, + Xapian::Query &output) +{ + Xapian::Stem stem = *(notmuch->stemmer); + + if (sx->aty == SEXP_BASIC && unicode_word_utf8 (sx->val)) { + std::string term = Xapian::Unicode::tolower (sx->val); + + output = Xapian::Query ("Z" + term_prefix + stem (term)); + return NOTMUCH_STATUS_SUCCESS; + } else { + return _sexp_parse_phrase (term_prefix, sx->val, output); + } + +} + +notmuch_status_t +_sexp_parse_regex (notmuch_database_t *notmuch, + const _sexp_prefix_t *prefix, const _sexp_prefix_t *parent, + unused(const _sexp_binding_t *env), + std::string val, Xapian::Query &output) +{ + if (! parent) { + _notmuch_database_log (notmuch, "illegal '%s' outside field\n", + prefix->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + if (! (parent->flags & SEXP_FLAG_REGEX)) { + _notmuch_database_log (notmuch, "'%s' not supported in field '%s'\n", + prefix->name, parent->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + std::string msg; /* ignored */ + + return _notmuch_regexp_to_query (notmuch, Xapian::BAD_VALUENO, parent->name, + val, output, msg); +} + + +static notmuch_status_t +_sexp_expand_query (notmuch_database_t *notmuch, + const _sexp_prefix_t *prefix, const _sexp_prefix_t *parent, + unused(const _sexp_binding_t *env), const sexp_t *sx, Xapian::Query &output) +{ + Xapian::Query subquery; + notmuch_status_t status; + std::string msg; + + if (! (parent->flags & SEXP_FLAG_EXPAND)) { + _notmuch_database_log (notmuch, "'%s' unsupported inside '%s'\n", prefix->name, parent->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + status = _sexp_combine_query (notmuch, NULL, NULL, prefix->xapian_op, prefix->initial, sx, + subquery); + if (status) + return status; + + status = _notmuch_query_expand (notmuch, parent->name, subquery, output, msg); + if (status) { + _notmuch_database_log (notmuch, "error expanding query %s\n", msg.c_str ()); + } + return status; +} + +static notmuch_status_t +_sexp_parse_infix (notmuch_database_t *notmuch, const sexp_t *sx, Xapian::Query &output) +{ + try { + output = notmuch->query_parser->parse_query (sx->val, NOTMUCH_QUERY_PARSER_FLAGS); + } catch (const Xapian::QueryParserError &error) { + _notmuch_database_log (notmuch, "Syntax error in infix query: %s\n", sx->val); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } catch (const Xapian::Error &error) { + if (! notmuch->exception_reported) { + _notmuch_database_log (notmuch, + "A Xapian exception occurred parsing query: %s\n", + error.get_msg ().c_str ()); + _notmuch_database_log_append (notmuch, + "Query string was: %s\n", + sx->val); + notmuch->exception_reported = true; + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + } + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_sexp_parse_header (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, + const _sexp_binding_t *env, const sexp_t *sx, Xapian::Query &output) +{ + _sexp_prefix_t user_prefix; + + user_prefix.name = sx->list->val; + user_prefix.flags = SEXP_FLAG_FIELD | SEXP_FLAG_WILDCARD; + + if (parent) { + _notmuch_database_log (notmuch, "nested field: '%s' inside '%s'\n", + sx->list->val, parent->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + parent = &user_prefix; + + return _sexp_combine_query (notmuch, parent, env, Xapian::Query::OP_AND, Xapian::Query::MatchAll, + sx->list->next, output); +} + +static _sexp_binding_t * +_sexp_bind (void *ctx, const _sexp_binding_t *env, const char *name, const sexp_t *sx, const + _sexp_binding_t *context) +{ + _sexp_binding_t *binding = talloc (ctx, _sexp_binding_t); + + binding->name = talloc_strdup (ctx, name); + binding->sx = sx; + binding->context = context; + binding->next = env; + return binding; +} + +static notmuch_status_t +maybe_apply_macro (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, + const _sexp_binding_t *env, const sexp_t *sx, const sexp_t *args, + Xapian::Query &output) +{ + const sexp_t *params, *param, *arg, *body; + void *local = talloc_new (notmuch); + _sexp_binding_t *new_env = NULL; + notmuch_status_t status = NOTMUCH_STATUS_SUCCESS; + + if (sx->list->ty != SEXP_VALUE || strcmp (sx->list->val, "macro") != 0) { + status = NOTMUCH_STATUS_IGNORED; + goto DONE; + } + + params = sx->list->next; + + if (! params || (params->ty != SEXP_LIST)) { + _notmuch_database_log (notmuch, "missing (possibly empty) list of arguments to macro\n"); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + body = params->next; + + if (! body) { + _notmuch_database_log (notmuch, "missing body of macro\n"); + status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + goto DONE; + } + + for (param = params->list, arg = args; + param && arg; + param = param->next, arg = arg->next) { + if (param->ty != SEXP_VALUE || param->aty != SEXP_BASIC) { + _notmuch_database_log (notmuch, "macro parameters must be unquoted atoms\n"); + status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + goto DONE; + } + new_env = _sexp_bind (local, new_env, param->val, arg, env); + } + + if (param && ! arg) { + _notmuch_database_log (notmuch, "too few arguments to macro\n"); + status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + goto DONE; + } + + if (! param && arg) { + _notmuch_database_log (notmuch, "too many arguments to macro\n"); + status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + goto DONE; + } + + status = _sexp_to_xapian_query (notmuch, parent, new_env, body, output); + + DONE: + if (local) + talloc_free (local); + + return status; +} + +static notmuch_status_t +maybe_saved_squery (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, + const _sexp_binding_t *env, const sexp_t *sx, Xapian::Query &output) +{ + char *key; + char *expansion = NULL; + notmuch_status_t status; + sexp_t *saved_sexp; + void *local = talloc_new (notmuch); + char *buf; + + key = talloc_asprintf (local, "squery.%s", sx->list->val); + if (! key) { + status = NOTMUCH_STATUS_OUT_OF_MEMORY; + goto DONE; + } + + status = notmuch_database_get_config (notmuch, key, &expansion); + if (status) + goto DONE; + if (EMPTY_STRING (expansion)) { + status = NOTMUCH_STATUS_IGNORED; + goto DONE; + } + + buf = talloc_strdup (local, expansion); + /* XXX TODO: free this memory */ + saved_sexp = parse_sexp (buf, strlen (expansion)); + if (! saved_sexp) { + _notmuch_database_log (notmuch, "invalid saved s-expression query: '%s'\n", expansion); + status = NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + goto DONE; + } + + status = maybe_apply_macro (notmuch, parent, env, saved_sexp, sx->list->next, output); + if (status == NOTMUCH_STATUS_IGNORED) + status = _sexp_to_xapian_query (notmuch, parent, env, saved_sexp, output); + + DONE: + if (local) + talloc_free (local); + + return status; +} + +static notmuch_status_t +_sexp_expand_param (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, + const _sexp_binding_t *env, const char *name, + Xapian::Query &output) +{ + for (; env; env = env->next) { + if (strcmp (name, env->name) == 0) { + return _sexp_to_xapian_query (notmuch, parent, env->context, env->sx, + output); + } + } + _notmuch_database_log (notmuch, "undefined parameter %s\n", name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; +} + +/* Here we expect the s-expression to be a proper list, with first + * element defining and operation, or as a special case the empty + * list */ + +static notmuch_status_t +_sexp_to_xapian_query (notmuch_database_t *notmuch, const _sexp_prefix_t *parent, + const _sexp_binding_t *env, const sexp_t *sx, Xapian::Query &output) +{ + notmuch_status_t status; + + if (sx->ty == SEXP_VALUE && sx->aty == SEXP_BASIC && sx->val[0] == ',') { + return _sexp_expand_param (notmuch, parent, env, sx->val + 1, output); + } + + if (sx->ty == SEXP_VALUE) { + std::string term_prefix = parent ? _notmuch_database_prefix (notmuch, parent->name) : ""; + + if (sx->aty == SEXP_BASIC && strcmp (sx->val, "*") == 0) { + return _sexp_parse_wildcard (notmuch, parent, env, "", output); + } + + if (parent && (parent->flags & SEXP_FLAG_BOOLEAN)) { + output = Xapian::Query (term_prefix + sx->val); + return NOTMUCH_STATUS_SUCCESS; + } + + if (parent) { + return _sexp_parse_one_term (notmuch, term_prefix, sx, output); + } else { + Xapian::Query accumulator; + for (_sexp_prefix_t *prefix = prefixes; prefix->name; prefix++) { + if (prefix->flags & SEXP_FLAG_FIELD) { + Xapian::Query subquery; + term_prefix = _notmuch_database_prefix (notmuch, prefix->name); + status = _sexp_parse_one_term (notmuch, term_prefix, sx, subquery); + if (status) + return status; + accumulator = Xapian::Query (Xapian::Query::OP_OR, accumulator, subquery); + } + } + output = accumulator; + return NOTMUCH_STATUS_SUCCESS; + } + } + + /* Empty list */ + if (! sx->list) { + output = Xapian::Query::MatchAll; + return NOTMUCH_STATUS_SUCCESS; + } + + if (sx->list->ty == SEXP_LIST) { + _notmuch_database_log (notmuch, "unexpected list in field/operation position\n", + sx->list->val); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + status = maybe_saved_squery (notmuch, parent, env, sx, output); + if (status != NOTMUCH_STATUS_IGNORED) + return status; + + /* Check for user defined field */ + if (_notmuch_string_map_get (notmuch->user_prefix, sx->list->val)) { + return _sexp_parse_header (notmuch, parent, env, sx, output); + } + + if (strcmp (sx->list->val, "macro") == 0) { + _notmuch_database_log (notmuch, "macro definition not permitted here\n"); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + for (_sexp_prefix_t *prefix = prefixes; prefix && prefix->name; prefix++) { + if (strcmp (prefix->name, sx->list->val) == 0) { + if (prefix->flags & SEXP_FLAG_FIELD) { + if (parent) { + _notmuch_database_log (notmuch, "nested field: '%s' inside '%s'\n", + prefix->name, parent->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + parent = prefix; + } + + if (parent && (prefix->flags & SEXP_FLAG_ORPHAN)) { + _notmuch_database_log (notmuch, "'%s' not supported inside '%s'\n", + prefix->name, parent->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + if ((prefix->flags & SEXP_FLAG_SINGLE) && + (! sx->list->next || sx->list->next->next || sx->list->next->ty != SEXP_VALUE)) { + _notmuch_database_log (notmuch, "'%s' expects single atom as argument\n", + prefix->name); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + if (strcmp (prefix->name, "infix") == 0) { + return _sexp_parse_infix (notmuch, sx->list->next, output); + } + + if (strcmp (prefix->name, "query") == 0) { + return _notmuch_query_name_to_query (notmuch, sx->list->next->val, output); + } + + if (prefix->xapian_op == Xapian::Query::OP_WILDCARD) + return _sexp_parse_wildcard (notmuch, parent, env, sx->list->next->val, output); + + if (prefix->flags & SEXP_FLAG_DO_REGEX) { + return _sexp_parse_regex (notmuch, prefix, parent, env, sx->list->next->val, output); + } + + if (prefix->flags & SEXP_FLAG_DO_EXPAND) { + return _sexp_expand_query (notmuch, prefix, parent, env, sx->list->next, output); + } + + return _sexp_combine_query (notmuch, parent, env, prefix->xapian_op, prefix->initial, + sx->list->next, output); + } + } + + _notmuch_database_log (notmuch, "unknown prefix '%s'\n", sx->list->val); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; +} + +notmuch_status_t +_notmuch_sexp_string_to_xapian_query (notmuch_database_t *notmuch, const char *querystr, + Xapian::Query &output) +{ + const sexp_t *sx = NULL; + char *buf = talloc_strdup (notmuch, querystr); + + sx = parse_sexp (buf, strlen (querystr)); + if (! sx) { + _notmuch_database_log (notmuch, "invalid s-expression: '%s'\n", querystr); + return NOTMUCH_STATUS_BAD_QUERY_SYNTAX; + } + + return _sexp_to_xapian_query (notmuch, NULL, NULL, sx, output); +} +#endif diff --git a/lib/query-fp.cc b/lib/query-fp.cc index b980b7f0..75b1d875 100644 --- a/lib/query-fp.cc +++ b/lib/query-fp.cc @@ -24,17 +24,33 @@ #include "query-fp.h" #include -Xapian::Query -QueryFieldProcessor::operator() (const std::string & name) +notmuch_status_t +_notmuch_query_name_to_query (notmuch_database_t *notmuch, const std::string name, + Xapian::Query &output) { std::string key = "query." + name; char *expansion; notmuch_status_t status; status = notmuch_database_get_config (notmuch, key.c_str (), &expansion); + if (status) + return status; + + output = notmuch->query_parser->parse_query (expansion, NOTMUCH_QUERY_PARSER_FLAGS); + return NOTMUCH_STATUS_SUCCESS; +} + +Xapian::Query +QueryFieldProcessor::operator() (const std::string & name) +{ + notmuch_status_t status; + Xapian::Query output; + + status = _notmuch_query_name_to_query (notmuch, name, output); if (status) { throw Xapian::QueryParserError ("error looking up key" + name); } - return parser.parse_query (expansion, NOTMUCH_QUERY_PARSER_FLAGS); + return output; + } diff --git a/lib/query.cc b/lib/query.cc index 792aba21..b0937fcc 100644 --- a/lib/query.cc +++ b/lib/query.cc @@ -30,6 +30,7 @@ struct _notmuch_query { notmuch_string_list_t *exclude_terms; notmuch_exclude_t omit_excluded; bool parsed; + notmuch_query_syntax_t syntax; Xapian::Query xapian_query; std::set terms; }; @@ -84,9 +85,9 @@ _notmuch_query_destructor (notmuch_query_t *query) return 0; } -notmuch_query_t * -notmuch_query_create (notmuch_database_t *notmuch, - const char *query_string) +static notmuch_query_t * +_notmuch_query_constructor (notmuch_database_t *notmuch, + const char *query_string) { notmuch_query_t *query; @@ -105,7 +106,10 @@ notmuch_query_create (notmuch_database_t *notmuch, query->notmuch = notmuch; - query->query_string = talloc_strdup (query, query_string); + if (query_string) + query->query_string = talloc_strdup (query, query_string); + else + query->query_string = NULL; query->sort = NOTMUCH_SORT_NEWEST_FIRST; @@ -116,44 +120,144 @@ notmuch_query_create (notmuch_database_t *notmuch, return query; } -static notmuch_status_t -_notmuch_query_ensure_parsed (notmuch_query_t *query) +notmuch_query_t * +notmuch_query_create (notmuch_database_t *notmuch, + const char *query_string) { - if (query->parsed) - return NOTMUCH_STATUS_SUCCESS; - try { - query->xapian_query = - query->notmuch->query_parser-> - parse_query (query->query_string, NOTMUCH_QUERY_PARSER_FLAGS); + notmuch_query_t *query; + notmuch_status_t status; - /* Xapian doesn't support skip_to on terms from a query since - * they are unordered, so cache a copy of all terms in - * something searchable. - */ + status = notmuch_query_create_with_syntax (notmuch, query_string, + NOTMUCH_QUERY_SYNTAX_XAPIAN, + &query); + if (status) + return NULL; + + return query; +} + +notmuch_status_t +notmuch_query_create_with_syntax (notmuch_database_t *notmuch, + const char *query_string, + notmuch_query_syntax_t syntax, + notmuch_query_t **output) +{ + + notmuch_query_t *query; + + if (! output) + return NOTMUCH_STATUS_NULL_POINTER; - for (Xapian::TermIterator t = query->xapian_query.get_terms_begin (); - t != query->xapian_query.get_terms_end (); ++t) - query->terms.insert (*t); + query = _notmuch_query_constructor (notmuch, query_string); + if (! query) + return NOTMUCH_STATUS_OUT_OF_MEMORY; + + if (syntax == NOTMUCH_QUERY_SYNTAX_SEXP && ! HAVE_SFSEXP) { + _notmuch_database_log (notmuch, "sexp query parser not available"); + return NOTMUCH_STATUS_ILLEGAL_ARGUMENT; + } - query->parsed = true; + query->syntax = syntax; + *output = query; + + return NOTMUCH_STATUS_SUCCESS; +} + +static void +_notmuch_query_cache_terms (notmuch_query_t *query) +{ + /* Xapian doesn't support skip_to on terms from a query since + * they are unordered, so cache a copy of all terms in + * something searchable. + */ + + for (Xapian::TermIterator t = query->xapian_query.get_terms_begin (); + t != query->xapian_query.get_terms_end (); ++t) + query->terms.insert (*t); +} + +notmuch_status_t +_notmuch_query_string_to_xapian_query (notmuch_database_t *notmuch, + std::string query_string, + Xapian::Query &output, + std::string &msg) +{ + try { + if (query_string == "" || query_string == "*") { + output = Xapian::Query::MatchAll; + } else { + output = + notmuch->query_parser-> + parse_query (query_string, NOTMUCH_QUERY_PARSER_FLAGS); + } } catch (const Xapian::Error &error) { - if (! query->notmuch->exception_reported) { - _notmuch_database_log (query->notmuch, + if (! notmuch->exception_reported) { + _notmuch_database_log (notmuch, "A Xapian exception occurred parsing query: %s\n", error.get_msg ().c_str ()); - _notmuch_database_log_append (query->notmuch, + _notmuch_database_log_append (notmuch, "Query string was: %s\n", - query->query_string); - query->notmuch->exception_reported = true; + query_string.c_str ()); + notmuch->exception_reported = true; } + msg = error.get_msg (); return NOTMUCH_STATUS_XAPIAN_EXCEPTION; } return NOTMUCH_STATUS_SUCCESS; } +static notmuch_status_t +_notmuch_query_ensure_parsed_xapian (notmuch_query_t *query) +{ + notmuch_status_t status; + std::string msg; /* ignored */ + + status = _notmuch_query_string_to_xapian_query (query->notmuch, query->query_string, + query->xapian_query, msg); + if (status) + return status; + + query->parsed = true; + + _notmuch_query_cache_terms (query); + + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_notmuch_query_ensure_parsed_sexpr (notmuch_query_t *query) +{ + notmuch_status_t status; + + if (query->parsed) + return NOTMUCH_STATUS_SUCCESS; + + status = _notmuch_sexp_string_to_xapian_query (query->notmuch, query->query_string, + query->xapian_query); + if (status) + return status; + + _notmuch_query_cache_terms (query); + return NOTMUCH_STATUS_SUCCESS; +} + +static notmuch_status_t +_notmuch_query_ensure_parsed (notmuch_query_t *query) +{ + if (query->parsed) + return NOTMUCH_STATUS_SUCCESS; + +#if HAVE_SFSEXP + if (query->syntax == NOTMUCH_QUERY_SYNTAX_SEXP) + return _notmuch_query_ensure_parsed_sexpr (query); +#endif + + return _notmuch_query_ensure_parsed_xapian (query); +} + const char * notmuch_query_get_query_string (const notmuch_query_t *query) { @@ -249,7 +353,6 @@ _notmuch_query_search_documents (notmuch_query_t *query, notmuch_messages_t **out) { notmuch_database_t *notmuch = query->notmuch; - const char *query_string = query->query_string; notmuch_mset_messages_t *messages; notmuch_status_t status; @@ -279,13 +382,9 @@ _notmuch_query_search_documents (notmuch_query_t *query, Xapian::MSet mset; Xapian::MSetIterator iterator; - if (strcmp (query_string, "") == 0 || - strcmp (query_string, "*") == 0) { - final_query = mail_query; - } else { - final_query = Xapian::Query (Xapian::Query::OP_AND, - mail_query, query->xapian_query); - } + final_query = Xapian::Query (Xapian::Query::OP_AND, + mail_query, query->xapian_query); + messages->base.excluded_doc_ids = NULL; if ((query->omit_excluded != NOTMUCH_EXCLUDE_FALSE) && (query->exclude_terms)) { @@ -606,7 +705,6 @@ notmuch_status_t _notmuch_query_count_documents (notmuch_query_t *query, const char *type, unsigned *count_out) { notmuch_database_t *notmuch = query->notmuch; - const char *query_string = query->query_string; Xapian::doccount count = 0; notmuch_status_t status; @@ -622,13 +720,8 @@ _notmuch_query_count_documents (notmuch_query_t *query, const char *type, unsign Xapian::Query final_query, exclude_query; Xapian::MSet mset; - if (strcmp (query_string, "") == 0 || - strcmp (query_string, "*") == 0) { - final_query = mail_query; - } else { - final_query = Xapian::Query (Xapian::Query::OP_AND, - mail_query, query->xapian_query); - } + final_query = Xapian::Query (Xapian::Query::OP_AND, + mail_query, query->xapian_query); exclude_query = _notmuch_exclude_tags (query); @@ -728,3 +821,51 @@ notmuch_query_get_database (const notmuch_query_t *query) { return query->notmuch; } + +notmuch_status_t +_notmuch_query_expand (notmuch_database_t *notmuch, const char *field, Xapian::Query subquery, + Xapian::Query &output, std::string &msg) +{ + std::set terms; + const std::string term_prefix = _find_prefix (field); + + if (_debug_query ()) { + fprintf (stderr, "Expanding subquery:\n%s\n", + subquery.get_description ().c_str ()); + } + + try { + Xapian::Enquire enquire (*notmuch->xapian_db); + Xapian::MSet mset; + + enquire.set_weighting_scheme (Xapian::BoolWeight ()); + enquire.set_query (subquery); + + mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ()); + + for (Xapian::MSetIterator iterator = mset.begin (); iterator != mset.end (); iterator++) { + Xapian::docid doc_id = *iterator; + Xapian::Document doc = notmuch->xapian_db->get_document (doc_id); + Xapian::TermIterator i = doc.termlist_begin (); + + for (i.skip_to (term_prefix); + i != doc.termlist_end () && ((*i).rfind (term_prefix, 0) == 0); i++) { + terms.insert (*i); + } + } + output = Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ()); + if (_debug_query ()) { + fprintf (stderr, "Expanded query:\n%s\n", + subquery.get_description ().c_str ()); + } + + } catch (const Xapian::Error &error) { + _notmuch_database_log (notmuch, + "A Xapian exception occurred expanding query: %s\n", + error.get_msg ().c_str ()); + msg = error.get_msg (); + return NOTMUCH_STATUS_XAPIAN_EXCEPTION; + } + + return NOTMUCH_STATUS_SUCCESS; +} diff --git a/lib/regexp-fields.cc b/lib/regexp-fields.cc index 0feb50e5..c6d9d94f 100644 --- a/lib/regexp-fields.cc +++ b/lib/regexp-fields.cc @@ -26,27 +26,32 @@ #include "notmuch-private.h" #include "database-private.h" -static void -compile_regex (regex_t ®exp, const char *str) +notmuch_status_t +compile_regex (regex_t ®exp, const char *str, std::string &msg) { int err = regcomp (®exp, str, REG_EXTENDED | REG_NOSUB); if (err != 0) { size_t len = regerror (err, ®exp, NULL, 0); char *buffer = new char[len]; - std::string msg = "Regexp error: "; + msg = "Regexp error: "; (void) regerror (err, ®exp, buffer, len); msg.append (buffer, len); delete[] buffer; - throw Xapian::QueryParserError (msg); + return NOTMUCH_STATUS_ILLEGAL_ARGUMENT; } + return NOTMUCH_STATUS_SUCCESS; } RegexpPostingSource::RegexpPostingSource (Xapian::valueno slot, const std::string ®exp) : slot_ (slot) { - compile_regex (regexp_, regexp.c_str ()); + std::string msg; + notmuch_status_t status = compile_regex (regexp_, regexp.c_str (), msg); + + if (status) + throw Xapian::QueryParserError (msg); } RegexpPostingSource::~RegexpPostingSource () @@ -141,18 +146,54 @@ _find_slot (std::string prefix) return Xapian::BAD_VALUENO; } -RegexpFieldProcessor::RegexpFieldProcessor (std::string prefix, +RegexpFieldProcessor::RegexpFieldProcessor (std::string field_, notmuch_field_flag_t options_, Xapian::QueryParser &parser_, notmuch_database_t *notmuch_) - : slot (_find_slot (prefix)), - term_prefix (_find_prefix (prefix.c_str ())), + : slot (_find_slot (field_)), + field (field_), + term_prefix (_find_prefix (field_.c_str ())), options (options_), parser (parser_), notmuch (notmuch_) { }; +notmuch_status_t +_notmuch_regexp_to_query (notmuch_database_t *notmuch, Xapian::valueno slot, std::string field, + std::string regexp_str, + Xapian::Query &output, std::string &msg) +{ + regex_t regexp; + notmuch_status_t status; + + status = compile_regex (regexp, regexp_str.c_str (), msg); + if (status) { + _notmuch_database_log_append (notmuch, "error compiling regex %s", msg.c_str ()); + return status; + } + + if (slot == Xapian::BAD_VALUENO) + slot = _find_slot (field); + + if (slot == Xapian::BAD_VALUENO) { + std::string term_prefix = _find_prefix (field.c_str ()); + std::vector terms; + + for (Xapian::TermIterator it = notmuch->xapian_db->allterms_begin (term_prefix); + it != notmuch->xapian_db->allterms_end (); ++it) { + if (regexec (®exp, (*it).c_str () + term_prefix.size (), + 0, NULL, 0) == 0) + terms.push_back (*it); + } + output = Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ()); + } else { + RegexpPostingSource *postings = new RegexpPostingSource (slot, regexp_str); + output = Xapian::Query (postings->release ()); + } + return NOTMUCH_STATUS_SUCCESS; +} + Xapian::Query RegexpFieldProcessor::operator() (const std::string & str) { @@ -168,23 +209,15 @@ RegexpFieldProcessor::operator() (const std::string & str) if (str.at (0) == '/') { if (str.length () > 1 && str.at (str.size () - 1) == '/') { + Xapian::Query query; std::string regexp_str = str.substr (1, str.size () - 2); - if (slot != Xapian::BAD_VALUENO) { - RegexpPostingSource *postings = new RegexpPostingSource (slot, regexp_str); - return Xapian::Query (postings->release ()); - } else { - std::vector terms; - regex_t regexp; - - compile_regex (regexp, regexp_str.c_str ()); - for (Xapian::TermIterator it = notmuch->xapian_db->allterms_begin (term_prefix); - it != notmuch->xapian_db->allterms_end (); ++it) { - if (regexec (®exp, (*it).c_str () + term_prefix.size (), - 0, NULL, 0) == 0) - terms.push_back (*it); - } - return Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ()); - } + std::string msg; + notmuch_status_t status; + + status = _notmuch_regexp_to_query (notmuch, slot, field, regexp_str, query, msg); + if (status) + throw Xapian::QueryParserError (msg); + return query; } else { throw Xapian::QueryParserError ("unmatched regex delimiter in '" + str + "'"); } diff --git a/lib/regexp-fields.h b/lib/regexp-fields.h index a8cca243..9c871de7 100644 --- a/lib/regexp-fields.h +++ b/lib/regexp-fields.h @@ -30,6 +30,11 @@ #include "database-private.h" #include "notmuch-private.h" +notmuch_status_t +_notmuch_regex_to_query (notmuch_database_t *notmuch, Xapian::valueno slot, std::string field, + std::string regexp_str, + Xapian::Query &output, std::string &msg); + /* A posting source that returns documents where a value matches a * regexp. */ @@ -64,6 +69,7 @@ public: class RegexpFieldProcessor : public Xapian::FieldProcessor { protected: Xapian::valueno slot; + std::string field; std::string term_prefix; notmuch_field_flag_t options; Xapian::QueryParser &parser; diff --git a/lib/thread-fp.cc b/lib/thread-fp.cc index 06708ef2..3aa9c423 100644 --- a/lib/thread-fp.cc +++ b/lib/thread-fp.cc @@ -34,28 +34,20 @@ ThreadFieldProcessor::operator() (const std::string & str) if (str.size () <= 1 || str.at (str.size () - 1) != '}') { throw Xapian::QueryParserError ("missing } in '" + str + "'"); } else { + Xapian::Query subquery; + Xapian::Query query; + std::string msg; std::string subquery_str = str.substr (1, str.size () - 2); - notmuch_query_t *subquery = notmuch_query_create (notmuch, subquery_str.c_str ()); - notmuch_messages_t *messages; - std::set terms; - if (! subquery) - throw Xapian::QueryParserError ("failed to create subquery for '" + subquery_str + - "'"); + status = _notmuch_query_string_to_xapian_query (notmuch, subquery_str, subquery, msg); + if (status) + throw Xapian::QueryParserError (msg); - status = notmuch_query_search_messages (subquery, &messages); + status = _notmuch_query_expand (notmuch, "thread", subquery, query, msg); if (status) - throw Xapian::QueryParserError ("failed to search messages for '" + subquery_str + - "'"); + throw Xapian::QueryParserError (msg); - for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) { - std::string term = thread_prefix; - notmuch_message_t *message; - message = notmuch_messages_get (messages); - term += _notmuch_message_get_thread_id_only (message); - terms.insert (term); - } - return Xapian::Query (Xapian::Query::OP_OR, terms.begin (), terms.end ()); + return query; } } else { /* literal thread id */ diff --git a/notmuch-client.h b/notmuch-client.h index 8643a63f..96d81166 100644 --- a/notmuch-client.h +++ b/notmuch-client.h @@ -485,11 +485,11 @@ print_status_gzbytes (const char *loc, #include "command-line-arguments.h" -extern const char *notmuch_requested_db_uuid; extern const notmuch_opt_desc_t notmuch_shared_options []; -void notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch); -void notmuch_process_shared_options (const char *subcommand_name); +notmuch_query_syntax_t shared_option_query_syntax (); + +void notmuch_process_shared_options (notmuch_database_t *notmuch, const char *subcommand_name); int notmuch_minimal_options (const char *subcommand_name, int argc, char **argv); diff --git a/notmuch-compact.c b/notmuch-compact.c index 2648434e..40ffb428 100644 --- a/notmuch-compact.c +++ b/notmuch-compact.c @@ -45,12 +45,7 @@ notmuch_compact_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - if (notmuch_requested_db_uuid) { - fprintf (stderr, "Error: --uuid not implemented for compact\n"); - return EXIT_FAILURE; - } - - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (NULL, argv[0]); if (! quiet) printf ("Compacting database...\n"); diff --git a/notmuch-config.c b/notmuch-config.c index 4de55e5f..db00a26c 100644 --- a/notmuch-config.c +++ b/notmuch-config.c @@ -517,6 +517,7 @@ static const struct config_key { "index.decrypt", false, NULL }, { "index.header.", true, validate_field_name }, { "query.", true, NULL }, + { "squery.", true, validate_field_name }, }; static const config_key_info_t * @@ -679,6 +680,9 @@ _notmuch_config_list_built_with () printf ("%sretry_lock=%s\n", BUILT_WITH_PREFIX, notmuch_built_with ("retry_lock") ? "true" : "false"); + printf ("%ssexpr_query=%s\n", + BUILT_WITH_PREFIX, + notmuch_built_with ("sexpr_query") ? "true" : "false"); } static int @@ -708,10 +712,6 @@ notmuch_config_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - if (notmuch_requested_db_uuid) - fprintf (stderr, "Warning: ignoring --uuid=%s\n", - notmuch_requested_db_uuid); - /* skip at least subcommand argument */ argc -= opt_index; argv += opt_index; diff --git a/notmuch-count.c b/notmuch-count.c index 5ac4292b..0d9046a8 100644 --- a/notmuch-count.c +++ b/notmuch-count.c @@ -74,10 +74,12 @@ print_count (notmuch_database_t *notmuch, const char *query_str, int ret = 0; notmuch_status_t status; - query = notmuch_query_create (notmuch, query_str); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); - return -1; + status = notmuch_query_create_with_syntax (notmuch, query_str, + shared_option_query_syntax (), + &query); + if (print_status_database ("notmuch count", notmuch, status)) { + ret = -1; + goto DONE; } for (notmuch_config_values_start (exclude_tags); @@ -182,7 +184,7 @@ notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); if (input_file_name) { batch = true; @@ -201,8 +203,6 @@ notmuch_count_command (notmuch_database_t *notmuch, int argc, char *argv[]) return EXIT_FAILURE; } - notmuch_exit_if_unmatched_db_uuid (notmuch); - query_str = query_string_from_args (notmuch, argc - opt_index, argv + opt_index); if (query_str == NULL) { fprintf (stderr, "Out of memory.\n"); diff --git a/notmuch-dump.c b/notmuch-dump.c index ae89e4da..cb82d61f 100644 --- a/notmuch-dump.c +++ b/notmuch-dump.c @@ -232,11 +232,12 @@ database_dump_file (notmuch_database_t *notmuch, gzFile output, if (! query_str) query_str = ""; - query = notmuch_query_create (notmuch, query_str); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); + status = notmuch_query_create_with_syntax (notmuch, query_str, + shared_option_query_syntax (), + &query); + if (print_status_database ("notmuch dump", notmuch, status)) return EXIT_FAILURE; - } + /* Don't ask xapian to sort by Message-ID. Xapian optimizes returning the * first results quickly at the expense of total time. */ @@ -366,8 +367,6 @@ notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[]) const char *query_str = NULL; int ret; - notmuch_exit_if_unmatched_db_uuid (notmuch); - const char *output_file_name = NULL; int opt_index; @@ -394,7 +393,7 @@ notmuch_dump_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); if (include == 0) include = DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_TAGS | DUMP_INCLUDE_PROPERTIES; diff --git a/notmuch-insert.c b/notmuch-insert.c index e3d87e4a..72e2e35f 100644 --- a/notmuch-insert.c +++ b/notmuch-insert.c @@ -478,7 +478,7 @@ notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); mail_root = notmuch_config_get (notmuch, NOTMUCH_CONFIG_MAIL_ROOT); @@ -550,8 +550,6 @@ notmuch_insert_command (notmuch_database_t *notmuch, int argc, char *argv[]) return EXIT_FAILURE; } - notmuch_exit_if_unmatched_db_uuid (notmuch); - status = notmuch_process_shared_indexing_options (notmuch); if (status != NOTMUCH_STATUS_SUCCESS) { fprintf (stderr, "Error: Failed to process index options. (%s)\n", diff --git a/notmuch-new.c b/notmuch-new.c index 1ee498fa..b7a5f2ea 100644 --- a/notmuch-new.c +++ b/notmuch-new.c @@ -1142,7 +1142,7 @@ notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); /* quiet trumps verbose */ if (quiet) @@ -1197,8 +1197,6 @@ notmuch_new_command (notmuch_database_t *notmuch, int argc, char *argv[]) return EXIT_FAILURE; } - notmuch_exit_if_unmatched_db_uuid (notmuch); - if (notmuch_database_get_revision (notmuch, NULL) == 0) { int count = 0; count_files (mail_root, &count, &add_files_state); diff --git a/notmuch-reindex.c b/notmuch-reindex.c index a7380a4b..49eacd47 100644 --- a/notmuch-reindex.c +++ b/notmuch-reindex.c @@ -49,11 +49,11 @@ reindex_query (notmuch_database_t *notmuch, const char *query_string, notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS; - query = notmuch_query_create (notmuch, query_string); - if (query == NULL) { - fprintf (stderr, "Out of memory.\n"); + status = notmuch_query_create_with_syntax (notmuch, query_string, + shared_option_query_syntax (), + &query); + if (print_status_database ("notmuch reindex", notmuch, status)) return 1; - } /* reindexing is not interested in any special sort order */ notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED); @@ -108,9 +108,7 @@ notmuch_reindex_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); - - notmuch_exit_if_unmatched_db_uuid (notmuch); + notmuch_process_shared_options (notmuch, argv[0]); status = notmuch_process_shared_indexing_options (notmuch); if (status != NOTMUCH_STATUS_SUCCESS) { diff --git a/notmuch-reply.c b/notmuch-reply.c index 08140799..9fca22db 100644 --- a/notmuch-reply.c +++ b/notmuch-reply.c @@ -464,8 +464,8 @@ guess_from_in_received_by (notmuch_database_t *notmuch, const char *received) * (last Received: header added) and try to extract from them * indications to which email address this message was delivered. * - * The Received: header is special in our get_header function and is - * always concatenated. + * The Received: header is among special ones in our get_header function + * and is always concatenated. * * Return the address that was found, if any, and NULL otherwise. */ @@ -499,6 +499,9 @@ guess_from_in_received_headers (notmuch_message_t *message) * headers: Envelope-To, X-Original-To, and Delivered-To (searched in * that order). * + * The Delivered-To: header is among special ones in our get_header + * function and is always concatenated. + * * Return the address that was found, if any, and NULL otherwise. */ static const char * @@ -716,6 +719,7 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]) }; int format = FORMAT_DEFAULT; int reply_all = true; + notmuch_status_t status; notmuch_opt_desc_t options[] = { { .opt_keyword = &format, .name = "format", .keywords = @@ -743,7 +747,7 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); notmuch_exit_if_unsupported_format (); @@ -758,13 +762,11 @@ notmuch_reply_command (notmuch_database_t *notmuch, int argc, char *argv[]) return EXIT_FAILURE; } - notmuch_exit_if_unmatched_db_uuid (notmuch); - - query = notmuch_query_create (notmuch, query_string); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); + status = notmuch_query_create_with_syntax (notmuch, query_string, + shared_option_query_syntax (), + &query); + if (print_status_database ("notmuch reply", notmuch, status)) return EXIT_FAILURE; - } if (do_reply (notmuch, query, ¶ms, format, reply_all) != 0) return EXIT_FAILURE; diff --git a/notmuch-restore.c b/notmuch-restore.c index 1a81212f..1cce004a 100644 --- a/notmuch-restore.c +++ b/notmuch-restore.c @@ -272,8 +272,7 @@ notmuch_restore_command (notmuch_database_t *notmuch, int argc, char *argv[]) goto DONE; } - notmuch_process_shared_options (argv[0]); - notmuch_exit_if_unmatched_db_uuid (notmuch); + notmuch_process_shared_options (notmuch, argv[0]); if (include == 0) { include = DUMP_INCLUDE_CONFIG | DUMP_INCLUDE_PROPERTIES | DUMP_INCLUDE_TAGS; diff --git a/notmuch-search.c b/notmuch-search.c index 244817a9..327e1445 100644 --- a/notmuch-search.c +++ b/notmuch-search.c @@ -56,6 +56,7 @@ typedef struct { int format_sel; sprinter_t *format; int exclude; + int query_syntax; notmuch_query_t *query; int sort; int output; @@ -709,8 +710,6 @@ _notmuch_search_prepare (search_context_t *ctx, int argc, char *argv[]) notmuch_exit_if_unsupported_format (); - notmuch_exit_if_unmatched_db_uuid (ctx->notmuch); - query_str = query_string_from_args (ctx->notmuch, argc, argv); if (query_str == NULL) { fprintf (stderr, "Out of memory.\n"); @@ -721,11 +720,11 @@ _notmuch_search_prepare (search_context_t *ctx, int argc, char *argv[]) return EXIT_FAILURE; } - ctx->query = notmuch_query_create (ctx->notmuch, query_str); - if (ctx->query == NULL) { - fprintf (stderr, "Out of memory\n"); + if (print_status_database ("notmuch search", ctx->notmuch, + notmuch_query_create_with_syntax (ctx->notmuch, query_str, + shared_option_query_syntax (), + &ctx->query))) return EXIT_FAILURE; - } notmuch_query_set_sort (ctx->query, ctx->sort); @@ -771,6 +770,7 @@ static search_context_t search_context = { .format_sel = NOTMUCH_FORMAT_TEXT, .exclude = NOTMUCH_EXCLUDE_TRUE, .sort = NOTMUCH_SORT_NEWEST_FIRST, + .query_syntax = NOTMUCH_QUERY_SYNTAX_XAPIAN, .output = 0, .offset = 0, .limit = -1, /* unlimited */ @@ -827,7 +827,7 @@ notmuch_search_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); if (ctx->output != OUTPUT_FILES && ctx->output != OUTPUT_MESSAGES && ctx->dupe != -1) { @@ -893,7 +893,7 @@ notmuch_address_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); if (! (ctx->output & (OUTPUT_SENDER | OUTPUT_RECIPIENTS))) ctx->output |= OUTPUT_SENDER; diff --git a/notmuch-setup.c b/notmuch-setup.c index 28f4b7de..9382e279 100644 --- a/notmuch-setup.c +++ b/notmuch-setup.c @@ -147,10 +147,6 @@ notmuch_setup_command (notmuch_database_t *notmuch, if (notmuch_minimal_options ("setup", argc, argv) < 0) return EXIT_FAILURE; - if (notmuch_requested_db_uuid) - fprintf (stderr, "Warning: ignoring --uuid=%s\n", - notmuch_requested_db_uuid); - config = notmuch_conffile_open (notmuch, notmuch_config_path (notmuch), true); if (! config) diff --git a/notmuch-show.c b/notmuch-show.c index c8f1a40f..2848c9c3 100644 --- a/notmuch-show.c +++ b/notmuch-show.c @@ -1287,7 +1287,7 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); /* explicit decryption implies verification */ if (params.crypto.decrypt == NOTMUCH_DECRYPT_NOSTASH || @@ -1353,8 +1353,6 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) } } - notmuch_exit_if_unmatched_db_uuid (notmuch); - query_string = query_string_from_args (notmuch, argc - opt_index, argv + opt_index); if (query_string == NULL) { fprintf (stderr, "Out of memory\n"); @@ -1366,11 +1364,11 @@ notmuch_show_command (notmuch_database_t *notmuch, int argc, char *argv[]) return EXIT_FAILURE; } - query = notmuch_query_create (notmuch, query_string); - if (query == NULL) { - fprintf (stderr, "Out of memory\n"); + status = notmuch_query_create_with_syntax (notmuch, query_string, + shared_option_query_syntax (), + &query); + if (print_status_database ("notmuch show", notmuch, status)) return EXIT_FAILURE; - } notmuch_query_set_sort (query, sort); diff --git a/notmuch-tag.c b/notmuch-tag.c index de428c8e..71ff06bf 100644 --- a/notmuch-tag.c +++ b/notmuch-tag.c @@ -39,8 +39,8 @@ handle_sigint (unused (int sig)) static char * -_optimize_tag_query (void *ctx, const char *orig_query_string, - const tag_op_list_t *list) +_optimize_tag_query_infix (void *ctx, const char *orig_query_string, + const tag_op_list_t *list) { /* This is subtler than it looks. Xapian ignores the '-' operator * at the beginning both queries and parenthesized groups and, @@ -88,6 +88,33 @@ _optimize_tag_query (void *ctx, const char *orig_query_string, return query_string; } +static char * +_optimize_tag_query (void *ctx, const char *orig_query_string, + notmuch_query_syntax_t stx, + const tag_op_list_t *list) +{ + char *query_string; + + if (stx == NOTMUCH_QUERY_SYNTAX_XAPIAN) + return _optimize_tag_query_infix (ctx, orig_query_string, list); + + /* Don't optimize if there are no tag changes. */ + if (tag_op_list_size (list) == 0) + return talloc_strdup (ctx, orig_query_string); + + query_string = talloc_asprintf (ctx, "(and %s", orig_query_string); + for (size_t i = 0; i < tag_op_list_size (list) && query_string; i++) { + query_string = talloc_asprintf_append_buffer ( + query_string, tag_op_list_isremove (list, i) ? " (tag \"%s\")" : " (not (tag \"%s\"))", + tag_op_list_tag (list, i)); + } + + if (query_string) + query_string = talloc_strdup_append_buffer (query_string, ")"); + + return query_string; +} + /* Tag messages matching 'query_string' according to 'tag_ops' */ static int @@ -104,7 +131,9 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string, if (! (flags & TAG_FLAG_REMOVE_ALL)) { /* Optimize the query so it excludes messages that already * have the specified set of tags. */ - query_string = _optimize_tag_query (ctx, query_string, tag_ops); + query_string = _optimize_tag_query (ctx, query_string, + shared_option_query_syntax (), + tag_ops); if (query_string == NULL) { fprintf (stderr, "Out of memory.\n"); return 1; @@ -112,11 +141,11 @@ tag_query (void *ctx, notmuch_database_t *notmuch, const char *query_string, flags |= TAG_FLAG_PRE_OPTIMIZED; } - query = notmuch_query_create (notmuch, query_string); - if (query == NULL) { - fprintf (stderr, "Out of memory.\n"); + status = notmuch_query_create_with_syntax (notmuch, query_string, + shared_option_query_syntax (), + &query); + if (print_status_database ("notmuch tag", notmuch, status)) return 1; - } /* tagging is not interested in any special sort order */ notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED); @@ -220,7 +249,7 @@ notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[]) if (opt_index < 0) return EXIT_FAILURE; - notmuch_process_shared_options (argv[0]); + notmuch_process_shared_options (notmuch, argv[0]); if (input_file_name) { batch = true; @@ -261,8 +290,6 @@ notmuch_tag_command (notmuch_database_t *notmuch, int argc, char *argv[]) } } - notmuch_exit_if_unmatched_db_uuid (notmuch); - if (print_status_database ( "notmuch restore", notmuch, diff --git a/notmuch.c b/notmuch.c index 1404b70c..3fb58bf2 100644 --- a/notmuch.c +++ b/notmuch.c @@ -49,22 +49,38 @@ notmuch_command (notmuch_database_t *notmuch, int argc, char *argv[]); static int _help_for (const char *topic); +static void +notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch); + static bool print_version = false, print_help = false; -const char *notmuch_requested_db_uuid = NULL; +static const char *notmuch_requested_db_uuid = NULL; +static int query_syntax = NOTMUCH_QUERY_SYNTAX_XAPIAN; const notmuch_opt_desc_t notmuch_shared_options [] = { { .opt_bool = &print_version, .name = "version" }, { .opt_bool = &print_help, .name = "help" }, { .opt_string = ¬much_requested_db_uuid, .name = "uuid" }, + { .opt_keyword = &query_syntax, .name = "query", .keywords = + (notmuch_keyword_t []){ { "infix", NOTMUCH_QUERY_SYNTAX_XAPIAN }, + { "sexp", NOTMUCH_QUERY_SYNTAX_SEXP }, + { 0, 0 } } }, + { } }; +notmuch_query_syntax_t +shared_option_query_syntax () +{ + return query_syntax; +} + /* any subcommand wanting to support these options should call * inherit notmuch_shared_options and call - * notmuch_process_shared_options (subcommand_name); + * notmuch_process_shared_options (notmuch, subcommand_name); + * with notmuch = an open database, or NULL. */ void -notmuch_process_shared_options (const char *subcommand_name) +notmuch_process_shared_options (notmuch_database_t *notmuch, const char *subcommand_name) { if (print_version) { printf ("notmuch " STRINGIFY (NOTMUCH_VERSION) "\n"); @@ -75,6 +91,14 @@ notmuch_process_shared_options (const char *subcommand_name) int ret = _help_for (subcommand_name); exit (ret); } + + if (notmuch) { + notmuch_exit_if_unmatched_db_uuid (notmuch); + } else { + if (notmuch_requested_db_uuid) + fprintf (stderr, "Warning: ignoring --uuid=%s\n", + notmuch_requested_db_uuid); + } } /* This is suitable for subcommands that do not actually open the @@ -97,7 +121,7 @@ notmuch_minimal_options (const char *subcommand_name, return -1; /* We can't use argv here as it is sometimes NULL */ - notmuch_process_shared_options (subcommand_name); + notmuch_process_shared_options (NULL, subcommand_name); return opt_index; } @@ -280,7 +304,7 @@ be supported in the future.\n", notmuch_format_version); } } -void +static void notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch) { const char *uuid = NULL; @@ -480,7 +504,7 @@ main (int argc, char *argv[]) if (opt_index < argc) command_name = argv[opt_index]; - notmuch_process_shared_options (command_name); + notmuch_process_shared_options (NULL, command_name); command = find_command (command_name); /* if command->function is NULL, try external command */ diff --git a/test/T030-config.sh b/test/T030-config.sh index 636c6356..3a585d1b 100755 --- a/test/T030-config.sh +++ b/test/T030-config.sh @@ -51,6 +51,7 @@ cat < EXPECTED built_with.compact=something built_with.field_processor=something built_with.retry_lock=something +built_with.sexpr_query=something database.autocommit=8000 database.mail_root=MAIL_DIR database.path=MAIL_DIR diff --git a/test/T055-path-config.sh b/test/T055-path-config.sh index 27dd209a..ef22e964 100755 --- a/test/T055-path-config.sh +++ b/test/T055-path-config.sh @@ -266,7 +266,7 @@ EOF test_expect_equal "${output}+${output2}" "${value}+" test_begin_subtest "Config list ($config)" - notmuch config list | notmuch_dir_sanitize | \ + notmuch config list | notmuch_config_sanitize | \ sed -e "s/^database.backup_dir=.*$/database.backup_dir/" \ -e "s/^database.hook_dir=.*$/database.hook_dir/" \ -e "s/^database.path=.*$/database.path/" \ @@ -274,9 +274,10 @@ EOF -e "s,^database.mail_root=CWD/home/env_points_here,database.mail_root=MAIL_DIR," \ > OUTPUT cat < EXPECTED -built_with.compact=true -built_with.field_processor=true -built_with.retry_lock=true +built_with.compact=something +built_with.field_processor=something +built_with.retry_lock=something +built_with.sexpr_query=something database.autocommit=8000 database.backup_dir database.hook_dir diff --git a/test/T060-count.sh b/test/T060-count.sh index a1ebf8ba..6ad80df9 100755 --- a/test/T060-count.sh +++ b/test/T060-count.sh @@ -154,4 +154,28 @@ print("4: {} messages".format(query.count_messages())) EOF test_expect_equal_file EXPECTED OUTPUT +if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then + + test_begin_subtest "and of exact terms (query=sexp)" + output=$(notmuch count --query=sexp '(and "wonderful" "wizard")') + test_expect_equal "$output" 1 + + test_begin_subtest "or of exact terms (query=sexp)" + output=$(notmuch count --query=sexp '(or "php" "wizard")') + test_expect_equal "$output" 2 + + test_begin_subtest "starts-with, case-insensitive (query=sexp)" + output=$(notmuch count --query=sexp '(starts-with FreeB)') + test_expect_equal "$output" 5 + + test_begin_subtest "query that matches no messages (query=sexp)" + count=$(notmuch count --query=sexp '(and (from keithp) (to keithp))') + test_expect_equal 0 "$count" + + test_begin_subtest "Compound subquery (query=sexp)" + output=$(notmuch count --query=sexp '(thread (of (from keithp) (subject Maildir)))') + test_expect_equal "$output" 7 + +fi + test_done diff --git a/test/T081-sexpr-search.sh b/test/T081-sexpr-search.sh new file mode 100755 index 00000000..2a8ad5f1 --- /dev/null +++ b/test/T081-sexpr-search.sh @@ -0,0 +1,986 @@ +#!/usr/bin/env bash +test_description='"notmuch search" in several variations' +. $(dirname "$0")/test-lib.sh || exit 1 + +if [ $NOTMUCH_HAVE_SFSEXP -ne 1 ]; then + printf "Skipping due to missing sfsexp library\n" + test_done +fi + +add_email_corpus + +for query in '()' '(not)' '(and)' '(or ())' '(or (not))' '(or (and))' \ + '(or (and) (or) (not (and)))'; do + test_begin_subtest "all messages: $query" + notmuch search '*' > EXPECTED + notmuch search --query=sexp "$query" > OUTPUT + test_expect_equal_file EXPECTED OUTPUT +done + +for query in '(or)' '(not ())' '(not (not))' '(not (and))' \ + '(not (or (and) (or) (not (and))))'; do + test_begin_subtest "no messages: $query" + notmuch search --query=sexp "$query" > OUTPUT + test_expect_equal_file /dev/null OUTPUT +done + +test_begin_subtest "and of exact terms" +notmuch search --query=sexp '(and "wonderful" "wizard")' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "or of exact terms" +notmuch search --query=sexp '(or "php" "wizard")' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2010-12-29 [1/1] François Boulogne; [aur-general] Guidelines: cp, mkdir vs install (inbox unread) +thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "single term in body" +notmuch search --query=sexp 'wizard' | notmuch_search_sanitize>OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "single term in body (case insensitive)" +notmuch search --query=sexp 'Wizard' | notmuch_search_sanitize>OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "single term in body, stemmed version" +notmuch search arriv > EXPECTED +notmuch search --query=sexp arriv > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "single term in body, unstemmed version" +notmuch search --query=sexp '"arriv"' > OUTPUT +test_expect_equal_file /dev/null OUTPUT + +test_begin_subtest "Search by 'subject'" +add_message [subject]=subjectsearchtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(notmuch search --query=sexp '(subject subjectsearchtest)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread)" + +test_begin_subtest "Search by 'subject' (case insensitive)" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(subject "Maildir")' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'subject' (utf-8):" +add_message [subject]=utf8-sübjéct '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(notmuch search --query=sexp '(subject utf8 sübjéct)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)" + +test_begin_subtest "Search by 'subject' (utf-8, and):" +output=$(notmuch search --query=sexp '(subject (and utf8 sübjéct))' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)" + +test_begin_subtest "Search by 'subject' (utf-8, and outside):" +output=$(notmuch search --query=sexp '(and (subject utf8) (subject sübjéct))' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)" + +test_begin_subtest "Search by 'subject' (utf-8, or):" +notmuch search --query=sexp '(subject (or utf8 subjectsearchtest))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'subject' (utf-8, or outside):" +notmuch search --query=sexp '(or (subject utf8) (subject subjectsearchtest))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'attachment'" +notmuch search attachment:notmuch-help.patch > EXPECTED +notmuch search --query=sexp '(attachment notmuch-help.patch)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'body'" +add_message '[subject]="body search"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [body]=bodysearchtest +output=$(notmuch search --query=sexp '(body bodysearchtest)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (inbox unread)" + +test_begin_subtest "Search by 'body' (phrase)" +add_message '[subject]="body search (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="body search (phrase)"' +add_message '[subject]="negative result"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="This phrase should not match the body search"' +output=$(notmuch search --query=sexp '(body "body search phrase")' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (phrase) (inbox unread)" + +test_begin_subtest "Search by 'body' (utf-8):" +add_message '[subject]="utf8-message-body-subject"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="message body utf8: bödý"' +output=$(notmuch search --query=sexp '(body 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)" + +add_message "[body]=thebody-1" "[subject]=kryptonite-1" +add_message "[body]=nothing-to-see-here-1" "[subject]=thebody-1" + +test_begin_subtest 'search without body: prefix' +notmuch search thebody > EXPECTED +notmuch search --query=sexp '(and thebody)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest 'negated body: prefix' +notmuch search thebody and not body:thebody > EXPECTED +notmuch search --query=sexp '(and (not (body thebody)) thebody)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest 'search unprefixed for prefixed term' +notmuch search kryptonite > EXPECTED +notmuch search --query=sexp '(and kryptonite)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest 'search with body: prefix for term only in subject' +notmuch search body:kryptonite > EXPECTED +notmuch search --query=sexp '(body kryptonite)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'from'" +add_message '[subject]="search by from"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom +output=$(notmuch search --query=sexp '(from searchbyfrom)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] searchbyfrom; search by from (inbox unread)" + +test_begin_subtest "Search by 'from' (address)" +add_message '[subject]="search by from (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom@example.com +output=$(notmuch search --query=sexp '(from searchbyfrom@example.com)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] searchbyfrom@example.com; search by from (address) (inbox unread)" + +test_begin_subtest "Search by 'from' (name)" +add_message '[subject]="search by from (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[from]="Search By From Name "' +output=$(notmuch search --query=sexp '(from "Search By From Name")' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)" + +test_begin_subtest "Search by 'from' (name and address)" +output=$(notmuch search --query=sexp '(from "Search By From Name ")' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)" + +add_message '[dir]=bad' '[subject]="To the bone"' +add_message '[dir]=.' '[subject]="Top level"' +add_message '[dir]=bad/news' '[subject]="Bears"' +mkdir -p "${MAIL_DIR}/duplicate/bad/news" +cp "$gen_msg_filename" "${MAIL_DIR}/duplicate/bad/news" + +add_message '[dir]=things' '[subject]="These are a few"' +add_message '[dir]=things/favorite' '[subject]="Raindrops, whiskers, kettles"' +add_message '[dir]=things/bad' '[subject]="Bites, stings, sad feelings"' + +test_begin_subtest "Search by 'folder' (multiple)" +output=$(notmuch search --query=sexp '(folder bad bad/news things/bad)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread) +thread:XXX 2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread) +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)" + +test_begin_subtest "Search by 'folder': top level." +notmuch search folder:'""' > EXPECTED +notmuch search --query=sexp '(folder "")' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'id'" +add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(notmuch search --query=sexp "(id ${gen_msg_id})" | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)" + +test_begin_subtest "Search by 'id' (or)" +add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(notmuch search --query=sexp "(id non-existent-mid ${gen_msg_id})" | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)" + +test_begin_subtest "Search by 'is' (multiple)" +notmuch tag -inbox tag:searchbytag +notmuch search is:inbox AND is:unread | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(is inbox unread)' | notmuch_search_sanitize > OUTPUT +notmuch tag +inbox tag:searchbytag +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'mid'" +add_message '[subject]="search by mid"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(notmuch search --query=sexp "(mid ${gen_msg_id})" | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by mid (inbox unread)" + +test_begin_subtest "Search by 'mid' (or)" +add_message '[subject]="search by mid"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(notmuch search --query=sexp "(mid non-existent-mid ${gen_msg_id})" | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by mid (inbox unread)" + +test_begin_subtest "Search by 'mimetype'" +notmuch search mimetype:text/html > EXPECTED +notmuch search --query=sexp '(mimetype text html)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +QUERYSTR="date:2009-11-18..2009-11-18 and tag:unread" +QUERYSTR2="query:test and subject:Maildir" +notmuch config set --database query.test "$QUERYSTR" +notmuch config set query.test2 "$QUERYSTR2" + +test_begin_subtest "ill-formed named query search" +notmuch search --query=sexp '(query)' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'query' expects single atom as argument +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "ill-formed named query search 2" +notmuch search --query=sexp '(to (query))' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'query' not supported inside 'to' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search named query" +notmuch search --query=sexp '(query test)' > OUTPUT +notmuch search $QUERYSTR > EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'subject' (utf-8, phrase-token):" +output=$(notmuch search --query=sexp '(subject utf8-sübjéct)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)" + +test_begin_subtest "search named query with other terms" +notmuch search --query=sexp '(and (query test) (subject Maildir))' > OUTPUT +notmuch search $QUERYSTR and subject:Maildir > EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search nested named query" +notmuch search --query=sexp '(query test2)' > OUTPUT +notmuch search $QUERYSTR2 > EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'subject' (utf-8, quoted string):" +output=$(notmuch search --query=sexp '(subject "utf8 sübjéct")' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread)" + +test_begin_subtest "Search by 'subject' (combine phrase, term):" +output=$(notmuch search --query=sexp '(subject Mac "compatibility issues")' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread)" + +test_begin_subtest "Search by 'subject' (combine phrase, term 2):" +notmuch search --query=sexp '(subject (or utf8 "compatibility issues"))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-sübjéct (inbox unread) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; utf8-message-body-subject (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'subject' (combine phrase, term 3):" +notmuch search --query=sexp '(subject issues X/Darwin)' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'tag'" +add_message '[subject]="search by tag"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +notmuch tag +searchbytag id:${gen_msg_id} +output=$(notmuch search --query=sexp '(tag searchbytag)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)" + +test_begin_subtest "Search by 'tag' (multiple)" +notmuch tag -inbox tag:searchbytag +notmuch search tag:inbox AND tag:unread | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(tag inbox unread)' | notmuch_search_sanitize > OUTPUT +notmuch tag +inbox tag:searchbytag +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'tag' and 'subject'" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (tag inbox) (subject maildir))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search by 'thread'" +add_message '[subject]="search by thread"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +thread_id=$(notmuch search id:${gen_msg_id} | sed -e "s/thread:\([a-f0-9]*\).*/\1/") +output=$(notmuch search --query=sexp "(thread ${thread_id})" | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by thread (inbox unread)" + +test_begin_subtest "Search by 'to'" +add_message '[subject]="search by to"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto +output=$(notmuch search --query=sexp '(to searchbyto)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread)" + +test_begin_subtest "Search by 'to' (address)" +add_message '[subject]="search by to (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto@example.com +output=$(notmuch search --query=sexp '(to searchbyto@example.com)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (address) (inbox unread)" + +test_begin_subtest "Search by 'to' (name)" +add_message '[subject]="search by to (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[to]="Search By To Name "' +output=$(notmuch search --query=sexp '(to "Search By To Name")' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)" + +test_begin_subtest "Search by 'to' (name and address)" +output=$(notmuch search --query=sexp '(to "Search By To Name ")' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)" + +test_begin_subtest "starts-with, no prefix" +output=$(notmuch search --query=sexp '(starts-with prelim)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth; [notmuch] preliminary FreeBSD support (attachment inbox unread)" + +test_begin_subtest "starts-with, case-insensitive" +notmuch search --query=sexp '(starts-with FreeB)' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [3/4] Alexander Botero-Lowry, Jjgod Jiang; [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) +thread:XXX 2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth; [notmuch] preliminary FreeBSD support (attachment inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, no prefix, all messages" +notmuch search --query=sexp '(starts-with "")' | notmuch_search_sanitize > OUTPUT +notmuch search '*' | notmuch_search_sanitize > EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, attachment" +output=$(notmuch search --query=sexp '(attachment (starts-with not))' | notmuch_search_sanitize) +test_expect_equal "$output" 'thread:XXX 2009-11-18 [2/2] Lars Kellogg-Stedman; [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread)' + +test_begin_subtest "starts-with, folder" +notmuch search --output=files --query=sexp '(folder (starts-with bad))' | notmuch_dir_sanitize | sed 's/[0-9]*$/XXX/' > OUTPUT +cat < EXPECTED +MAIL_DIR/bad/msg-XXX +MAIL_DIR/bad/news/msg-XXX +MAIL_DIR/duplicate/bad/news/msg-XXX +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, from" +notmuch search --query=sexp '(from (starts-with Mik))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-17 [1/1] Mikhail Gusarov; [notmuch] [PATCH] Handle rename of message file (inbox unread) +thread:XXX 2009-11-17 [2/7] Mikhail Gusarov| Lars Kellogg-Stedman, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread) +thread:XXX 2009-11-17 [2/5] Mikhail Gusarov| Carl Worth, Keith Packard; [notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, id" +notmuch search --query=sexp --output=messages '(id (starts-with 877))' > OUTPUT +cat < EXPECTED +id:877h1wv7mg.fsf@inf-8657.int-evry.fr +id:877htoqdbo.fsf@yoom.home.cworth.org +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, is" +output=$(notmuch search --query=sexp '(is (starts-with searchby))' | notmuch_search_sanitize) +test_expect_equal "$output" 'thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)' + +test_begin_subtest "starts-with, mid" +notmuch search --query=sexp --output=messages '(mid (starts-with 877))' > OUTPUT +cat < EXPECTED +id:877h1wv7mg.fsf@inf-8657.int-evry.fr +id:877htoqdbo.fsf@yoom.home.cworth.org +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, mimetype" +notmuch search --query=sexp '(mimetype (starts-with sig))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-18 [2/2] Lars Kellogg-Stedman; [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) +thread:XXX 2009-11-18 [4/7] Lars Kellogg-Stedman, Mikhail Gusarov| Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread) +thread:XXX 2009-11-17 [1/3] Adrian Perez de Castro| Keith Packard, Carl Worth; [notmuch] Introducing myself (inbox signed unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +add_message '[subject]="message with properties"' +notmuch restore < OUTPUT +cat < EXPECTED +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; message with properties (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, subject" +notmuch search --query=sexp '(subject (starts-with FreeB))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth; [notmuch] preliminary FreeBSD support (attachment inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, tag" +output=$(notmuch search --query=sexp '(tag (starts-with searchby))' | notmuch_search_sanitize) +test_expect_equal "$output" 'thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)' + +add_message '[subject]="no tags"' +notag_mid=${gen_msg_id} +notmuch tag -unread -inbox id:${notag_mid} + +test_begin_subtest "negated starts-with, tag" +output=$(notmuch search --query=sexp '(tag (not (starts-with in)))' | notmuch_search_sanitize) +test_expect_equal "$output" 'thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; no tags ()' + +test_begin_subtest "negated starts-with, tag 2" +output=$(notmuch search --query=sexp '(not (tag (starts-with in)))' | notmuch_search_sanitize) +test_expect_equal "$output" 'thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; no tags ()' + +test_begin_subtest "negated starts-with, tag 3" +output=$(notmuch search --query=sexp '(not (tag (starts-with "")))' | notmuch_search_sanitize) +test_expect_equal "$output" 'thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; no tags ()' + +test_begin_subtest "starts-with, thread" +notmuch search --query=sexp '(thread (starts-with "00"))' > OUTPUT +notmuch search '*' > EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, to" +notmuch search --query=sexp '(to (starts-with "search"))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (address) (inbox unread) +thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "wildcard search for 'is'" +notmuch search not id:${notag_mid} > EXPECTED +notmuch search --query=sexp '(is *)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "negated wildcard search for 'is'" +notmuch search id:${notag_mid} > EXPECTED +notmuch search --query=sexp '(not (is *))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "wildcard search for 'property'" +notmuch search property:foo=bar > EXPECTED +notmuch search --query=sexp '(property *)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "wildcard search for 'tag'" +notmuch search not id:${notag_mid} > EXPECTED +notmuch search --query=sexp '(tag *)' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "negated wildcard search for 'tag'" +notmuch search id:${notag_mid} > EXPECTED +notmuch search --query=sexp '(not (tag *))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +add_message '[subject]="message with tag \"*\""' +notmuch tag '+*' id:${gen_msg_id} + +test_begin_subtest "search for 'tag' \"*\"" +output=$(notmuch search --query=sexp --output=messages '(tag "*")') +test_expect_equal "$output" "id:$gen_msg_id" + +test_begin_subtest "search for missing / empty to" +add_message [to]="undisclosed-recipients:" +notmuch search --query=sexp '(not (to *))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; search for missing / empty to (inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Unbalanced parens" +# A code 1 indicates the error was handled (a crash will return e.g. 139). +test_expect_code 1 "notmuch search --query=sexp '('" + +test_begin_subtest "Unbalanced parens, error message" +notmuch search --query=sexp '(' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +invalid s-expression: '(' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "unknown prefix" +notmuch search --query=sexp '(foo)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +unknown prefix 'foo' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "list as prefix" +notmuch search --query=sexp '((foo))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +unexpected list in field/operation position +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "illegal nesting" +notmuch search --query=sexp '(subject (subject foo))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +nested field: 'subject' inside 'subject' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, no argument" +notmuch search --query=sexp '(starts-with)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'starts-with' expects single atom as argument +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, list argument" +notmuch search --query=sexp '(starts-with (stuff))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'starts-with' expects single atom as argument +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, too many arguments" +notmuch search --query=sexp '(starts-with a b c)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'starts-with' expects single atom as argument +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "starts-with, illegal field" +notmuch search --query=sexp '(body (starts-with foo))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'body' does not support wildcard queries +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "wildcard, illegal field" +notmuch search --query=sexp '(body *)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +'body' does not support wildcard queries +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search, exclude \"deleted\" messages from search" +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 --query=sexp '(subject deleted)' | notmuch_search_sanitize) +test_expect_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Not deleted (inbox unread)" + +test_begin_subtest "Search, exclude \"deleted\" messages from message search --exclude=false" +output=$(notmuch search --query=sexp --exclude=false --output=messages '(subject deleted)' | notmuch_search_sanitize) +test_expect_equal "$output" "id:$not_deleted_id +id:$deleted_id" + +test_begin_subtest "Search, exclude \"deleted\" messages from search, overridden" +notmuch search --query=sexp '(and (subject deleted) (tag deleted))' | notmuch_search_sanitize > OUTPUT +cat < EXPECTED +thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Deleted (deleted inbox unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Search, exclude \"deleted\" messages from threads" +add_message '[subject]="Not deleted reply"' '[in-reply-to]="<$gen_msg_id>"' +output=$(notmuch search --query=sexp '(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 "Search, don't exclude \"deleted\" messages when --exclude=flag specified" +output=$(notmuch search --query=sexp --exclude=flag '(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; Deleted (deleted inbox unread)" + +test_begin_subtest "Search, don't exclude \"deleted\" messages from search if not configured" +notmuch config set search.exclude_tags +output=$(notmuch search --query=sexp '(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 "regex at top level" +notmuch search --query=sexp '(rx foo)' >& OUTPUT +cat < EXPECTED +notmuch search: Syntax error in query +illegal 'rx' outside field +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "regex in illegal field" +notmuch search --query=sexp '(body (regex foo))' >& OUTPUT +cat < EXPECTED +notmuch search: Syntax error in query +'regex' not supported in field 'body' +EOF +test_expect_equal_file EXPECTED OUTPUT + +notmuch search --output=messages from:cworth > cworth.msg-ids + +test_begin_subtest "regexp 'from' search" +notmuch search --output=messages --query=sexp '(from (rx cworth))' > OUTPUT +test_expect_equal_file cworth.msg-ids OUTPUT + +test_begin_subtest "regexp search for 'from' 2" +notmuch search from:/cworth@cworth.org/ and subject:patch | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (from (rx cworth@cworth.org)) (subject patch))' \ + | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "regexp 'folder' search" +notmuch search 'folder:/^bar$/' | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(folder (rx ^bar$))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "regexp 'id' search" +notmuch search --output=messages --query=sexp '(id (rx yoom))' > OUTPUT +test_expect_equal_file cworth.msg-ids OUTPUT + +test_begin_subtest "unanchored 'is' search" +notmuch search tag:signed or tag:inbox > EXPECTED +notmuch search --query=sexp '(is (rx i))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "anchored 'is' search" +notmuch search tag:signed > EXPECTED +notmuch search --query=sexp '(is (rx ^si))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "combine regexp mid and subject" +notmuch search subject:/-C/ and mid:/y..m/ | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(and (subject (rx -C)) (mid (rx y..m)))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "regexp 'path' search" +notmuch search 'path:/^bar$/' | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(path (rx ^bar$))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "regexp 'property' search" +notmuch search property:foo=bar > EXPECTED +notmuch search --query=sexp '(property (rx foo=.*))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "anchored 'tag' search" +notmuch search tag:signed > EXPECTED +notmuch search --query=sexp '(tag (rx ^si))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "regexp 'thread' search" +notmuch search --output=threads '*' | grep '7$' > EXPECTED +notmuch search --output=threads --query=sexp '(thread (rx 7$))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Basic query that matches no messages" +count=$(notmuch count from:keithp and to:keithp) +test_expect_equal 0 "$count" + +test_begin_subtest "Same query against threads" +notmuch search --query=sexp '(and (thread (of (from keithp))) (thread (matching (to keithp))))' \ + | notmuch_search_sanitize > OUTPUT +cat< EXPECTED +thread:XXX 2009-11-18 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Mix thread and non-threads query" +notmuch search --query=sexp '(and (thread (matching keithp)) (to keithp))' | notmuch_search_sanitize > OUTPUT +cat< EXPECTED +thread:XXX 2009-11-18 [1/7] Lars Kellogg-Stedman| Mikhail Gusarov, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Compound subquery" +notmuch search --query=sexp '(thread (of (from keithp) (subject Maildir)))' | notmuch_search_sanitize > OUTPUT +cat< EXPECTED +thread:XXX 2009-11-18 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread) +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "empty subquery" +notmuch search --query=sexp '(thread (of))' 1>OUTPUT 2>&1 +notmuch search '*' > EXPECTED +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "illegal expansion" +notmuch search --query=sexp '(id (of ego))' 1>OUTPUT 2>&1 +cat< EXPECTED +notmuch search: Syntax error in query +'of' unsupported inside 'id' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "(folder (of subquery))" +notmuch search --query=sexp --output=messages '(folder (of (id yun3a4cegoa.fsf@aiko.keithp.com)))' > OUTPUT +cat < EXPECTED +id:yun1vjwegii.fsf@aiko.keithp.com +id:yun3a4cegoa.fsf@aiko.keithp.com +id:1258509400-32511-1-git-send-email-stewart@flamingspork.com +id:1258506353-20352-1-git-send-email-stewart@flamingspork.com +id:20091118010116.GC25380@dottiness.seas.harvard.edu +id:20091118005829.GB25380@dottiness.seas.harvard.edu +id:cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "infix query" +notmuch search to:searchbyto | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(infix "to:searchbyto")' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "bad infix query 1" +notmuch search --query=sexp '(infix "from:/unbalanced")' 2>&1| notmuch_search_sanitize > OUTPUT +cat < EXPECTED +notmuch search: Syntax error in query +Syntax error in infix query: from:/unbalanced +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "bad infix query 2" +notmuch search --query=sexp '(infix "thread:{unbalanced")' 2>&1| notmuch_search_sanitize > OUTPUT +cat < EXPECTED +notmuch search: Syntax error in query +Syntax error in infix query: thread:{unbalanced +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "bad infix query 3: bad nesting" +notmuch search --query=sexp '(subject (infix "tag:inbox"))' 2>&1| notmuch_search_sanitize > OUTPUT +cat < EXPECTED +notmuch search: Syntax error in query +'infix' not supported inside 'subject' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "infix query that matches no messages" +notmuch search --query=sexp '(and (infix "from:keithp") (infix "to:keithp"))' > OUTPUT +test_expect_equal_file /dev/null OUTPUT + +test_begin_subtest "compound infix query" +notmuch search date:2009-11-18..2009-11-18 and tag:unread > EXPECTED +notmuch search --query=sexp '(infix "date:2009-11-18..2009-11-18 and tag:unread")' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "compound infix query 2" +notmuch search date:2009-11-18..2009-11-18 and tag:unread > EXPECTED +notmuch search --query=sexp '(and (infix "date:2009-11-18..2009-11-18") (infix "tag:unread"))' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "user header (unknown header)" +notmuch search --query=sexp '(FooBar)' >& OUTPUT +cat < EXPECTED +notmuch search: Syntax error in query +unknown prefix 'FooBar' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "adding user header" +test_expect_code 0 "notmuch config set index.header.List \"List-Id\"" + +test_begin_subtest "reindexing" +test_expect_code 0 'notmuch reindex "*"' + +test_begin_subtest "wildcard search for user header" +grep -Ril List-Id ${MAIL_DIR} | sort | notmuch_dir_sanitize > EXPECTED +notmuch search --output=files --query=sexp '(List *)' | sort | notmuch_dir_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "wildcard search for user header 2" +grep -Ril List-Id ${MAIL_DIR} | sort | notmuch_dir_sanitize > EXPECTED +notmuch search --output=files --query=sexp '(List (starts-with not))' | sort | notmuch_dir_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search for user header" +notmuch search List:notmuch | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(List notmuch)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search for user header (list token)" +notmuch search List:notmuch | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(List notmuch.notmuchmail.org)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search for user header (quoted string)" +notmuch search 'List:"notmuch notmuchmail org"' | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(List "notmuch notmuchmail org")' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "search for user header (atoms)" +notmuch search 'List:"notmuch notmuchmail org"' | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(List notmuch notmuchmail org)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "check saved query name" +test_expect_code 1 "notmuch config set squery.test '(subject utf8-sübjéct)'" + +test_begin_subtest "roundtrip saved query (database)" +notmuch config set --database squery.Test '(subject utf8-sübjéct)' +output=$(notmuch config get squery.Test) +test_expect_equal "$output" '(subject utf8-sübjéct)' + +test_begin_subtest "roundtrip saved query" +notmuch config set squery.Test '(subject override subject)' +output=$(notmuch config get squery.Test) +test_expect_equal "$output" '(subject override subject)' + +test_begin_subtest "unknown saved query" +notmuch search --query=sexp '(Unknown foo bar)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +unknown prefix 'Unknown' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "syntax error in saved query" +notmuch config set squery.Bad '(Bad' +notmuch search --query=sexp '(Bad foo bar)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +invalid saved s-expression query: '(Bad' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search by 'tag' and 'subject'" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.TagSubject '(and (tag inbox) (subject maildir))' +notmuch search --query=sexp '(TagSubject)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: illegal nesting" +notmuch config set squery.TagSubject '(and (tag inbox) (subject maildir))' +notmuch search --query=sexp '(subject (TagSubject))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +nested field: 'tag' inside 'subject' +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: list as prefix" +notmuch config set squery.Bad2 '((and) (tag inbox) (subject maildir))' +notmuch search --query=sexp '(Bad2)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +unexpected list in field/operation position +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: bad parameter syntax" +notmuch config set squery.Bad3 '(macro a b)' +notmuch search --query=sexp '(Bad3)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +missing (possibly empty) list of arguments to macro +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: bad parameter syntax 2" +notmuch config set squery.Bad4 '(macro ((a b)) a)' +notmuch search --query=sexp '(Bad4 1)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +macro parameters must be unquoted atoms +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: bad parameter syntax 3" +notmuch config set squery.Bad5 '(macro (a b) a)' +notmuch search --query=sexp '(Bad5 1)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +too few arguments to macro +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: bad parameter syntax 4" +notmuch search --query=sexp '(Bad5 1 2 3)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +too many arguments to macro +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Saved Search: macro without body" +notmuch config set squery.Bad3 '(macro (a b))' +notmuch search --query=sexp '(Bad3)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +missing body of macro +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "macro in query" +notmuch search --query=sexp '(macro (a) (and ,b (subject maildir)))' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +macro definition not permitted here +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "zero argument macro" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.TagSubject0 '(macro () (and (tag inbox) (subject maildir)))' +notmuch search --query=sexp '(TagSubject0)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "undefined argument" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.Bad6 '(macro (a) (and ,b (subject maildir)))' +notmuch search --query=sexp '(Bad6 foo)' >OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +undefined parameter b +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Single argument macro" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.TagSubject1 '(macro (tagname) (and (tag ,tagname) (subject maildir)))' +notmuch search --query=sexp '(TagSubject1 inbox)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "Single argument macro, list argument" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.ThingSubject '(macro (thing) (and ,thing (subject maildir)))' +notmuch search --query=sexp '(ThingSubject (tag inbox))' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "two argument macro" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.TagSubject2 '(macro (tagname subj) (and (tag ,tagname) (subject ,subj)))' +notmuch search --query=sexp '(TagSubject2 inbox maildir)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "nested macros (shadowing)" +notmuch search tag:inbox and subject:maildir | notmuch_search_sanitize > EXPECTED +notmuch config set squery.Inner '(macro (x) (subject ,x))' +notmuch config set squery.Outer '(macro (x y) (and (tag ,x) (Inner ,y)))' +notmuch search --query=sexp '(Outer inbox maildir)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "nested macros (no dynamic scope)" +notmuch config set squery.Inner2 '(macro (x) (subject ,y))' +notmuch config set squery.Outer2 '(macro (x y) (and (tag ,x) (Inner2 ,y)))' +notmuch search --query=sexp '(Outer2 inbox maildir)' > OUTPUT 2>&1 +cat < EXPECTED +notmuch search: Syntax error in query +undefined parameter y +EOF +test_expect_equal_file EXPECTED OUTPUT + +test_begin_subtest "combine macro and user defined header" +notmuch config set squery.About '(macro (name) (or (subject ,name) (List ,name)))' +notmuch search subject:notmuch or List:notmuch | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(About notmuch)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + + +test_begin_subtest "combine macro and user defined header" +notmuch config set squery.About '(macro (name) (or (subject ,name) (List ,name)))' +notmuch search subject:notmuch or List:notmuch | notmuch_search_sanitize > EXPECTED +notmuch search --query=sexp '(About notmuch)' | notmuch_search_sanitize > OUTPUT +test_expect_equal_file EXPECTED OUTPUT + + +test_done diff --git a/test/T095-address.sh b/test/T095-address.sh index 817be538..8bb3627a 100755 --- a/test/T095-address.sh +++ b/test/T095-address.sh @@ -325,4 +325,11 @@ cat <EXPECTED EOF test_expect_equal_file EXPECTED OUTPUT +if [[ NOTMUCH_HAVE_SFSEXP = 1 ]]; then + test_begin_subtest "sexpr query: all messages" + notmuch address '*' > EXPECTED + notmuch address --query=sexp '()' > OUTPUT + test_expect_equal_file EXPECTED OUTPUT +fi + test_done diff --git a/test/T150-tagging.sh b/test/T150-tagging.sh index c292b24e..1a2fd77e 100755 --- a/test/T150-tagging.sh +++ b/test/T150-tagging.sh @@ -2,6 +2,21 @@ test_description='"notmuch tag"' . $(dirname "$0")/test-lib.sh || exit 1 +test_query_syntax () { + # use a tag with a space to stress the query string munging code. + local new_tag="${RANDOM} ${RANDOM}" + test_begin_subtest "sexpr query: $1" + backup_database + notmuch tag --query=sexp "+${new_tag}" -- "$1" + notmuch dump > OUTPUT + restore_database + backup_database + notmuch tag "+${new_tag}" -- "$2" + notmuch dump > EXPECTED + restore_database + test_expect_equal_file_nonempty EXPECTED OUTPUT +} + add_message '[subject]=One' add_message '[subject]=Two' @@ -310,4 +325,32 @@ output=$(notmuch tag +something '*' 2>&1 | sed 's/: .*$//' ) chmod u+w ${MAIL_DIR}/.notmuch/xapian/*.* test_expect_equal "$output" "A Xapian exception occurred opening database" +add_email_corpus + +if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then + + test_query_syntax '(and "wonderful" "wizard")' 'wonderful and wizard' + test_query_syntax '(or "php" "wizard")' 'php or wizard' + test_query_syntax 'wizard' 'wizard' + test_query_syntax 'Wizard' 'Wizard' + test_query_syntax '(attachment notmuch-help.patch)' 'attachment:notmuch-help.patch' + test_query_syntax '(mimetype text/html)' 'mimetype:text/html' + + test_begin_subtest "--batch --query=sexp" + notmuch dump --format=batch-tag > backup.tags + notmuch tag --batch --query=sexp < OUTPUT + cat < EXPECTED + #notmuch-dump batch-tag:3 config,properties,tags + +all +inbox +tag5 +unread -- id:msg-001@notmuch-test-suite + +all +inbox +tag4 +tag5 +unread -- id:msg-002@notmuch-test-suite +EOF + notmuch restore --format=batch-tag < backup.tags + test_expect_equal_file EXPECTED OUTPUT + +fi + test_done diff --git a/test/T220-reply.sh b/test/T220-reply.sh index b6d8f42a..4e9984d2 100755 --- a/test/T220-reply.sh +++ b/test/T220-reply.sh @@ -2,15 +2,14 @@ test_description="\"notmuch reply\" in several variations" . $(dirname "$0")/test-lib.sh || exit 1 -test_begin_subtest "Basic reply" add_message '[from]="Sender "' \ [to]=test_suite@notmuchmail.org \ [subject]=notmuch-reply-test \ '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ '[body]="basic reply test"' -output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK) -test_expect_equal "$output" "From: Notmuch Test Suite +cat < basic.expected +From: Notmuch Test Suite Subject: Re: notmuch-reply-test To: Sender In-Reply-To: <${gen_msg_id}> @@ -18,7 +17,19 @@ References: <${gen_msg_id}> On Tue, 05 Jan 2010 15:43:56 -0000, Sender wrote: > basic reply test -OK" +OK +EOF + +test_begin_subtest "Basic reply" +notmuch reply id:${gen_msg_id} >OUTPUT 2>&1 && echo OK >> OUTPUT +test_expect_equal_file basic.expected OUTPUT + +if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then + + test_begin_subtest "Basic reply (query=sexp)" + notmuch reply --query=sexp "(id ${gen_msg_id})" >OUTPUT 2>&1 && echo OK >> OUTPUT + test_expect_equal_file basic.expected OUTPUT +fi test_begin_subtest "Multiple recipients" add_message '[from]="Sender "' \ @@ -245,6 +256,26 @@ On Tue, 05 Jan 2010 15:43:56 -0000, Sender wrote: > From guessing OK" +test_begin_subtest "From guessing: multiple Delivered-To" +add_message '[from]="Sender "' \ + '[to]="Recipient "' \ + '[subject]="From guessing"' \ + '[date]="Tue, 05 Jan 2010 15:43:56 -0000"' \ + '[body]="From guessing"' \ + '[header]="Delivered-To: test_suite_other@notmuchmail.org +Delivered-To: test_suite@notmuchmail.org"' + +output=$(notmuch reply id:${gen_msg_id} 2>&1 && echo OK) +test_expect_equal "$output" "From: Notmuch Test Suite +Subject: Re: From guessing +To: Sender , Recipient +In-Reply-To: <${gen_msg_id}> +References: <${gen_msg_id}> + +On Tue, 05 Jan 2010 15:43:56 -0000, Sender wrote: +> From guessing +OK" + test_begin_subtest "Reply with RFC 2047-encoded headers" add_message '[subject]="=?iso-8859-1?q?=e0=df=e7?="' \ '[from]="=?utf-8?q?=e2=98=83?= "' \ @@ -281,7 +312,7 @@ test_expect_equal_json "$output" ' "crypto": {}, "date_relative": "2010-01-05", "excluded": false, - "filename": ["'${MAIL_DIR}'/msg-014"], + "filename": ["'${MAIL_DIR}'/msg-015"], "headers": { "Date": "Tue, 05 Jan 2010 15:43:56 +0000", "From": "\u2603 ", diff --git a/test/T240-dump-restore.sh b/test/T240-dump-restore.sh index 105de130..a86f0fb7 100755 --- a/test/T240-dump-restore.sh +++ b/test/T240-dump-restore.sh @@ -117,6 +117,19 @@ test_begin_subtest "dump -- from:cworth" notmuch dump -- from:cworth > dump-dash-cworth.actual test_expect_equal_file dump-cworth.expected dump-dash-cworth.actual + +if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then + + test_begin_subtest "dump --query=sexp -- '(from cworth)'" + notmuch dump --query=sexp -- '(from cworth)' > dump-dash-cworth.actual2 + test_expect_equal_file_nonempty dump-cworth.expected dump-dash-cworth.actual2 + + test_begin_subtest "dump --query=sexp --output=outfile '(from cworth)'" + notmuch dump --output=dump-outfile-cworth.actual2 --query=sexp '(from cworth)' + test_expect_equal_file dump-cworth.expected dump-outfile-cworth.actual2 + +fi + test_begin_subtest "dump --output=outfile from:cworth" notmuch dump --output=dump-outfile-cworth.actual from:cworth test_expect_equal_file dump-cworth.expected dump-outfile-cworth.actual diff --git a/test/T310-emacs.sh b/test/T310-emacs.sh index d69d94a3..6c1e8979 100755 --- a/test/T310-emacs.sh +++ b/test/T310-emacs.sh @@ -41,6 +41,20 @@ test_emacs '(notmuch-search "tag:inbox") (test-output)' test_expect_equal_file $EXPECTED/notmuch-search-tag-inbox OUTPUT +test_begin_subtest "Functions in search-result-format" +test_emacs '(let + ((notmuch-search-result-format + (quote ((notmuch-test-result-flags . "%s ") + ("date" . "%12s ") + ("count" . "%9s ") + ("authors" . "%-30s ") + ("subject" . "%s ") + ("tags" . "(%s)"))))) + (notmuch-search "tag:inbox") + (notmuch-test-wait) + (test-output))' +test_expect_equal_file $EXPECTED/search-result-format-function OUTPUT + test_begin_subtest "Incremental parsing of search results" test_emacs "(cl-letf* (((symbol-function 'orig) (symbol-function 'notmuch-search-process-filter)) diff --git a/test/T357-index-decryption.sh b/test/T357-index-decryption.sh index a96c1b5e..a7497489 100755 --- a/test/T357-index-decryption.sh +++ b/test/T357-index-decryption.sh @@ -226,6 +226,7 @@ output=$(notmuch dump | LC_ALL=C sort) expected='#= simple-encrypted@crypto.notmuchmail.org index.decryption=failure #notmuch-dump batch-tag:3 config,properties,tags +encrypted +inbox +unread -- id:basic-encrypted@crypto.notmuchmail.org ++encrypted +inbox +unread -- id:encrypted-rfc822-attachment@crypto.notmuchmail.org +encrypted +inbox +unread -- id:encrypted-signed@crypto.notmuchmail.org +encrypted +inbox +unread -- id:simple-encrypted@crypto.notmuchmail.org' test_expect_equal \ diff --git a/test/T450-emacs-show.sh b/test/T450-emacs-show.sh index a750cc4d..5bb5b201 100755 --- a/test/T450-emacs-show.sh +++ b/test/T450-emacs-show.sh @@ -219,6 +219,12 @@ test_emacs '(notmuch-show "id:basic-encrypted@crypto.notmuchmail.org") (test-visible-output)' test_expect_equal_file $EXPECTED/notmuch-show-decrypted-message OUTPUT +test_begin_subtest "show encrypted rfc822 message" +test_subtest_known_broken +test_emacs '(notmuch-show "id:encrypted-rfc822-attachment@crypto.notmuchmail.org") + (test-visible-output)' +test_expect_code 1 'fgrep "!!!" OUTPUT' + test_begin_subtest "show undecryptable message" test_emacs '(notmuch-show "id:simple-encrypted@crypto.notmuchmail.org") (test-visible-output)' diff --git a/test/T460-emacs-tree.sh b/test/T460-emacs-tree.sh index 405d7ee7..946583d9 100755 --- a/test/T460-emacs-tree.sh +++ b/test/T460-emacs-tree.sh @@ -179,4 +179,19 @@ output=$(test_emacs '(notmuch-tree "tag:inbox") (notmuch-show-stash-message-id)') test_expect_equal "$output" "\"Stashed: id:1258493565-13508-1-git-send-email-keithp@keithp.com\"" +test_begin_subtest "Functions in tree-result-format" +test_emacs ' +(let + ((notmuch-tree-result-format + (quote (("date" . "%12s ") + ("authors" . "%-20s") + ((("tree" . "%s") + ("subject" . "%s")) . " %-54s ") + (notmuch-test-result-flags . "(%s)"))))) + (notmuch-tree "tag:inbox") + (notmuch-test-wait) + (test-output)) +' +test_expect_equal_file $EXPECTED/result-format-function OUTPUT + test_done diff --git a/test/T465-emacs-unthreaded.sh b/test/T465-emacs-unthreaded.sh index f9a09426..1f386bf3 100755 --- a/test/T465-emacs-unthreaded.sh +++ b/test/T465-emacs-unthreaded.sh @@ -6,6 +6,8 @@ test_description="emacs unthreaded interface" test_require_emacs +EXPECTED=$NOTMUCH_SRCDIR/test/emacs-unthreaded.expected-output + generate_message "[id]=large-thread-1" '[subject]="large thread"' printf " 2001-01-05 Notmuch Test Suite large thread%43s(inbox unread)\n" >> EXPECTED.unthreaded @@ -34,4 +36,19 @@ output=$(test_emacs '(let ((max-lisp-eval-depth 10)) "SUCCESS")' ) test_expect_equal "$output" '"SUCCESS"' +add_email_corpus +test_begin_subtest "Functions in unthreaded-result-format" +test_emacs ' +(let + ((notmuch-unthreaded-result-format + (quote (("date" . "%12s ") + ("authors" . "%-20s") + ("subject" . "%-54s") + (notmuch-test-result-flags . "(%s)"))))) + (notmuch-unthreaded "tag:inbox") + (notmuch-test-wait) + (test-output)) +' +test_expect_equal_file $EXPECTED/result-format-function OUTPUT + test_done diff --git a/test/T520-show.sh b/test/T520-show.sh index 6f42ca12..12bde6c7 100755 --- a/test/T520-show.sh +++ b/test/T520-show.sh @@ -3,6 +3,13 @@ test_description='"notmuch show"' . $(dirname "$0")/test-lib.sh || exit 1 +test_query_syntax () { + test_begin_subtest "sexpr query: $1" + sexp=$(notmuch show --format=json --query=sexp "$1") + infix=$(notmuch show --format=json "$2") + test_expect_equal_json "$sexp" "$infix" +} + add_email_corpus test_begin_subtest "exit code for show invalid query" @@ -27,4 +34,15 @@ notmuch show --entire-thread=true --sort=newest-first $QUERY > EXPECTED notmuch show --entire-thread=true --sort=oldest-first $QUERY > OUTPUT test_expect_equal_file EXPECTED OUTPUT + +if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then + + test_query_syntax '(and "wonderful" "wizard")' 'wonderful and wizard' + test_query_syntax '(or "php" "wizard")' 'php or wizard' + test_query_syntax 'wizard' 'wizard' + test_query_syntax 'Wizard' 'Wizard' + test_query_syntax '(attachment notmuch-help.patch)' 'attachment:notmuch-help.patch' + +fi + test_done diff --git a/test/T700-reindex.sh b/test/T700-reindex.sh index bac43dc5..347f8483 100755 --- a/test/T700-reindex.sh +++ b/test/T700-reindex.sh @@ -4,6 +4,21 @@ test_description='reindexing messages' add_email_corpus + +if [ $NOTMUCH_HAVE_SFSEXP -eq 1 ]; then + + count=$(notmuch count --lastmod '*' | cut -f 3) + for query in '()' '(not)' '(and)' '(or ())' '(or (not))' '(or (and))' \ + '(or (and) (or) (not (and)))'; do + test_begin_subtest "reindex all messages: $query" + notmuch reindex --query=sexp "$query" + output=$(notmuch count --lastmod '*' | cut -f 3) + count=$((count + 1)) + test_expect_equal "$output" "$count" + done + +fi + notmuch tag +usertag1 '*' notmuch search '*' 2>1 | notmuch_search_sanitize > initial-threads @@ -41,6 +56,7 @@ notmuch dump > OUTPUT notmuch tag -attachment2 -encrypted2 -signed2 '*' test_expect_equal_file EXPECTED OUTPUT +backup_database test_begin_subtest 'reindex moves a message between threads' notmuch search --output=threads id:87iqd9rn3l.fsf@vertex.dottedmag > EXPECTED # re-parent @@ -48,7 +64,19 @@ sed -i 's/1258471718-6781-1-git-send-email-dottedmag@dottedmag.net/87iqd9rn3l.fs notmuch reindex id:1258471718-6781-2-git-send-email-dottedmag@dottedmag.net notmuch search --output=threads id:1258471718-6781-2-git-send-email-dottedmag@dottedmag.net > OUTPUT test_expect_equal_file EXPECTED OUTPUT +restore_database + +backup_database +test_begin_subtest 'reindex detects removal of all files' +notmuch search --output=messages not id:20091117232137.GA7669@griffis1.net> EXPECTED +# remove both copies +mv $MAIL_DIR/cur/51:2,* duplicate-message-2.eml +notmuch reindex id:20091117232137.GA7669@griffis1.net +notmuch search --output=messages '*' > OUTPUT +test_expect_equal_file EXPECTED OUTPUT +restore_database +backup_database test_begin_subtest 'reindex detects removal of all files' notmuch search --output=messages not id:20091117232137.GA7669@griffis1.net> EXPECTED # remove both copies @@ -56,6 +84,7 @@ mv $MAIL_DIR/cur/51:2,* duplicate-message-2.eml notmuch reindex id:20091117232137.GA7669@griffis1.net notmuch search --output=messages '*' > OUTPUT test_expect_equal_file EXPECTED OUTPUT +restore_database test_begin_subtest "reindex preserves properties" cat < prop-dump diff --git a/test/corpora/crypto/encrypted-rfc822-attachment b/test/corpora/crypto/encrypted-rfc822-attachment new file mode 100644 index 00000000..56fe3169 --- /dev/null +++ b/test/corpora/crypto/encrypted-rfc822-attachment @@ -0,0 +1,52 @@ +Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="===============9060418334135509864==" +MIME-Version: 1.0 +From: Notmuch test suite +To: test_suite@notmuchmail.org +Subject: testing encrypted rfc822 attachments +Date: Sat, 03 Jul 2021 16:00:02 -0300 +Message-ID: +User-Agent: alot/0.9.1 + +--===============9060418334135509864== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: application/pgp-encrypted; charset="us-ascii" + +Version: 1 +--===============9060418334135509864== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: application/octet-stream; charset="us-ascii" + +-----BEGIN PGP MESSAGE----- + +hIwDxE023q1UqxYBBACGKSDv5/rcwScSf9n33cZZPPxltQgxkDaClMGY2DARgebE +9rpE2O/4eoaEbdu+2shahPLIbD0wuRiGVpMIIloNNucl3f15h1wXPZbTwK7sNxJq +HycSf8sT1fkolmC9s9X0r5xHgk0G4klAqr5C3GOk7Y6wsHTYGqzDvBFEB0LvaoUC +DANw48DehwaEUQEP/iaiYeMDUsOzFZodKZOlQWese2JPTsRwF7KrTl8C93MrZqAh +A1pQjUH9cafj8mwDXA9ZCsYZs7r84IxShI+dUhinBSCq8F61OlLP859wq+wpKU7n +PNVA5bfd//4hRFvDT33ZlgeeeCcRo7h4IDjJwFDYsf0Ysqvo+IKipVNXXlAcGYYI +DVBucB0fYaVHWRKxw50mo02zKP2/GW6K3p1nxGTf73cKjc9of+VOvvMByODaJ+ne +WIuzZMqz0vfQ0UvRVBjlsXtB7VpCqJZWkubqqwwP6+2WOCA/c5LC3z2f/BK90EQh +/JrKfDR083UNhu4SjNwL/TF4ET33JHf1u5Gmzqx+eO00pfhVvkyz6LYImkE8ky/+ +bXyJY8iDq7dxtfqhzZbeNe4fafU/avXxTA5UkWTnYhCqyd2bvAYH3Ep3L7lSv6SQ +Tsy0jjTsWJoSq6jRIzJuo2mX2MBKPBfLZs4tH71/4RppECletNnS4ZlxiV4LNrWE +LrXQvE1V+mJ82muucIe7w52nf3UWjQqTA73+Ml0aK+lIhbckRIovAw1sGzRrbTEX +xLCgz7BYDMhs5mgtfiMAzGox4xGxi56Ge519vdbddan4G92mPlLl1IPOXkO8GyLO +D4IiPp5ilPy6uThuxxIFemxxUREbPrfLJNA8W7aRPrHz4YcgZhrAV9I4C+xE0ukB +i8MoJeFvbCGPyTwVDn8XfFKynlZYm1f8NIVMSj5JfV8J3Om9jzDp6hx+52iUQEbW +C9g4kfPZY8h0RMggdOlZsaR8j26xuW+fEtz7ucJIqfJ/ElTH+4bm8MK2qPZniRWv +ej2md4bP4Bo5DXydzxz7O7TBL6/Jsp7pJhHUUb36OnTWvInyg71LPT1QIxdRvXr9 +vNhrEBDX3MNf7RyXczvBcc+cLRo+zV+T4b8wd2kwXskWgKrGUJEe2TItdsafaQ9B +BkuVGu6cIDa6STyCJiOB68KIXiDuADSWJiiR+gZr4eU6vzfhR27LMQt/g+oPW+U4 +1AvzUl9uXjTMC2vFuTQ4M2g0WmksCNpPpzOu/QlBmRqpP2Fg9UuLv6ITWeCxp439 +g5NT5KXE2IiruL/DS0KEpWVNe4ayGzRvMawFuU582xbIzXjvilUZrW+p6req+oeF +QWTWHGDojTvULQPV2c91lWnLaNXVethfF4hrM5MIL+EwVs3sUXFMr1kX7VNrK0QH +Uos7nc4G9xngpdvwF4ImldD83O8qxOVzIfk5Dhz+etTH4HbnnDvmQ/FIYvjzGviR +ZeVwdCjv/9TC4yY3nJFKMwGp70jVa1vbmWL68HVNRyAVwnQnu2UlI8UR43iVZyUH +ZY0Nr0rbse/pvZyX4//EVOFLUR7K4GG0N4Kz41q5ZB8rI4Fl6QJhgIFJds13iM77 +n+wqLQE7kllgI32E4U9B +=YlXg +-----END PGP MESSAGE----- + +--===============9060418334135509864==-- diff --git a/test/emacs-tree.expected-output/result-format-function b/test/emacs-tree.expected-output/result-format-function new file mode 100644 index 00000000..7eb24696 --- /dev/null +++ b/test/emacs-tree.expected-output/result-format-function @@ -0,0 +1,53 @@ + 2010-12-29 François Boulogne ─►[aur-general] Guidelines: cp, mkdir vs install ( ui) + 2010-12-16 Olivier Berger ─►Essai accentué ( ui) + 2009-11-18 Chris Wilson ─►[notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once ( ui) + 2009-11-18 Alex Botero-Lowry ┬►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (& ui) + 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop ( ui) + 2009-11-17 Ingmar Vanhassel ┬►[notmuch] [PATCH] Typsos ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-17 Adrian Perez de Cast ┬►[notmuch] Introducing myself ( =ui) + 2009-11-18 Keith Packard ├─► ... ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-17 Israel Herraiz ┬►[notmuch] New to the list ( ui) + 2009-11-18 Keith Packard ├─► ... ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-17 Jan Janak ┬►[notmuch] What a great idea! ( ui) + 2009-11-17 Jan Janak ├─► ... ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-17 Jan Janak ┬►[notmuch] [PATCH] Older versions of install do not support -C. ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-17 Aron Griffis ┬►[notmuch] archive ( ui) + 2009-11-18 Keith Packard ╰┬► ... ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-17 Keith Packard ┬►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags ( ui) + 2009-11-18 Carl Worth ╰─►[notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags ( ui) + 2009-11-17 Lars Kellogg-Stedman ┬►[notmuch] Working with Maildir storage? ( = i) + 2009-11-17 Mikhail Gusarov ├┬► ... ( =ui) + 2009-11-17 Lars Kellogg-Stedman │╰┬► ... ( =ui) + 2009-11-17 Mikhail Gusarov │ ├─► ... ( ui) + 2009-11-17 Keith Packard │ ╰┬► ... ( ui) + 2009-11-18 Lars Kellogg-Stedman │ ╰─► ... ( =ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-17 Mikhail Gusarov ┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers ( i) + 2009-11-17 Mikhail Gusarov ├─►[notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4 ( ui) + 2009-11-17 Carl Worth ╰┬►[notmuch] [PATCH 1/2] Close message file after parsing message headers ( ui) + 2009-11-17 Keith Packard ╰┬► ... ( ui) + 2009-11-18 Carl Worth ╰─► ... ( ui) + 2009-11-18 Keith Packard ┬►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap ( ui) + 2009-11-18 Alexander Botero-Low ╰─►[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap ( ui) + 2009-11-18 Alexander Botero-Low ─►[notmuch] request for pull ( ui) + 2009-11-18 Jjgod Jiang ┬►[notmuch] Mac OS X/Darwin compatibility issues ( ui) + 2009-11-18 Alexander Botero-Low ╰┬► ... ( ui) + 2009-11-18 Jjgod Jiang ╰┬► ... ( ui) + 2009-11-18 Alexander Botero-Low ╰─► ... ( ui) + 2009-11-18 Rolland Santimano ─►[notmuch] Link to mailing list archives ? ( ui) + 2009-11-18 Jan Janak ─►[notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags ( ui) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] count_files: sort directory in inode order before statting ( ui) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH 2/2] Read mail directory in inode number order ( ui) + 2009-11-18 Stewart Smith ─►[notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. ( ui) + 2009-11-18 Lars Kellogg-Stedman ┬►[notmuch] "notmuch help" outputs to stderr? (&=ui) + 2009-11-18 Lars Kellogg-Stedman ╰─► ... (&=ui) + 2009-11-17 Mikhail Gusarov ─►[notmuch] [PATCH] Handle rename of message file ( ui) + 2009-11-17 Alex Botero-Lowry ┬►[notmuch] preliminary FreeBSD support (& ui) + 2009-11-17 Carl Worth ╰─► ... ( ui) +End of search results. diff --git a/test/emacs-unthreaded.expected-output/result-format-function b/test/emacs-unthreaded.expected-output/result-format-function new file mode 100644 index 00000000..bcb10b96 --- /dev/null +++ b/test/emacs-unthreaded.expected-output/result-format-function @@ -0,0 +1,53 @@ + 2010-12-29 François Boulogne [aur-general] Guidelines: cp, mkdir vs install ( ui) + 2010-12-16 Olivier Berger Essai accentué ( ui) + 2009-11-18 Chris Wilson [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once( ui) + 2009-11-18 Carl Worth [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop( ui) + 2009-11-18 Carl Worth [notmuch] [PATCH] Typsos ( ui) + 2009-11-18 Carl Worth [notmuch] Introducing myself ( ui) + 2009-11-18 Carl Worth [notmuch] New to the list ( ui) + 2009-11-18 Carl Worth [notmuch] What a great idea! ( ui) + 2009-11-18 Carl Worth [notmuch] [PATCH] Older versions of install do not support -C.( ui) + 2009-11-18 Carl Worth [notmuch] archive ( ui) + 2009-11-18 Carl Worth [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags( ui) + 2009-11-18 Carl Worth [notmuch] Working with Maildir storage? ( ui) + 2009-11-18 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) + 2009-11-18 Alexander Botero-Low[notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap( ui) + 2009-11-18 Keith Packard [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap( ui) + 2009-11-18 Alexander Botero-Low[notmuch] request for pull ( ui) + 2009-11-18 Alexander Botero-Low[notmuch] Mac OS X/Darwin compatibility issues ( ui) + 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues ( ui) + 2009-11-18 Alexander Botero-Low[notmuch] Mac OS X/Darwin compatibility issues ( ui) + 2009-11-18 Rolland Santimano [notmuch] Link to mailing list archives ? ( ui) + 2009-11-18 Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags( ui) + 2009-11-18 Jjgod Jiang [notmuch] Mac OS X/Darwin compatibility issues ( ui) + 2009-11-18 Stewart Smith [notmuch] [PATCH] count_files: sort directory in inode order before statting( ui) + 2009-11-18 Keith Packard [notmuch] archive ( ui) + 2009-11-18 Keith Packard [notmuch] Introducing myself ( ui) + 2009-11-18 Keith Packard [notmuch] New to the list ( ui) + 2009-11-18 Stewart Smith [notmuch] [PATCH 2/2] Read mail directory in inode number order( ui) + 2009-11-18 Stewart Smith [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs.( ui) + 2009-11-18 Lars Kellogg-Stedman[notmuch] "notmuch help" outputs to stderr? (&=ui) + 2009-11-18 Lars Kellogg-Stedman[notmuch] "notmuch help" outputs to stderr? (&=ui) + 2009-11-18 Lars Kellogg-Stedman[notmuch] Working with Maildir storage? ( =ui) + 2009-11-18 Alex Botero-Lowry [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop(& ui) + 2009-11-17 Ingmar Vanhassel [notmuch] [PATCH] Typsos ( ui) + 2009-11-17 Aron Griffis [notmuch] archive ( ui) + 2009-11-17 Adrian Perez de Cast[notmuch] Introducing myself ( =ui) + 2009-11-17 Israel Herraiz [notmuch] New to the list ( ui) + 2009-11-17 Jan Janak [notmuch] What a great idea! ( ui) + 2009-11-17 Jan Janak [notmuch] What a great idea! ( ui) + 2009-11-17 Jan Janak [notmuch] [PATCH] Older versions of install do not support -C.( ui) + 2009-11-17 Keith Packard [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags( ui) + 2009-11-17 Keith Packard [notmuch] Working with Maildir storage? ( ui) + 2009-11-17 Keith Packard [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH] Handle rename of message file ( ui) + 2009-11-17 Mikhail Gusarov [notmuch] Working with Maildir storage? ( ui) + 2009-11-17 Lars Kellogg-Stedman[notmuch] Working with Maildir storage? ( =ui) + 2009-11-17 Carl Worth [notmuch] preliminary FreeBSD support ( ui) + 2009-11-17 Alex Botero-Lowry [notmuch] preliminary FreeBSD support (& ui) + 2009-11-17 Mikhail Gusarov [notmuch] Working with Maildir storage? ( =ui) + 2009-11-17 Lars Kellogg-Stedman[notmuch] Working with Maildir storage? ( =ui) + 2009-11-17 Carl Worth [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 2/2] Include to get uint32_t in C++ file with gcc 4.4( ui) + 2009-11-17 Mikhail Gusarov [notmuch] [PATCH 1/2] Close message file after parsing message headers( ui) +End of search results. diff --git a/test/emacs.expected-output/search-result-format-function b/test/emacs.expected-output/search-result-format-function new file mode 100644 index 00000000..08b4bee7 --- /dev/null +++ b/test/emacs.expected-output/search-result-format-function @@ -0,0 +1,25 @@ + ui 2010-12-29 [1/1] François Boulogne [aur-general] Guidelines: cp, mkdir vs install (inbox unread) + ui 2010-12-16 [1/1] Olivier Berger Essai accentué (inbox unread) + ui 2009-11-18 [1/1] Chris Wilson [notmuch] [PATCH 1/2] Makefile: evaluate pkg-config once (inbox unread) +& ui 2009-11-18 [2/2] Alex Botero-Lowry, Carl Worth [notmuch] [PATCH] Error out if no query is supplied to search instead of going into an infinite loop (attachment inbox unread) + ui 2009-11-18 [2/2] Ingmar Vanhassel, Carl Worth [notmuch] [PATCH] Typsos (inbox unread) + =ui 2009-11-18 [3/3] Adrian Perez de Castro, Keith Packard, Carl Worth [notmuch] Introducing myself (inbox signed unread) + ui 2009-11-18 [3/3] Israel Herraiz, Keith Packard, Carl Worth [notmuch] New to the list (inbox unread) + ui 2009-11-18 [3/3] Jan Janak, Carl Worth [notmuch] What a great idea! (inbox unread) + ui 2009-11-18 [2/2] Jan Janak, Carl Worth [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread) + ui 2009-11-18 [3/3] Aron Griffis, Keith Packard, Carl Worth [notmuch] archive (inbox unread) + ui 2009-11-18 [2/2] Keith Packard, Carl Worth [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread) + =ui 2009-11-18 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth [notmuch] Working with Maildir storage? (inbox signed unread) + ui 2009-11-18 [5/5] Mikhail Gusarov, Carl Worth, Keith Packard [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread) + ui 2009-11-18 [2/2] Keith Packard, Alexander Botero-Lowry [notmuch] [PATCH] Create a default notmuch-show-hook that highlights URLs and uses word-wrap (inbox unread) + ui 2009-11-18 [1/1] Alexander Botero-Lowry [notmuch] request for pull (inbox unread) + ui 2009-11-18 [4/4] Jjgod Jiang, Alexander Botero-Lowry [notmuch] Mac OS X/Darwin compatibility issues (inbox unread) + ui 2009-11-18 [1/1] Rolland Santimano [notmuch] Link to mailing list archives ? (inbox unread) + ui 2009-11-18 [1/1] Jan Janak [notmuch] [PATCH] notmuch new: Support for conversion of spool subdirectories into tags (inbox unread) + ui 2009-11-18 [1/1] Stewart Smith [notmuch] [PATCH] count_files: sort directory in inode order before statting (inbox unread) + ui 2009-11-18 [1/1] Stewart Smith [notmuch] [PATCH 2/2] Read mail directory in inode number order (inbox unread) + ui 2009-11-18 [1/1] Stewart Smith [notmuch] [PATCH] Fix linking with gcc to use g++ to link in C++ libs. (inbox unread) +&=ui 2009-11-18 [2/2] Lars Kellogg-Stedman [notmuch] "notmuch help" outputs to stderr? (attachment inbox signed unread) + ui 2009-11-17 [1/1] Mikhail Gusarov [notmuch] [PATCH] Handle rename of message file (inbox unread) +& ui 2009-11-17 [2/2] Alex Botero-Lowry, Carl Worth [notmuch] preliminary FreeBSD support (attachment inbox unread) +End of search results. diff --git a/test/random-corpus.c b/test/random-corpus.c index 7cde22c5..8ae08971 100644 --- a/test/random-corpus.c +++ b/test/random-corpus.c @@ -122,7 +122,8 @@ const notmuch_opt_desc_t notmuch_shared_options[] = { const char *notmuch_requested_db_uuid = NULL; void -notmuch_process_shared_options (unused (const char *dummy)) +notmuch_process_shared_options (unused (notmuch_database_t *notmuch), + unused (const char *dummy)) { } diff --git a/test/test-lib.el b/test/test-lib.el index 32d53736..c840bc98 100644 --- a/test/test-lib.el +++ b/test/test-lib.el @@ -159,6 +159,21 @@ running, quit if it terminated." (lambda (x) `(prog1 ,x (notmuch-post-command))) body))) +;; For testing functions in +;; notmuch-{search,tree,unsorted}-result-format +(defun notmuch-test-result-flags (format-string result) + (let ((tags-to-letters (quote (("attachment" . "&") + ("signed" . "=") + ("unread" . "u") + ("inbox" . "i")))) + (tags (plist-get result :tags))) + (format format-string + (mapconcat (lambda (t2l) + (if (member (car t2l) tags) + (cdr t2l) + " ")) + tags-to-letters "")))) + ;; For historical reasons, we hide deleted tags by default in the test ;; suite (setq notmuch-tag-deleted-formats diff --git a/util/unicode-util.h b/util/unicode-util.h index 32d1e6ef..1bb9336a 100644 --- a/util/unicode-util.h +++ b/util/unicode-util.h @@ -4,9 +4,16 @@ #include #include +#ifdef __cplusplus +extern "C" { +#endif + /* The utf8 encoded string would tokenize as a single word, according * to xapian. */ bool unicode_word_utf8 (const char *str); typedef gunichar notmuch_unichar; +#ifdef __cplusplus +} +#endif #endif