1This file describes the schemata used for notmuch's structured output
2format (currently JSON and S-Expressions).
3
4[]'s indicate lists.  List items can be marked with a '?', meaning
5they are optional; or a '*', meaning there can be zero or more of that
6item.  {}'s indicate an object that maps from field identifiers to
7values.  An object field marked '?' is optional; one marked with '*'
8can repeat (with a different name). |'s indicate alternates (e.g.,
9int|string means something can be an int or a string).
10
11For S-Expression output, lists are printed delimited by () instead of
12[]. Objects are printed as p-lists, i.e. lists where the keys and values
13are interleaved. Keys are printed as keywords (symbols preceded by a
14colon), e.g. (:id "123" :time 54321 :from "foobar"). Null is printed as
15nil, true as t and false as nil.
16
17This is version 5 of the structured output format.
18
19Version history
20---------------
21
22v1
23- First versioned schema release.
24- Added part.content-length and part.content-transfer-encoding fields.
25
26v2
27- Added the thread_summary.query field.
28
29v3
30- Replaced message.filename string with a list of filenames.
31- Added part.content-disposition field.
32
33v4
34- replace signature error integer bitmask with a set of flags for
35  individual errors.
36- (notmuch 0.29) added message.crypto to identify overall message
37  cryptographic state
38
39v5
40- sorting support for notmuch show (no change to actual schema,
41  just new command line argument)
42
43Common non-terminals
44--------------------
45
46# Number of seconds since the Epoch
47unix_time = int
48
49# Thread ID, sans "thread:"
50threadid = string
51
52# Message ID, sans "id:"
53messageid = string
54
55# E-mail header name, sans trailing colon, like "Subject" or "In-Reply-To"
56header_name = string
57
58notmuch show schema
59-------------------
60
61# A top-level set of threads (do_show)
62# Returned by notmuch show without a --part argument
63thread_set = [thread*]
64
65# Top-level messages in a thread (show_messages)
66thread = [thread_node*]
67
68# A message and its replies (show_messages)
69thread_node = [
70    message|null,             # null if not matched and not --entire-thread
71    [thread_node*]            # children of message
72]
73
74# A message (format_part_sprinter)
75message = {
76    # (format_message_sprinter)
77    id:             messageid,
78    match:          bool,
79    filename:	    [string*],
80    timestamp:      unix_time, # date header as unix time
81    date_relative:  string,   # user-friendly timestamp
82    tags:           [string*],
83
84    headers:        headers,
85    crypto:         crypto,
86    body?:          [part]    # omitted if --body=false
87}
88
89# when showing the message, was any or all of it decrypted?
90msgdecstatus: "full"|"partial"
91
92# The overall cryptographic state of the message as a whole:
93crypto = {
94    signed?:    {
95                  status:      sigstatus,
96                  # was the set of signatures described under encrypted cover?
97                  encrypted:   bool,
98                  # which of the headers is covered by sigstatus?
99                  headers:     [header_name*]
100                },
101    decrypted?: {
102                  status: msgdecstatus,
103                  # map encrypted headers that differed from the outside headers.
104                  # the value of each item in the map is what that field showed externally
105                  # (maybe null if it was not present in the external headers).
106                  header-mask:  { header_name*: string|null }
107                }
108}
109
110# A MIME part (format_part_sprinter)
111part = {
112    id:             int|string, # part id (currently DFS part number)
113
114    encstatus?:     encstatus,
115    sigstatus?:     sigstatus,
116
117    content-type:   string,
118    content-disposition?:       string,
119    content-id?:    string,
120    # if content-type starts with "multipart/":
121    content:        [part*],
122    # if content-type is "message/rfc822":
123    content:        [{headers: headers, body: [part]}],
124    # otherwise (leaf parts):
125    filename?:      string,
126    content-charset?: string,
127    # A leaf part's body content is optional, but may be included if
128    # it can be correctly encoded as a string.  Consumers should use
129    # this in preference to fetching the part content separately.
130    content?:       string,
131    # If a leaf part's body content is not included, the length of
132    # the encoded content (in bytes) may be given instead.
133    content-length?: int,
134    # If a leaf part's body content is not included, its transfer encoding
135    # may be given.  Using this and the encoded content length, it is
136    # possible for the consumer to estimate the decoded content length.
137    content-transfer-encoding?: string
138}
139
140# The headers of a message or part (format_headers_sprinter with reply = FALSE)
141headers = {
142    Subject:        string,
143    From:           string,
144    To?:            string,
145    Cc?:            string,
146    Bcc?:           string,
147    Reply-To?:      string,
148    Date:           string
149}
150
151# Encryption status (format_part_sprinter)
152encstatus = [{status: "good"|"bad"}]
153
154# Signature status (format_part_sigstatus_sprinter)
155sigstatus = [signature*]
156
157signature = {
158    # (signature_status_to_string)
159    status:         "good"|"bad"|"error"|"unknown",
160    # if status is "good":
161    fingerprint?:   string,
162    created?:       unix_time,
163    expires?:       unix_time,
164    userid?:        string
165    email?:         string
166    # if status is not "good":
167    keyid?:         string
168    errors?: 	    sig_errors
169}
170
171sig_errors = {
172    key-revoked?: bool,
173    key-expired?: bool,
174    sig-expired?: bool,
175    key-missing?: bool,
176    alg-unsupported?: bool,
177    crl-missing?: bool,
178    crl-too-old?: bool,
179    bad-policy?: bool,
180    sys-error?: bool,
181    tofu-conflict?: bool
182}
183
184notmuch search schema
185---------------------
186
187# --output=summary
188search_summary = [thread_summary*]
189
190# --output=threads
191search_threads = [threadid*]
192
193# --output=messages
194search_messages = [messageid*]
195
196# --output=files
197search_files = [string*]
198
199# --output=tags
200search_tags = [string*]
201
202thread_summary = {
203    thread:         threadid,
204    timestamp:      unix_time,
205    date_relative:  string,   # user-friendly timestamp
206    matched:        int,      # number of matched messages
207    total:          int,      # total messages in thread
208    authors:        string,   # comma-separated names with | between
209                              # matched and unmatched
210    subject:        string,
211    tags:           [string*],
212
213    # Two stable query strings identifying exactly the matched and
214    # unmatched messages currently in this thread.  The messages
215    # matched by these queries will not change even if more messages
216    # arrive in the thread.  If there are no matched or unmatched
217    # messages, the corresponding query will be null (there is no
218    # query that matches nothing).  (Added in schema version 2.)
219    query:          [string|null, string|null],
220}
221
222notmuch reply schema
223--------------------
224
225reply = {
226    # The headers of the constructed reply
227    reply-headers: reply_headers,
228
229    # As in the show format (format_part_sprinter)
230    original: message
231}
232
233# Reply headers (format_headers_sprinter with reply = TRUE)
234reply_headers = {
235    Subject:        string,
236    From:           string,
237    To?:            string,
238    Cc?:            string,
239    Bcc?:           string,
240    In-reply-to:    string,
241    References:     string
242}
243