···122122/// don't need to call `ThreadsIterator.deinit` and all the memory will still be
123123/// reclaimed when the query is destroyed.
124124pub fn searchThreads(self: *const Query) Error!ThreadsIterator {
125125- var out: ?*c.notmuch_threads_t = undefined;
125125+ var threads: ?*c.notmuch_threads_t = null;
126126127127- try wrap(c.notmuch_query_search_threads(self.query, &out));
127127+ try wrap(c.notmuch_query_search_threads(self.query, &threads));
128128129129 return .{
130130- .threads = out,
130130+ .threads = threads,
131131 };
132132}
133133134134/// Execute a query for messages, returning a MessagesIterator object which can
135135/// be used to iterate over the results. The returned messages object is owned
136136-/// by the query and as such, will only be valid until Query.deinit.
136136+/// by the query and as such, will only be valid until `Query.deinit`.
137137+///
138138+/// Typical usage might be:
139139+/// ```
140140+/// const db = try Database.open(…);
141141+/// defer db.deinit();
142142+/// const query = db.queryCreate(query_string);
143143+/// defer query.deinit();
144144+/// var it = query.searchMessages();
145145+/// defer it.deinit();
146146+/// while (try it.next()) |message| {
147147+/// defer message.deinit();
148148+/// …
149149+/// }
150150+/// ```
137151///
138152/// Note: If you are finished with a message before its containing query, you
139139-/// can call Message.deinit to clean up some memory sooner (as in the
153153+/// can call `Message.deinit` to clean up some memory sooner (as in the
140154/// above example). Otherwise, if your message objects are long-lived, then you
141141-/// don't need to call Message.deinit and all the memory will still be
142142-/// reclaimed when the query is destroyed.
155155+/// don't need to call `Message.deinit` and all the memory will still be
156156+/// reclaimed when the query is deinitialized.
143157pub fn searchMessages(self: *const Query) Error!MessagesIterator {
144158 var out: ?*c.notmuch_messages_t = undefined;
145159···150164 };
151165}
152166167167+pub const CountMessagesError = error{
168168+ /// A Xapian error occurred.
169169+ XapianError,
170170+};
171171+153172/// Return the number of messages matching a search.
154173///
155174/// This function performs a search and returns the number of matching messages.
156156-pub fn countMessages(self: *const Query) Error!usize {
175175+pub fn countMessages(self: *const Query) CountMessagesError!u32 {
176176+ std.debug.assert(@typeInfo(c_uint).int.bits == 32);
157177 var count: c_uint = undefined;
158158- try wrap(c.notmuch_query_count_messages(self.query, &count));
159159- return @intCast(count);
178178+ return switch (status(c.notmuch_query_count_messages(self.query, &count))) {
179179+ .success => @intCast(count),
180180+ .xapian_error => error.XapianError,
181181+ else => unreachable,
182182+ };
160183}
161184185185+pub const CountThreadsError = error{
186186+ /// Memory allocation failed.
187187+ OutOfMemory,
188188+ /// A Xapian error occurred.
189189+ XapianError,
190190+};
191191+162192/// Return the number of threads matching a search.
163193///
164164-/// This function performs a search and returns the number of matching threads.
165165-pub fn countThreads(self: *const Query) Error!usize {
194194+/// This function performs a search and returns the number of unique thread IDs
195195+/// in the matching messages. This is the same as number of threads matching
196196+/// a search.
197197+///
198198+/// Note that this is a significantly heavier operation than
199199+/// `countMessages`.
200200+pub fn countThreads(self: *const Query) CountThreadsError!u32 {
201201+ std.debug.assert(@typeInfo(c_uint).int.bits == 32);
166202 var count: c_uint = undefined;
167167- try wrap(c.notmuch_query_count_threads(self.query, &count));
168168- return @intCast(count);
203203+ return switch (status(c.notmuch_query_count_threads(self.query, &count))) {
204204+ .success => @intCast(count),
205205+ .out_of_memory => error.OutOfMemory,
206206+ .xapian_exception => error.XapianError,
207207+ else => unreachable,
208208+ };
169209}
170210211211+/// Deinitialize the `Query` along with any associated resources.
212212+///
213213+/// This will in turn deinitialize any `ThreadsIterator` and `MessageIterator`
214214+/// objects generated by this query, (and in turn any `Thread` and `Message`
215215+/// objects generated from those results, etc.), if such objects haven't already
216216+/// been deinitialized.
171217pub fn deinit(self: *const Query) void {
172218 c.notmuch_query_destroy(self.query);
173219}
+52-41
src/Thread.zig
···18181919/// Get the thread ID of `thread`.
2020///
2121-/// The returned string belongs to 'thread' and as such, should not be modified
2121+/// The returned string belongs to `Thread` and as such, should not be modified
2222/// by the caller and will only be valid for as long as the thread is valid,
2323-/// (which is until notmuch_thread_destroy or until the query from which it
2424-/// derived is destroyed).
2323+/// (which is until `deinit` is called or until the query from which it derived
2424+/// is deinitialized).
2525pub fn getThreadID(self: *const Thread) [:0]const u8 {
2626 return std.mem.span(c.notmuch_thread_get_thread_id(self.thread));
2727}
28282929-/// Get the total number of messages in 'thread'.
2929+/// Get the total number of messages in `Thread`.
3030///
3131/// This count consists of all messages in the database belonging to this
3232-/// thread. Contrast with getMatchedMessages().
3232+/// thread. Contrast with `getMatchedMessages`.
3333pub fn getTotalMessages(self: *const Thread) usize {
3434- return @intCast(c.notmuch_thread_get_total_messages(self.thread));
3535-}
3636-3737-/// Get the number of messages in 'thread' that matched the search.
3838-///
3939-/// This count includes only the messages in this thread that were matched by
4040-/// the search from which the thread was created and were not excluded by any
4141-/// exclude tags passed in with the query (see Query.addTagExclude). Contrast
4242-/// with getTotalMessages() .
4343-pub fn getMatchedMessages(self: *const Thread) usize {
4444- return @intCast(c.notmuch_thread_get_matched_messages(self.thread));
3434+ std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits);
3535+ return @intCast(@max(0, c.notmuch_thread_get_total_messages(self.thread)));
4536}
46374738/// Get the total number of files in 'thread'.
4839///
4940/// This sums Message.countFiles over all messages in the thread.
5041pub fn getTotalFiles(self: *const Thread) usize {
5151- return @intCast(c.notmuch_thread_get_total_files(self.thread));
4242+ std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits);
4343+ return @intCast(@max(0, c.notmuch_thread_get_total_files(self.thread)));
5244}
53455454-/// Get a MessagesIterator for the top-level messages in 'thread' in
4646+/// Get a `MessagesIterator` for the top-level messages in `thread` in
5547/// oldest-first order.
5648///
5749/// This iterator will not necessarily iterate over all of the messages in the
5850/// thread. It will only iterate over the messages in the thread which are not
5951/// replies to other messages in the thread.
6052///
6161-/// The returned list will be destroyed when the thread is destroyed.
5353+/// The returned list will be denitialized when the thread is deinitialized.
6254pub fn getToplevelMessages(self: *const Thread) MessagesIterator {
6355 return .{
6456 .messages = c.notmuch_thread_get_toplevel_messages(self.thread),
6557 };
6658}
67596868-// Get a MessagesIterator for all messages in 'thread' in oldest-first order.
6060+/// Get a `MessagesIterator` for all messages in `Thread` in oldest-first order.
6161+///
6262+/// The returned list will be denitialized when the thread is deinitialized.
6963pub fn getMessages(self: *const Thread) MessagesIterator {
7064 return .{
7165 .messages = c.notmuch_thread_get_messages(self.thread),
7266 };
7367}
74687575-/// Get the authors of 'thread' as a UTF-8 string.
6969+/// Get the number of messages in `Thread` that matched the search.
7070+///
7171+/// This count includes only the messages in this thread that were matched by
7272+/// the search from which the thread was created and were not excluded by any
7373+/// exclude tags passed in with the query (see `Query.addTagExclude`). Contrast
7474+/// with `getTotalMessages`.
7575+pub fn getMatchedMessages(self: *const Thread) usize {
7676+ std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits);
7777+ return @intCast(@max(0, c.notmuch_thread_get_matched_messages(self.thread)));
7878+}
7979+8080+/// Get the authors of `Thread` as a UTF-8 string.
7681///
7782/// The returned string is a comma-separated list of the names of the authors of
7883/// mail messages in the query results that belong to this thread.
7984///
8085/// The string contains authors of messages matching the query first, then
8181-/// non-matched authors (with the two groups separated by '|'). Within each
8686+/// non-matched authors (with the two groups separated by `|`). Within each
8287/// group, authors are ordered by date.
8388///
8484-/// The returned string belongs to 'thread' and as such, should not be modified
8989+/// The returned string belongs to `Thread` and as such, should not be modified
8590/// by the caller and will only be valid for as long as the thread is valid,
8686-/// (which is until notmuch_thread_destroy or until the query from which it
8787-/// derived is destroyed).
9191+/// (which is until `deinit` is called or until the query from which it derived
9292+/// is deinitialized).
9393+///
9494+/// TODO: Create an iterator object that makes dealing with this list easier.
8895pub fn getAuthors(self: *const Thread) [:0]const u8 {
8996 return std.mem.span(c.notmuch_thread_get_authors(self.thread));
9097}
91989292-/// Get the subject of 'thread' as a UTF-8 string.
9999+/// Get the subject of `Thread` as a UTF-8 string.
93100///
94101/// The subject is taken from the first message (according to the query
9595-/// order---see Query.setSort) in the query results that belongs to this thread.
102102+/// order—see `Query.setSort`) in the query results that belongs to this thread.
96103///
9797-/// The returned string belongs to 'thread' and as such, should not be modified
104104+/// The returned string belongs to `Thread` and as such, should not be modified
98105/// by the caller and will only be valid for as long as the thread is valid,
9999-/// (which is until notmuch_thread_destroy or until the query from which it
100100-/// derived is destroyed).
106106+/// which is until `deinit` is called or until the query from which it derived
107107+/// is deinitialized.
101108pub fn getSubject(self: *const Thread) [:0]const u8 {
102109 return std.mem.span(c.notmuch_thread_get_subject(self.thread));
103110}
104111105105-/// Get the date of the oldest message in 'thread' as a nanosecond timestamp.
106106-pub fn getOldestDate(self: *const Thread) i128 {
107107- return c.notmuch_thread_get_oldest_date(self.thread) * std.time.ns_per_s;
112112+/// Get the date of the oldest message in `Thread` as the number of seconds
113113+/// since the Unix epoch (1970-01-01 00:00:00 UTC).
114114+pub fn getOldestDate(self: *const Thread) i64 {
115115+ std.debug.assert(@typeInfo(c_long).int.bits <= @typeInfo(i64).int.bits);
116116+ return c.notmuch_thread_get_oldest_date(self.thread);
108117}
109118110110-/// Get the date of the newest message in 'thread' as a nanosecond timestamp.
111111-pub fn getNewestDate(self: *const Thread) i128 {
112112- return c.notmuch_thread_get_newest_date(self.thread) * std.time.ns_per_s;
119119+/// Get the date of the newest message in `Thread` as the number of seconds
120120+/// since the Unix epoch (1970-01-01 00:00:00 UTC).
121121+pub fn getNewestDate(self: *const Thread) i64 {
122122+ std.debug.assert(@typeInfo(c_long).int.bits <= @typeInfo(i64).int.bits);
123123+ return c.notmuch_thread_get_newest_date(self.thread);
113124}
114125115115-/// Get the tags for 'thread', returning a TagsIterator object which can be used
116116-/// to iterate over all tags.
126126+/// Get the tags for `Thread`, returning a `TagsIterator` object which can be
127127+/// used to iterate over all tags.
117128///
118118-/// Note: In the Notmuch database, tags are stored on individual messages, not
129129+/// Note: In the `notmuch` database, tags are stored on individual messages, not
119130/// on threads. So the tags returned here will be all tags of the messages which
120131/// matched the search and which belong to this thread.
121132///
122133/// The tags object is owned by the thread and as such, will only be valid for
123123-/// as long as the thread is valid, (for example, until notmuch_thread_destroy
124124-/// or until the query from which it derived is destroyed).
134134+/// as long as the thread is valid, which is until `deinit` is called or until
135135+/// the query from which it derived is destroyed).
125136pub fn getTags(self: *const Thread) TagsIterator {
126137 return .{
127138 .tags = c.notmuch_thread_get_tags(self.thread),
···4040 );
4141}
42424343+fn checkEnum(comptime E: type, comptime prefix: []const u8, comptime skips: []const []const u8) void {
4444+ @setEvalBranchQuota(24000);
4545+4646+ const e_fields = @typeInfo(E).@"enum".fields;
4747+ const c_decls = @typeInfo(c).@"struct".decls;
4848+4949+ loop: for (c_decls) |decl| {
5050+ for (skips) |skip| if (std.mem.eql(u8, skip, decl.name)) continue :loop;
5151+ const suffix = std.mem.cutPrefix(u8, decl.name, prefix) orelse continue :loop;
5252+ var b: [suffix.len]u8 = undefined;
5353+ const s = std.ascii.lowerString(&b, suffix);
5454+ if (@hasField(E, s)) continue :loop;
5555+ @panic(std.fmt.comptimePrint("{s} is missing a value {s}", .{ @typeName(E), s }));
5656+ }
5757+ for (e_fields) |field| {
5858+ var buf: [prefix.len + field.name.len]u8 = undefined;
5959+ @memcpy(buf[0..prefix.len], prefix);
6060+ _ = std.ascii.upperString(buf[prefix.len..], field.name);
6161+ if (!@hasDecl(c, &buf)) @panic(std.fmt.comptimePrint("{s} has a value {s} without a matching C value {s}.", .{ @typeName(E), field.name, &buf }));
6262+ if (field.value != @field(c, &buf)) @panic(std.fmt.comptimePrint(
6363+ "{s} value {s}({d}) != c.{s}({d}).",
6464+ .{
6565+ @typeName(E),
6666+ field.name,
6767+ field.value,
6868+ &buf,
6969+ @field(c, &buf),
7070+ },
7171+ ));
7272+ }
7373+}
7474+4375/// Configuration keys known to notmuch.
4476pub const Config = generateEnum("NOTMUCH_CONFIG_", &.{ "NOTMUCH_CONFIG_FIRST", "NOTMUCH_CONFIG_LAST" });
45774646-pub const DatabaseMode = generateEnum("NOTMUCH_DATABASE_MODE_", &.{});
7878+pub const DatabaseMode = enum(u1) {
7979+ read_only = c.NOTMUCH_DATABASE_MODE_READ_ONLY,
8080+ read_write = c.NOTMUCH_DATABASE_MODE_READ_WRITE,
8181+8282+ comptime {
8383+ checkEnum(@This(), "NOTMUCH_DATABASE_MODE_", &.{});
8484+ }
8585+};
47864887pub const Decrypt = generateEnum("NOTMUCH_DECRYPT_", &.{});
49885089/// Exclude values for `Query.setOmitExcluded`
5190pub const Exclude = generateEnum("NOTMUCH_EXCLUDE_", &.{});
52915353-pub const MessageFlag = generateEnum("NOTMUCH_MESSAGE_FLAG_", &.{});
9292+pub const MessageFlag = enum(u2) {
9393+ match = c.NOTMUCH_MESSAGE_FLAG_MATCH,
9494+ excluded = c.NOTMUCH_MESSAGE_FLAG_EXCLUDED,
9595+ /// This message is a "ghost message", meaning it has no filenames or
9696+ /// content, but we know it exists because it was referenced by some other
9797+ /// message. A ghost message has only a message ID and thread ID.
9898+ ghost = c.NOTMUCH_MESSAGE_FLAG_GHOST,
9999+100100+ comptime {
101101+ checkEnum(@This(), "NOTMUCH_MESSAGE_FLAG_", &.{});
102102+ }
103103+};
5410455105/// query syntax
56106pub const QuerySyntax = generateEnum("NOTMUCH_QUERY_SYNTAX_", &.{});
···59109pub const Sort = generateEnum("NOTMUCH_SORT_", &.{});
6011061111/// Status codes used for the return values of most functions.
6262-pub const Status = generateEnum("NOTMUCH_STATUS_", &.{"NOTMUCH_STATUS_LAST_STATUS"});
112112+pub const Status = enum(u5) {
113113+ /// No error occurred.
114114+ success = c.NOTMUCH_STATUS_SUCCESS,
115115+ /// Out of memory.
116116+ out_of_memory = c.NOTMUCH_STATUS_OUT_OF_MEMORY,
117117+ /// An attempt was made to write to a database opened in read-only
118118+ /// mode.
119119+ read_only_database = c.NOTMUCH_STATUS_READ_ONLY_DATABASE,
120120+ /// A Xapian exception occurred.
121121+ xapian_exception = c.NOTMUCH_STATUS_XAPIAN_EXCEPTION,
122122+ /// An error occurred trying to read or write to a file (this could be file not
123123+ /// found, permission denied, etc.)
124124+ file_error = c.NOTMUCH_STATUS_FILE_ERROR,
125125+ /// A file was presented that doesn't appear to be an email message.
126126+ file_not_email = c.NOTMUCH_STATUS_FILE_NOT_EMAIL,
127127+ /// A file contains a message ID that is identical to a message already in the
128128+ /// database.
129129+ duplicate_message_id = c.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
130130+ /// The user erroneously passed a `null` pointer to a notmuch function.
131131+ null_pointer = c.NOTMUCH_STATUS_NULL_POINTER,
132132+ /// A tag value is too long (exceeds NOTMUCH_TAG_MAX).
133133+ tag_too_long = c.NOTMUCH_STATUS_TAG_TOO_LONG,
134134+ /// The `Message.thaw` function has been called more times than
135135+ /// `Message.freeze`.
136136+ unbalanced_freeze_thaw = c.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
137137+ /// `Database.endAtomic` has been called more times than `Database.beginAtomic`.
138138+ unbalanced_atomic = c.NOTMUCH_STATUS_UNBALANCED_ATOMIC,
139139+ /// The operation is not supported.
140140+ unsupported_operation = c.NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
141141+ /// The operation requires a database upgrade.
142142+ upgrade_required = c.NOTMUCH_STATUS_UPGRADE_REQUIRED,
143143+ /// There is a problem with the proposed path, e.g. a relative path passed to a
144144+ /// function expecting an absolute path.
145145+ path_error = c.NOTMUCH_STATUS_PATH_ERROR,
146146+ /// The requested operation was ignored. Depending on the function, this may not
147147+ /// be an actual error.
148148+ ignored = c.NOTMUCH_STATUS_IGNORED,
149149+ /// One of the arguments violates the preconditions for the function, in a way
150150+ /// not covered by a more specific argument.
151151+ illegal_argument = c.NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
152152+ /// A MIME object claimed to have cryptographic protection which notmuch tried
153153+ /// to handle, but the protocol was not specified in an intelligible way.
154154+ malformed_crypto_protocol = c.NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL,
155155+ /// Notmuch attempted to do crypto processing, but could not initialize the
156156+ /// engine needed to do so.
157157+ failed_crypto_context_creation = c.NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION,
158158+ /// A MIME object claimed to have cryptographic protection, and notmuch
159159+ /// attempted to process it, but the specific protocol was something that
160160+ /// notmuch doesn't know how to handle.
161161+ unknown_crypto_protocol = c.NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
162162+ /// Unable to load a config file
163163+ no_config = c.NOTMUCH_STATUS_NO_CONFIG,
164164+ /// Unable to load a database.
165165+ no_database = c.NOTMUCH_STATUS_NO_DATABASE,
166166+ /// Database exists, so not (re)-created.
167167+ database_exists = c.NOTMUCH_STATUS_DATABASE_EXISTS,
168168+ /// Syntax error in query.
169169+ bad_query_syntax = c.NOTMUCH_STATUS_BAD_QUERY_SYNTAX,
170170+ /// No mail root could be deduced from parameters and environment.
171171+ no_mail_root = c.NOTMUCH_STATUS_NO_MAIL_ROOT,
172172+ /// Database is not fully opened, or has been closed.
173173+ closed_database = c.NOTMUCH_STATUS_CLOSED_DATABASE,
174174+ /// The iterator being examined has been exhausted and contains no more items.
175175+ iterator_exhausted = c.NOTMUCH_STATUS_ITERATOR_EXHAUSTED,
176176+ /// An operation that was being performed on the database has been invalidated
177177+ /// while in progress, and must be re-executed. This will typically happen
178178+ /// while iterating over query results and the underlying Xapian database is
179179+ /// modified by another process so that the currently open version cannot be
180180+ /// read anymore.
181181+ operation_invalidated = c.NOTMUCH_STATUS_OPERATION_INVALIDATED,
182182+183183+ comptime {
184184+ checkEnum(@This(), "NOTMUCH_STATUS_", &.{"NOTMUCH_STATUS_LAST_STATUS"});
185185+ }
186186+};
6318764188/// Convenience function to convert a notmuch API return code to a Status enum.
65189pub fn status(rc: c_uint) Status {