Zig bindings for the notmuch C library
0

Configure Feed

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

expand covered apis

Jeffrey C. Ollie (Apr 2, 2025, 10:13 AM -0500) 8d239f96 56d4e7a6

+342 -35
+342 -35
src/notmuch.zig
··· 1 1 const std = @import("std"); 2 2 3 3 const c = @cImport({ 4 + @cInclude("stdlib.h"); 4 5 @cInclude("notmuch.h"); 5 6 }); 6 7 7 8 const log = std.log.scoped(.notmuch); 8 9 9 10 fn generateEnum(comptime prefix: []const u8) type { 10 - @setEvalBranchQuota(10000); 11 + @setEvalBranchQuota(16000); 11 12 const info = @typeInfo(c); 12 13 var count: usize = 0; 13 14 for (info.@"struct".decls) |d| { ··· 30 31 index += 1; 31 32 } 32 33 } 33 - return @Type(.{ .@"enum" = .{ 34 - .tag_type = std.math.IntFittingRange(0, max), 35 - .fields = &fields, 36 - .decls = &.{}, 37 - .is_exhaustive = true, 38 - } }); 34 + return @Type( 35 + .{ 36 + .@"enum" = .{ 37 + .tag_type = std.math.IntFittingRange(0, max), 38 + .fields = &fields, 39 + .decls = &.{}, 40 + .is_exhaustive = true, 41 + }, 42 + }, 43 + ); 39 44 } 40 45 41 - pub const STATUS = generateEnum("NOTMUCH_STATUS_"); 42 46 pub const DATABASE_MODE = generateEnum("NOTMUCH_DATABASE_MODE_"); 47 + pub const DECRYPT = generateEnum("NOTMUCH_DECRYPT_"); 48 + pub const MESSAGE_FLAG = generateEnum("NOTMUCH_MESSAGE_FLAG_"); 49 + pub const STATUS = generateEnum("NOTMUCH_STATUS_"); 43 50 44 51 const Error = error{ 45 52 BadQuerySyntax, ··· 69 76 XapianException, 70 77 }; 71 78 72 - fn statusToError(comptime T: type, rc: c.notmuch_status_t, value: T) Error!T { 79 + fn wrapMessage(rc: c.notmuch_status_t, message: [*c]const u8) Error!void { 80 + if (message) |msg| { 81 + log.err("{s}", .{msg}); 82 + c.free(@ptrCast(@constCast(msg))); 83 + } 84 + try wrap(rc); 85 + } 86 + 87 + fn wrap(rc: c.notmuch_status_t) Error!void { 73 88 return switch (@as(STATUS, @enumFromInt(rc))) { 74 - .SUCCESS => value, 89 + .SUCCESS => {}, 75 90 .BAD_QUERY_SYNTAX => error.BadQuerySyntax, 76 91 .CLOSED_DATABASE => error.ClosedDatabase, 77 92 .DATABASE_EXISTS => error.DatabaseExists, ··· 100 115 } 101 116 102 117 pub const Database = struct { 103 - database: ?*c.notmuch_database_t = null, 118 + database: *c.notmuch_database_t, 104 119 105 - pub fn open_with_config( 120 + pub fn open( 106 121 database_path: ?[*:0]const u8, 107 122 mode: DATABASE_MODE, 108 123 config_path: ?[:0]const u8, ··· 113 128 return error.NotmuchVersion; 114 129 } 115 130 131 + var error_message: [*c]u8 = null; 116 132 var database: ?*c.notmuch_database_t = null; 117 - const rc = c.notmuch_database_open_with_config( 118 - if (database_path) |p| p else null, 133 + try wrapMessage(c.notmuch_database_open_with_config( 134 + database_path orelse null, 119 135 @intFromEnum(mode), 120 - if (config_path) |p| p else null, 121 - if (profile) |p| p else null, 136 + config_path orelse null, 137 + profile orelse null, 122 138 &database, 123 - null, 139 + &error_message, 140 + ), error_message); 141 + return .{ 142 + .database = database orelse unreachable, 143 + }; 144 + } 145 + 146 + pub fn create( 147 + database_path: ?[*:0]const u8, 148 + config_path: ?[:0]const u8, 149 + profile: ?[:0]const u8, 150 + ) Error!Database { 151 + if (!c.LIBNOTMUCH_CHECK_VERSION(5, 6, 0)) { 152 + log.err("need newer notmuch", .{}); 153 + return error.NotmuchVersion; 154 + } 155 + 156 + var error_message: [*c]u8 = null; 157 + var database: ?*c.notmuch_database_t = null; 158 + try wrapMessage( 159 + c.notmuch_database_create_with_config( 160 + database_path orelse null, 161 + config_path orelse null, 162 + profile orelse null, 163 + &database, 164 + &error_message, 165 + ), 166 + error_message, 124 167 ); 125 - return try statusToError(Database, rc, .{ .database = database }); 168 + return .{ 169 + .database = database orelse unreachable, 170 + }; 126 171 } 127 172 128 173 pub fn close(self: *const Database) void { 129 174 _ = c.notmuch_database_close(self.database); 130 175 } 131 176 132 - pub fn index_file(self: *const Database, filename: [:0]const u8, indexopts: ?*c.notmuch_indexopts_t) Error!void { 133 - const rc = c.notmuch_database_index_file(self.database, filename, indexopts, null); 134 - return try statusToError(void, rc, {}); 177 + pub fn indexFile(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!void { 178 + try wrap(c.notmuch_database_index_file( 179 + self.database, 180 + filename, 181 + if (indexopts) |i| i.indexopts else null, 182 + null, 183 + )); 135 184 } 136 185 137 - pub fn index_file_get_message(self: *const Database, filename: [:0]const u8, indexopts: ?*c.notmuch_indexopts_t) Error!Message { 186 + pub fn indexFileGetMessage(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!Message { 138 187 var message: ?*c.notmuch_message_t = null; 139 - const rc = c.notmuch_database_index_file(self.database, filename, indexopts, &message); 140 - return statusToError(Message, rc, .{ .duplicate = false, .message = message }) catch |err| switch (err) { 141 - error.DuplicateMessageID => return .{ .duplicate = true, .message = message }, 188 + wrap(c.notmuch_database_index_file( 189 + self.database, 190 + filename, 191 + if (indexopts) |i| i.indexopts else null, 192 + &message, 193 + )) catch |err| switch (err) { 194 + error.DuplicateMessageID => return .{ 195 + .duplicate = true, 196 + .message = message orelse unreachable, 197 + }, 142 198 else => |e| return e, 199 + }; 200 + return .{ 201 + .duplicate = false, 202 + .message = message orelse unreachable, 143 203 }; 144 204 } 145 205 146 - pub fn find_message_by_filename(self: *const Database, filename: [:0]const u8) Error!Message { 206 + pub fn findMessageByFilename(self: *const Database, filename: [:0]const u8) Error!Message { 147 207 var message: ?*c.notmuch_message_t = null; 148 - const rc = c.notmuch_database_find_message_by_filename(self.database, filename, &message); 149 - return try statusToError(Message, rc, .{ .message = message }); 208 + try wrap(c.notmuch_database_find_message_by_filename(self.database, filename, &message)); 209 + return .{ 210 + .message = message orelse unreachable, 211 + }; 150 212 } 151 213 152 - pub fn remove_message(self: *const Database, filename: [:0]const u8) Error!void { 153 - const rc = c.notmuch_database_remove_message(self.database, filename); 154 - return try statusToError(void, rc, {}); 214 + pub fn removeMessage(self: *const Database, filename: [:0]const u8) Error!void { 215 + try wrap(c.notmuch_database_remove_message(self.database, filename)); 216 + } 217 + 218 + pub fn getDefaultIndexOpts(self: *const Database) ?IndexOpts { 219 + return .{ 220 + .indexopts = c.notmuch_database_get_default_indexopts(self.database) orelse return null, 221 + }; 222 + } 223 + 224 + /// 225 + pub fn getConfigPath(self: *const Database) ?[]const u8 { 226 + const config = c.notmuch_config_path(self.database); 227 + return std.mem.span(config orelse return null); 155 228 } 156 229 }; 157 230 158 231 pub const Message = struct { 159 232 duplicate: ?bool = null, 160 - message: ?*c.notmuch_message_t = null, 233 + message: *c.notmuch_message_t, 161 234 162 - pub fn add_tag(self: *const Message, tag: [:0]const u8) Error!void { 163 - const rc = c.notmuch_message_add_tag(self.message, tag); 164 - return try statusToError(void, rc, {}); 235 + /// Get the message ID of 'message'. 236 + /// 237 + /// The returned string belongs to 'message' and as such, should not be 238 + /// modified by the caller and will only be valid for as long as the message 239 + /// is valid, (which is until the query from which it derived is destroyed). 240 + /// 241 + /// This function will return NULL if triggers an unhandled Xapian 242 + /// exception. 243 + pub fn getMessageID(self: *const Message) ?[]const u8 { 244 + return std.mem.span(c.notmuch_message_get_message_id(self.message) orelse return null); 245 + } 246 + 247 + /// Get the thread ID of 'message'. 248 + /// 249 + /// The returned string belongs to 'message' and as such, should not be 250 + /// modified by the caller and will only be valid for as long as the message 251 + /// is valid, (for example, until the user calls notmuch_message_destroy on 252 + /// 'message' or until a query from which it derived is destroyed). 253 + /// 254 + /// This function will return NULL if triggers an unhandled Xapian 255 + /// exception. 256 + pub fn getThreadID(self: *const Message) ?[:0]const u8 { 257 + return std.mem.span(c.notmuch_message_get_thread_id(self.message) orelse return null); 258 + } 259 + 260 + /// Add a tag to the given message. 261 + pub fn addTag(self: *const Message, tag: [:0]const u8) Error!void { 262 + try wrap(c.notmuch_message_add_tag(self.message, tag)); 263 + } 264 + 265 + /// Remove a tag from the given message. 266 + pub fn removeTag(self: *const Message, tag: [:0]const u8) Error!void { 267 + try wrap(c.notmuch_message_add_tag(self.message, tag)); 268 + } 269 + 270 + /// Remove all tags from the given message. 271 + /// 272 + /// See freeze for an example showing how to safely replace tag values. 273 + pub fn removeAllTags(self: *const Message) Error!void { 274 + try wrap(c.notmuch_message_remove_all_tags(self.message)); 275 + } 276 + 277 + /// Freeze the current state of 'message' within the database. 278 + /// 279 + /// This means that changes to the message state, (via 280 + /// notmuch_message_add_tag, notmuch_message_remove_tag, and 281 + /// notmuch_message_remove_all_tags), will not be committed to the database 282 + /// until the message is thawed with notmuch_message_thaw. 283 + /// 284 + /// Multiple calls to freeze/thaw are valid and these calls will "stack". 285 + /// That is there must be as many calls to thaw as to freeze before a 286 + /// message is actually thawed. 287 + /// 288 + /// The ability to do freeze/thaw allows for safe transactions to change tag 289 + /// values. For example, explicitly setting a message to have a given set of 290 + /// tags might look like this: 291 + /// 292 + /// notmuch_message_freeze (message); 293 + /// 294 + /// notmuch_message_remove_all_tags (message); 295 + /// 296 + /// for (i = 0; i < NUM_TAGS; i++) 297 + /// notmuch_message_add_tag (message, tags[i]); 298 + /// 299 + /// notmuch_message_thaw (message); 300 + /// 301 + /// With freeze/thaw used like this, the message in the database is 302 + /// guaranteed to have either the full set of original tag values, or the 303 + /// full set of new tag values, but nothing in between. 304 + /// 305 + /// Imagine the example above without freeze/thaw and the operation somehow 306 + /// getting interrupted. This could result in the message being left with no 307 + /// tags if the interruption happened after notmuch_message_remove_all_tags 308 + /// but before notmuch_message_add_tag. Get a value of a flag for the email 309 + /// corresponding to 'message'. 310 + pub fn freeze(self: *const Message) Error!void { 311 + try wrap(c.notmuch_message_freeze(self.message)); 312 + } 313 + 314 + /// Thaw the current 'message', synchronizing any changes that may have 315 + /// occurred while 'message' was frozen into the notmuch database. 316 + /// 317 + /// See notmuch_message_freeze for an example of how to use this function to 318 + /// safely provide tag changes. 319 + /// 320 + /// Multiple calls to freeze/thaw are valid and these calls with "stack". 321 + /// That is there must be as many calls to thaw as to freeze before a 322 + /// message is actually thawed. 323 + pub fn thaw(self: *const Message) Error!void { 324 + try wrap(c.notmuch_message_thaw(self.message)); 325 + } 326 + 327 + /// Get a value of a flag for the email corresponding to 'message'. 328 + pub fn getFlag(self: *const Message, flag: MESSAGE_FLAG) Error!bool { 329 + var is_set: c.notmuch_bool_t = undefined; 330 + try wrap(c.notmuch_message_get_flag_st(self.message, @intFromEnum(flag), &is_set)); 331 + return is_set != 0; 332 + } 333 + 334 + /// Set a value of a flag for the email corresponding to 'message'. 335 + pub fn setFlag(self: *const Message, flag: MESSAGE_FLAG, value: bool) void { 336 + c.notmuch_message_set_flag(self.message, @intFromEnum(flag), @intFromBool(value)); 337 + } 338 + 339 + /// Get the date of 'message' as a nanosecond timestamp value. 340 + /// 341 + /// For the original textual representation of the Date header from the 342 + /// message call getHeader() with a header value of 343 + /// "date". 344 + /// 345 + /// Returns `null` in case of error. 346 + pub fn getDate(self: *const Message) ?i128 { 347 + const time = c.notmuch_message_get_date(self.message); 348 + if (time == 0) return null; 349 + return time * std.time.ns_per_s; 350 + } 351 + 352 + /// Get the value of the specified header from 'message' as a UTF-8 string. 353 + /// 354 + /// Common headers are stored in the database when the message is indexed and 355 + /// will be returned from the database. Other headers will be read from the 356 + /// actual message file. 357 + /// 358 + /// The header name is case insensitive. 359 + /// 360 + /// The returned string belongs to the message so should not be modified or 361 + /// freed by the caller (nor should it be referenced after the message is 362 + /// destroyed). 363 + /// 364 + /// Returns an empty string ("") if the message does not contain a header line 365 + /// matching 'header'. Returns NULL if any error occurs. 366 + pub fn getHeader(self: *const Message, header: [:0]const u8) ?[:0]const u8 { 367 + return std.mem.span(c.notmuch_message_get_header(self.message, header) orelse return null); 368 + } 369 + 370 + /// Retrieve the value for a single property key 371 + /// 372 + /// Returns a string owned by the message or NULL if there is no such 373 + /// key. In the case of multiple values for the given key, the first one 374 + /// is retrieved. 375 + pub fn getProperty(self: *const Message, key: [:0]const u8) Error!?[:0]const u8 { 376 + var value: [*c]const u8 = undefined; 377 + try wrap(c.notmuch_message_get_property(self.message, key, &value)); 378 + return std.mem.span(value orelse return null); 379 + } 380 + 381 + /// Get the properties for *message*, returning a PropertyIterator object 382 + /// which can be used to iterate over all properties. 383 + /// 384 + /// The PropertyIterator object is owned by the message and as such, will 385 + /// only be valid for as long as the message is valid, (which is until the 386 + /// query from which it derived is destroyed). 387 + pub fn getProperties( 388 + /// the message to examine 389 + self: *const Message, 390 + /// key or key prefix 391 + key: [:0]const u8, 392 + /// if true, require exact match with key, otherwise treat as prefix 393 + exact: bool, 394 + ) PropertyIterator { 395 + return .{ 396 + .properties_ = c.notmuch_message_get_properties(self.message, key, @intFromBool(exact)), 397 + }; 398 + } 399 + 400 + /// Add a (key,value) pair to a message. 401 + pub fn addProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) Error!void { 402 + try wrap(c.notmuch_message_add_property(self.message, key, value)); 403 + } 404 + 405 + /// Remove a (key,value) pair from a message. 406 + /// 407 + /// It is not an error to remove a non-existent (key,value) pair 408 + pub fn removeProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) Error!void { 409 + try wrap(c.notmuch_message_remove_property(self.message, key, value)); 410 + } 411 + 412 + /// Remove all (key,value) pairs from the given message. 413 + pub fn removeAllProperties( 414 + /// the message to operate on 415 + self: *const Message, 416 + /// key to delete properties for. If NULL, delete properties for all keys 417 + key: ?[:0]const u8, 418 + ) Error!void { 419 + try wrap(c.notmuch_message_remove_all_properties(self.message, key orelse null)); 165 420 } 166 421 167 422 pub fn deinit(self: *const Message) void { 168 423 _ = c.notmuch_message_destroy(self.message); 424 + } 425 + }; 426 + 427 + pub const IndexOpts = struct { 428 + indexopts: *c.notmuch_indexopts_t, 429 + 430 + pub fn getDecryptPolicy(self: IndexOpts) DECRYPT { 431 + return @enumFromInt(c.notmuch_indexopts_get_decrypt_policy(self.indexopts)); 432 + } 433 + 434 + pub fn setDecryptPolicy(self: IndexOpts, decrypt_policy: DECRYPT) Error!void { 435 + try wrap(c.notmuch_indexopts_set_decrypt_policy(self.indexopts, @intFromEnum(decrypt_policy))); 436 + } 437 + 438 + pub fn deinit(self: IndexOpts) void { 439 + c.notmuch_indexopts_destroy(self.indexopts); 440 + } 441 + }; 442 + 443 + pub const TagIterator = struct { 444 + tags: *c.notmuch_tags_t, 445 + 446 + pub fn next(self: *TagIterator) ?[]const u8 { 447 + if (c.notmuch_tags_valid(self.tags) == 0) return null; 448 + defer c.notmuch_tags_move_to_next(self.tags); 449 + return std.mem.span(c.notmuch_tags_get(self.tags) orelse unreachable); 450 + } 451 + 452 + pub fn deinit(self: *TagIterator) void { 453 + c.notmuch_tags_destroy(self.tags); 454 + } 455 + }; 456 + 457 + pub const PropertyIterator = struct { 458 + properties_: ?*c.notmuch_message_properties_t, 459 + 460 + pub fn next(self: PropertyIterator) ?struct { 461 + key: [:0]const u8, 462 + value: [:0]const u8, 463 + } { 464 + const properties = self.properties_ orelse return null; 465 + if (c.notmuch_message_properties_valid(properties) == 0) return null; 466 + defer c.notmuch_message_properties_move_to_next(properties); 467 + return .{ 468 + .key = std.mem.span(c.notmuch_message_properties_key(properties) orelse unreachable), 469 + .value = std.mem.span(c.notmuch_message_properties_value(properties) orelse unreachable), 470 + }; 471 + } 472 + 473 + pub fn deinit(self: PropertyIterator) void { 474 + const properties = self.properties_ orelse return; 475 + c.notmuch_message_properties_destroy(properties); 169 476 } 170 477 }; 171 478