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 3, 2026, 1:17 AM -0600) af29ea10 04bfdb1f

+593 -263
+42 -70
src/Database.zig
··· 19 19 const Error = @import("error.zig").Error; 20 20 const IndexOpts = @import("IndexOpts.zig"); 21 21 const Message = @import("Message.zig"); 22 + const PairsIterator = @import("PairsIterator.zig"); 22 23 const Query = @import("Query.zig"); 23 24 const QuerySyntax = enums.QuerySyntax; 24 25 const status = enums.status; 25 26 const Status = enums.Status; 26 27 const TagsIterator = @import("TagsIterator.zig"); 28 + const ValuesIterator = @import("ValuesIterator.zig"); 27 29 const wrap = @import("error.zig").wrap; 28 30 const wrapMessage = @import("error.zig").wrapMessage; 29 31 ··· 749 751 return .init(query orelse return error.OutOfMemory); 750 752 } 751 753 752 - pub fn getDefaultIndexOpts(self: *const Database) ?IndexOpts { 753 - return .{ 754 - .indexopts = c.notmuch_database_get_default_indexopts(self.database) orelse return null, 755 - }; 756 - } 757 - 758 - /// 759 - pub fn configPath(self: *const Database) ?[:0]const u8 { 760 - const config = c.notmuch_config_path(self.database); 761 - return std.mem.span(config orelse return null); 762 - } 763 - 764 754 /// Get a configuration value from an open database. 765 755 /// 766 756 /// This value reflects all configuration information given at the time ··· 773 763 return std.mem.span(c.notmuch_config_get(self.database, @intFromEnum(key)) orelse return null); 774 764 } 775 765 776 - /// Set a configuration value 766 + /// Set config `key` to `value`. 777 767 pub fn configSet(self: *const Database, key: Config, value: [:0]const u8) Error!void { 778 768 try wrap(c.notmuch_config_set(self.database, @intFromEnum(key), value)); 779 769 } ··· 786 776 self: *const Database, 787 777 /// configuration key 788 778 key: Config, 789 - ) ValuesIterator { 779 + ) ?ValuesIterator { 780 + return .{ 781 + .values = c.notmuch_config_get_values(self.database, @intFromEnum(key)) orelse return null, 782 + }; 783 + } 784 + 785 + /// Returns an iterator for a ';'-delimited list of configuration values 786 + /// 787 + /// These values reflect all configuration information given at the 788 + /// time the database was opened. 789 + pub fn configGetValuesString( 790 + self: *const Database, 791 + /// configuration key 792 + key: [:0]const u8, 793 + ) ?ValuesIterator { 790 794 return .{ 791 - .values = c.notmuch_config_get_values(self.database, @intFromEnum(key)), 795 + .values = c.notmuch_config_get_values_string(self.database, key) orelse return null, 796 + }; 797 + } 798 + 799 + /// Returns an iterator for a (key, value) configuration pairs. Returns `null` 800 + /// in case of error. 801 + pub fn configGetPairs(self: *const Database, prefix: [:0]const u8) ?PairsIterator { 802 + return .{ 803 + .pairs = c.notmuch_config_get_pairs(self.database, prefix) orelse return null, 792 804 }; 793 805 } 794 806 ··· 800 812 /// Returns IllegalArgument error if either key is unknown or the 801 813 /// corresponding value does not convert to boolean. 802 814 pub fn configGetBool( 803 - /// the database 815 + /// The database 804 816 self: *const Database, 805 817 /// configuration key 806 818 key: Config, ··· 810 822 return value != 0; 811 823 } 812 824 813 - /// Returns an iterator for a ';'-delimited list of configuration values 825 + /// Return the path of the config file loaded. Returns `null` if no config file 826 + /// was loaded. 827 + pub fn configPath(self: *const Database) ?[:0]const u8 { 828 + return std.mem.span(c.notmuch_config_path(self.database) orelse return null); 829 + } 830 + 831 + /// Get the current default indexing options for a given database. 814 832 /// 815 - /// These values reflect all configuration information given at the 816 - /// time the database was opened. 817 - pub fn configGetValuesString( 818 - self: *const Database, 819 - /// configuration key 820 - key: Config, 821 - ) ValuesIterator { 833 + /// This object will survive until the database itself is destroyed, but the 834 + /// caller may also release it earlier with `IndexOpts.deinit`. 835 + /// 836 + /// This object represents a set of options on how a message can be added to the 837 + /// index. At the moment it is a featureless stub. 838 + pub fn getDefaultIndexOpts(self: *const Database) ?IndexOpts { 822 839 return .{ 823 - .values = c.notmuch_config_get_values_string(self.database, @intFromEnum(key)), 840 + .indexopts = c.notmuch_database_get_default_indexopts(self.database) orelse return null, 824 841 }; 825 842 } 826 - 827 - pub const ValuesIterator = struct { 828 - values: ?*c.notmuch_config_values_t, 829 - 830 - pub fn next(self: *ValuesIterator) ?[:0]const u8 { 831 - const values = self.values orelse return null; 832 - if (c.notmuch_config_values_valid(values) == 0) return null; 833 - defer c.notmuch_config_values_move_to_next(values); 834 - return std.mem.span(c.notmuch_config_values_get(values) orelse unreachable); 835 - } 836 - 837 - pub fn start(self: *ValuesIterator) void { 838 - const values = self.values orelse return; 839 - c.notmuch_config_values_start(values); 840 - } 841 - 842 - pub fn deinit(self: *ValuesIterator) void { 843 - const values = self.values orelse return; 844 - c.notmuch_config_values_destroy(values); 845 - } 846 - }; 847 - 848 - pub const PairsIterator = struct { 849 - pairs: ?*c.notmuch_config_pairs_t, 850 - 851 - pub const Pair = struct { 852 - key: [:0]const u8, 853 - value: [:0]const u8, 854 - }; 855 - 856 - pub fn next(self: *PairsIterator) ?Pair { 857 - const pairs = self.pairs orelse return null; 858 - if (c.notmuch_config_pairs_valid(pairs) == 0) return null; 859 - defer c.notmuch_config_pairs_move_to_next(pairs); 860 - return .{ 861 - .key = std.mem.span(c.notmuch_config_pairs_key(pairs) orelse unreachable), 862 - .value = std.mem.span(c.notmuch_config_pairs_value(pairs) orelse unreachable), 863 - }; 864 - } 865 - 866 - pub fn deinit(self: *PairsIterator) void { 867 - const pairs = self.pairs orelse return; 868 - c.notmuch_config_pairs_destroy(pairs); 869 - } 870 - }; 871 843 872 844 test { 873 845 std.testing.refAllDecls(@This());
+79 -3
src/Directory.zig
··· 2 2 // SPDX-License-Identifier: GPL-3.0-or-later 3 3 4 4 //! Zig wrapper around the `notmuch` directory APIs. 5 - 6 5 const Directory = @This(); 7 6 8 7 const std = @import("std"); 9 - const log = std.log.scoped(.notmuch); 10 - 11 8 const c = @import("c"); 12 9 13 10 const Error = @import("error.zig").Error; 11 + const FilenamesIterator = @import("FilenamesIterator.zig"); 14 12 const wrap = @import("error.zig").wrap; 15 13 16 14 directory: *c.notmuch_directory_t, 15 + 16 + /// Store an mtime within the database for `Directory`. 17 + /// 18 + /// The `Directory` should be an object retrieved from the database 19 + /// with `Database.getDirectory` for a particular path. 20 + /// 21 + /// The intention is for the caller to use the mtime to allow efficient 22 + /// identification of new messages to be added to the database. The recommended 23 + /// usage is as follows: 24 + /// 25 + /// o Read the mtime of a directory from the filesystem 26 + /// 27 + /// o Call `Directory.indexFile` for all mail files in the directory 28 + /// 29 + /// o Call `setMtime` with the mtime read from the 30 + /// filesystem. 31 + /// 32 + /// Then, when wanting to check for updates to the directory in the future, the 33 + /// client can call `getMtime` and know that it only needs to add files if the 34 + /// mtime of the directory and files are newer than the stored timestamp. 35 + /// 36 + /// Note: The `getMtime` function does not allow the caller to distinguish a 37 + /// timestamp of 0 from a non-existent timestamp. So don't store a timestamp of 38 + /// 0 unless you are comfortable with that. 39 + pub fn setMtime(self: *const Directory, mtime: i64) Error!void { 40 + try wrap(c.notmuch_directory_set_mtime(self.directory, mtime)); 41 + } 42 + 43 + /// Get the mtime of a directory, (as previously stored with `setMtime`). 44 + /// 45 + /// Returns `null` if no mtime has previously been stored for this directory. 46 + pub fn getMtime(self: *const Directory) Error!?i64 { 47 + const mtime = try wrap(c.notmuch_directory_get_mtime(self.directory)); 48 + if (mtime == 0) return null; 49 + return @intCast(mtime); 50 + } 51 + 52 + pub const GetChildFilesError = error{ 53 + XapianException, 54 + }; 55 + 56 + /// Get a `FilenamesIterator` listing all the filenames of messages in the 57 + /// database within the given directory. 58 + /// 59 + /// The returned filenames will be the basename-entries only (not complete 60 + /// paths). 61 + pub fn getChildFiles(self: *const Directory) GetChildFilesError!FilenamesIterator { 62 + return .{ 63 + .filenames = c.notmuch_directory_get_child_files(self.directory) orelse return error.XapianException, 64 + }; 65 + } 66 + 67 + pub const GetChildDirectoriesError = error{ 68 + XapianException, 69 + }; 70 + 71 + /// Get a `FilenamesIterator` listing all the filenames of sub-directories in 72 + /// the database within the given directory. 73 + /// 74 + /// The returned filenames will be the basename-entries only (not complete 75 + /// paths). 76 + pub fn getChildDirectories(self: *const Directory) GetChildDirectoriesError!FilenamesIterator { 77 + return .{ 78 + .filenames = c.notmuch_directory_get_child_files(self.directory) orelse return error.XapianException, 79 + }; 80 + } 81 + 82 + /// Delete directory document from the database, and deinitialize the 83 + /// `Directory` object. Assumes any child directories and files have been 84 + /// deleted by the caller. 85 + pub fn delete(self: *const Directory) Error!void { 86 + try wrap(c.notmuch_directory_delete(self.directory)); 87 + } 88 + 89 + /// Deinitialize a `Directory` object. 90 + pub fn deinit(self: *const Directory) void { 91 + c.notmuch_directory_destroy(self.directory); 92 + }
+17 -30
src/FilenamesIterator.zig
··· 5 5 //! lists of filenames. 6 6 pub const FilenamesIterator = @This(); 7 7 8 + const std = @import("std"); 8 9 const c = @import("c"); 9 - 10 - const status = @import("enums.zig").status; 11 10 12 11 filenames: ?*c.notmuch_filenames_t, 13 12 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, 13 + /// Get the next filename from `FilenamesIterator` as a string. 14 + /// 15 + /// Note: The returned string belongs to `filenames` and has a lifetime 16 + /// identical to it (and the object to which it ultimately belongs). 17 + pub fn next(self: *FilenamesIterator) ?[:0]const u8 { 18 + const filenames = self.filenames orelse return null; 19 + if (c.notmuch_threads_valid(filenames) != 0) return null; 20 + defer c.notmuch_threads_move_to_next(filenames); 21 + return .{ 22 + .thread = std.mem.span(c.notmuch_threads_get(filenames) orelse unreachable), 36 23 }; 37 24 } 38 25 39 - /// Deinitialize a `ThreadIterator` object. 26 + /// Deinitialize a `FilenamesIterator` object. 40 27 /// 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. 28 + /// It's not strictly necessary to call this function. All memory from the 29 + /// `FilenamesIterator` object will be reclaimed when the containing object 30 + /// is deinitialized. 44 31 pub fn deinit(self: *FilenamesIterator) void { 45 - const threads = self.threads orelse return; 46 - c.notmuch_threads_destroy(threads); 32 + const filenames = self.filenames orelse return; 33 + c.notmuch_filenames_destroy(filenames); 47 34 }
+14 -4
src/IndexOpts.zig
··· 14 14 15 15 indexopts: *c.notmuch_indexopts_t, 16 16 17 - pub fn getDecryptPolicy(self: IndexOpts) Decrypt { 18 - return @enumFromInt(c.notmuch_indexopts_get_decrypt_policy(self.indexopts)); 19 - } 20 - 17 + /// Specify whether to decrypt encrypted parts while indexing. 18 + /// 19 + /// Be aware that the index is likely sufficient to reconstruct the cleartext 20 + /// of the message itself, so please ensure that the notmuch message index is 21 + /// adequately protected. DO NOT SET THIS FLAG TO TRUE without considering the 22 + /// security of your index. 21 23 pub fn setDecryptPolicy(self: IndexOpts, decrypt_policy: Decrypt) Error!void { 22 24 try wrap(c.notmuch_indexopts_set_decrypt_policy(self.indexopts, @intFromEnum(decrypt_policy))); 23 25 } 24 26 27 + /// Return whether to decrypt encrypted parts while indexing. 28 + /// 29 + /// See `setDecryptPolicy`. 30 + pub fn getDecryptPolicy(self: IndexOpts) Decrypt { 31 + return @enumFromInt(c.notmuch_indexopts_get_decrypt_policy(self.indexopts)); 32 + } 33 + 34 + /// Deinitialize the `IndexOpts` object. 25 35 pub fn deinit(self: IndexOpts) void { 26 36 c.notmuch_indexopts_destroy(self.indexopts); 27 37 }
+283 -140
src/Message.zig
··· 12 12 const Error = @import("error.zig").Error; 13 13 const FilenamesIterator = @import("FilenamesIterator.zig"); 14 14 const IndexOpts = @import("IndexOpts.zig"); 15 + const MaildirFlag = @import("enums.zig").MaildirFlag; 15 16 const MessageFlag = @import("enums.zig").MessageFlag; 16 17 const MessagesIterator = @import("MessagesIterator.zig"); 18 + const PropertiesIterator = @import("PropertiesIterator.zig"); 17 19 const status = @import("enums.zig").status; 18 20 const TagsIterator = @import("TagsIterator.zig"); 19 21 const wrap = @import("error.zig").wrap; ··· 160 162 }; 161 163 } 162 164 163 - /// Add a tag to the given message. 164 - pub fn addTag(self: *const Message, tag: [:0]const u8) Error!void { 165 - try wrap(c.notmuch_message_add_tag(self.message, tag)); 165 + /// Set a value of a flag for the email corresponding to `Message`. 166 + pub fn setFlag(self: *const Message, flag: MessageFlag, value: bool) void { 167 + c.notmuch_message_set_flag(self.message, @intFromEnum(flag), @intFromBool(value)); 166 168 } 167 169 168 - /// Remove a tag from the given message. 169 - pub fn removeTag(self: *const Message, tag: [:0]const u8) Error!void { 170 - try wrap(c.notmuch_message_add_tag(self.message, tag)); 170 + /// Get the date of `Message` as the number of seconds since the Unix epoch 171 + /// (1970-01-01 00:00:00 UTC). 172 + /// 173 + /// For the original textual representation of the `Date` header from the 174 + /// message call `getHeader` with a header value of "date". 175 + /// 176 + /// Returns `null` in case of error. 177 + pub fn getDate(self: *const Message) ?i64 { 178 + std.debug.assert(@typeInfo(c.time_t).int.bits <= @typeInfo(i64).int.bits); 179 + const time = c.notmuch_message_get_date(self.message); 180 + if (time == 0) return null; 181 + return @intCast(time); 171 182 } 172 183 173 - /// Remove all tags from the given message. 184 + /// Get the value of the specified header from `Message` as a UTF-8 string. 174 185 /// 175 - /// See freeze for an example showing how to safely replace tag values. 176 - pub fn removeAllTags(self: *const Message) Error!void { 177 - try wrap(c.notmuch_message_remove_all_tags(self.message)); 186 + /// Common headers are stored in the database when the message is indexed and 187 + /// will be returned from the database. Other headers will be read from the 188 + /// actual message file. 189 + /// 190 + /// The header name is case insensitive. 191 + /// 192 + /// The returned string belongs to the message so should not be modified or 193 + /// freed by the caller (nor should it be referenced after the message is 194 + /// deinitialized). 195 + /// 196 + /// Returns an empty string ("") if the message does not contain a header line 197 + /// matching 'header'. Returns `null` if any error occurs. 198 + pub fn getHeader(self: *const Message, header: [:0]const u8) ?[:0]const u8 { 199 + return std.mem.span(c.notmuch_message_get_header(self.message, header) orelse return null); 178 200 } 179 201 180 - /// Get the tags for 'message', returning a TagsIterator object which can be 202 + /// Get the tags for `Message`, returning a `TagsIterator` object which can be 181 203 /// used to iterate over all tags. 182 204 /// 183 205 /// The tags object is owned by the message and as such, will only be valid 184 - /// for as long as the message is valid, (which is until the query from 185 - /// which it derived is destroyed). 206 + /// for as long as the message is valid, which is until the query from which it 207 + /// derived is deinitialized. 186 208 pub fn getTags(self: *const Message) TagsIterator { 187 209 return .{ 188 210 .tags = c.notmuch_message_get_tags(self.message), 189 211 }; 190 212 } 191 213 192 - /// Freeze the current state of 'message' within the database. 214 + pub const AddTagError = error{ 215 + /// The length of `tag` is too long (exceeds NOTMUCH_TAG_MAX). 216 + TagTooLong, 217 + /// Database was opened in read-only mode so message cannot be modified. 218 + ReadOnlyDatabase, 219 + }; 220 + 221 + /// Add a tag to the given message. 222 + pub fn addTag(self: *const Message, tag: [:0]const u8) AddTagError!void { 223 + return switch (status(c.notmuch_message_add_tag(self.message, tag))) { 224 + .success => {}, 225 + .null_pointer => unreachable, 226 + .tag_too_long => error.TagTooLong, 227 + .read_only_database => error.ReadOnlyDatabase, 228 + else => unreachable, 229 + }; 230 + } 231 + 232 + pub const RemoveTagError = error{ 233 + /// The length of `tag` is too long (exceeds NOTMUCH_TAG_MAX). 234 + TagTooLong, 235 + /// Database was opened in read-only mode so message cannot be modified. 236 + ReadOnlyDatabase, 237 + }; 238 + 239 + /// Remove a tag from the given message. 240 + pub fn removeTag(self: *const Message, tag: [:0]const u8) RemoveTagError!void { 241 + return switch (status(c.notmuch_message_add_tag(self.message, tag))) { 242 + .success => {}, 243 + .null_pointer => unreachable, 244 + .tag_too_long => error.TagTooLong, 245 + .read_only_database => error.ReadOnlyDatabase, 246 + else => unreachable, 247 + }; 248 + } 249 + 250 + pub const RemoveAllTagsError = error{ 251 + /// Database was opened in read-only mode so message cannot be modified. 252 + ReadOnlyDatabase, 253 + XapianException, 254 + }; 255 + 256 + /// Remove all tags from the given message. 193 257 /// 194 - /// This means that changes to the message state, (via 195 - /// notmuch_message_add_tag, notmuch_message_remove_tag, and 196 - /// notmuch_message_remove_all_tags), will not be committed to the database 197 - /// until the message is thawed with notmuch_message_thaw. 258 + /// See `freeze` for an example showing how to safely replace tag values. 259 + pub fn removeAllTags(self: *const Message) RemoveAllTagsError!void { 260 + return switch (status(c.notmuch_message_remove_all_tags(self.message))) { 261 + .success => {}, 262 + .read_only_database => error.ReadOnlyDatabase, 263 + .xapian_exception => error.XapianException, 264 + else => unreachable, 265 + }; 266 + } 267 + 268 + /// Add/remove tags according to maildir flags in the message filename(s). 198 269 /// 199 - /// Multiple calls to freeze/thaw are valid and these calls will "stack". 200 - /// That is there must be as many calls to thaw as to freeze before a 201 - /// message is actually thawed. 270 + /// This function examines the filenames of `Message` for maildir flags, and 271 + /// adds or removes tags on `Message` as follows when these flags are present: 272 + /// 273 + /// Flag Action if present 274 + /// ---- ----------------- 275 + /// 'D' Adds the "draft" tag to the message 276 + /// 'F' Adds the "flagged" tag to the message 277 + /// 'P' Adds the "passed" tag to the message 278 + /// 'R' Adds the "replied" tag to the message 279 + /// 'S' Removes the "unread" tag from the message 280 + /// 281 + /// For each flag that is not present, the opposite action (add/remove) is 282 + /// performed for the corresponding tags. 283 + /// 284 + /// Flags are identified as trailing components of the filename after a 285 + /// sequence of ":2,". 286 + /// 287 + /// If there are multiple filenames associated with this message, the flag 288 + /// is considered present if it appears in one or more filenames. (That is, 289 + /// the flags from the multiple filenames are combined with the logical OR 290 + /// operator.) 291 + /// 292 + /// A client can ensure that notmuch database tags remain synchronized 293 + /// with maildir flags by calling this function after each call to 294 + /// `Database.indexFile`. See also `tagsToMaildirFlags` for synchronizing tag 295 + /// changes back to maildir flags. 296 + pub fn maildirFlagsToTags(self: *const Message) Error!void { 297 + try wrap(c.notmuch_message_maildir_flags_to_tags(self.message)); 298 + } 299 + 300 + pub const HasMaildirFlagError = error{ 301 + XapianException, 302 + }; 303 + 304 + /// Check message for maildir flag. 305 + pub fn hasMaildirFlag(self: *const Message, flag: MaildirFlag) HasMaildirFlagError!bool { 306 + var is_set: c.notmuch_bool_t = undefined; 307 + return switch (status(c.notmuch_message_has_maildir_flag_st(self.message, @intFromEnum(flag), &is_set))) { 308 + .success => is_set != 0, 309 + .null_pointer => unreachable, 310 + .xapian_exception => error.XapianException, 311 + else => unreachable, 312 + }; 313 + } 314 + 315 + /// Rename message filename(s) to encode tags as maildir flags. 202 316 /// 203 - /// The ability to do freeze/thaw allows for safe transactions to change tag 204 - /// values. For example, explicitly setting a message to have a given set of 317 + /// Specifically, for each filename corresponding to this message: 318 + /// 319 + /// If the filename is not in a maildir directory, do nothing. (A maildir 320 + /// directory is determined as a directory named "new" or "cur".) Similarly, if 321 + /// the filename has invalid maildir info, (repeated or outof-ASCII-order flag 322 + /// characters after ":2,"), then do nothing. 323 + /// 324 + /// If the filename is in a maildir directory, rename the file so that its 325 + /// filename ends with the sequence ":2," followed by zero or more of the 326 + /// following single-character flags (in ASCII order): 327 + /// 328 + /// * flag 'D' iff the message has the "draft" tag 329 + /// * flag 'F' iff the message has the "flagged" tag 330 + /// * flag 'P' iff the message has the "passed" tag 331 + /// * flag 'R' iff the message has the "replied" tag 332 + /// * flag 'S' iff the message does not have the "unread" tag 333 + /// 334 + /// Any existing flags unmentioned in the list above will be preserved in the 335 + /// renaming. 336 + /// 337 + /// Also, if this filename is in a directory named "new", rename it to be within 338 + /// the neighboring directory named "cur". 339 + /// 340 + /// A client can ensure that maildir filename flags remain synchronized 341 + /// with notmuch database tags by calling this function after changing tags 342 + /// (after calls to `addTag`, `removeTag`, or `freeze`/`thaw`). See also 343 + /// `maildirFlagsToTags` for synchronizing maildir flag changes back to tags. 344 + pub fn tagsToMaildirFlags(self: *const Message) Error!void { 345 + try wrap(c.notmuch_message_tags_to_maildir_flags(self.message)); 346 + } 347 + 348 + /// Freeze the current state of `Message` within the database. 349 + /// 350 + /// This means that changes to the message state, (via `addTag`, `removeTag`, 351 + /// and `removeAllTags`), will not be committed to the database until the 352 + /// message is thawed with `thaw`. 353 + /// 354 + /// Multiple calls to `freeze`/`thaw` are valid and these calls will "stack". 355 + /// That is there must be as many calls to thaw as to freeze before a message is 356 + /// actually thawed. 357 + /// 358 + /// The ability to do `freeze`/`thaw` allows for safe transactions to change 359 + /// tag values. For example, explicitly setting a message to have a given set of 205 360 /// tags might look like this: 206 361 /// 207 - /// notmuch_message_freeze (message); 362 + /// try message.freeze(); 363 + /// defer message.thaw() catch {}; 208 364 /// 209 - /// notmuch_message_remove_all_tags (message); 365 + /// message.removeAllTags(); 210 366 /// 211 - /// for (i = 0; i < NUM_TAGS; i++) 212 - /// notmuch_message_add_tag (message, tags[i]); 367 + /// for (tags) |tag| 368 + /// try message.addTag(tag); 213 369 /// 214 - /// notmuch_message_thaw (message); 370 + /// With `freeze`/`thaw` used like this, the message in the database is 371 + /// guaranteed to have either the full set of original tag values, or the full 372 + /// set of new tag values, but nothing in between. 215 373 /// 216 - /// With freeze/thaw used like this, the message in the database is 217 - /// guaranteed to have either the full set of original tag values, or the 218 - /// full set of new tag values, but nothing in between. 219 - /// 220 - /// Imagine the example above without freeze/thaw and the operation somehow 374 + /// Imagine the example above without `freeze`/`thaw` and the operation somehow 221 375 /// getting interrupted. This could result in the message being left with no 222 - /// tags if the interruption happened after notmuch_message_remove_all_tags 223 - /// but before notmuch_message_add_tag. Get a value of a flag for the email 224 - /// corresponding to 'message'. 376 + /// tags if the interruption happened after `removeAllTags` but before `addTag`. 225 377 pub fn freeze(self: *const Message) Error!void { 226 378 try wrap(c.notmuch_message_freeze(self.message)); 227 379 } 228 380 229 - /// Thaw the current 'message', synchronizing any changes that may have 230 - /// occurred while 'message' was frozen into the notmuch database. 381 + /// Thaw the current `Message`, synchronizing any changes that may have 382 + /// occurred while `Message` was frozen into the `notmuch` database. 231 383 /// 232 - /// See notmuch_message_freeze for an example of how to use this function to 384 + /// See `freeze` for an example of how to use this function to 233 385 /// safely provide tag changes. 234 386 /// 235 - /// Multiple calls to freeze/thaw are valid and these calls with "stack". 236 - /// That is there must be as many calls to thaw as to freeze before a 387 + /// Multiple calls to `freeze`/`thaw` are valid and these calls with "stack". 388 + /// That is there must be as many calls to `thaw` as to `freeze` before a 237 389 /// message is actually thawed. 238 390 pub fn thaw(self: *const Message) Error!void { 239 391 try wrap(c.notmuch_message_thaw(self.message)); 240 392 } 241 393 242 - /// Set a value of a flag for the email corresponding to 'message'. 243 - pub fn setFlag(self: *const Message, flag: MessageFlag, value: bool) void { 244 - c.notmuch_message_set_flag(self.message, @intFromEnum(flag), @intFromBool(value)); 245 - } 246 - 247 - /// Get the date of 'message' as a nanosecond timestamp value. 394 + /// Deinitialize a `Message` object. 248 395 /// 249 - /// For the original textual representation of the Date header from the 250 - /// message call getHeader() with a header value of 251 - /// "date". 252 - /// 253 - /// Returns `null` in case of error. 254 - pub fn getDate(self: *const Message) ?i128 { 255 - const time = c.notmuch_message_get_date(self.message); 256 - if (time == 0) return null; 257 - return time * std.time.ns_per_s; 396 + /// It can be useful to call this function in the case of a single query 397 + /// object with many messages in the result, such as iterating over the entire 398 + /// database. Otherwise, it's fine to never call this function and there will 399 + /// still be no memory leaks. (The memory from the messages get reclaimed when 400 + /// the containing query is deinitialized.) 401 + pub fn deinit(self: *const Message) void { 402 + c.notmuch_message_destroy(self.message); 258 403 } 259 404 260 - /// Get the value of the specified header from 'message' as a UTF-8 string. 261 - /// 262 - /// Common headers are stored in the database when the message is indexed and 263 - /// will be returned from the database. Other headers will be read from the 264 - /// actual message file. 265 - /// 266 - /// The header name is case insensitive. 267 - /// 268 - /// The returned string belongs to the message so should not be modified or 269 - /// freed by the caller (nor should it be referenced after the message is 270 - /// destroyed). 405 + /// Retrieve the value for a single property key. 271 406 /// 272 - /// Returns an empty string ("") if the message does not contain a header line 273 - /// matching 'header'. Returns NULL if any error occurs. 274 - pub fn getHeader(self: *const Message, header: [:0]const u8) ?[:0]const u8 { 275 - return std.mem.span(c.notmuch_message_get_header(self.message, header) orelse return null); 407 + /// Returns a string owned by the `Message` or `null` if there is no such 408 + /// key. In the case of multiple values for the given key, the first one is 409 + /// retrieved. 410 + pub fn getProperty(self: *const Message, key: [:0]const u8) ?[:0]const u8 { 411 + var value: [*c]const u8 = null; 412 + return switch (status(c.notmuch_message_get_property(self.message, key, &value))) { 413 + .success => std.mem.span(value orelse return null), 414 + .null_pointer => unreachable, 415 + else => unreachable, 416 + }; 276 417 } 277 418 278 - /// Retrieve the value for a single property key 279 - /// 280 - /// Returns a string owned by the message or NULL if there is no such 281 - /// key. In the case of multiple values for the given key, the first one 282 - /// is retrieved. 283 - pub fn getProperty(self: *const Message, key: [:0]const u8) Error!?[:0]const u8 { 284 - var value: [*c]const u8 = undefined; 285 - try wrap(c.notmuch_message_get_property(self.message, key, &value)); 286 - return std.mem.span(value orelse return null); 287 - } 419 + pub const AddPropertyError = error{ 420 + /// `key` may not contain an '=' character. 421 + IllegalArgument, 422 + }; 288 423 289 - /// Get the properties for *message*, returning a PropertyIterator object 290 - /// which can be used to iterate over all properties. 291 - /// 292 - /// The PropertyIterator object is owned by the message and as such, will 293 - /// only be valid for as long as the message is valid, (which is until the 294 - /// query from which it derived is destroyed). 295 - pub fn getProperties( 296 - /// the message to examine 297 - self: *const Message, 298 - /// key or key prefix 299 - key: [:0]const u8, 300 - /// if true, require exact match with key, otherwise treat as prefix 301 - exact: bool, 302 - ) PropertyIterator { 303 - return .{ 304 - .properties_ = c.notmuch_message_get_properties(self.message, key, @intFromBool(exact)), 424 + /// Add a (key,value) pair to a message. 425 + pub fn addProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) AddPropertyError!void { 426 + return switch (status(c.notmuch_message_add_property(self.message, key, value))) { 427 + .success => {}, 428 + .null_pointer => unreachable, 429 + .illegal_argument => return error.IllegalArgument, 430 + else => unreachable, 305 431 }; 306 432 } 307 433 308 - /// Add a (key,value) pair to a message. 309 - pub fn addProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) Error!void { 310 - try wrap(c.notmuch_message_add_property(self.message, key, value)); 311 - } 434 + pub const RemovePropertyError = error{ 435 + /// `key` may not contain an '=' character. 436 + IllegalArgument, 437 + }; 312 438 313 439 /// Remove a (key,value) pair from a message. 314 440 /// 315 441 /// It is not an error to remove a non-existent (key,value) pair 316 - pub fn removeProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) Error!void { 317 - try wrap(c.notmuch_message_remove_property(self.message, key, value)); 442 + pub fn removeProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) RemovePropertyError!void { 443 + return switch (status(c.notmuch_message_remove_property(self.message, key, value))) { 444 + .success => {}, 445 + .null_pointer => unreachable, 446 + .illegal_argument => error.IllegalArgument, 447 + else => unreachable, 448 + }; 318 449 } 319 450 451 + pub const RemoveAllPropertiesError = error{ 452 + /// Database was opened in read-only mode so message cannot be modified. 453 + ReadOnlyDatabase, 454 + }; 455 + 320 456 /// Remove all (key,value) pairs from the given message. 321 457 pub fn removeAllProperties( 322 - /// the message to operate on 458 + /// The message to operate on. 323 459 self: *const Message, 324 - /// key to delete properties for. If NULL, delete properties for all keys 460 + /// key to delete properties for. If `null`, delete properties for all keys 325 461 key: ?[:0]const u8, 326 - ) Error!void { 327 - try wrap(c.notmuch_message_remove_all_properties(self.message, key orelse null)); 462 + ) RemoveAllPropertiesError!void { 463 + return switch (status(c.notmuch_message_remove_all_properties(self.message, key orelse null))) { 464 + .success => {}, 465 + .read_only_database => error.ReadOnlyDatabase, 466 + else => unreachable, 467 + }; 328 468 } 329 469 330 - /// Return the number of properties named "key" belonging to the specific message. 331 - pub fn countProperties(self: *const Message, key: [:0]const u8) Error!usize { 332 - var count: c_uint = undefined; 333 - try wrap(c.notmuch_message_count_properties(self.message, key, &count)); 334 - return @intCast(count); 335 - } 470 + pub const RemoveAllPropertiesWithPrefixError = error{ 471 + /// Database was opened in read-only mode so message cannot be modified. 472 + ReadOnlyDatabase, 473 + }; 336 474 337 475 /// Remove all (prefix*,value) pairs from the given message 338 476 pub fn removeAllPropertiesWithPrefix( 339 - /// message to operate on 477 + /// The message to operate on. 340 478 self: *const Message, 341 - /// delete properties with keys that start with prefix. If NULL, delete all properties 479 + /// Delete properties with keys that start with prefix. If `null`, delete all properties. 342 480 prefix: ?[:0]const u8, 343 - ) Error!void { 344 - try wrap(c.notmuch_message_remove_all_properties_with_prefix(self.message, prefix orelse null)); 481 + ) RemoveAllPropertiesWithPrefixError!void { 482 + return switch (status(c.notmuch_message_remove_all_properties_with_prefix(self.message, prefix orelse null))) { 483 + .success => {}, 484 + .read_only_database => error.ReadOnlyDatabase, 485 + else => unreachable, 486 + }; 345 487 } 346 488 347 - pub fn deinit(self: *const Message) void { 348 - c.notmuch_message_destroy(self.message); 489 + /// Get the properties for `Message`, returning a `PropertyIterator` object 490 + /// which can be used to iterate over all properties. 491 + /// 492 + /// The `PropertyIterator` object is owned by the message and as such, will only 493 + /// be valid for as long as the message is valid, which is until the query from 494 + /// which it derived is deinitialized. 495 + pub fn getProperties( 496 + /// The message to examine. 497 + self: *const Message, 498 + /// Key or key prefix. 499 + key: [:0]const u8, 500 + /// If `true`, require exact match with key, otherwise treat as prefix. 501 + exact: bool, 502 + ) PropertiesIterator { 503 + return .{ 504 + .properties = c.notmuch_message_get_properties(self.message, key, @intFromBool(exact)), 505 + }; 349 506 } 350 507 351 - pub const PropertyIterator = struct { 352 - properties_: ?*c.notmuch_message_properties_t, 353 - 354 - pub fn next(self: PropertyIterator) ?struct { 355 - key: [:0]const u8, 356 - value: [:0]const u8, 357 - } { 358 - const properties = self.properties_ orelse return null; 359 - if (c.notmuch_message_properties_valid(properties) == 0) return null; 360 - defer c.notmuch_message_properties_move_to_next(properties); 361 - return .{ 362 - .key = std.mem.span(c.notmuch_message_properties_key(properties) orelse unreachable), 363 - .value = std.mem.span(c.notmuch_message_properties_value(properties) orelse unreachable), 364 - }; 365 - } 366 - 367 - pub fn deinit(self: PropertyIterator) void { 368 - const properties = self.properties_ orelse return; 369 - c.notmuch_message_properties_destroy(properties); 370 - } 371 - }; 508 + /// Return the number of properties named "key" belonging to the specific message. 509 + pub fn countProperties(self: *const Message, key: [:0]const u8) Error!usize { 510 + std.debug.assert(@typeInfo(c_uint).int.bits <= @typeInfo(usize).int.bits); 511 + var count: c_uint = undefined; 512 + try wrap(c.notmuch_message_count_properties(self.message, key, &count)); 513 + return @intCast(count); 514 + } 372 515 373 516 test { 374 517 _ = std.testing.refAllDecls(@This());
+30
src/PairsIterator.zig
··· 1 + // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + //! Zig wrapper around the `notmuch` C APIs that deal with config pair lists. 5 + pub const PairsIterator = @This(); 6 + 7 + const std = @import("std"); 8 + const c = @import("c"); 9 + 10 + pairs: *c.notmuch_config_pairs_t, 11 + 12 + pub const Pair = struct { 13 + key: [:0]const u8, 14 + value: [:0]const u8, 15 + }; 16 + 17 + /// Get the next pair, or `null` if there are no more pairs. 18 + pub fn next(self: *PairsIterator) ?Pair { 19 + if (c.notmuch_config_pairs_valid(self.pairs) == 0) return null; 20 + defer c.notmuch_config_pairs_move_to_next(self.pairs); 21 + return .{ 22 + .key = std.mem.span(c.notmuch_config_pairs_key(self.pairs) orelse unreachable), 23 + .value = std.mem.span(c.notmuch_config_pairs_value(self.pairs) orelse unreachable), 24 + }; 25 + } 26 + 27 + /// Deinitialize a config pairs iterator, along with any associated resources. 28 + pub fn deinit(self: *PairsIterator) void { 29 + c.notmuch_config_pairs_destroy(self.pairs); 30 + }
+34
src/PropertiesIterator.zig
··· 1 + // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + //! Zig wrapper around the `notmuch` C APIs that deal with property lists. 5 + const PropertiesIterator = @This(); 6 + 7 + const std = @import("std"); 8 + const c = @import("c"); 9 + 10 + properties: ?*c.notmuch_message_properties_t, 11 + 12 + pub const KV = struct { 13 + key: [:0]const u8, 14 + value: [:0]const u8, 15 + }; 16 + 17 + pub fn next(self: PropertiesIterator) ?KV { 18 + const properties = self.properties orelse return null; 19 + if (c.notmuch_message_properties_valid(properties) == 0) return null; 20 + defer c.notmuch_message_properties_move_to_next(properties); 21 + return .{ 22 + .key = std.mem.span(c.notmuch_message_properties_key(properties) orelse unreachable), 23 + .value = std.mem.span(c.notmuch_message_properties_value(properties) orelse unreachable), 24 + }; 25 + } 26 + 27 + pub fn deinit(self: PropertiesIterator) void { 28 + const properties = self.properties orelse return; 29 + c.notmuch_message_properties_destroy(properties); 30 + } 31 + 32 + test { 33 + _ = std.testing.refAllDecls(@This()); 34 + }
+32
src/ValuesIterator.zig
··· 1 + // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + //! Zig wrapper around the `notmuch` C APIs that deal with value lists. 5 + pub const ValuesIterator = @This(); 6 + 7 + const std = @import("std"); 8 + const c = @import("c"); 9 + 10 + values: *c.notmuch_config_values_t, 11 + 12 + /// Get the next value, or `null` if there are no more values. 13 + pub fn next(self: *ValuesIterator) ?[:0]const u8 { 14 + if (c.notmuch_config_values_valid(self.values) == 0) return null; 15 + defer c.notmuch_config_values_move_to_next(self.values); 16 + return std.mem.span(c.notmuch_config_values_get(self.values) orelse unreachable); 17 + } 18 + 19 + /// Reset the `ValuesIterator` to the first element. 20 + pub fn start(self: *ValuesIterator) void { 21 + c.notmuch_config_values_start(self.values); 22 + } 23 + 24 + /// Deinitialize a config values iterator, along with any associated 25 + /// resources. 26 + pub fn deinit(self: *ValuesIterator) void { 27 + c.notmuch_config_values_destroy(self.values); 28 + } 29 + 30 + test { 31 + std.testing.refAllDecls(@This()); 32 + }
+55 -4
src/enums.zig
··· 73 73 } 74 74 75 75 /// Configuration keys known to notmuch. 76 - pub const Config = generateEnum("NOTMUCH_CONFIG_", &.{ "NOTMUCH_CONFIG_FIRST", "NOTMUCH_CONFIG_LAST" }); 76 + pub const Config = enum(u4) { 77 + database_path = c.NOTMUCH_CONFIG_DATABASE_PATH, 78 + mail_root = c.NOTMUCH_CONFIG_MAIL_ROOT, 79 + hook_dir = c.NOTMUCH_CONFIG_HOOK_DIR, 80 + backup_dir = c.NOTMUCH_CONFIG_BACKUP_DIR, 81 + exclude_tags = c.NOTMUCH_CONFIG_EXCLUDE_TAGS, 82 + new_tags = c.NOTMUCH_CONFIG_NEW_TAGS, 83 + new_ignore = c.NOTMUCH_CONFIG_NEW_IGNORE, 84 + sync_maildir_flags = c.NOTMUCH_CONFIG_SYNC_MAILDIR_FLAGS, 85 + primary_email = c.NOTMUCH_CONFIG_PRIMARY_EMAIL, 86 + other_email = c.NOTMUCH_CONFIG_OTHER_EMAIL, 87 + user_name = c.NOTMUCH_CONFIG_USER_NAME, 88 + autocommit = c.NOTMUCH_CONFIG_AUTOCOMMIT, 89 + extra_headers = c.NOTMUCH_CONFIG_EXTRA_HEADERS, 90 + index_as_text = c.NOTMUCH_CONFIG_INDEX_AS_TEXT, 91 + authors_separator = c.NOTMUCH_CONFIG_AUTHORS_SEPARATOR, 92 + authors_matched_separator = c.NOTMUCH_CONFIG_AUTHORS_MATCHED_SEPARATOR, 93 + // git_fail_on_missing = c.NOTMUCH_CONFIG_GIT_FAIL_ON_MISSING, 94 + // git_metadata_prefix = c.NOTMUCH_CONFIG_GIT_METADATA_PREFIX, 95 + // git_ref = c.NOTMUCH_CONFIG_GIT_REF, 96 + 97 + comptime { 98 + checkEnum(@This(), "NOTMUCH_CONFIG_", &.{ "NOTMUCH_CONFIG_FIRST", "NOTMUCH_CONFIG_LAST" }); 99 + } 100 + }; 77 101 78 102 pub const DatabaseMode = enum(u1) { 79 103 read_only = c.NOTMUCH_DATABASE_MODE_READ_ONLY, ··· 84 108 } 85 109 }; 86 110 87 - pub const Decrypt = generateEnum("NOTMUCH_DECRYPT_", &.{}); 111 + /// Stating a policy about how to decrypt messages. 112 + pub const Decrypt = enum(u2) { 113 + false = c.NOTMUCH_DECRYPT_FALSE, 114 + true = c.NOTMUCH_DECRYPT_TRUE, 115 + auto = c.NOTMUCH_DECRYPT_AUTO, 116 + nostash = c.NOTMUCH_DECRYPT_NOSTASH, 117 + 118 + comptime { 119 + checkEnum(@This(), "NOTMUCH_DECRYPT_", &.{}); 120 + } 121 + }; 88 122 89 123 /// Exclude values for `Query.setOmitExcluded` 90 124 pub const Exclude = generateEnum("NOTMUCH_EXCLUDE_", &.{}); ··· 100 134 comptime { 101 135 checkEnum(@This(), "NOTMUCH_MESSAGE_FLAG_", &.{}); 102 136 } 137 + }; 138 + 139 + pub const MaildirFlag = enum(u8) { 140 + /// Adds the "draft" tag to the message. 141 + D = 'D', 142 + /// Adds the "flagged" tag to the message. 143 + F = 'F', 144 + /// Adds the "passed" tag to the message. 145 + P = 'P', 146 + /// Adds the "replied" tag to the message. 147 + R = 'R', 148 + /// Removes the "unread" tag from the message. 149 + S = 'S', 103 150 }; 104 151 105 152 /// query syntax ··· 180 227 /// read anymore. 181 228 operation_invalidated = c.NOTMUCH_STATUS_OPERATION_INVALIDATED, 182 229 230 + pub fn init(rc: c.notmuch_status_t) Status { 231 + return @enumFromInt(rc); 232 + } 233 + 183 234 comptime { 184 235 checkEnum(@This(), "NOTMUCH_STATUS_", &.{"NOTMUCH_STATUS_LAST_STATUS"}); 185 236 } 186 237 }; 187 238 188 239 /// Convenience function to convert a notmuch API return code to a Status enum. 189 - pub fn status(rc: c_uint) Status { 190 - return @enumFromInt(rc); 240 + pub fn status(rc: c.notmuch_status_t) Status { 241 + return .init(rc); 191 242 }
-11
src/error.zig
··· 3 3 4 4 const std = @import("std"); 5 5 6 - const log = std.log.scoped(.notmuch); 7 - 8 6 const c = @import("c"); 9 7 10 - const Status = @import("enums.zig").Status; 11 8 const status = @import("enums.zig").status; 12 9 13 10 pub const Error = error{ ··· 86 83 /// A Xapian exception occurred. 87 84 XapianException, 88 85 }; 89 - 90 - pub fn wrapMessage(rc: c.notmuch_status_t, message: [*c]const u8) Error!void { 91 - if (message) |msg| { 92 - log.err("{s}", .{msg}); 93 - c.free(@ptrCast(@constCast(msg))); 94 - } 95 - try wrap(rc); 96 - } 97 86 98 87 pub fn wrap(rc: c.notmuch_status_t) Error!void { 99 88 return switch (status(rc)) {
+7 -1
src/notmuch.zig
··· 9 9 10 10 pub const Database = @import("Database.zig"); 11 11 pub const Error = @import("error.zig").Error; 12 + pub const FilenamesIterator = @import("FilenamesIterator.zig"); 12 13 pub const Message = @import("Message.zig"); 13 14 pub const MessagesIterator = @import("MessagesIterator.zig"); 14 15 pub const Query = @import("Query.zig"); ··· 16 17 pub const ThreadsIterator = @import("ThreadsIterator.zig"); 17 18 18 19 /// The maximum length of a tag. 19 - pub const tag_max = c.NOTMUCH_TAG_MAX; 20 + pub const tag_max: usize = @intCast(c.NOTMUCH_TAG_MAX); 20 21 21 22 const wrap = @import("error.zig").wrap; 22 23 ··· 31 32 /// passed verbatim to any callback invoked. 32 33 pub fn compact(path: [:0]const u8, backup_path: [:0]const u8, status_cb: ?Database.StatusCallback, closure: ?*anyopaque) Error!void { 33 34 try wrap(c.notmuch_database_compact_db(path, backup_path, status_cb, closure)); 35 + } 36 + 37 + /// Interrogate the library for compile time features. 38 + pub fn builtWith(name: [:0]const u8) bool { 39 + return c.notmuch_built_with(name) != 0; 34 40 } 35 41 36 42 test {