split things up into multiple files and add more APIs
This commit is contained in:
parent
34046b95f8
commit
401f95333e
11 changed files with 1004 additions and 473 deletions
287
src/Database.zig
Normal file
287
src/Database.zig
Normal 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
258
src/Message.zig
Normal 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
20
src/MessagesIterator.zig
Normal 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
132
src/Query.zig
Normal 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
18
src/TagsIterator.zig
Normal 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
129
src/Thread.zig
Normal 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
20
src/ThreadsIterator.zig
Normal 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
4
src/c.zig
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub const c = @cImport({
|
||||
@cInclude("stdlib.h");
|
||||
@cInclude("notmuch.h");
|
||||
});
|
59
src/enums.zig
Normal file
59
src/enums.zig
Normal 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
73
src/error.zig
Normal 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,
|
||||
};
|
||||
}
|
477
src/notmuch.zig
477
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());
|
||||
|
|
Loading…
Add table
Reference in a new issue