From 401f95333ee8f2da964d62a14e174229770d4cfa Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 3 Apr 2025 00:43:00 -0500 Subject: [PATCH] split things up into multiple files and add more APIs --- src/Database.zig | 287 +++++++++++++++++++++++ src/Message.zig | 258 +++++++++++++++++++++ src/MessagesIterator.zig | 20 ++ src/Query.zig | 132 +++++++++++ src/TagsIterator.zig | 18 ++ src/Thread.zig | 129 +++++++++++ src/ThreadsIterator.zig | 20 ++ src/c.zig | 4 + src/enums.zig | 59 +++++ src/error.zig | 73 ++++++ src/notmuch.zig | 477 +-------------------------------------- 11 files changed, 1004 insertions(+), 473 deletions(-) create mode 100644 src/Database.zig create mode 100644 src/Message.zig create mode 100644 src/MessagesIterator.zig create mode 100644 src/Query.zig create mode 100644 src/TagsIterator.zig create mode 100644 src/Thread.zig create mode 100644 src/ThreadsIterator.zig create mode 100644 src/c.zig create mode 100644 src/enums.zig create mode 100644 src/error.zig diff --git a/src/Database.zig b/src/Database.zig new file mode 100644 index 0000000..be4293e --- /dev/null +++ b/src/Database.zig @@ -0,0 +1,287 @@ +const Database = @This(); + +const std = @import("std"); +const log = std.log.scoped(.notmuch); + +const c = @import("c.zig").c; + +const Error = @import("error.zig").Error; +const wrap = @import("error.zig").wrap; +const wrapMessage = @import("error.zig").wrapMessage; + +const CONFIG = @import("enums.zig").CONFIG; +const DATABASE_MODE = @import("enums.zig").DATABASE_MODE; +const DECRYPT = @import("enums.zig").DECRYPT; +const QUERY_SYNTAX = @import("enums.zig").QUERY_SYNTAX; + +const Message = @import("Message.zig"); +const Query = @import("Query.zig"); + +database: *c.notmuch_database_t, + +pub fn open( + database_path: ?[*:0]const u8, + mode: DATABASE_MODE, + 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_open_with_config( + database_path orelse null, + @intFromEnum(mode), + config_path orelse null, + profile orelse null, + &database, + &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 .{ + .database = database orelse unreachable, + }; +} + +pub fn close(self: *const Database) void { + _ = c.notmuch_database_close(self.database); +} + +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 indexFileGetMessage(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!Message { + var message: ?*c.notmuch_message_t = null; + 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 findMessageByFilename(self: *const Database, filename: [:0]const u8) Error!Message { + var message: ?*c.notmuch_message_t = null; + 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 getDefaultIndexOpts(self: *const Database) ?IndexOpts { + return .{ + .indexopts = c.notmuch_database_get_default_indexopts(self.database) orelse return null, + }; +} + +/// +pub fn configPath(self: *const Database) ?[:0]const u8 { + const config = c.notmuch_config_path(self.database); + return std.mem.span(config orelse return null); +} + +/// get a configuration value from an open database. +/// +/// This value reflects all configuration information given at the time +/// the database was opened. +/// +/// Returns NULL if 'key' unknown or if no value is known for 'key'. +/// Otherwise returns a string owned by notmuch which should not be modified +/// nor freed by the caller. +pub fn configGet(self: *const Database, key: CONFIG) Error!?[:0]const u8 { + return std.mem.span(c.notmuch_config_get(self.database, @intFromEnum(key)) orelse return null); +} + +/// set a configuration value +pub fn configSet(self: *const Database, key: CONFIG, value: [:0]const u8) Error!void { + try wrap(c.notmuch_config_set(self.database, @intFromEnum(key), value)); +} + +/// Returns an iterator for a ';'-delimited list of configuration values +/// +/// These values reflect all configuration information given at the +/// time the database was opened. +pub fn configGetValues( + self: *const Database, + /// configuration key + key: CONFIG, +) ValuesIterator { + return .{ + .values = c.notmuch_config_get_values(self.database, @intFromEnum(key)), + }; +} + +/// Get a configuration value from an open database as boolean. +/// +/// This value reflects all configuration information given at the time the +/// database was opened. +/// +/// Returns IllegalArgument error if either key is unknown or the +/// corresponding value does not convert to boolean. +pub fn configGetBool( + /// the database + self: *const Database, + /// configuration key + key: CONFIG, +) Error!bool { + var value: c.notmuch_bool_t = undefined; + try wrap(c.notmuch_config_get_bool(self.database, @intFromEnum(key), &value)); + return value != 0; +} + +/// Returns an iterator for a ';'-delimited list of configuration values +/// +/// These values reflect all configuration information given at the +/// time the database was opened. +pub fn configGetValuesString( + self: *const Database, + /// configuration key + key: CONFIG, +) ValuesIterator { + return .{ + .values = c.notmuch_config_get_values_string(self.database, @intFromEnum(key)), + }; +} + +/// Create a new query for 'database'. +/// +/// Here, 'database' should be an open database, (see `open` and `create`). +/// +/// For the query string, we'll document the syntax here more completely in the +/// future, but it's likely to be a specialized version of the general Xapian +/// query syntax: +/// +/// https://xapian.org/docs/queryparser.html +/// +/// As a special case, passing either a length-zero string, (that is ""), or a +/// string consisting of a single asterisk (that is "*"), will result in a query +/// that returns all messages in the database. +/// +/// See `Query.setSort` for controlling the order of results. See +/// `Query.searchMessages` and `Query.searchThreads` to actually execute the +/// query. +pub fn queryCreate(self: *const Database, query_string: [:0]const u8) Error!Query { + return .{ + .query = c.notmuch_query_create(self.database, query_string) orelse return error.OutOfMemory, + }; +} + +pub fn queryCreateWithSyntax(self: *const Database, query_string: [:0]const u8, syntax: QUERY_SYNTAX) Error!Query { + var query: ?*c.notmuch_query_t = undefined; + + try wrap(c.notmuch_query_create_with_syntax(self.database, query_string, @intFromEnum(syntax), &query)); + + return .{ + .query = query orelse return error.OutOfMemory, + }; +} + +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 ValuesIterator = struct { + values: ?*c.notmuch_config_values_t, + + pub fn next(self: *ValuesIterator) ?[:0]const u8 { + const values = self.values orelse return null; + if (c.notmuch_config_values_valid(values) == 0) return null; + defer c.notmuch_config_values_move_to_next(values); + return std.mem.span(c.notmuch_config_values_get(values) orelse unreachable); + } + + pub fn start(self: *ValuesIterator) void { + const values = self.values orelse return; + c.notmuch_config_values_start(values); + } + + pub fn deinit(self: *ValuesIterator) void { + const values = self.values orelse return; + c.notmuch_config_values_destroy(values); + } +}; + +pub const PairsIterator = struct { + pairs: ?*c.notmuch_config_pairs_t, + + pub const Pair = struct { + key: [:0]const u8, + value: [:0]const u8, + }; + + pub fn next(self: *PairsIterator) ?Pair { + const pairs = self.pairs orelse return null; + if (c.notmuch_config_pairs_valid(pairs) == 0) return null; + defer c.notmuch_config_pairs_move_to_next(pairs); + return .{ + .key = std.mem.span(c.notmuch_config_pairs_key(pairs) orelse unreachable), + .value = std.mem.span(c.notmuch_config_pairs_value(pairs) orelse unreachable), + }; + } + + pub fn deinit(self: *PairsIterator) void { + const pairs = self.pairs orelse return; + c.notmuch_config_pairs_destroy(pairs); + } +}; diff --git a/src/Message.zig b/src/Message.zig new file mode 100644 index 0000000..5b226e1 --- /dev/null +++ b/src/Message.zig @@ -0,0 +1,258 @@ +const Message = @This(); + +const std = @import("std"); +const log = std.log.scoped(.notmuch); + +const c = @import("c.zig").c; + +const Error = @import("error.zig").Error; +const wrap = @import("error.zig").wrap; + +const MESSAGE_FLAG = @import("enums.zig").MESSAGE_FLAG; + +const TagsIterator = @import("TagsIterator.zig"); + +duplicate: ?bool = 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) ?[:0]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)); +} + +/// Get the tags for 'message', returning a TagsIterator object which can be +/// used to iterate over all tags. +/// +/// The tags 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 getTags(self: *const Message) TagsIterator { + return .{ + .tags = c.notmuch_message_get_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)); +} + +/// 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)); +} + +/// Return the number of properties named "key" belonging to the specific message. +pub fn countProperties(self: *const Message, key: [:0]const u8) Error!usize { + var count: c_uint = undefined; + try wrap(c.notmuch_message_count_properties(self.message, key, &count)); + return @intCast(count); +} + +/// Remove all (prefix*,value) pairs from the given message +pub fn removeAllPropertiesWithPrefix( + /// message to operate on + self: *const Message, + /// delete properties with keys that start with prefix. If NULL, delete all properties + prefix: ?[:0]const u8, +) Error!void { + try wrap(c.notmuch_message_remove_all_properties_with_prefix(self.message, prefix orelse null)); +} + +pub fn deinit(self: *const Message) void { + c.notmuch_message_destroy(self.message); +} + +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); + } +}; diff --git a/src/MessagesIterator.zig b/src/MessagesIterator.zig new file mode 100644 index 0000000..ddd2444 --- /dev/null +++ b/src/MessagesIterator.zig @@ -0,0 +1,20 @@ +const MessagesIterator = @This(); + +const c = @import("c.zig").c; + +const Message = @import("Message.zig"); + +messages: ?*c.notmuch_messages_t, + +pub fn next(self: *MessagesIterator) ?Message { + const messages = self.messages orelse return null; + if (c.notmuch_messages_valid(messages)) return null; + defer c.notmuch_messages_move_to_next(messages); + return .{ + .message = c.notmuch_threads_get(messages) orelse unreachable, + }; +} + +pub fn deinit(self: *MessagesIterator) void { + c.notmuch_threads_destroy(self.threads); +} diff --git a/src/Query.zig b/src/Query.zig new file mode 100644 index 0000000..303c525 --- /dev/null +++ b/src/Query.zig @@ -0,0 +1,132 @@ +const Query = @This(); + +const std = @import("std"); + +const c = @import("c.zig").c; + +const Error = @import("error.zig").Error; +const wrap = @import("error.zig").wrap; + +const EXCLUDE = @import("enums.zig").EXCLUDE; +const SORT = @import("enums.zig").SORT; + +const MessagesIterator = @import("MessagesIterator.zig"); +const ThreadsIterator = @import("ThreadsIterator.zig"); + +query: *c.notmuch_query_t, + +/// Return the query_string of this query. +pub fn getQueryString(self: *const Query) [:0]const u8 { + return std.mem.span(c.notmuch_query_get_query_string(self.query)); +} + +/// Specify whether to omit excluded results or simply flag them. By default, +/// this is set to TRUE. +/// +/// If set to TRUE or ALL, notmuch_query_search_messages will omit excluded +/// messages from the results, and notmuch_query_search_threads will +/// omit threads that match only in excluded messages. If set to TRUE, +/// notmuch_query_search_threads will include all messages in threads that +/// match in at least one non-excluded message. Otherwise, if set to ALL, +/// notmuch_query_search_threads will omit excluded messages from all threads. +/// +/// If set to FALSE or FLAG then both notmuch_query_search_messages and +/// notmuch_query_search_threads will return all matching messages/threads +/// regardless of exclude status. If set to FLAG then the exclude +/// flag will be set for any excluded message that is returned by +/// notmuch_query_search_messages, and the thread counts for threads returned +/// by notmuch_query_search_threads will be the number of non-excluded +/// messages/matches. Otherwise, if set to FALSE, then the exclude status is +/// completely ignored. +/// +/// The performance difference when calling notmuch_query_search_messages should +/// be relatively small (and both should be very fast). However, in some cases, +/// notmuch_query_search_threads is very much faster when omitting excluded +/// messages as it does not need to construct the threads that only match in +/// excluded messages. +pub fn setOmitExcluded(self: *const Query, omit_excluded: EXCLUDE) void { + c.notmuch_query_set_omit_excluded(self.query, @intFromEnum(omit_excluded)); +} + +/// Specify the sorting desired for this query. +pub fn setSort(self: *const Query, sort: SORT) void { + c.notmuch_query_set_sort(self.query, @intFromEnum(sort)); +} + +/// Return the sort specified for this query. +pub fn getSort(self: *const Query) SORT { + return @enumFromInt(c.notmuch_query_get_sort(self.query)); +} + +/// Add a tag that will be excluded from the query results by default. This +/// exclusion will be ignored if this tag appears explicitly in the query. +/// +/// Errors returned: +/// +/// XapianException: a Xapian exception occurred. Most likely a problem lazily +/// parsing the query string. +/// +/// Ignored: tag is explicitly present in the query, so not excluded. +pub fn addTagExclude(self: *const Query, tag: [:0]const u8) Error!void { + try wrap(c.notmuch_query_add_tag_exclude(self.query, tag)); +} + +/// Execute a query for threads, returning a ThreadsIterator object which can +/// be used to iterate over the results. The returned threads object is owned by +/// the query and as such, will only be valid until `Query.deinit`. +/// +/// Note: If you are finished with a thread before its containing query, you +/// can call `Thread.deinit` to clean up some memory sooner (as in the above +/// example). Otherwise, if your thread objects are long-lived, then you don't +/// need to call `Thread.deinit` and all the memory will still be reclaimed when +/// the query is destroyed. +pub fn searchThreads(self: *const Query) Error!ThreadsIterator { + var out: ?*c.notmuch_threads_t = undefined; + + try wrap(c.notmuch_query_search_threads(self.query, &out)); + + return .{ + .threads = out, + }; +} + +/// Execute a query for messages, returning a MessagesIterator object which can +/// be used to iterate over the results. The returned messages object is owned +/// by the query and as such, will only be valid until Query.deinit. +/// +/// Note: If you are finished with a message before its containing query, you +/// can call Message.deinit to clean up some memory sooner (as in the +/// above example). Otherwise, if your message objects are long-lived, then you +/// don't need to call Message.deinit and all the memory will still be +/// reclaimed when the query is destroyed. +pub fn searchMessages(self: *const Query) Error!MessagesIterator { + var out: ?*c.notmuch_messages_t = undefined; + + try wrap(c.notmuch_query_search_messages(self.query, &out)); + + return .{ + .messages = out, + }; +} + +/// Return the number of messages matching a search. +/// +/// This function performs a search and returns the number of matching messages. +pub fn countMessages(self: *const Query) Error!usize { + var count: c_uint = undefined; + try wrap(c.notmuch_query_count_messages(self.query, &count)); + return @intCast(count); +} + +/// Return the number of threads matching a search. +/// +/// This function performs a search and returns the number of matching threads. +pub fn countThreads(self: *const Query) Error!usize { + var count: c_uint = undefined; + try wrap(c.notmuch_query_count_threads(self.query, &count)); + return @intCast(count); +} + +pub fn deinit(self: *const Query) void { + c.notmuch_query_destroy(self.query); +} diff --git a/src/TagsIterator.zig b/src/TagsIterator.zig new file mode 100644 index 0000000..21883d1 --- /dev/null +++ b/src/TagsIterator.zig @@ -0,0 +1,18 @@ +const TagsIterator = @This(); + +const std = @import("std"); + +const c = @import("c.zig").c; + +tags: ?*c.notmuch_tags_t, + +pub fn next(self: *TagsIterator) ?[:0]const u8 { + const tags = self.tags orelse return null; + if (c.notmuch_tags_valid(tags) == 0) return null; + defer c.notmuch_tags_move_to_next(tags); + return std.mem.span(c.notmuch_tags_get(tags) orelse unreachable); +} + +pub fn deinit(self: *TagsIterator) void { + c.notmuch_tags_destroy(self.tags); +} diff --git a/src/Thread.zig b/src/Thread.zig new file mode 100644 index 0000000..de8654e --- /dev/null +++ b/src/Thread.zig @@ -0,0 +1,129 @@ +const Thread = @This(); + +const std = @import("std"); + +const c = @import("c.zig").c; + +const Error = @import("error.zig").Error; +const wrap = @import("error.zig").wrap; + +const MessagesIterator = @import("MessagesIterator.zig"); +const TagsIterator = @import("TagsIterator.zig"); + +thread: *c.notmuch_thread_t, + +/// Get the thread ID of 'thread'. +/// +/// The returned string belongs to 'thread' and as such, should not be modified +/// by the caller and will only be valid for as long as the thread is valid, +/// (which is until notmuch_thread_destroy or until the query from which it +/// derived is destroyed). +pub fn getThreadID(self: *const Thread) [:0]const u8 { + return std.mem.span(c.notmuch_thread_get_thread_id(self.thread)); +} + +/// Get the total number of messages in 'thread'. +/// +/// This count consists of all messages in the database belonging to this +/// thread. Contrast with getMatchedMessages(). +pub fn getTotalMessages(self: *const Thread) usize { + return @intCast(c.notmuch_thread_get_total_messages(self.thread)); +} + +/// Get the number of messages in 'thread' that matched the search. +/// +/// This count includes only the messages in this thread that were matched by +/// the search from which the thread was created and were not excluded by any +/// exclude tags passed in with the query (see Query.addTagExclude). Contrast +/// with getTotalMessages() . +pub fn getMatchedMessages(self: *const Thread) usize { + return @intCast(c.notmuch_thread_get_matched_messages(self.thread)); +} + +/// Get the total number of files in 'thread'. +/// +/// This sums Message.countFiles over all messages in the thread. +pub fn getTotalFiles(self: *const Thread) usize { + return @intCast(c.notmuch_thread_get_total_files(self.thread)); +} + +/// Get a MessagesIterator for the top-level messages in 'thread' in +/// oldest-first order. +/// +/// This iterator will not necessarily iterate over all of the messages in the +/// thread. It will only iterate over the messages in the thread which are not +/// replies to other messages in the thread. +/// +/// The returned list will be destroyed when the thread is destroyed. +pub fn getToplevelMessages(self: *const Thread) MessagesIterator { + return .{ + .messages = c.notmuch_thread_get_toplevel_messages(self.thread), + }; +} + +// Get a MessagesIterator for all messages in 'thread' in oldest-first order. +pub fn getMessages(self: *const Thread) MessagesIterator { + return .{ + .messages = c.notmuch_thread_get_messages(self.thread), + }; +} + +/// Get the authors of 'thread' as a UTF-8 string. +/// +/// The returned string is a comma-separated list of the names of the authors of +/// mail messages in the query results that belong to this thread. +/// +/// The string contains authors of messages matching the query first, then +/// non-matched authors (with the two groups separated by '|'). Within each +/// group, authors are ordered by date. +/// +/// The returned string belongs to 'thread' and as such, should not be modified +/// by the caller and will only be valid for as long as the thread is valid, +/// (which is until notmuch_thread_destroy or until the query from which it +/// derived is destroyed). +pub fn getAuthors(self: *const Thread) [:0]const u8 { + return std.mem.span(c.notmuch_thread_get_authors(self.thread)); +} + +/// Get the subject of 'thread' as a UTF-8 string. +/// +/// The subject is taken from the first message (according to the query +/// order---see Query.setSort) in the query results that belongs to this thread. +/// +/// The returned string belongs to 'thread' and as such, should not be modified +/// by the caller and will only be valid for as long as the thread is valid, +/// (which is until notmuch_thread_destroy or until the query from which it +/// derived is destroyed). +pub fn getSubject(self: *const Thread) [:0]const u8 { + return std.mem.span(c.notmuch_thread_get_subject(self.thread)); +} + +/// Get the date of the oldest message in 'thread' as a nanosecond timestamp. +pub fn getOldestDate(self: *const Thread) i128 { + return c.notmuch_thread_get_oldest_date(self.thread) * std.time.ns_per_s; +} + +/// Get the date of the newest message in 'thread' as a nanosecond timestamp. +pub fn getNewestDate(self: *const Thread) i128 { + return c.notmuch_thread_get_newest_date(self.thread) * std.time.ns_per_s; +} + +/// Get the tags for 'thread', returning a TagsIterator object which can be used +/// to iterate over all tags. +/// +/// Note: In the Notmuch database, tags are stored on individual messages, not +/// on threads. So the tags returned here will be all tags of the messages which +/// matched the search and which belong to this thread. +/// +/// The tags object is owned by the thread and as such, will only be valid for +/// as long as the thread is valid, (for example, until notmuch_thread_destroy +/// or until the query from which it derived is destroyed). +pub fn getTags(self: *const Thread) TagsIterator { + return .{ + .tags = c.notmuch_thread_get_tags(self.thread), + }; +} + +pub fn deinit(self: *const Thread) void { + c.notmuch_thread_destroy(self.thread); +} diff --git a/src/ThreadsIterator.zig b/src/ThreadsIterator.zig new file mode 100644 index 0000000..761e32b --- /dev/null +++ b/src/ThreadsIterator.zig @@ -0,0 +1,20 @@ +pub const ThreadsIterator = @This(); + +const c = @import("c.zig").c; + +const Thread = @import("Thread.zig"); + +threads: ?*c.notmuch_threads_t, + +pub fn next(self: *ThreadsIterator) ?Thread { + const threads = self.threads orelse return null; + if (c.notmuch_threads_valid(threads)) return null; + defer c.notmuch_threads_move_to_next(threads); + return .{ + .thread = c.notmuch_threads_get(threads) orelse unreachable, + }; +} + +pub fn deinit(self: *ThreadsIterator) void { + c.notmuch_threads_destroy(self.threads); +} diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 0000000..01f8f15 --- /dev/null +++ b/src/c.zig @@ -0,0 +1,4 @@ +pub const c = @cImport({ + @cInclude("stdlib.h"); + @cInclude("notmuch.h"); +}); diff --git a/src/enums.zig b/src/enums.zig new file mode 100644 index 0000000..559e9fc --- /dev/null +++ b/src/enums.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +const c = @import("c.zig").c; + +fn generateEnum(comptime prefix: []const u8, skips: []const []const u8) type { + @setEvalBranchQuota(24000); + const info = @typeInfo(c); + var count: usize = 0; + outer: for (info.@"struct".decls) |decl| { + for (skips) |skip| if (std.mem.eql(u8, skip, decl.name)) continue :outer; + if (std.mem.startsWith(u8, decl.name, prefix)) { + count += 1; + } + } + var fields: [count]std.builtin.Type.EnumField = undefined; + var index: usize = 0; + var max: c.notmuch_status_t = 0; + outer: for (info.@"struct".decls) |decl| { + for (skips) |skip| if (std.mem.eql(u8, skip, decl.name)) continue :outer; + if (std.mem.startsWith(u8, decl.name, prefix)) { + max = @max(max, @field(c, decl.name)); + fields[index] = .{ + .name = decl.name[prefix.len..], + .value = @field(c, decl.name), + }; + index += 1; + } + } + return @Type( + .{ + .@"enum" = .{ + .tag_type = std.math.IntFittingRange(0, max), + .fields = &fields, + .decls = &.{}, + .is_exhaustive = true, + }, + }, + ); +} + +/// Configuration keys known to notmuch. +pub const CONFIG = generateEnum("NOTMUCH_CONFIG_", &.{ "NOTMUCH_CONFIG_FIRST", "NOTMUCH_CONFIG_LAST" }); + +pub const DATABASE_MODE = generateEnum("NOTMUCH_DATABASE_MODE_", &.{}); + +pub const DECRYPT = generateEnum("NOTMUCH_DECRYPT_", &.{}); + +/// Exclude values for `Query.setOmitExcluded` +pub const EXCLUDE = generateEnum("NOTMUCH_EXCLUDE_", &.{}); + +pub const MESSAGE_FLAG = generateEnum("NOTMUCH_MESSAGE_FLAG_", &.{}); + +/// query syntax +pub const QUERY_SYNTAX = generateEnum("NOTMUCH_QUERY_SYNTAX_", &.{}); + +/// Sort values for notmuch_query_set_sort. +pub const SORT = generateEnum("NOTMUCH_SORT_", &.{}); + +pub const STATUS = generateEnum("NOTMUCH_STATUS_", &.{"NOTMUCH_STATUS_LAST_STATUS"}); diff --git a/src/error.zig b/src/error.zig new file mode 100644 index 0000000..d90d671 --- /dev/null +++ b/src/error.zig @@ -0,0 +1,73 @@ +const std = @import("std"); + +const log = std.log.scoped(.notmuch); + +const c = @import("c.zig").c; + +const STATUS = @import("enums.zig").STATUS; + +pub const Error = error{ + BadQuerySyntax, + ClosedDatabase, + DatabaseExists, + DuplicateMessageID, + FailedCryptoContextCreation, + FileError, + FileNotEmail, + Ignored, + IllegalArgument, + MaformedCryptoProtocol, + NoConfig, + NoDatabase, + NoMailRoot, + NotmuchVersion, + NullPointer, + OutOfMemory, + PathError, + ReadOnlyDatabase, + TagTooLong, + UnbalancedAtomic, + UnbalancedFreezeThaw, + UnknownCryptoProtocol, + UnsupportedOperation, + UpgradeRequired, + XapianException, +}; + +pub 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); +} + +pub fn wrap(rc: c.notmuch_status_t) Error!void { + return switch (@as(STATUS, @enumFromInt(rc))) { + .SUCCESS => {}, + .BAD_QUERY_SYNTAX => error.BadQuerySyntax, + .CLOSED_DATABASE => error.ClosedDatabase, + .DATABASE_EXISTS => error.DatabaseExists, + .DUPLICATE_MESSAGE_ID => error.DuplicateMessageID, + .FAILED_CRYPTO_CONTEXT_CREATION => error.FailedCryptoContextCreation, + .FILE_ERROR => error.FileError, + .FILE_NOT_EMAIL => error.FileNotEmail, + .IGNORED => error.Ignored, + .ILLEGAL_ARGUMENT => error.IllegalArgument, + .MALFORMED_CRYPTO_PROTOCOL => error.MaformedCryptoProtocol, + .NO_CONFIG => error.NoConfig, + .NO_DATABASE => error.NoDatabase, + .NO_MAIL_ROOT => error.NoMailRoot, + .NULL_POINTER => error.NullPointer, + .OUT_OF_MEMORY => error.OutOfMemory, + .PATH_ERROR => error.PathError, + .READ_ONLY_DATABASE => error.ReadOnlyDatabase, + .TAG_TOO_LONG => error.TagTooLong, + .UNBALANCED_ATOMIC => error.UnbalancedAtomic, + .UNBALANCED_FREEZE_THAW => error.UnbalancedFreezeThaw, + .UNKNOWN_CRYPTO_PROTOCOL => error.UnknownCryptoProtocol, + .UNSUPPORTED_OPERATION => error.UnsupportedOperation, + .UPGRADE_REQUIRED => error.UpgradeRequired, + .XAPIAN_EXCEPTION => error.XapianException, + }; +} diff --git a/src/notmuch.zig b/src/notmuch.zig index b26ed47..07087ff 100644 --- a/src/notmuch.zig +++ b/src/notmuch.zig @@ -1,480 +1,11 @@ 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(16000); - const info = @typeInfo(c); - var count: usize = 0; - for (info.@"struct".decls) |d| { - if (std.mem.eql(u8, "NOTMUCH_STATUS_LAST_STATUS", d.name)) continue; - if (std.mem.startsWith(u8, d.name, prefix)) { - count += 1; - } - } - var fields: [count]std.builtin.Type.EnumField = undefined; - var index: usize = 0; - var max: c.notmuch_status_t = 0; - for (info.@"struct".decls) |d| { - if (std.mem.eql(u8, "NOTMUCH_STATUS_LAST_STATUS", d.name)) continue; - if (std.mem.startsWith(u8, d.name, prefix)) { - max = @max(max, @field(c, d.name)); - fields[index] = .{ - .name = d.name[prefix.len..], - .value = @field(c, d.name), - }; - index += 1; - } - } - return @Type( - .{ - .@"enum" = .{ - .tag_type = std.math.IntFittingRange(0, max), - .fields = &fields, - .decls = &.{}, - .is_exhaustive = true, - }, - }, - ); -} - -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, - ClosedDatabase, - DatabaseExists, - DuplicateMessageID, - FailedCryptoContextCreation, - FileError, - FileNotEmail, - Ignored, - IllegalArgument, - MaformedCryptoProtocol, - NoConfig, - NoDatabase, - NoMailRoot, - NotmuchVersion, - NullPointer, - OutOfMemory, - PathError, - ReadOnlyDatabase, - TagTooLong, - UnbalancedAtomic, - UnbalancedFreezeThaw, - UnknownCryptoProtocol, - UnsupportedOperation, - UpgradeRequired, - XapianException, -}; - -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 => {}, - .BAD_QUERY_SYNTAX => error.BadQuerySyntax, - .CLOSED_DATABASE => error.ClosedDatabase, - .DATABASE_EXISTS => error.DatabaseExists, - .DUPLICATE_MESSAGE_ID => error.DuplicateMessageID, - .FAILED_CRYPTO_CONTEXT_CREATION => error.FailedCryptoContextCreation, - .FILE_ERROR => error.FileError, - .FILE_NOT_EMAIL => error.FileNotEmail, - .IGNORED => error.Ignored, - .ILLEGAL_ARGUMENT => error.IllegalArgument, - .MALFORMED_CRYPTO_PROTOCOL => error.MaformedCryptoProtocol, - .NO_CONFIG => error.NoConfig, - .NO_DATABASE => error.NoDatabase, - .NO_MAIL_ROOT => error.NoMailRoot, - .NULL_POINTER => error.NullPointer, - .OUT_OF_MEMORY => error.OutOfMemory, - .PATH_ERROR => error.PathError, - .READ_ONLY_DATABASE => error.ReadOnlyDatabase, - .TAG_TOO_LONG => error.TagTooLong, - .UNBALANCED_ATOMIC => error.UnbalancedAtomic, - .UNBALANCED_FREEZE_THAW => error.UnbalancedFreezeThaw, - .UNKNOWN_CRYPTO_PROTOCOL => error.UnknownCryptoProtocol, - .UNSUPPORTED_OPERATION => error.UnsupportedOperation, - .UPGRADE_REQUIRED => error.UpgradeRequired, - .XAPIAN_EXCEPTION => error.XapianException, - }; -} - -pub const Database = struct { - database: *c.notmuch_database_t, - - pub fn open( - database_path: ?[*:0]const u8, - mode: DATABASE_MODE, - 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_open_with_config( - database_path orelse null, - @intFromEnum(mode), - config_path orelse null, - profile orelse null, - &database, - &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 .{ - .database = database orelse unreachable, - }; - } - - pub fn close(self: *const Database) void { - _ = c.notmuch_database_close(self.database); - } - - 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 indexFileGetMessage(self: *const Database, filename: [:0]const u8, indexopts: ?IndexOpts) Error!Message { - var message: ?*c.notmuch_message_t = null; - 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 findMessageByFilename(self: *const Database, filename: [:0]const u8) Error!Message { - var message: ?*c.notmuch_message_t = null; - 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 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, - - /// 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)); - } - - /// 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 { - _ = c.notmuch_message_destroy(self.message); - } -}; - -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); - } -}; +pub const Error = @import("error.zig").Error; +pub const Database = @import("Database.zig"); +pub const Message = @import("Message.zig"); +pub const Query = @import("Query.zig"); test { std.testing.refAllDeclsRecursive(@This()); -- 2.51.2