NOTMUCH-SEXP-QUERIES(7)

πŸŽ€ notmuch-sexp-queries πŸ’…

okay so bestie, this is section 7 of the manual and it's the one about s-expression syntax for notmuch queries 🌸 β€” aka the cute parentheses way to ask notmuch for your mail. iykyk.


✨ NAME ✨

notmuch-sexp-queries β€” s-expression syntax for notmuch queries πŸ¦‹


πŸ’– SYNOPSIS πŸ’–

notmuch subcommand --query=sexp [option ...]  -- '(and (to santa) (date
december))'

🌈 DESCRIPTION 🌈

so notmuch also speaks an alternative query syntax built on S-expressions <https://en.wikipedia.org/wiki/S-expression> πŸ’…. you can pick it with the command line option --query=sexp, or by passing the right option to the library function notmuch_query_create_with_syntax().

and here's the thing β€” support for this syntax is currently optional, so like, your build might not even have it. you can vibe-check your own build with this:

$ notmuch config get built_with.sexp_queries

🧚 S-EXPRESSIONS 🧚

an s-expression is either an atom, or a list of whitespace delimited s-expressions inside parentheses. and atoms are either…

  • basic value β€” a basic value is an unquoted string that contains no whitespace, double quotes, or parentheses. clean, simple, no drama πŸ’œ
  • quoted string β€” double quotes (") delimit strings that can contain whitespace or parentheses. and you can even put double quote characters in there by escaping with a 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 β€” which is a prefix atom (the first element) defining a field, logical operation, or modifier, followed by 0 or more subqueries. iconic structure honestly.

  • * β€” matches any non-empty string in the current field ⭐
  • () β€” the empty list matches all messages πŸ‘‘
  • term β€” matches all messages containing term, possibly after stemming or phrase splitting. for the whole stemming discussion in notmuch, go peek at notmuch-search-terms <#notmuch-search-terms-7>. quick tea: stemming only applies to unquoted terms (basic values) in s-expression queries. and for the phrase splitting situation, see FIELDS.
  • ( field q1 q2 ... qn ) β€” restricts the queries q1 to qn to field, and combines with and (for most fields) or or. more on that in FIELDS.
  • ( operator q1 q2 ... qn ) β€” combines queries q1 to qn. the operators supported right now are and, or, and not. heads up: (not q1 ... qn ) is equivalent to (and (not q1 ) ... (not qn )) 😭
  • ( modifier q1 q2 ... qn ) β€” combines queries q1 to qn, and reinterprets the result (e.g. as a regular expression). see MODIFIERS for the whole deal.
  • (macro ( p1 ... pn ) body) β€” defines a saved query with parameter substitution. this syntax is recognized only in saved s-expression queries (see squery.* in notmuch-config <#notmuch-config-1>). parameter names in body must be prefixed with , to be expanded (see MACRO EXAMPLES). macros may refer to other macros, but only to their own parameters [1].

🌸 FIELDS 🌸

fields [2] correspond to attributes of mail messages πŸ’Œ. some are inherent (and immutable) like subject, while others like tag and property are settable by the user. every concrete field in the list below gets discussed further under "Search prefixes" in notmuch-search-terms <#notmuch-search-terms-7>. and the row called user refers to user defined fields, described in notmuch-config <#notmuch-config-1>.

most fields are either phrase fields [3] (which match sequences of words) or term fields [4] (which match exact strings). phrase splitting breaks the term (basic value or quoted string) into words, ignoring punctuation. phrase splitting is applied to terms in phrase (probabilistic) fields. and both phrase splitting and stemming apply only in phrase fields, no cap.

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. generally it's or for the 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. πŸ™ˆ

Range fields take one or two arguments specifying lower and upper bounds. one argument is interpreted as identical upper and lower bounds. either the upper or lower bound may be specified as "" or * to specify the lowest possible lower bound or the highest possible upper bound. periodt.

lastmod ranges support negative arguments, interpreted relative to the most recent database revision (see count --lastmod <#cmdoption-count-lastmod>). πŸ”₯


πŸ‘‘ Fields with supported modifiers πŸ‘‘

  • none β€” combine: and; type: (none); expand: no; wildcard: yes; regex: no
  • user β€” combine: and; type: phrase; expand: no; wildcard: yes; regex: no
  • attachment β€” combine: and; type: phrase; expand: yes; wildcard: yes; regex: no
  • body β€” combine: and; type: phrase; expand: no; wildcard: no; regex: no
  • date β€” combine: (blank); type: range; expand: no; wildcard: no; regex: no
  • folder β€” combine: or; type: phrase; expand: yes; wildcard: yes; regex: yes
  • from β€” combine: and; type: phrase; expand: yes; wildcard: yes; regex: yes
  • id β€” combine: or; type: term; expand: no; wildcard: yes; regex: yes
  • is β€” combine: and; type: term; expand: yes; wildcard: yes; regex: yes
  • lastmod β€” combine: (blank); type: range; expand: no; wildcard: no; regex: no
  • mid β€” combine: or; type: term; expand: no; wildcard: yes; regex: yes
  • mimetype β€” combine: or; type: phrase; expand: yes; wildcard: yes; regex: no
  • path β€” combine: or; type: term; expand: no; wildcard: yes; regex: yes
  • property β€” combine: and; type: term; expand: yes; wildcard: yes; regex: yes
  • subject β€” combine: and; type: phrase; expand: yes; wildcard: yes; regex: yes
  • tag β€” combine: and; type: term; expand: yes; wildcard: yes; regex: yes
  • thread β€” combine: or; type: term; expand: yes; wildcard: yes; regex: yes
  • to β€” combine: and; type: phrase; expand: yes; wildcard: yes; regex: no

πŸ’… MODIFIERS πŸ’…

modifiers refer to any prefixes (first elements of compound queries) that are neither operators nor fields. the little wildcards of the syntax, if you will ✨

  • (infix atom ) β€” interpret atom as an infix notmuch query (see notmuch-search-terms <#notmuch-search-terms-7>). not supported inside fields. 😀
  • (matching q1 q2 ... qn ) / (of q1 q2 ... qn ) β€” match all messages that have the same values of the current field as those matching all of q1 ... qn. supported in most term [7] or phrase fields. most commonly used in the thread field. this one's a real one.
  • (query atom ) β€” expand to the saved query named by atom. see notmuch-config <#notmuch-config-1> for more. note that the saved query must be in infix syntax (notmuch-search-terms <#notmuch-search-terms-7>). not supported inside fields.
  • (regex atom ) / (rx atom ) β€” interpret atom as a POSIX.2 regular expression (see regex(7) <https://manpages.debian.org/regex.7.html>). this applies in term fields and a subset [5] of phrase fields (see Fields with supported modifiers).
  • (starts-with subword ) β€” matches any term starting with subword. this applies in either phrase or term fields, or outside of fields [6]. ⚠️ note that a starts-with query cannot be part of a phrase. and the atom * is a synonym for (starts-with ""). slay.

more parts are coming bestie, don't you dare close the tab 🫢

πŸŽ€ EXAMPLES πŸŽ€

ok bestie these are the lil' glow-ups of what s-expressions actually do, one by one, no skips πŸ’–

  • Wizard β€” matches every message with the word "wizard" in it, and it does not care about case, periodt.
  • added β€” matches every message with "added"... but ALSO "add", "additional", "Additional", "adds", etc. ✨ that's stemming, babe, it's giving word-family reunion.
  • (and Bob Marley) β€” matches messages with the words "Bob" AND "Marley", or their stems. and no, they do NOT need to be sitting next to each other, they can be whole paragraphs apart and still slay 🫢
  • (not Bob Marley) β€” matches messages with neither "Bob" nor "Marley", nor their stems. ghosted, blocked, unfollowed πŸ’…
  • "quick fox" quick-fox quick@fox β€” matches the phrase "quick" followed by "fox" in phrase fields (or outside a field). in a term field it matches the literal string. two totally different vibes depending on where it lands, iykyk.
  • (folder (of (id 1234@invalid))) β€” matches any message in the same folder as the one with Message-Id "1234@invalid". ride or die by folder πŸ¦‹
  • (id 1234@invalid blah@test) β€” matches Message-Id "1234@invalid" OR Message-Id "blah@test". either one, it's not picky.
  • (and (infix "date:2009-11-18..2009-11-18") (tag unread)) β€” matches messages in the given date range with tag unread.
  • (and (date 2009-11-18 2009-11-18) (tag unread)) β€” matches messages in the given date range with tag unread. (same energy as the infix one above, different outfit ✨)
  • (and (date 2009-11-18 *) (tag unread)) β€” matches messages from 2009-11-18 or later with tag unread. the * means "and onwards, no cap".
  • (and (date * 2009-11-18) (tag unread)) β€” matches messages from 2009-11-18 or earlier with tag unread.
  • (starts-with prelim) β€” matches any words starting with "prelim". yes, it's giving autocomplete but make it a query.
  • (subject quick "brown fox") β€” matches 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. it's the same word, but it lives in the subject now, main character energy πŸ’–
  • (subject (starts-with quick) "brown fox") β€” matches messages whose subject contains "quick brown fox", but ALSO "brown fox quicksand". sneaky lil' bonus match, we love her.
  • (thread (of (id 1234@invalid))) β€” matches any message in the same thread as the one with Message-Id "1234@invalid". the whole group chat, not just the one message.
  • (thread (matching (from bob@example.com) (to bob@example.com))) β€” matches any (messages in) a thread containing a message from "bob@example.com" and a (possibly distinct) message to "bob@example.com". bob said it, bob heard it, iconic.
  • (to (or bob@example.com mallory@example.org)) (or (to bob@example.com) (to mallory@example.org)) β€” matches 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". heads up bestie, that's a real side effect, not a typo β€” substring matching said hi.
  • (not (to *)) β€” matches messages with an empty or invalid 'To' and 'Cc' field. the ones nobody got tagged in 😭
  • (List *) β€” matches messages with a non-empty List-Id header, assuming configuration index.header.List=List-Id. config first, then she eats πŸ’…

🧚 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. yes girl, macros inside macros, it's giving inception but cute 🌸

$ 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. build once, spam forever, that's the dream 🫢

$ notmuch config set squery.About '(macro (name) (or (subject ,name) (List ,name)))'
$ notmuch search --query=sexp '(About notmuch)'

πŸ‘‘ NOTES πŸ‘‘

  1. Technically macros implement 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.
  2. a.k.a. prefixes
  3. a.k.a. probabilistic prefixes
  4. a.k.a. boolean prefixes
  5. Due to the implementation of phrase fields in Xapian, regex queries could only match individual words.
  6. Due to the way body is implemented in notmuch, this modifier is not supported in the body field.
  7. Due to the way recursive path queries are implemented in notmuch, this modifier is not supported in the path field.

✨ Author ✨

Carl Worth and many others

2009-2026, Carl Worth and many others


and that's a wrap bestie!! πŸŽ€ this lil' masterpiece is from the notmuch 0.40 era, dated Jan 31, 2026, and it answers to the name NOTMUCH-SEXP-QUERIES(7) πŸ’– go forth and query, you absolute legend πŸ‘‘πŸŒΈ

πŸ“¦ where this page comes from