Zig bindings for the notmuch C library
0

Configure Feed

Select the types of activity you want to include in your feed.

more documentation and api updates

Jeffrey C. Ollie (Mar 2, 2026, 10:34 PM -0600) 04bfdb1f cca3fe38

+552 -123
+17 -29
src/Database.zig
··· 10 10 11 11 const c = @import("c"); 12 12 13 - const Error = @import("error.zig").Error; 14 - const wrap = @import("error.zig").wrap; 15 - const wrapMessage = @import("error.zig").wrapMessage; 16 - 17 - const enums = @import("enums.zig"); 18 13 pub const Config = enums.Config; 19 14 pub const Mode = enums.DatabaseMode; 20 - const Decrypt = enums.DECRYPT; 21 - const QuerySyntax = enums.QuerySyntax; 22 - const STATUS = enums.Status; 23 - const status = enums.status; 24 15 16 + const Decrypt = enums.Decrypt; 25 17 const Directory = @import("Directory.zig"); 18 + const enums = @import("enums.zig"); 19 + const Error = @import("error.zig").Error; 20 + const IndexOpts = @import("IndexOpts.zig"); 26 21 const Message = @import("Message.zig"); 27 22 const Query = @import("Query.zig"); 23 + const QuerySyntax = enums.QuerySyntax; 24 + const status = enums.status; 25 + const Status = enums.Status; 28 26 const TagsIterator = @import("TagsIterator.zig"); 27 + const wrap = @import("error.zig").wrap; 28 + const wrapMessage = @import("error.zig").wrapMessage; 29 29 30 30 /// A callback invoked by `compact` to notify the user of the progress of the 31 31 /// compaction process. ··· 282 282 283 283 const r = create(.{ .config_path = config_path }); 284 284 defer switch (r) { 285 - .ok => |db| db.destroy() catch {}, 285 + .ok => |db| db.deinit() catch {}, 286 286 .err => |e| e.deinit(), 287 287 }; 288 288 try std.testing.expect(r == .ok); ··· 322 322 try wrap(c.notmuch_database_compact_db(self.database, backup_path, status_cb, closure)); 323 323 } 324 324 325 - /// Destroy the notmuch database, closing it if necessary and freeing all 325 + /// Deinit the notmuch database, closing it if necessary and freeing all 326 326 /// associated resources. 327 327 /// 328 - /// Return value as in `close` if the database was open; `destroy` itself has no 328 + /// Return value as in `close` if the database was open; `deinit` itself has no 329 329 /// failure modes. 330 - pub fn destroy(self: *const Database) Error!void { 330 + /// 331 + /// NOTE: In the `notmuch` C API, this is referred to as a `destroy` operation. 332 + /// This has been renamed to `deinit` to align with Zig's nomenclature and to 333 + /// avoid the inference that this API call would delete data from disk. 334 + pub fn deinit(self: *const Database) Error!void { 331 335 switch (status(c.notmuch_database_destroy(self.database))) { 332 336 .success => {}, 333 337 .xapian_exception => return error.XapianException, ··· 819 823 .values = c.notmuch_config_get_values_string(self.database, @intFromEnum(key)), 820 824 }; 821 825 } 822 - 823 - pub const IndexOpts = struct { 824 - indexopts: *c.notmuch_indexopts_t, 825 - 826 - pub fn getDecryptPolicy(self: IndexOpts) Decrypt { 827 - return @enumFromInt(c.notmuch_indexopts_get_decrypt_policy(self.indexopts)); 828 - } 829 - 830 - pub fn setDecryptPolicy(self: IndexOpts, decrypt_policy: Decrypt) Error!void { 831 - try wrap(c.notmuch_indexopts_set_decrypt_policy(self.indexopts, @intFromEnum(decrypt_policy))); 832 - } 833 - 834 - pub fn deinit(self: IndexOpts) void { 835 - c.notmuch_indexopts_destroy(self.indexopts); 836 - } 837 - }; 838 826 839 827 pub const ValuesIterator = struct { 840 828 values: ?*c.notmuch_config_values_t,
+47
src/FilenamesIterator.zig
··· 1 + // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + //! A Zig wrapper around the `notmuch` C APIs for dealing with 5 + //! lists of filenames. 6 + pub const FilenamesIterator = @This(); 7 + 8 + const c = @import("c"); 9 + 10 + const status = @import("enums.zig").status; 11 + 12 + filenames: ?*c.notmuch_filenames_t, 13 + 14 + pub const NextError = error{ 15 + /// Iteration failed to allocate memory. 16 + OutOfMemory, 17 + /// Iteration was invalidated by the database. Re-open the database and 18 + /// try again. 19 + OperationInvalidated, 20 + }; 21 + 22 + pub fn next(self: *FilenamesIterator) NextError!?[:0]const u8 { 23 + const threads = self.threads orelse return null; 24 + return switch (status(c.notmuch_threads_status(threads))) { 25 + .success => thread: { 26 + if (c.notmuch_threads_valid(threads) != 0) break :thread null; 27 + defer c.notmuch_threads_move_to_next(threads); 28 + break :thread .{ 29 + .thread = c.notmuch_threads_get(threads) orelse unreachable, 30 + }; 31 + }, 32 + .iterator_exhausted => return null, 33 + .operation_invalidated => error.OperationInvalidated, 34 + .out_of_memory => error.OutOfMemory, 35 + else => unreachable, 36 + }; 37 + } 38 + 39 + /// Deinitialize a `ThreadIterator` object. 40 + /// 41 + /// It's not strictly necessary to call this function. All memory from 42 + /// the `ThreadIterator` object will be reclaimed when the 43 + /// containing query object is deinitialized. 44 + pub fn deinit(self: *FilenamesIterator) void { 45 + const threads = self.threads orelse return; 46 + c.notmuch_threads_destroy(threads); 47 + }
+27
src/IndexOpts.zig
··· 1 + // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + //! Zig wrapped around the `notmuch` C APIs that deal with index options. 5 + const IndexOpts = @This(); 6 + 7 + const std = @import("std"); 8 + 9 + const c = @import("c"); 10 + 11 + const Decrypt = @import("enums.zig").Decrypt; 12 + const Error = @import("error.zig").Error; 13 + const wrap = @import("error.zig").wrap; 14 + 15 + indexopts: *c.notmuch_indexopts_t, 16 + 17 + pub fn getDecryptPolicy(self: IndexOpts) Decrypt { 18 + return @enumFromInt(c.notmuch_indexopts_get_decrypt_policy(self.indexopts)); 19 + } 20 + 21 + pub fn setDecryptPolicy(self: IndexOpts, decrypt_policy: Decrypt) Error!void { 22 + try wrap(c.notmuch_indexopts_set_decrypt_policy(self.indexopts, @intFromEnum(decrypt_policy))); 23 + } 24 + 25 + pub fn deinit(self: IndexOpts) void { 26 + c.notmuch_indexopts_destroy(self.indexopts); 27 + }
+138 -24
src/Message.zig
··· 1 1 // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 2 // SPDX-License-Identifier: GPL-3.0-or-later 3 3 4 + //! Zig wrapped around the `notmuch` C APIs that deal with messages. 4 5 const Message = @This(); 5 6 6 7 const std = @import("std"); 7 - const log = std.log.scoped(.notmuch); 8 8 9 9 const c = @import("c"); 10 10 11 + const Database = @import("Database.zig"); 11 12 const Error = @import("error.zig").Error; 12 - const wrap = @import("error.zig").wrap; 13 - 13 + const FilenamesIterator = @import("FilenamesIterator.zig"); 14 + const IndexOpts = @import("IndexOpts.zig"); 14 15 const MessageFlag = @import("enums.zig").MessageFlag; 15 - 16 + const MessagesIterator = @import("MessagesIterator.zig"); 17 + const status = @import("enums.zig").status; 16 18 const TagsIterator = @import("TagsIterator.zig"); 19 + const wrap = @import("error.zig").wrap; 17 20 18 21 duplicate: ?bool = null, 19 22 message: *c.notmuch_message_t, 20 23 21 - /// Get the message ID of 'message'. 24 + /// Get the database associated with this message. 25 + pub fn getDatabase(self: *const Message) Database { 26 + return .{ 27 + .database = c.notmuch_message_get_database(self.message) orelse unreachable, 28 + }; 29 + } 30 + 31 + /// Get the message ID of `Message`. 22 32 /// 23 - /// The returned string belongs to 'message' and as such, should not be 24 - /// modified by the caller and will only be valid for as long as the message 25 - /// is valid, (which is until the query from which it derived is destroyed). 33 + /// The returned string belongs to `Message` and as such, should not be modified 34 + /// or freed by the caller and will only be valid for as long as the message is 35 + /// valid, which is until the query from which it derived is destroyed. 26 36 /// 27 - /// This function will return NULL if triggers an unhandled Xapian 28 - /// exception. 37 + /// This function will return `null` if triggers an unhandled Xapian exception. 29 38 pub fn getMessageID(self: *const Message) ?[:0]const u8 { 30 39 return std.mem.span(c.notmuch_message_get_message_id(self.message) orelse return null); 31 40 } 32 41 33 - /// Get the thread ID of 'message'. 42 + /// Get the thread ID of `Message`. 34 43 /// 35 - /// The returned string belongs to 'message' and as such, should not be 36 - /// modified by the caller and will only be valid for as long as the message 37 - /// is valid, (for example, until the user calls notmuch_message_destroy on 38 - /// 'message' or until a query from which it derived is destroyed). 44 + /// The returned string belongs to `Message` and as such, should not be modified 45 + /// or freed by the caller and will only be valid for as long as the message is 46 + /// valid, which is until the user calls `deinit` or until the query from which 47 + /// it derived is deinitialized). 39 48 /// 40 - /// This function will return NULL if triggers an unhandled Xapian 41 - /// exception. 49 + /// This function will return `null` if triggers an unhandled Xapian exception. 42 50 pub fn getThreadID(self: *const Message) ?[:0]const u8 { 43 51 return std.mem.span(c.notmuch_message_get_thread_id(self.message) orelse return null); 44 52 } 45 53 54 + /// Get a `MessagesIterator` for all of the replies to `Message`. 55 + /// 56 + /// Note: This call only makes sense if `Message` was ultimately obtained from 57 + /// a `Thread` object, (such as by coming directly from the result of calling 58 + /// `Threads.getToplevelMessages` or by any number of subsequent calls to 59 + /// `getReplies`). 60 + /// 61 + /// If `Message` was obtained through some non-thread means, (such as by a 62 + /// call to `Query.searchMessages`), then this function will return an empty 63 + /// iterator. 64 + /// 65 + /// If there are no replies to `Message`, this function will return an empty 66 + /// iterator. 67 + /// 68 + /// This function also return an empty iterator if it triggers a Xapian 69 + /// exception. 70 + /// 71 + /// The returned list will be deinitialized when the thread is denitialized. 72 + pub fn getReplies(self: *const Message) MessagesIterator { 73 + return .{ 74 + .messages = c.notmuch_message_get_replies(self.message), 75 + }; 76 + } 77 + 78 + pub const CountFilesError = error{ 79 + /// An error occurred while trying to count files. 80 + CountFilesError, 81 + }; 82 + 83 + /// Get the total number of files associated with a message. 84 + pub fn countFiles(self: *const Message) CountFilesError!usize { 85 + std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); 86 + const count = c.notmuch_message_count_files(self.message); 87 + if (count < 0) return error.CountFilesError; 88 + return @intCast(@max(0, count)); 89 + } 90 + 91 + pub const GetFilenameError = error{ 92 + XapianException, 93 + }; 94 + 95 + /// Get a filename for the email corresponding to `Message`. 96 + /// 97 + /// The returned filename is an absolute filename (the initial component will 98 + /// match `Database.getPath`). 99 + /// 100 + /// The returned string belongs to the message so should not be modified or 101 + /// freed by the caller (nor should it be referenced after the message is 102 + /// deinitialized). 103 + /// 104 + /// Note: If this message corresponds to multiple files in the mail store, 105 + /// (that is, multiple files contain identical message IDs), this function will 106 + /// arbitrarily return a single one of those filenames. See `getFilenames` for 107 + /// returning the complete list of filenames. 108 + /// 109 + /// This function returns NULL if it triggers a Xapian exception. 110 + pub fn getFilename(self: *const Message) GetFilenameError![:0]const u8 { 111 + return std.mem.span(c.notmuch_message_get_filename(self.message) orelse return error.XapianException); 112 + } 113 + 114 + pub const GetFilenamesError = error{ 115 + XapianException, 116 + }; 117 + 118 + /// Get all filenames for the email corresponding to `Message`. 119 + /// 120 + /// Returns a `FilenamesIterator` listing all the filenames associated with 121 + /// `Message`. These files may not have identical content, but each will have 122 + /// the identical Message-ID. 123 + /// 124 + /// Each filename in the iterator is an absolute filename (the initial component 125 + /// will match `Database.getPath`). 126 + pub fn getFilenames(self: *const Message) GetFilenamesError!FilenamesIterator { 127 + return .{ 128 + .filenames = c.notmuch_message_get_filenames(self.message) orelse return error.XapianException, 129 + }; 130 + } 131 + 132 + pub fn reindex(self: *const Message, indexopts: ?IndexOpts) Error!void { 133 + return switch (status(c.notmuch_message_reindex(self.message, if (indexopts) |i| i.indexopts else null))) { 134 + .success => {}, 135 + .duplicate_message_id => {}, 136 + .file_error => error.FileError, 137 + .file_not_email => error.FileNotEmail, 138 + .read_only_database => error.ReadOnlyDatabase, 139 + .upgrade_required => error.UpgradeRequired, 140 + else => unreachable, 141 + }; 142 + } 143 + 144 + pub const GetFlagError = error{ 145 + XapianException, 146 + }; 147 + 148 + /// Get a value of a flag for the email corresponding to `Message`. 149 + pub fn getFlag(self: *const Message, flag: MessageFlag) GetFlagError!bool { 150 + var is_set: c.notmuch_bool_t = undefined; 151 + return switch (status(c.notmuch_message_get_flag_st( 152 + self.message, 153 + @intFromEnum(flag), 154 + &is_set, 155 + ))) { 156 + .success => is_set != 0, 157 + .null_pointer => unreachable, 158 + .xapian_exception => error.XapianException, 159 + else => unreachable, 160 + }; 161 + } 162 + 46 163 /// Add a tag to the given message. 47 164 pub fn addTag(self: *const Message, tag: [:0]const u8) Error!void { 48 165 try wrap(c.notmuch_message_add_tag(self.message, tag)); ··· 120 237 /// message is actually thawed. 121 238 pub fn thaw(self: *const Message) Error!void { 122 239 try wrap(c.notmuch_message_thaw(self.message)); 123 - } 124 - 125 - /// Get a value of a flag for the email corresponding to 'message'. 126 - pub fn getFlag(self: *const Message, flag: MessageFlag) Error!bool { 127 - var is_set: c.notmuch_bool_t = undefined; 128 - try wrap(c.notmuch_message_get_flag_st(self.message, @intFromEnum(flag), &is_set)); 129 - return is_set != 0; 130 240 } 131 241 132 242 /// Set a value of a flag for the email corresponding to 'message'. ··· 259 369 c.notmuch_message_properties_destroy(properties); 260 370 } 261 371 }; 372 + 373 + test { 374 + _ = std.testing.refAllDecls(@This()); 375 + }
+49 -6
src/MessagesIterator.zig
··· 1 1 // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 2 // SPDX-License-Identifier: GPL-3.0-or-later 3 3 4 + //! A Zig wrapper around the `notmuch` C APIs for dealing with 5 + //! lists of messages. 4 6 const MessagesIterator = @This(); 5 7 6 8 const c = @import("c"); 7 9 8 10 const Message = @import("Message.zig"); 11 + const TagsIterator = @import("TagsIterator.zig"); 12 + const status = @import("enums.zig").status; 9 13 10 14 messages: ?*c.notmuch_messages_t, 11 15 12 - pub fn next(self: *MessagesIterator) ?Message { 16 + pub const NextError = error{ 17 + /// Iteration failed to allocate memory. 18 + OutOfMemory, 19 + /// Iteration was invalidated by the database. Re-open the database and 20 + /// try again. 21 + OperationInvalidated, 22 + }; 23 + 24 + /// Return a list of tags from all messages. 25 + /// 26 + /// The resulting list is guaranteed not to contain duplicated tags. 27 + /// 28 + /// WARNING: You can no longer iterate over messages after calling this 29 + /// function, because the iterator will point at the end of the list. We do 30 + /// not have a function to reset the iterator yet and the only way how you can 31 + /// iterate over the list again is to recreate the message list. 32 + /// 33 + /// The function returns `null` on error. 34 + pub fn collectTags(self: *const MessagesIterator) ?TagsIterator { 13 35 const messages = self.messages orelse return null; 14 - if (c.notmuch_messages_valid(messages)) return null; 15 - defer c.notmuch_messages_move_to_next(messages); 16 36 return .{ 17 - .message = c.notmuch_threads_get(messages) orelse unreachable, 37 + .tags = c.notmuch_messages_collect_tags(messages) orelse return null, 38 + }; 39 + } 40 + 41 + pub fn next(self: *const MessagesIterator) NextError!?Message { 42 + const messages = self.messages orelse return null; 43 + return switch (status(c.notmuch_threads_status(messages))) { 44 + .success => message: { 45 + if (c.notmuch_messages_valid(messages) != 0) break :message null; 46 + defer c.notmuch_messages_move_to_next(messages); 47 + break :message .{ 48 + .message = c.notmuch_messages_get(messages) orelse unreachable, 49 + }; 50 + }, 51 + .iterator_exhausted => return null, 52 + .operation_invalidated => error.OperationInvalidated, 53 + .out_of_memory => error.OutOfMemory, 54 + else => unreachable, 18 55 }; 19 56 } 20 57 21 - pub fn deinit(self: *MessagesIterator) void { 22 - c.notmuch_threads_destroy(self.threads); 58 + /// Deinitialize a `MessageIterator` object. 59 + /// 60 + /// It's not strictly necessary to call this function. All memory from 61 + /// the `MessageIterator` object will be reclaimed when the 62 + /// containing query object is deinitialized. 63 + pub fn deinit(self: *const MessagesIterator) void { 64 + const messages = self.messages orelse return; 65 + c.notmuch_messages_destroy(messages); 23 66 }
+60 -14
src/Query.zig
··· 122 122 /// don't need to call `ThreadsIterator.deinit` and all the memory will still be 123 123 /// reclaimed when the query is destroyed. 124 124 pub fn searchThreads(self: *const Query) Error!ThreadsIterator { 125 - var out: ?*c.notmuch_threads_t = undefined; 125 + var threads: ?*c.notmuch_threads_t = null; 126 126 127 - try wrap(c.notmuch_query_search_threads(self.query, &out)); 127 + try wrap(c.notmuch_query_search_threads(self.query, &threads)); 128 128 129 129 return .{ 130 - .threads = out, 130 + .threads = threads, 131 131 }; 132 132 } 133 133 134 134 /// Execute a query for messages, returning a MessagesIterator object which can 135 135 /// be used to iterate over the results. The returned messages object is owned 136 - /// by the query and as such, will only be valid until Query.deinit. 136 + /// by the query and as such, will only be valid until `Query.deinit`. 137 + /// 138 + /// Typical usage might be: 139 + /// ``` 140 + /// const db = try Database.open(…); 141 + /// defer db.deinit(); 142 + /// const query = db.queryCreate(query_string); 143 + /// defer query.deinit(); 144 + /// var it = query.searchMessages(); 145 + /// defer it.deinit(); 146 + /// while (try it.next()) |message| { 147 + /// defer message.deinit(); 148 + /// … 149 + /// } 150 + /// ``` 137 151 /// 138 152 /// Note: If you are finished with a message before its containing query, you 139 - /// can call Message.deinit to clean up some memory sooner (as in the 153 + /// can call `Message.deinit` to clean up some memory sooner (as in the 140 154 /// above example). Otherwise, if your message objects are long-lived, then you 141 - /// don't need to call Message.deinit and all the memory will still be 142 - /// reclaimed when the query is destroyed. 155 + /// don't need to call `Message.deinit` and all the memory will still be 156 + /// reclaimed when the query is deinitialized. 143 157 pub fn searchMessages(self: *const Query) Error!MessagesIterator { 144 158 var out: ?*c.notmuch_messages_t = undefined; 145 159 ··· 150 164 }; 151 165 } 152 166 167 + pub const CountMessagesError = error{ 168 + /// A Xapian error occurred. 169 + XapianError, 170 + }; 171 + 153 172 /// Return the number of messages matching a search. 154 173 /// 155 174 /// This function performs a search and returns the number of matching messages. 156 - pub fn countMessages(self: *const Query) Error!usize { 175 + pub fn countMessages(self: *const Query) CountMessagesError!u32 { 176 + std.debug.assert(@typeInfo(c_uint).int.bits == 32); 157 177 var count: c_uint = undefined; 158 - try wrap(c.notmuch_query_count_messages(self.query, &count)); 159 - return @intCast(count); 178 + return switch (status(c.notmuch_query_count_messages(self.query, &count))) { 179 + .success => @intCast(count), 180 + .xapian_error => error.XapianError, 181 + else => unreachable, 182 + }; 160 183 } 161 184 185 + pub const CountThreadsError = error{ 186 + /// Memory allocation failed. 187 + OutOfMemory, 188 + /// A Xapian error occurred. 189 + XapianError, 190 + }; 191 + 162 192 /// Return the number of threads matching a search. 163 193 /// 164 - /// This function performs a search and returns the number of matching threads. 165 - pub fn countThreads(self: *const Query) Error!usize { 194 + /// This function performs a search and returns the number of unique thread IDs 195 + /// in the matching messages. This is the same as number of threads matching 196 + /// a search. 197 + /// 198 + /// Note that this is a significantly heavier operation than 199 + /// `countMessages`. 200 + pub fn countThreads(self: *const Query) CountThreadsError!u32 { 201 + std.debug.assert(@typeInfo(c_uint).int.bits == 32); 166 202 var count: c_uint = undefined; 167 - try wrap(c.notmuch_query_count_threads(self.query, &count)); 168 - return @intCast(count); 203 + return switch (status(c.notmuch_query_count_threads(self.query, &count))) { 204 + .success => @intCast(count), 205 + .out_of_memory => error.OutOfMemory, 206 + .xapian_exception => error.XapianError, 207 + else => unreachable, 208 + }; 169 209 } 170 210 211 + /// Deinitialize the `Query` along with any associated resources. 212 + /// 213 + /// This will in turn deinitialize any `ThreadsIterator` and `MessageIterator` 214 + /// objects generated by this query, (and in turn any `Thread` and `Message` 215 + /// objects generated from those results, etc.), if such objects haven't already 216 + /// been deinitialized. 171 217 pub fn deinit(self: *const Query) void { 172 218 c.notmuch_query_destroy(self.query); 173 219 }
+52 -41
src/Thread.zig
··· 18 18 19 19 /// Get the thread ID of `thread`. 20 20 /// 21 - /// The returned string belongs to 'thread' and as such, should not be modified 21 + /// The returned string belongs to `Thread` and as such, should not be modified 22 22 /// by the caller and will only be valid for as long as the thread is valid, 23 - /// (which is until notmuch_thread_destroy or until the query from which it 24 - /// derived is destroyed). 23 + /// (which is until `deinit` is called or until the query from which it derived 24 + /// is deinitialized). 25 25 pub fn getThreadID(self: *const Thread) [:0]const u8 { 26 26 return std.mem.span(c.notmuch_thread_get_thread_id(self.thread)); 27 27 } 28 28 29 - /// Get the total number of messages in 'thread'. 29 + /// Get the total number of messages in `Thread`. 30 30 /// 31 31 /// This count consists of all messages in the database belonging to this 32 - /// thread. Contrast with getMatchedMessages(). 32 + /// thread. Contrast with `getMatchedMessages`. 33 33 pub fn getTotalMessages(self: *const Thread) usize { 34 - return @intCast(c.notmuch_thread_get_total_messages(self.thread)); 35 - } 36 - 37 - /// Get the number of messages in 'thread' that matched the search. 38 - /// 39 - /// This count includes only the messages in this thread that were matched by 40 - /// the search from which the thread was created and were not excluded by any 41 - /// exclude tags passed in with the query (see Query.addTagExclude). Contrast 42 - /// with getTotalMessages() . 43 - pub fn getMatchedMessages(self: *const Thread) usize { 44 - return @intCast(c.notmuch_thread_get_matched_messages(self.thread)); 34 + std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); 35 + return @intCast(@max(0, c.notmuch_thread_get_total_messages(self.thread))); 45 36 } 46 37 47 38 /// Get the total number of files in 'thread'. 48 39 /// 49 40 /// This sums Message.countFiles over all messages in the thread. 50 41 pub fn getTotalFiles(self: *const Thread) usize { 51 - return @intCast(c.notmuch_thread_get_total_files(self.thread)); 42 + std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); 43 + return @intCast(@max(0, c.notmuch_thread_get_total_files(self.thread))); 52 44 } 53 45 54 - /// Get a MessagesIterator for the top-level messages in 'thread' in 46 + /// Get a `MessagesIterator` for the top-level messages in `thread` in 55 47 /// oldest-first order. 56 48 /// 57 49 /// This iterator will not necessarily iterate over all of the messages in the 58 50 /// thread. It will only iterate over the messages in the thread which are not 59 51 /// replies to other messages in the thread. 60 52 /// 61 - /// The returned list will be destroyed when the thread is destroyed. 53 + /// The returned list will be denitialized when the thread is deinitialized. 62 54 pub fn getToplevelMessages(self: *const Thread) MessagesIterator { 63 55 return .{ 64 56 .messages = c.notmuch_thread_get_toplevel_messages(self.thread), 65 57 }; 66 58 } 67 59 68 - // Get a MessagesIterator for all messages in 'thread' in oldest-first order. 60 + /// Get a `MessagesIterator` for all messages in `Thread` in oldest-first order. 61 + /// 62 + /// The returned list will be denitialized when the thread is deinitialized. 69 63 pub fn getMessages(self: *const Thread) MessagesIterator { 70 64 return .{ 71 65 .messages = c.notmuch_thread_get_messages(self.thread), 72 66 }; 73 67 } 74 68 75 - /// Get the authors of 'thread' as a UTF-8 string. 69 + /// Get the number of messages in `Thread` that matched the search. 70 + /// 71 + /// This count includes only the messages in this thread that were matched by 72 + /// the search from which the thread was created and were not excluded by any 73 + /// exclude tags passed in with the query (see `Query.addTagExclude`). Contrast 74 + /// with `getTotalMessages`. 75 + pub fn getMatchedMessages(self: *const Thread) usize { 76 + std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); 77 + return @intCast(@max(0, c.notmuch_thread_get_matched_messages(self.thread))); 78 + } 79 + 80 + /// Get the authors of `Thread` as a UTF-8 string. 76 81 /// 77 82 /// The returned string is a comma-separated list of the names of the authors of 78 83 /// mail messages in the query results that belong to this thread. 79 84 /// 80 85 /// The string contains authors of messages matching the query first, then 81 - /// non-matched authors (with the two groups separated by '|'). Within each 86 + /// non-matched authors (with the two groups separated by `|`). Within each 82 87 /// group, authors are ordered by date. 83 88 /// 84 - /// The returned string belongs to 'thread' and as such, should not be modified 89 + /// The returned string belongs to `Thread` and as such, should not be modified 85 90 /// by the caller and will only be valid for as long as the thread is valid, 86 - /// (which is until notmuch_thread_destroy or until the query from which it 87 - /// derived is destroyed). 91 + /// (which is until `deinit` is called or until the query from which it derived 92 + /// is deinitialized). 93 + /// 94 + /// TODO: Create an iterator object that makes dealing with this list easier. 88 95 pub fn getAuthors(self: *const Thread) [:0]const u8 { 89 96 return std.mem.span(c.notmuch_thread_get_authors(self.thread)); 90 97 } 91 98 92 - /// Get the subject of 'thread' as a UTF-8 string. 99 + /// Get the subject of `Thread` as a UTF-8 string. 93 100 /// 94 101 /// The subject is taken from the first message (according to the query 95 - /// order---see Query.setSort) in the query results that belongs to this thread. 102 + /// order—see `Query.setSort`) in the query results that belongs to this thread. 96 103 /// 97 - /// The returned string belongs to 'thread' and as such, should not be modified 104 + /// The returned string belongs to `Thread` and as such, should not be modified 98 105 /// by the caller and will only be valid for as long as the thread is valid, 99 - /// (which is until notmuch_thread_destroy or until the query from which it 100 - /// derived is destroyed). 106 + /// which is until `deinit` is called or until the query from which it derived 107 + /// is deinitialized. 101 108 pub fn getSubject(self: *const Thread) [:0]const u8 { 102 109 return std.mem.span(c.notmuch_thread_get_subject(self.thread)); 103 110 } 104 111 105 - /// Get the date of the oldest message in 'thread' as a nanosecond timestamp. 106 - pub fn getOldestDate(self: *const Thread) i128 { 107 - return c.notmuch_thread_get_oldest_date(self.thread) * std.time.ns_per_s; 112 + /// Get the date of the oldest message in `Thread` as the number of seconds 113 + /// since the Unix epoch (1970-01-01 00:00:00 UTC). 114 + pub fn getOldestDate(self: *const Thread) i64 { 115 + std.debug.assert(@typeInfo(c_long).int.bits <= @typeInfo(i64).int.bits); 116 + return c.notmuch_thread_get_oldest_date(self.thread); 108 117 } 109 118 110 - /// Get the date of the newest message in 'thread' as a nanosecond timestamp. 111 - pub fn getNewestDate(self: *const Thread) i128 { 112 - return c.notmuch_thread_get_newest_date(self.thread) * std.time.ns_per_s; 119 + /// Get the date of the newest message in `Thread` as the number of seconds 120 + /// since the Unix epoch (1970-01-01 00:00:00 UTC). 121 + pub fn getNewestDate(self: *const Thread) i64 { 122 + std.debug.assert(@typeInfo(c_long).int.bits <= @typeInfo(i64).int.bits); 123 + return c.notmuch_thread_get_newest_date(self.thread); 113 124 } 114 125 115 - /// Get the tags for 'thread', returning a TagsIterator object which can be used 116 - /// to iterate over all tags. 126 + /// Get the tags for `Thread`, returning a `TagsIterator` object which can be 127 + /// used to iterate over all tags. 117 128 /// 118 - /// Note: In the Notmuch database, tags are stored on individual messages, not 129 + /// Note: In the `notmuch` database, tags are stored on individual messages, not 119 130 /// on threads. So the tags returned here will be all tags of the messages which 120 131 /// matched the search and which belong to this thread. 121 132 /// 122 133 /// The tags object is owned by the thread and as such, will only be valid for 123 - /// as long as the thread is valid, (for example, until notmuch_thread_destroy 124 - /// or until the query from which it derived is destroyed). 134 + /// as long as the thread is valid, which is until `deinit` is called or until 135 + /// the query from which it derived is destroyed). 125 136 pub fn getTags(self: *const Thread) TagsIterator { 126 137 return .{ 127 138 .tags = c.notmuch_thread_get_tags(self.thread),
+31 -6
src/ThreadsIterator.zig
··· 1 1 // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 2 // SPDX-License-Identifier: GPL-3.0-or-later 3 3 4 + //! A Zig wrapper around the `notmuch` C APIs for dealing with 5 + //! lists of threads. 4 6 pub const ThreadsIterator = @This(); 5 7 6 8 const c = @import("c"); 7 9 8 10 const Thread = @import("Thread.zig"); 11 + const status = @import("enums.zig").status; 9 12 10 13 threads: ?*c.notmuch_threads_t, 11 14 12 - pub fn next(self: *ThreadsIterator) ?Thread { 15 + pub const NextError = error{ 16 + /// Iteration failed to allocate memory. 17 + OutOfMemory, 18 + /// Iteration was invalidated by the database. Re-open the database and 19 + /// try again. 20 + OperationInvalidated, 21 + }; 22 + 23 + pub fn next(self: *ThreadsIterator) NextError!?Thread { 13 24 const threads = self.threads orelse return null; 14 - if (c.notmuch_threads_valid(threads)) return null; 15 - defer c.notmuch_threads_move_to_next(threads); 16 - return .{ 17 - .thread = c.notmuch_threads_get(threads) orelse unreachable, 25 + return switch (status(c.notmuch_threads_status(threads))) { 26 + .success => thread: { 27 + if (c.notmuch_threads_valid(threads) != 0) break :thread null; 28 + defer c.notmuch_threads_move_to_next(threads); 29 + break :thread .{ 30 + .thread = c.notmuch_threads_get(threads) orelse unreachable, 31 + }; 32 + }, 33 + .iterator_exhausted => return null, 34 + .operation_invalidated => error.OperationInvalidated, 35 + .out_of_memory => error.OutOfMemory, 36 + else => unreachable, 18 37 }; 19 38 } 20 39 40 + /// Deinitialize a `ThreadIterator` object. 41 + /// 42 + /// It's not strictly necessary to call this function. All memory from 43 + /// the `ThreadIterator` object will be reclaimed when the 44 + /// containing query object is deinitialized. 21 45 pub fn deinit(self: *ThreadsIterator) void { 22 - c.notmuch_threads_destroy(self.threads); 46 + const threads = self.threads orelse return; 47 + c.notmuch_threads_destroy(threads); 23 48 }
+127 -3
src/enums.zig
··· 40 40 ); 41 41 } 42 42 43 + fn checkEnum(comptime E: type, comptime prefix: []const u8, comptime skips: []const []const u8) void { 44 + @setEvalBranchQuota(24000); 45 + 46 + const e_fields = @typeInfo(E).@"enum".fields; 47 + const c_decls = @typeInfo(c).@"struct".decls; 48 + 49 + loop: for (c_decls) |decl| { 50 + for (skips) |skip| if (std.mem.eql(u8, skip, decl.name)) continue :loop; 51 + const suffix = std.mem.cutPrefix(u8, decl.name, prefix) orelse continue :loop; 52 + var b: [suffix.len]u8 = undefined; 53 + const s = std.ascii.lowerString(&b, suffix); 54 + if (@hasField(E, s)) continue :loop; 55 + @panic(std.fmt.comptimePrint("{s} is missing a value {s}", .{ @typeName(E), s })); 56 + } 57 + for (e_fields) |field| { 58 + var buf: [prefix.len + field.name.len]u8 = undefined; 59 + @memcpy(buf[0..prefix.len], prefix); 60 + _ = std.ascii.upperString(buf[prefix.len..], field.name); 61 + if (!@hasDecl(c, &buf)) @panic(std.fmt.comptimePrint("{s} has a value {s} without a matching C value {s}.", .{ @typeName(E), field.name, &buf })); 62 + if (field.value != @field(c, &buf)) @panic(std.fmt.comptimePrint( 63 + "{s} value {s}({d}) != c.{s}({d}).", 64 + .{ 65 + @typeName(E), 66 + field.name, 67 + field.value, 68 + &buf, 69 + @field(c, &buf), 70 + }, 71 + )); 72 + } 73 + } 74 + 43 75 /// Configuration keys known to notmuch. 44 76 pub const Config = generateEnum("NOTMUCH_CONFIG_", &.{ "NOTMUCH_CONFIG_FIRST", "NOTMUCH_CONFIG_LAST" }); 45 77 46 - pub const DatabaseMode = generateEnum("NOTMUCH_DATABASE_MODE_", &.{}); 78 + pub const DatabaseMode = enum(u1) { 79 + read_only = c.NOTMUCH_DATABASE_MODE_READ_ONLY, 80 + read_write = c.NOTMUCH_DATABASE_MODE_READ_WRITE, 81 + 82 + comptime { 83 + checkEnum(@This(), "NOTMUCH_DATABASE_MODE_", &.{}); 84 + } 85 + }; 47 86 48 87 pub const Decrypt = generateEnum("NOTMUCH_DECRYPT_", &.{}); 49 88 50 89 /// Exclude values for `Query.setOmitExcluded` 51 90 pub const Exclude = generateEnum("NOTMUCH_EXCLUDE_", &.{}); 52 91 53 - pub const MessageFlag = generateEnum("NOTMUCH_MESSAGE_FLAG_", &.{}); 92 + pub const MessageFlag = enum(u2) { 93 + match = c.NOTMUCH_MESSAGE_FLAG_MATCH, 94 + excluded = c.NOTMUCH_MESSAGE_FLAG_EXCLUDED, 95 + /// This message is a "ghost message", meaning it has no filenames or 96 + /// content, but we know it exists because it was referenced by some other 97 + /// message. A ghost message has only a message ID and thread ID. 98 + ghost = c.NOTMUCH_MESSAGE_FLAG_GHOST, 99 + 100 + comptime { 101 + checkEnum(@This(), "NOTMUCH_MESSAGE_FLAG_", &.{}); 102 + } 103 + }; 54 104 55 105 /// query syntax 56 106 pub const QuerySyntax = generateEnum("NOTMUCH_QUERY_SYNTAX_", &.{}); ··· 59 109 pub const Sort = generateEnum("NOTMUCH_SORT_", &.{}); 60 110 61 111 /// Status codes used for the return values of most functions. 62 - pub const Status = generateEnum("NOTMUCH_STATUS_", &.{"NOTMUCH_STATUS_LAST_STATUS"}); 112 + pub const Status = enum(u5) { 113 + /// No error occurred. 114 + success = c.NOTMUCH_STATUS_SUCCESS, 115 + /// Out of memory. 116 + out_of_memory = c.NOTMUCH_STATUS_OUT_OF_MEMORY, 117 + /// An attempt was made to write to a database opened in read-only 118 + /// mode. 119 + read_only_database = c.NOTMUCH_STATUS_READ_ONLY_DATABASE, 120 + /// A Xapian exception occurred. 121 + xapian_exception = c.NOTMUCH_STATUS_XAPIAN_EXCEPTION, 122 + /// An error occurred trying to read or write to a file (this could be file not 123 + /// found, permission denied, etc.) 124 + file_error = c.NOTMUCH_STATUS_FILE_ERROR, 125 + /// A file was presented that doesn't appear to be an email message. 126 + file_not_email = c.NOTMUCH_STATUS_FILE_NOT_EMAIL, 127 + /// A file contains a message ID that is identical to a message already in the 128 + /// database. 129 + duplicate_message_id = c.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID, 130 + /// The user erroneously passed a `null` pointer to a notmuch function. 131 + null_pointer = c.NOTMUCH_STATUS_NULL_POINTER, 132 + /// A tag value is too long (exceeds NOTMUCH_TAG_MAX). 133 + tag_too_long = c.NOTMUCH_STATUS_TAG_TOO_LONG, 134 + /// The `Message.thaw` function has been called more times than 135 + /// `Message.freeze`. 136 + unbalanced_freeze_thaw = c.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW, 137 + /// `Database.endAtomic` has been called more times than `Database.beginAtomic`. 138 + unbalanced_atomic = c.NOTMUCH_STATUS_UNBALANCED_ATOMIC, 139 + /// The operation is not supported. 140 + unsupported_operation = c.NOTMUCH_STATUS_UNSUPPORTED_OPERATION, 141 + /// The operation requires a database upgrade. 142 + upgrade_required = c.NOTMUCH_STATUS_UPGRADE_REQUIRED, 143 + /// There is a problem with the proposed path, e.g. a relative path passed to a 144 + /// function expecting an absolute path. 145 + path_error = c.NOTMUCH_STATUS_PATH_ERROR, 146 + /// The requested operation was ignored. Depending on the function, this may not 147 + /// be an actual error. 148 + ignored = c.NOTMUCH_STATUS_IGNORED, 149 + /// One of the arguments violates the preconditions for the function, in a way 150 + /// not covered by a more specific argument. 151 + illegal_argument = c.NOTMUCH_STATUS_ILLEGAL_ARGUMENT, 152 + /// A MIME object claimed to have cryptographic protection which notmuch tried 153 + /// to handle, but the protocol was not specified in an intelligible way. 154 + malformed_crypto_protocol = c.NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL, 155 + /// Notmuch attempted to do crypto processing, but could not initialize the 156 + /// engine needed to do so. 157 + failed_crypto_context_creation = c.NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION, 158 + /// A MIME object claimed to have cryptographic protection, and notmuch 159 + /// attempted to process it, but the specific protocol was something that 160 + /// notmuch doesn't know how to handle. 161 + unknown_crypto_protocol = c.NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL, 162 + /// Unable to load a config file 163 + no_config = c.NOTMUCH_STATUS_NO_CONFIG, 164 + /// Unable to load a database. 165 + no_database = c.NOTMUCH_STATUS_NO_DATABASE, 166 + /// Database exists, so not (re)-created. 167 + database_exists = c.NOTMUCH_STATUS_DATABASE_EXISTS, 168 + /// Syntax error in query. 169 + bad_query_syntax = c.NOTMUCH_STATUS_BAD_QUERY_SYNTAX, 170 + /// No mail root could be deduced from parameters and environment. 171 + no_mail_root = c.NOTMUCH_STATUS_NO_MAIL_ROOT, 172 + /// Database is not fully opened, or has been closed. 173 + closed_database = c.NOTMUCH_STATUS_CLOSED_DATABASE, 174 + /// The iterator being examined has been exhausted and contains no more items. 175 + iterator_exhausted = c.NOTMUCH_STATUS_ITERATOR_EXHAUSTED, 176 + /// An operation that was being performed on the database has been invalidated 177 + /// while in progress, and must be re-executed. This will typically happen 178 + /// while iterating over query results and the underlying Xapian database is 179 + /// modified by another process so that the currently open version cannot be 180 + /// read anymore. 181 + operation_invalidated = c.NOTMUCH_STATUS_OPERATION_INVALIDATED, 182 + 183 + comptime { 184 + checkEnum(@This(), "NOTMUCH_STATUS_", &.{"NOTMUCH_STATUS_LAST_STATUS"}); 185 + } 186 + }; 63 187 64 188 /// Convenience function to convert a notmuch API return code to a Status enum. 65 189 pub fn status(rc: c_uint) Status {
+4
src/notmuch.zig
··· 10 10 pub const Database = @import("Database.zig"); 11 11 pub const Error = @import("error.zig").Error; 12 12 pub const Message = @import("Message.zig"); 13 + pub const MessagesIterator = @import("MessagesIterator.zig"); 13 14 pub const Query = @import("Query.zig"); 14 15 pub const Thread = @import("Thread.zig"); 15 16 pub const ThreadsIterator = @import("ThreadsIterator.zig"); 17 + 18 + /// The maximum length of a tag. 19 + pub const tag_max = c.NOTMUCH_TAG_MAX; 16 20 17 21 const wrap = @import("error.zig").wrap; 18 22