Zig bindings for the notmuch C library
0

Configure Feed

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

more documentation updates and api updates

Jeffrey C. Ollie (Mar 1, 2026, 8:04 PM -0600) e9d616fd d343e645

+266 -70
+250 -70
src/Database.zig
··· 22 22 const STATUS = enums.STATUS; 23 23 const status = enums.status; 24 24 25 + const Directory = @import("Directory.zig"); 25 26 const Message = @import("Message.zig"); 26 27 const Query = @import("Query.zig"); 27 28 29 + /// A callback invoked by `compact` to notify the user of the progress of the 30 + /// compaction process. 31 + pub const StatusCallback = fn (message: [*c]const u8, closure: ?*anyopaque) callconv(.c) void; 32 + 33 + /// Compact a notmuch database, backing up the original database to the given 34 + /// path. 35 + /// 36 + /// The database will be opened in read-write mode during the compaction process 37 + /// to ensure no writes are made. 38 + /// 39 + /// If the optional callback function `status_cb` is non-`null`, it will be 40 + /// called with diagnostic and informational messages. The argument `closure` is 41 + /// passed verbatim to any callback invoked. 42 + pub fn compact(path: [:0]const u8, backup_path: [:0]const u8, status_cb: ?StatusCallback, closure: ?*anyopaque) Error!void { 43 + try wrap(c.notmuch_database_compact_db(path, backup_path, status_cb, closure)); 44 + } 45 + 28 46 database: *c.notmuch_database_t, 29 47 30 - pub const OpenOptions = struct { 48 + pub const OpenCreateOptions = struct { 31 49 /// Path to config file. 32 50 /// 33 51 /// Config file is key-value, with mandatory sections. See ··· 85 103 pub const OpenError = error{ 86 104 NullPointer, 87 105 NoConfig, 106 + NotmuchVersion, 88 107 OutOfMemory, 89 108 FileError, 90 109 XapianException, 91 110 }; 92 111 112 + pub const OpenResult = union(enum) { 113 + ok: Database, 114 + err: struct { 115 + err: OpenError, 116 + msg: [*c]u8, 117 + 118 + pub fn message(self: @This()) ?[:0]const u8 { 119 + return std.mem.span(self.msg orelse return null); 120 + } 121 + 122 + pub fn deinit(self: @This()) void { 123 + c.free(@ptrCast(@constCast(self.msg))); 124 + } 125 + }, 126 + }; 127 + 93 128 /// Open an existing notmuch database located at `database_path`, using 94 129 /// configuration in `config_path`. 95 - pub fn open(mode: Mode, options: OpenOptions) OpenError!Database { 130 + pub fn open(mode: Mode, options: OpenCreateOptions) OpenResult { 96 131 if (!c.LIBNOTMUCH_CHECK_VERSION(5, 6, 0)) { 97 - return error.NotmuchVersion; 132 + return .{ 133 + .err = .{ 134 + .err = error.NotmuchVersion, 135 + .msg = null, 136 + }, 137 + }; 98 138 } 99 139 100 - var error_message: [*c]u8 = null; 101 - defer if (error_message) |m| c.free(m); 140 + var message: [*c]u8 = null; 102 141 103 142 var database: ?*c.notmuch_database_t = null; 104 143 105 - switch (status(c.notmuch_database_open_with_config( 106 - options.database_path, 144 + const err = switch (status(c.notmuch_database_open_with_config( 145 + options.database_path orelse null, 107 146 @intFromEnum(mode), 108 - options.config_path, 109 - options.profile, 147 + options.config_path orelse null, 148 + options.profile orelse null, 110 149 &database, 111 - &error_message, 112 - ), error_message)) { 113 - .success => {}, 114 - .null_pointer => return error.NullPointer, 115 - .NO_CONFIG => return error.NoConfig, 116 - .out_of_memory => return error.OutOfMemory, 117 - .file_error => return error.FileError, 118 - .xapian_exception => return error.XapianException, 119 - } 150 + &message, 151 + ))) { 152 + .success => { 153 + return .{ 154 + .ok = .{ 155 + .database = database orelse unreachable, 156 + }, 157 + }; 158 + }, 159 + .file_error => error.FileError, 160 + .no_config => error.NoConfig, 161 + .null_pointer => error.NullPointer, 162 + .out_of_memory => error.OutOfMemory, 163 + .xapian_exception => error.XapianException, 164 + else => unreachable, 165 + }; 120 166 121 167 return .{ 122 - .database = database orelse unreachable, 168 + .err = .{ 169 + .err = err, 170 + .msg = message, 171 + }, 123 172 }; 124 173 } 125 174 126 - pub const CreateOptions = struct { 127 - /// Specify a config file. 128 - config_path: ?[:0]const u8 = null, 129 - /// Specify a database path. 130 - database_path: ?[*:0]const u8 = null, 131 - /// Specify a profile. 132 - profile: ?[:0]const u8 = null, 175 + pub const CreateError = error{ 176 + DatabaseExists, 177 + FileError, 178 + NoConfig, 179 + NotmuchVersion, 180 + OutOfMemory, 181 + XapianException, 182 + }; 183 + 184 + pub const CreateResult = union(enum) { 185 + ok: Database, 186 + err: struct { 187 + err: CreateError, 188 + msg: [*c]u8, 189 + 190 + pub fn message(self: @This()) ?[:0]const u8 { 191 + return std.mem.span(self.msg orelse return null); 192 + } 193 + 194 + pub fn deinit(self: @This()) void { 195 + c.free(@ptrCast(@constCast(self.msg))); 196 + } 197 + }, 133 198 }; 134 199 135 200 /// Create a new notmuch database. 136 - pub fn create(options: CreateOptions) Error!Database { 201 + pub fn create(options: OpenCreateOptions) CreateResult { 137 202 if (!c.LIBNOTMUCH_CHECK_VERSION(5, 6, 0)) { 138 - return error.NotmuchVersion; 203 + return .{ 204 + .err = .{ 205 + .err = error.NotmuchVersion, 206 + .msg = null, 207 + }, 208 + }; 139 209 } 140 210 141 - var error_message: [*c]u8 = null; 211 + var message: [*c]u8 = null; 212 + 142 213 var database: ?*c.notmuch_database_t = null; 143 - try wrapMessage( 144 - c.notmuch_database_create_with_config( 145 - options.database_path, 146 - options.config_path, 147 - options.profile, 148 - &database, 149 - &error_message, 150 - ), 151 - error_message, 152 - ); 214 + 215 + const err = switch (status(c.notmuch_database_create_with_config( 216 + options.database_path orelse null, 217 + options.config_path orelse null, 218 + options.profile orelse null, 219 + &database, 220 + &message, 221 + ))) { 222 + .success => { 223 + return .{ 224 + .ok = .{ 225 + .database = database orelse unreachable, 226 + }, 227 + }; 228 + }, 229 + .database_exists => error.DatabaseExists, 230 + .file_error => error.FileError, 231 + .no_config => error.NoConfig, 232 + .out_of_memory => error.OutOfMemory, 233 + .xapian_exception => error.XapianException, 234 + else => |v| { 235 + std.debug.print("{t}\n", .{v}); 236 + unreachable; 237 + }, 238 + }; 239 + 153 240 return .{ 154 - .database = database orelse unreachable, 241 + .err = .{ 242 + .err = err, 243 + .msg = message, 244 + }, 155 245 }; 156 246 } 157 247 158 - /// Close the database. 159 - pub fn close(self: *const Database) void { 160 - _ = c.notmuch_database_close(self.database); 248 + test create { 249 + const alloc = std.testing.allocator; 250 + const io = std.testing.io; 251 + var tmp = std.testing.tmpDir(.{}); 252 + 253 + try tmp.dir.createDir(io, "mail", .default_dir); 254 + try tmp.dir.createDir(io, "mail/cur", .default_dir); 255 + try tmp.dir.createDir(io, "mail/new", .default_dir); 256 + try tmp.dir.createDir(io, "mail/tmp", .default_dir); 257 + 258 + const config_path = cfg: { 259 + var dir_name_buf: [std.fs.max_path_bytes]u8 = undefined; 260 + const len = try tmp.dir.realPath(std.testing.io, &dir_name_buf); 261 + const dir_name = dir_name_buf[0..len]; 262 + 263 + const database_path = try std.fs.path.joinZ(alloc, &.{ dir_name, "mail" }); 264 + defer alloc.free(database_path); 265 + 266 + var cfg = try tmp.dir.createFile(io, "config", .{}); 267 + defer cfg.close(io); 268 + 269 + var buf: [64]u8 = undefined; 270 + var file_writer = cfg.writer(io, &buf); 271 + const writer = &file_writer.interface; 272 + 273 + try writer.print( 274 + \\[database] 275 + \\path={s} 276 + \\mail_root={s} 277 + \\[user] 278 + \\primary_email=zig@example.org 279 + \\[new] 280 + \\[search] 281 + \\[maildir] 282 + \\ 283 + , 284 + .{ 285 + database_path, 286 + database_path, 287 + }, 288 + ); 289 + try writer.flush(); 290 + 291 + break :cfg try std.fs.path.joinZ(alloc, &.{ dir_name, "config" }); 292 + }; 293 + defer alloc.free(config_path); 294 + 295 + const r = create(.{ .config_path = config_path }); 296 + defer switch (r) { 297 + .ok => |db| db.destroy() catch {}, 298 + .err => |e| e.deinit(), 299 + }; 300 + try std.testing.expect(r == .ok); 301 + } 302 + 303 + pub const CloseError = error{XapianException}; 304 + 305 + /// Commit changes and close the given notmuch database. 306 + /// 307 + /// After `close` has been called, calls to other functions on 308 + /// objects derived from this database may either behave as if the database had 309 + /// not been closed (e.g., if the required data has been cached) or may fail 310 + /// with a NOTMUCH_STATUS_XAPIAN_EXCEPTION. The only further operation permitted 311 + /// on the database itself is to call notmuch_database_destroy. 312 + /// 313 + /// `close` can be called multiple times. Later calls have no 314 + /// effect. 315 + /// 316 + /// For writable databases, `close` commits all changes to disk before closing 317 + /// the database, unless the caller is currently in an atomic section (there was 318 + /// a `beginAtomic` without a matching `endAtomic`). In this case changes since 319 + /// the last commit are discarded. See `endAtomic` for more information. 320 + pub fn close(self: *const Database) CloseError!void { 321 + switch (status(c.notmuch_database_close(self.database))) { 322 + .success => {}, 323 + .xapian_exception => return error.XapianException, 324 + else => unreachable, 325 + } 161 326 } 162 327 163 328 /// Destroy the notmuch database, closing it if necessary and freeing all 164 329 /// associated resources. 165 330 /// 166 - /// Return value as in notmuch_database_close if the database was open; 167 - /// notmuch_database_destroy itself has no failure modes. 331 + /// Return value as in `close` if the database was open; `destroy` itself has no 332 + /// failure modes. 168 333 pub fn destroy(self: *const Database) Error!void { 169 - try wrap(c.notmuch_database_destroy(self.database)); 334 + switch (status(c.notmuch_database_destroy(self.database))) { 335 + .success => {}, 336 + .xapian_exception => return error.XapianException, 337 + else => unreachable, 338 + } 170 339 } 171 340 172 - pub fn indexFile(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!void { 173 - try wrap(c.notmuch_database_index_file( 174 - self.database, 175 - filename, 176 - if (indexopts) |i| i.indexopts else null, 177 - null, 178 - )); 179 - } 180 - 181 - /// A callback invoked by Database.compact to notify the user of the 182 - /// progress of the compaction process. 183 - pub const StatusCallback = fn (message: [*c]const u8, closure: ?*anyopaque) callconv(.c) void; 184 - 185 - /// Compact a notmuch database, backing up the original database to the given 186 - /// path. 341 + /// Return the database path of the given database. 187 342 /// 188 - /// The database will be opened in read-write mode during the compaction process 189 - /// to ensure no writes are made. 190 - /// 191 - /// If the optional callback function `status_cb` is non-`null`, it will be 192 - /// called with diagnostic and informational messages. The argument `closure` is 193 - /// passed verbatim to any callback invoked. 194 - pub fn compact(path: [:0]const u8, backup_path: [:0]const u8, status_cb: ?StatusCallback, closure: ?*anyopaque) Error!void { 195 - try wrap(c.notmuch_database_compact(path, backup_path, status_cb, closure)); 343 + /// The return value is a string owned by notmuch so should not be modified nor 344 + /// freed by the caller. 345 + pub fn getPath(self: *const Database) ?[:0]const u8 { 346 + return std.mem.span(c.notmuch_database_get_path(self.database) orelse return null); 196 347 } 197 348 198 349 /// Return the database format version of the database. 199 - pub fn getVersion(self: *const Database) error{FormatVersionError}!c_uint { 350 + pub fn getVersion(self: *const Database) error{FormatVersionError}!u32 { 200 351 const version = c.notmuch_database_get_version(self.database); 201 352 if (version == 0) return error.FormatVersionError; 202 353 return version; ··· 295 446 .revision = revision, 296 447 .uuid = std.mem.span(uuid), 297 448 }; 449 + } 450 + 451 + pub fn getDirectory(self: *const Database, path: [:0]const u8) Error!Directory { 452 + var directory: ?*c.notmuch_directory_t = null; 453 + 454 + switch (status(c.notmuch_database_get_directory(self.database, path, &directory))) { 455 + .success => {}, 456 + .null_pointer => return error.NullPointer, 457 + .xapian_exception => return error.XapianException, 458 + .upgrade_required => return error.UpgradeRequired, 459 + else => unreachable, 460 + } 461 + 462 + return .{ 463 + .directory = directory orelse unreachable, 464 + }; 465 + } 466 + 467 + pub fn indexFile(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!void { 468 + try wrap(c.notmuch_database_index_file( 469 + self.database, 470 + filename, 471 + if (indexopts) |i| i.indexopts else null, 472 + null, 473 + )); 298 474 } 299 475 300 476 pub fn indexFileGetMessage(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!Message { ··· 491 667 c.notmuch_config_pairs_destroy(pairs); 492 668 } 493 669 }; 670 + 671 + test { 672 + std.testing.refAllDecls(@This()); 673 + }
+16
src/Directory.zig
··· 1 + // SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + //! Zig wrapper around the `notmuch` directory APIs. 5 + 6 + const Directory = @This(); 7 + 8 + const std = @import("std"); 9 + const log = std.log.scoped(.notmuch); 10 + 11 + const c = @import("c"); 12 + 13 + const Error = @import("error.zig").Error; 14 + const wrap = @import("error.zig").wrap; 15 + 16 + directory: *c.notmuch_directory_t,