From 8d239f965152a196eed98b73f0670b73e7543e73 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 2 Apr 2025 10:13:37 -0500 Subject: [PATCH] expand covered apis --- src/notmuch.zig | 377 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 342 insertions(+), 35 deletions(-) diff --git a/src/notmuch.zig b/src/notmuch.zig index 3ad65a2..b26ed47 100644 --- a/src/notmuch.zig +++ b/src/notmuch.zig @@ -1,13 +1,14 @@ const std = @import("std"); const c = @cImport({ + @cInclude("stdlib.h"); @cInclude("notmuch.h"); }); const log = std.log.scoped(.notmuch); fn generateEnum(comptime prefix: []const u8) type { - @setEvalBranchQuota(10000); + @setEvalBranchQuota(16000); const info = @typeInfo(c); var count: usize = 0; for (info.@"struct".decls) |d| { @@ -30,16 +31,22 @@ fn generateEnum(comptime prefix: []const u8) type { index += 1; } } - return @Type(.{ .@"enum" = .{ - .tag_type = std.math.IntFittingRange(0, max), - .fields = &fields, - .decls = &.{}, - .is_exhaustive = true, - } }); + return @Type( + .{ + .@"enum" = .{ + .tag_type = std.math.IntFittingRange(0, max), + .fields = &fields, + .decls = &.{}, + .is_exhaustive = true, + }, + }, + ); } -pub const STATUS = generateEnum("NOTMUCH_STATUS_"); pub const DATABASE_MODE = generateEnum("NOTMUCH_DATABASE_MODE_"); +pub const DECRYPT = generateEnum("NOTMUCH_DECRYPT_"); +pub const MESSAGE_FLAG = generateEnum("NOTMUCH_MESSAGE_FLAG_"); +pub const STATUS = generateEnum("NOTMUCH_STATUS_"); const Error = error{ BadQuerySyntax, @@ -69,9 +76,17 @@ const Error = error{ XapianException, }; -fn statusToError(comptime T: type, rc: c.notmuch_status_t, value: T) Error!T { +fn wrapMessage(rc: c.notmuch_status_t, message: [*c]const u8) Error!void { + if (message) |msg| { + log.err("{s}", .{msg}); + c.free(@ptrCast(@constCast(msg))); + } + try wrap(rc); +} + +fn wrap(rc: c.notmuch_status_t) Error!void { return switch (@as(STATUS, @enumFromInt(rc))) { - .SUCCESS => value, + .SUCCESS => {}, .BAD_QUERY_SYNTAX => error.BadQuerySyntax, .CLOSED_DATABASE => error.ClosedDatabase, .DATABASE_EXISTS => error.DatabaseExists, @@ -100,9 +115,9 @@ fn statusToError(comptime T: type, rc: c.notmuch_status_t, value: T) Error!T { } pub const Database = struct { - database: ?*c.notmuch_database_t = null, + database: *c.notmuch_database_t, - pub fn open_with_config( + pub fn open( database_path: ?[*:0]const u8, mode: DATABASE_MODE, config_path: ?[:0]const u8, @@ -113,55 +128,295 @@ pub const Database = struct { return error.NotmuchVersion; } + var error_message: [*c]u8 = null; var database: ?*c.notmuch_database_t = null; - const rc = c.notmuch_database_open_with_config( - if (database_path) |p| p else null, + try wrapMessage(c.notmuch_database_open_with_config( + database_path orelse null, @intFromEnum(mode), - if (config_path) |p| p else null, - if (profile) |p| p else null, + config_path orelse null, + profile orelse null, &database, - null, + &error_message, + ), error_message); + return .{ + .database = database orelse unreachable, + }; + } + + pub fn create( + database_path: ?[*:0]const u8, + config_path: ?[:0]const u8, + profile: ?[:0]const u8, + ) Error!Database { + if (!c.LIBNOTMUCH_CHECK_VERSION(5, 6, 0)) { + log.err("need newer notmuch", .{}); + return error.NotmuchVersion; + } + + var error_message: [*c]u8 = null; + var database: ?*c.notmuch_database_t = null; + try wrapMessage( + c.notmuch_database_create_with_config( + database_path orelse null, + config_path orelse null, + profile orelse null, + &database, + &error_message, + ), + error_message, ); - return try statusToError(Database, rc, .{ .database = database }); + return .{ + .database = database orelse unreachable, + }; } pub fn close(self: *const Database) void { _ = c.notmuch_database_close(self.database); } - pub fn index_file(self: *const Database, filename: [:0]const u8, indexopts: ?*c.notmuch_indexopts_t) Error!void { - const rc = c.notmuch_database_index_file(self.database, filename, indexopts, null); - return try statusToError(void, rc, {}); + pub fn indexFile(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!void { + try wrap(c.notmuch_database_index_file( + self.database, + filename, + if (indexopts) |i| i.indexopts else null, + null, + )); } - pub fn index_file_get_message(self: *const Database, filename: [:0]const u8, indexopts: ?*c.notmuch_indexopts_t) Error!Message { + pub fn indexFileGetMessage(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!Message { var message: ?*c.notmuch_message_t = null; - const rc = c.notmuch_database_index_file(self.database, filename, indexopts, &message); - return statusToError(Message, rc, .{ .duplicate = false, .message = message }) catch |err| switch (err) { - error.DuplicateMessageID => return .{ .duplicate = true, .message = message }, + wrap(c.notmuch_database_index_file( + self.database, + filename, + if (indexopts) |i| i.indexopts else null, + &message, + )) catch |err| switch (err) { + error.DuplicateMessageID => return .{ + .duplicate = true, + .message = message orelse unreachable, + }, else => |e| return e, }; + return .{ + .duplicate = false, + .message = message orelse unreachable, + }; } - pub fn find_message_by_filename(self: *const Database, filename: [:0]const u8) Error!Message { + pub fn findMessageByFilename(self: *const Database, filename: [:0]const u8) Error!Message { var message: ?*c.notmuch_message_t = null; - const rc = c.notmuch_database_find_message_by_filename(self.database, filename, &message); - return try statusToError(Message, rc, .{ .message = message }); + try wrap(c.notmuch_database_find_message_by_filename(self.database, filename, &message)); + return .{ + .message = message orelse unreachable, + }; + } + + pub fn removeMessage(self: *const Database, filename: [:0]const u8) Error!void { + try wrap(c.notmuch_database_remove_message(self.database, filename)); } - pub fn remove_message(self: *const Database, filename: [:0]const u8) Error!void { - const rc = c.notmuch_database_remove_message(self.database, filename); - return try statusToError(void, rc, {}); + pub fn getDefaultIndexOpts(self: *const Database) ?IndexOpts { + return .{ + .indexopts = c.notmuch_database_get_default_indexopts(self.database) orelse return null, + }; + } + + /// + pub fn getConfigPath(self: *const Database) ?[]const u8 { + const config = c.notmuch_config_path(self.database); + return std.mem.span(config orelse return null); } }; pub const Message = struct { duplicate: ?bool = null, - message: ?*c.notmuch_message_t = null, + message: *c.notmuch_message_t, + + /// Get the message ID of 'message'. + /// + /// The returned string belongs to 'message' and as such, should not be + /// modified by the caller and will only be valid for as long as the message + /// is valid, (which is until the query from which it derived is destroyed). + /// + /// This function will return NULL if triggers an unhandled Xapian + /// exception. + pub fn getMessageID(self: *const Message) ?[]const u8 { + return std.mem.span(c.notmuch_message_get_message_id(self.message) orelse return null); + } + + /// Get the thread ID of 'message'. + /// + /// The returned string belongs to 'message' and as such, should not be + /// modified by the caller and will only be valid for as long as the message + /// is valid, (for example, until the user calls notmuch_message_destroy on + /// 'message' or until a query from which it derived is destroyed). + /// + /// This function will return NULL if triggers an unhandled Xapian + /// exception. + pub fn getThreadID(self: *const Message) ?[:0]const u8 { + return std.mem.span(c.notmuch_message_get_thread_id(self.message) orelse return null); + } + + /// Add a tag to the given message. + pub fn addTag(self: *const Message, tag: [:0]const u8) Error!void { + try wrap(c.notmuch_message_add_tag(self.message, tag)); + } + + /// Remove a tag from the given message. + pub fn removeTag(self: *const Message, tag: [:0]const u8) Error!void { + try wrap(c.notmuch_message_add_tag(self.message, tag)); + } + + /// Remove all tags from the given message. + /// + /// See freeze for an example showing how to safely replace tag values. + pub fn removeAllTags(self: *const Message) Error!void { + try wrap(c.notmuch_message_remove_all_tags(self.message)); + } + + /// Freeze the current state of 'message' within the database. + /// + /// This means that changes to the message state, (via + /// notmuch_message_add_tag, notmuch_message_remove_tag, and + /// notmuch_message_remove_all_tags), will not be committed to the database + /// until the message is thawed with notmuch_message_thaw. + /// + /// Multiple calls to freeze/thaw are valid and these calls will "stack". + /// That is there must be as many calls to thaw as to freeze before a + /// message is actually thawed. + /// + /// The ability to do freeze/thaw allows for safe transactions to change tag + /// values. For example, explicitly setting a message to have a given set of + /// tags might look like this: + /// + /// notmuch_message_freeze (message); + /// + /// notmuch_message_remove_all_tags (message); + /// + /// for (i = 0; i < NUM_TAGS; i++) + /// notmuch_message_add_tag (message, tags[i]); + /// + /// notmuch_message_thaw (message); + /// + /// With freeze/thaw used like this, the message in the database is + /// guaranteed to have either the full set of original tag values, or the + /// full set of new tag values, but nothing in between. + /// + /// Imagine the example above without freeze/thaw and the operation somehow + /// getting interrupted. This could result in the message being left with no + /// tags if the interruption happened after notmuch_message_remove_all_tags + /// but before notmuch_message_add_tag. Get a value of a flag for the email + /// corresponding to 'message'. + pub fn freeze(self: *const Message) Error!void { + try wrap(c.notmuch_message_freeze(self.message)); + } - pub fn add_tag(self: *const Message, tag: [:0]const u8) Error!void { - const rc = c.notmuch_message_add_tag(self.message, tag); - return try statusToError(void, rc, {}); + /// Thaw the current 'message', synchronizing any changes that may have + /// occurred while 'message' was frozen into the notmuch database. + /// + /// See notmuch_message_freeze for an example of how to use this function to + /// safely provide tag changes. + /// + /// Multiple calls to freeze/thaw are valid and these calls with "stack". + /// That is there must be as many calls to thaw as to freeze before a + /// message is actually thawed. + pub fn thaw(self: *const Message) Error!void { + try wrap(c.notmuch_message_thaw(self.message)); + } + + /// Get a value of a flag for the email corresponding to 'message'. + pub fn getFlag(self: *const Message, flag: MESSAGE_FLAG) Error!bool { + var is_set: c.notmuch_bool_t = undefined; + try wrap(c.notmuch_message_get_flag_st(self.message, @intFromEnum(flag), &is_set)); + return is_set != 0; + } + + /// Set a value of a flag for the email corresponding to 'message'. + pub fn setFlag(self: *const Message, flag: MESSAGE_FLAG, value: bool) void { + c.notmuch_message_set_flag(self.message, @intFromEnum(flag), @intFromBool(value)); + } + + /// Get the date of 'message' as a nanosecond timestamp value. + /// + /// For the original textual representation of the Date header from the + /// message call getHeader() with a header value of + /// "date". + /// + /// Returns `null` in case of error. + pub fn getDate(self: *const Message) ?i128 { + const time = c.notmuch_message_get_date(self.message); + if (time == 0) return null; + return time * std.time.ns_per_s; + } + + /// Get the value of the specified header from 'message' as a UTF-8 string. + /// + /// Common headers are stored in the database when the message is indexed and + /// will be returned from the database. Other headers will be read from the + /// actual message file. + /// + /// The header name is case insensitive. + /// + /// The returned string belongs to the message so should not be modified or + /// freed by the caller (nor should it be referenced after the message is + /// destroyed). + /// + /// Returns an empty string ("") if the message does not contain a header line + /// matching 'header'. Returns NULL if any error occurs. + pub fn getHeader(self: *const Message, header: [:0]const u8) ?[:0]const u8 { + return std.mem.span(c.notmuch_message_get_header(self.message, header) orelse return null); + } + + /// Retrieve the value for a single property key + /// + /// Returns a string owned by the message or NULL if there is no such + /// key. In the case of multiple values for the given key, the first one + /// is retrieved. + pub fn getProperty(self: *const Message, key: [:0]const u8) Error!?[:0]const u8 { + var value: [*c]const u8 = undefined; + try wrap(c.notmuch_message_get_property(self.message, key, &value)); + return std.mem.span(value orelse return null); + } + + /// Get the properties for *message*, returning a PropertyIterator object + /// which can be used to iterate over all properties. + /// + /// The PropertyIterator object is owned by the message and as such, will + /// only be valid for as long as the message is valid, (which is until the + /// query from which it derived is destroyed). + pub fn getProperties( + /// the message to examine + self: *const Message, + /// key or key prefix + key: [:0]const u8, + /// if true, require exact match with key, otherwise treat as prefix + exact: bool, + ) PropertyIterator { + return .{ + .properties_ = c.notmuch_message_get_properties(self.message, key, @intFromBool(exact)), + }; + } + + /// Add a (key,value) pair to a message. + pub fn addProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) Error!void { + try wrap(c.notmuch_message_add_property(self.message, key, value)); + } + + /// Remove a (key,value) pair from a message. + /// + /// It is not an error to remove a non-existent (key,value) pair + pub fn removeProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) Error!void { + try wrap(c.notmuch_message_remove_property(self.message, key, value)); + } + + /// Remove all (key,value) pairs from the given message. + pub fn removeAllProperties( + /// the message to operate on + self: *const Message, + /// key to delete properties for. If NULL, delete properties for all keys + key: ?[:0]const u8, + ) Error!void { + try wrap(c.notmuch_message_remove_all_properties(self.message, key orelse null)); } pub fn deinit(self: *const Message) void { @@ -169,6 +424,58 @@ pub const Message = struct { } }; +pub const IndexOpts = struct { + indexopts: *c.notmuch_indexopts_t, + + pub fn getDecryptPolicy(self: IndexOpts) DECRYPT { + return @enumFromInt(c.notmuch_indexopts_get_decrypt_policy(self.indexopts)); + } + + pub fn setDecryptPolicy(self: IndexOpts, decrypt_policy: DECRYPT) Error!void { + try wrap(c.notmuch_indexopts_set_decrypt_policy(self.indexopts, @intFromEnum(decrypt_policy))); + } + + pub fn deinit(self: IndexOpts) void { + c.notmuch_indexopts_destroy(self.indexopts); + } +}; + +pub const TagIterator = struct { + tags: *c.notmuch_tags_t, + + pub fn next(self: *TagIterator) ?[]const u8 { + if (c.notmuch_tags_valid(self.tags) == 0) return null; + defer c.notmuch_tags_move_to_next(self.tags); + return std.mem.span(c.notmuch_tags_get(self.tags) orelse unreachable); + } + + pub fn deinit(self: *TagIterator) void { + c.notmuch_tags_destroy(self.tags); + } +}; + +pub const PropertyIterator = struct { + properties_: ?*c.notmuch_message_properties_t, + + pub fn next(self: PropertyIterator) ?struct { + key: [:0]const u8, + value: [:0]const u8, + } { + const properties = self.properties_ orelse return null; + if (c.notmuch_message_properties_valid(properties) == 0) return null; + defer c.notmuch_message_properties_move_to_next(properties); + return .{ + .key = std.mem.span(c.notmuch_message_properties_key(properties) orelse unreachable), + .value = std.mem.span(c.notmuch_message_properties_value(properties) orelse unreachable), + }; + } + + pub fn deinit(self: PropertyIterator) void { + const properties = self.properties_ orelse return; + c.notmuch_message_properties_destroy(properties); + } +}; + test { std.testing.refAllDeclsRecursive(@This()); } -- 2.51.2