committing with one failing test, because now we need to go read files
This commit is contained in:
parent
5d6a777965
commit
4718bce5af
4 changed files with 317 additions and 14 deletions
9
.envrc
Normal file
9
.envrc
Normal file
|
@ -0,0 +1,9 @@
|
|||
# vi: ft=sh
|
||||
# shellcheck shell=bash
|
||||
|
||||
if ! has zvm_direnv_version || ! zvm_direnv_version 2.0.0; then
|
||||
source_url "https://git.lerch.org/lobo/zvm-direnv/raw/tag/2.0.0/direnvrc" "sha256-8Umzxj32hFU6G0a7Wrq0KTNDQ8XEuje2A3s2ljh/hFY="
|
||||
fi
|
||||
|
||||
use zig 0.13.0
|
||||
use flake
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
.zig-cache/
|
||||
zig-out/
|
||||
.direnv/
|
||||
|
|
196
src/main.zig
196
src/main.zig
|
@ -166,3 +166,199 @@ test "can search threads" {
|
|||
// }
|
||||
// ]
|
||||
}
|
||||
|
||||
const Thread = struct {
|
||||
thread: *notmuch.Db.Thread,
|
||||
|
||||
pub fn init(t: *notmuch.Db.Thread) Thread {
|
||||
return .{ .thread = t };
|
||||
}
|
||||
|
||||
pub fn jsonStringify(self: Thread, jws: anytype) !void {
|
||||
try jws.beginArray();
|
||||
var mi = self.thread.getMessages() catch return error.OutOfMemory;
|
||||
while (mi.next()) |m| {
|
||||
try jws.beginObject();
|
||||
try jws.objectField("from");
|
||||
try jws.write(m.getHeader("from") catch return error.OutOfMemory);
|
||||
try jws.objectField("to");
|
||||
try jws.write(m.getHeader("to") catch return error.OutOfMemory);
|
||||
try jws.objectField("cc");
|
||||
try jws.write(m.getHeader("cc") catch return error.OutOfMemory);
|
||||
try jws.objectField("bcc");
|
||||
try jws.write(m.getHeader("bcc") catch return error.OutOfMemory);
|
||||
try jws.objectField("date");
|
||||
try jws.write(m.getHeader("date") catch return error.OutOfMemory);
|
||||
try jws.objectField("subject");
|
||||
try jws.write(m.getHeader("subject") catch return error.OutOfMemory);
|
||||
try jws.objectField("content");
|
||||
try jws.write(m.getFilename()); // TODO: Parse file
|
||||
try jws.objectField("content-type");
|
||||
try jws.write(m.getHeader("Content-Type") catch return error.OutOfMemory);
|
||||
|
||||
try jws.objectField("message_id");
|
||||
try jws.write(m.getMessageId());
|
||||
try jws.endObject();
|
||||
}
|
||||
|
||||
try jws.endArray();
|
||||
//[
|
||||
// {
|
||||
// "from": "The Washington Post <email@washingtonpost.com>",
|
||||
// "to": "elerch@lerch.org",
|
||||
// "cc": null,
|
||||
// "bcc": null,
|
||||
// "date": "Sun, 21 Jul 2024 19:23:38 +0000",
|
||||
// "subject": "Biden steps aside",
|
||||
// "content": "...content...",
|
||||
// "content_type": "text/html",
|
||||
// "attachments": [],
|
||||
// "message_id": "01010190d6bfe4e1-185e2720-e415-4086-8865-9604cde886c2-000000@us-west-2.amazonses.com"
|
||||
// }
|
||||
//]
|
||||
}
|
||||
};
|
||||
|
||||
const Threads = struct {
|
||||
iterator: *notmuch.Db.ThreadIterator,
|
||||
|
||||
pub fn init(it: *notmuch.Db.ThreadIterator) Threads {
|
||||
return .{
|
||||
.iterator = it,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn jsonStringify(self: Threads, jws: anytype) !void {
|
||||
// jws should be this:
|
||||
// https://ziglang.org/documentation/0.13.0/std/#std.json.stringify.WriteStream
|
||||
try jws.beginArray();
|
||||
while (self.iterator.next()) |t| {
|
||||
defer t.deinit();
|
||||
try jws.beginObject();
|
||||
// {
|
||||
// "authors": "The Washington Post",
|
||||
// "matched_messages": 1,
|
||||
// "newest_date": 1721664948,
|
||||
// "oldest_date": 1721664948,
|
||||
// "subject": "Biden is out. What now?",
|
||||
// "tags": [
|
||||
// "inbox",
|
||||
// "unread"
|
||||
// ],
|
||||
// "thread_id": "0000000000031723",
|
||||
// "total_messages": 1
|
||||
// },
|
||||
try jws.objectField("authors");
|
||||
try jws.write(t.getAuthors());
|
||||
try jws.objectField("matched_messages");
|
||||
try jws.write(t.getMatchedMessages());
|
||||
try jws.objectField("newest_date");
|
||||
try jws.write(t.getNewestDate());
|
||||
try jws.objectField("oldest_date");
|
||||
try jws.write(t.getOldestDate());
|
||||
try jws.objectField("subject");
|
||||
try jws.write(t.getSubject());
|
||||
try jws.objectField("tags");
|
||||
var tags = t.getTags() catch return error.OutOfMemory;
|
||||
try tags.jsonStringify(jws);
|
||||
try jws.objectField("thread_id");
|
||||
try jws.write(t.getThreadId());
|
||||
try jws.objectField("total_messages");
|
||||
try jws.write(t.getTotalMessages());
|
||||
|
||||
try jws.endObject();
|
||||
}
|
||||
try jws.endArray();
|
||||
}
|
||||
};
|
||||
|
||||
test "can stringify general queries" {
|
||||
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: notmuch.Status = undefined;
|
||||
var db = try notmuch.Db.open(db_path, &status);
|
||||
defer db.deinit();
|
||||
defer db.close();
|
||||
defer status.deinit();
|
||||
var al = std.ArrayList(u8).init(allocator);
|
||||
defer al.deinit();
|
||||
var ti = try db.searchThreads("Tablets");
|
||||
defer ti.deinit();
|
||||
try std.json.stringify(Threads.init(&ti), .{ .whitespace = .indent_2 }, al.writer());
|
||||
const actual = al.items;
|
||||
try std.testing.expectEqualStrings(
|
||||
\\[
|
||||
\\ {
|
||||
\\ "authors": "Top Medications",
|
||||
\\ "matched_messages": 1,
|
||||
\\ "newest_date": 1721484138,
|
||||
\\ "oldest_date": 1721484138,
|
||||
\\ "subject": "***SPAM*** Tablets without a prescription",
|
||||
\\ "tags": [
|
||||
\\ "inbox"
|
||||
\\ ],
|
||||
\\ "thread_id": "0000000000000001",
|
||||
\\ "total_messages": 1
|
||||
\\ }
|
||||
\\]
|
||||
, actual);
|
||||
}
|
||||
}
|
||||
|
||||
test "can stringify specific 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: notmuch.Status = undefined;
|
||||
var db = try notmuch.Db.open(db_path, &status);
|
||||
defer db.deinit();
|
||||
defer db.close();
|
||||
defer status.deinit();
|
||||
var al = std.ArrayList(u8).init(allocator);
|
||||
defer al.deinit();
|
||||
var ti = try db.searchThreads("Tablets");
|
||||
defer ti.deinit();
|
||||
var t = ti.next().?;
|
||||
try std.json.stringify(Thread.init(&t), .{ .whitespace = .indent_2 }, al.writer());
|
||||
const actual = al.items;
|
||||
try std.testing.expectEqualStrings(
|
||||
\\[
|
||||
\\ {
|
||||
\\ "from": "Top Medications <mail@youpharm.co>",
|
||||
\\ "to": "emil@lerch.org",
|
||||
\\ "cc": null,
|
||||
\\ "bcc": null,
|
||||
\\ "date": "Sat, 20 Jul 2024 16:02:18 +0200",
|
||||
\\ "subject": "***SPAM*** Tablets without a prescription",
|
||||
\\ "content": "...content...",
|
||||
\\ "content_type": "text/html",
|
||||
\\ "attachments": [],
|
||||
\\ "message_id": "01010190d6bfe4e1-185e2720-e415-4086-8865-9604cde886c2-000000@us-west-2.amazonses.com"
|
||||
\\ }
|
||||
\\]
|
||||
, actual);
|
||||
}
|
||||
}
|
||||
|
|
125
src/notmuch.zig
125
src/notmuch.zig
|
@ -1,3 +1,30 @@
|
|||
//! 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 = @cImport({
|
||||
@cInclude("notmuch.h");
|
||||
|
@ -107,10 +134,38 @@ pub const Db = struct {
|
|||
.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));
|
||||
}
|
||||
|
@ -139,28 +194,70 @@ pub const Db = struct {
|
|||
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).
|
||||
/// 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));
|
||||
}
|
||||
|
||||
// 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() .
|
||||
/// 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
|
||||
/// 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);
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue