367 lines
14 KiB
Zig
367 lines
14 KiB
Zig
//! Zig bindings for the notmuch email indexing library.
|
|
//!
|
|
//! This module provides a safe Zig interface to the notmuch C library,
|
|
//! allowing for searching, tagging, and managing email messages indexed
|
|
//! by notmuch.
|
|
//!
|
|
//! Main components:
|
|
//! - `Db`: Database access and query operations
|
|
//! - `Thread`: Email thread representation
|
|
//! - `Message`: Individual email message access
|
|
//! - `Status`: Error handling and status reporting
|
|
//!
|
|
//! Example usage:
|
|
//! ```
|
|
//! var status: Status = undefined;
|
|
//! var db = try Db.open("/path/to/maildir", &status);
|
|
//! defer db.close();
|
|
//!
|
|
//! var threads = try db.searchThreads("from:example.com");
|
|
//! defer threads.deinit();
|
|
//!
|
|
//! while (threads.next()) |thread| {
|
|
//! defer thread.deinit();
|
|
//! std.debug.print("Thread: {s}\n", .{thread.getSubject()});
|
|
//! }
|
|
//! ```
|
|
|
|
const std = @import("std");
|
|
const c = @import("c.zig").c;
|
|
|
|
pub const Status = struct {
|
|
err: ?anyerror = null,
|
|
status: c.notmuch_status_t = c.NOTMUCH_STATUS_SUCCESS,
|
|
msg: ?[*:0]u8 = null,
|
|
|
|
pub fn deinit(status: *Status) void {
|
|
if (status.msg) |m| std.c.free(m);
|
|
status.err = undefined;
|
|
status.status = c.NOTMUCH_STATUS_SUCCESS;
|
|
status.msg = null;
|
|
}
|
|
|
|
pub fn statusString(status: Status) []const u8 {
|
|
return std.mem.span(c.notmuch_status_to_string(status.status));
|
|
}
|
|
};
|
|
pub const Db = struct {
|
|
handle: *c.notmuch_database_t,
|
|
|
|
pub fn open(path: [:0]const u8, status: ?*Status) !Db {
|
|
var db: ?*c.notmuch_database_t = null;
|
|
var err: ?[*:0]u8 = null;
|
|
|
|
const open_status = c.notmuch_database_open_with_config(
|
|
path,
|
|
c.NOTMUCH_DATABASE_MODE_READ_ONLY,
|
|
"",
|
|
null,
|
|
&db,
|
|
@ptrCast(&err),
|
|
);
|
|
defer if (err) |e| if (status == null) std.c.free(e);
|
|
if (open_status != c.NOTMUCH_STATUS_SUCCESS) {
|
|
if (status) |s| s.* = .{
|
|
.msg = err,
|
|
.status = open_status,
|
|
.err = error.CouldNotOpenDatabase,
|
|
};
|
|
return error.CouldNotOpenDatabase;
|
|
}
|
|
if (db == null) unreachable; // If we opened the database successfully, this should never be null
|
|
if (status) |s| s.* = .{};
|
|
return .{ .handle = db.? };
|
|
}
|
|
|
|
pub fn close(db: *Db) void {
|
|
_ = c.notmuch_database_close(db.handle);
|
|
}
|
|
pub fn deinit(db: *Db) void {
|
|
_ = c.notmuch_database_destroy(db.handle);
|
|
db.handle = undefined;
|
|
}
|
|
|
|
//
|
|
// Execute a query for threads, returning a notmuch_threads_t 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
|
|
// notmuch_query_destroy.
|
|
//
|
|
// Typical usage might be:
|
|
//
|
|
// notmuch_query_t *query;
|
|
// notmuch_threads_t *threads;
|
|
// notmuch_thread_t *thread;
|
|
// notmuch_status_t stat;
|
|
//
|
|
// query = notmuch_query_create (database, query_string);
|
|
//
|
|
// for (stat = notmuch_query_search_threads (query, &threads);
|
|
// stat == NOTMUCH_STATUS_SUCCESS &&
|
|
// notmuch_threads_valid (threads);
|
|
// notmuch_threads_move_to_next (threads))
|
|
// {
|
|
// thread = notmuch_threads_get (threads);
|
|
// ....
|
|
// notmuch_thread_destroy (thread);
|
|
// }
|
|
//
|
|
// notmuch_query_destroy (query);
|
|
//
|
|
// Note: If you are finished with a thread before its containing
|
|
// query, you can call notmuch_thread_destroy 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 notmuch_thread_destroy
|
|
// and all the memory will still be reclaimed when the query is
|
|
// destroyed.
|
|
//
|
|
// Note that there's no explicit destructor needed for the
|
|
// notmuch_threads_t object. (For consistency, we do provide a
|
|
// notmuch_threads_destroy function, but there's no good reason
|
|
// to call it if the query is about to be destroyed).
|
|
pub fn searchThreads(db: Db, query: [:0]const u8) !ThreadIterator {
|
|
const nm_query = c.notmuch_query_create(db.handle, query);
|
|
if (nm_query == null) return error.CouldNotCreateQuery;
|
|
const handle = nm_query.?;
|
|
errdefer c.notmuch_query_destroy(handle);
|
|
var threads: ?*c.notmuch_threads_t = undefined;
|
|
const status = c.notmuch_query_search_threads(handle, &threads);
|
|
if (status != c.NOTMUCH_STATUS_SUCCESS) return error.CouldNotSearchThreads;
|
|
return .{
|
|
.query = handle,
|
|
.thread_state = threads orelse return error.CouldNotSearchThreads,
|
|
};
|
|
}
|
|
pub const TagsIterator = struct {
|
|
tags_state: *c.notmuch_tags_t,
|
|
first: bool = true,
|
|
|
|
pub fn next(self: *TagsIterator) ?[]const u8 {
|
|
if (!self.first) c.notmuch_tags_move_to_next(self.tags_state);
|
|
self.first = false;
|
|
if (c.notmuch_tags_valid(self.tags_state) == 0) return null;
|
|
return std.mem.span(c.notmuch_tags_get(self.tags_state));
|
|
}
|
|
|
|
pub fn jsonStringify(self: *TagsIterator, jws: anytype) !void {
|
|
try jws.beginArray();
|
|
while (self.next()) |t| try jws.write(t);
|
|
try jws.endArray();
|
|
}
|
|
// Docs imply strongly not to bother with deinitialization here
|
|
|
|
};
|
|
|
|
pub const Message = struct {
|
|
message_handle: *c.notmuch_message_t,
|
|
|
|
pub fn getHeader(self: Message, header: [:0]const u8) !?[]const u8 {
|
|
const val = c.notmuch_message_get_header(self.message_handle, header) orelse return error.ErrorGettingHeader; // null is an error
|
|
const ziggy_val = std.mem.span(val);
|
|
if (ziggy_val.len == 0) return null; // empty string indicates message does not contain the header
|
|
return ziggy_val;
|
|
}
|
|
pub fn getMessageId(self: Message) []const u8 {
|
|
return std.mem.span(c.notmuch_message_get_message_id(self.message_handle));
|
|
}
|
|
pub fn getFilename(self: Message) []const u8 {
|
|
return std.mem.span(c.notmuch_message_get_filename(self.message_handle));
|
|
}
|
|
|
|
pub fn deinit(self: Message) void {
|
|
c.notmuch_message_destroy(self.message_handle);
|
|
}
|
|
};
|
|
pub const MessageIterator = struct {
|
|
messages_state: *c.notmuch_messages_t,
|
|
first: bool = true,
|
|
|
|
pub fn next(self: *MessageIterator) ?Message {
|
|
if (!self.first) c.notmuch_messages_move_to_next(self.messages_state);
|
|
self.first = false;
|
|
if (c.notmuch_messages_valid(self.messages_state) == 0) return null;
|
|
const message = c.notmuch_messages_get(self.messages_state) orelse return null;
|
|
return .{
|
|
.message_handle = message,
|
|
};
|
|
}
|
|
|
|
// Docs imply strongly not to bother with deinitialization here
|
|
|
|
};
|
|
pub const Thread = struct {
|
|
thread_handle: *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 deinit() or the query from which
|
|
/// it derived is destroyed).
|
|
pub fn getThreadId(self: Thread) []const u8 {
|
|
return std.mem.span(c.notmuch_thread_get_thread_id(self.thread_handle));
|
|
}
|
|
|
|
/// 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.
|
|
pub fn getAuthors(self: Thread) []const u8 {
|
|
return std.mem.span(c.notmuch_thread_get_authors(self.thread_handle));
|
|
}
|
|
|
|
/// Gets the date of the newest message in 'thread' as a time_t value
|
|
pub fn getNewestDate(self: Thread) u64 {
|
|
return @intCast(c.notmuch_thread_get_newest_date(self.thread_handle));
|
|
}
|
|
|
|
/// Gets the date of the oldest message in 'thread' as a time_t value
|
|
pub fn getOldestDate(self: Thread) u64 {
|
|
return @intCast(c.notmuch_thread_get_oldest_date(self.thread_handle));
|
|
}
|
|
|
|
/// Gets the tags of the thread
|
|
pub fn getTags(self: Thread) !TagsIterator {
|
|
return .{
|
|
.tags_state = c.notmuch_thread_get_tags(self.thread_handle) orelse return error.CouldNotGetIterator,
|
|
};
|
|
}
|
|
|
|
/// Get the subject of 'thread' as a UTF-8 string.
|
|
///
|
|
/// The subject is taken from the first message (according to the query
|
|
/// order---see notmuch_query_set_sort) in the query results that
|
|
/// belongs to this thread.
|
|
pub fn getSubject(self: Thread) []const u8 {
|
|
return std.mem.span(c.notmuch_thread_get_subject(self.thread_handle));
|
|
}
|
|
|
|
/// Get the total 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
|
|
pub fn getMatchedMessages(self: Thread) c_int {
|
|
return c.notmuch_thread_get_matched_messages(self.thread_handle);
|
|
}
|
|
|
|
/// Get the total number of messages in 'thread'.
|
|
///
|
|
/// This count consists of all messages in the database belonging to
|
|
/// this thread. Contrast with notmuch_thread_get_matched_messages() .
|
|
pub fn getTotalMessages(self: Thread) c_int {
|
|
return c.notmuch_thread_get_total_messages(self.thread_handle);
|
|
}
|
|
|
|
/// Get the total number of files in 'thread'.
|
|
///
|
|
/// This sums notmuch_message_count_files over all messages in the
|
|
/// thread
|
|
pub fn getTotalFiles(self: Thread) c_int {
|
|
return c.notmuch_thread_get_total_files(self.thread_handle);
|
|
}
|
|
|
|
pub fn getMessages(self: Thread) !MessageIterator {
|
|
return .{
|
|
.messages_state = c.notmuch_thread_get_messages(self.thread_handle) orelse return error.CouldNotGetIterator,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: Thread) void {
|
|
c.notmuch_thread_destroy(self.thread_handle);
|
|
// self.thread_handle = undefined;
|
|
}
|
|
};
|
|
pub const ThreadIterator = struct {
|
|
query: *c.notmuch_query_t,
|
|
thread_state: *c.notmuch_threads_t,
|
|
first: bool = true,
|
|
|
|
pub fn next(self: *ThreadIterator) ?Thread {
|
|
if (!self.first) c.notmuch_threads_move_to_next(self.thread_state);
|
|
self.first = false;
|
|
if (c.notmuch_threads_valid(self.thread_state) == 0) return null;
|
|
const thread = c.notmuch_threads_get(self.thread_state) orelse return null;
|
|
return .{
|
|
.thread_handle = thread,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *ThreadIterator) void {
|
|
c.notmuch_query_destroy(self.query);
|
|
self.query = undefined;
|
|
}
|
|
};
|
|
};
|
|
|
|
test "can get status" {
|
|
var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const cwd = try std.fs.cwd().realpath(".", cwd_buf[0..]);
|
|
var path_buf: [std.fs.max_path_bytes:0]u8 = undefined;
|
|
var fba = std.heap.FixedBufferAllocator.init(path_buf[0..]);
|
|
const db_path = try std.fs.path.joinZ(fba.allocator(), &[_][]const u8{ cwd, "mail" });
|
|
{
|
|
var status: Status = undefined;
|
|
var db = try Db.open(db_path, &status);
|
|
defer db.deinit();
|
|
defer db.close();
|
|
defer status.deinit();
|
|
try std.testing.expectEqualStrings("No error occurred", status.statusString());
|
|
}
|
|
{
|
|
var db = try Db.open(db_path, null);
|
|
defer db.deinit();
|
|
defer db.close();
|
|
}
|
|
{
|
|
var status: Status = undefined;
|
|
try std.testing.expectError(error.CouldNotOpenDatabase, Db.open(
|
|
"NON-EXISTANT",
|
|
&status,
|
|
));
|
|
defer status.deinit();
|
|
try std.testing.expectEqualStrings(
|
|
"Path supplied is illegal for this function",
|
|
status.statusString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
test "can search threads" {
|
|
// const allocator = std.testing.allocator;
|
|
// const db_path = try std.fs.path.join(
|
|
// allocator,
|
|
// std.fs.cwd(),
|
|
// "mail",
|
|
// );
|
|
|
|
// Current directory under test is root of project
|
|
var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const cwd = try std.fs.cwd().realpath(".", cwd_buf[0..]);
|
|
var path_buf: [std.fs.max_path_bytes:0]u8 = undefined;
|
|
var fba = std.heap.FixedBufferAllocator.init(path_buf[0..]);
|
|
const db_path = try std.fs.path.joinZ(fba.allocator(), &[_][]const u8{ cwd, "mail" });
|
|
{
|
|
var status: Status = undefined;
|
|
var db = try Db.open(db_path, &status);
|
|
defer db.deinit();
|
|
defer db.close();
|
|
defer status.deinit();
|
|
try std.testing.expectEqualStrings("No error occurred", status.statusString());
|
|
var t_iter = try db.searchThreads("Tablets");
|
|
defer t_iter.deinit();
|
|
var inx: usize = 0;
|
|
while (t_iter.next()) |t| : (inx += 1) {
|
|
defer t.deinit();
|
|
try std.testing.expectEqual(@as(c_int, 1), t.getTotalMessages());
|
|
try std.testing.expectEqualStrings("0000000000000001", t.getThreadId());
|
|
var message_iter = try t.getMessages();
|
|
var jnx: usize = 0;
|
|
while (message_iter.next()) |m| : (jnx += 1) {
|
|
defer m.deinit();
|
|
try std.testing.expectStringEndsWith(m.getFilename(), "/1721591945.R4187135327503631514.nucman:2,S");
|
|
}
|
|
try std.testing.expectEqual(@as(usize, 1), jnx);
|
|
}
|
|
try std.testing.expectEqual(@as(usize, 1), inx);
|
|
}
|
|
}
|