split things up into multiple files and add more APIs

This commit is contained in:
Jeffrey C. Ollie 2025-04-03 00:43:00 -05:00
parent 34046b95f8
commit 401f95333e
Signed by: jeff
GPG key ID: 6F86035A6D97044E
11 changed files with 1004 additions and 473 deletions

287
src/Database.zig Normal file
View file

@ -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);
}
};

258
src/Message.zig Normal file
View file

@ -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);
}
};

20
src/MessagesIterator.zig Normal file
View file

@ -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);
}

132
src/Query.zig Normal file
View file

@ -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);
}

18
src/TagsIterator.zig Normal file
View file

@ -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);
}

129
src/Thread.zig Normal file
View file

@ -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);
}

20
src/ThreadsIterator.zig Normal file
View file

@ -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);
}

4
src/c.zig Normal file
View file

@ -0,0 +1,4 @@
pub const c = @cImport({
@cInclude("stdlib.h");
@cInclude("notmuch.h");
});

59
src/enums.zig Normal file
View file

@ -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"});

73
src/error.zig Normal file
View file

@ -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,
};
}

View file

@ -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());