diff --git a/README.md b/README.md new file mode 100644 index 0000000..06723f9 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +Zetviel +------- + +As some background, I've had some issues with the very usable [netviel](https://github.com/DavidMStraub/netviel). + +I wanted to address those issues, but also simplify the deployment. And, I like zig, +so I decided this was small enough I'd just re-write the thing to make my own. + +This is still very work in progress, to the point it is not yet usable. It has +some basic notmuch integration and a usable build system. + +Building +-------- + +If you have notmuch installed (libnotmuch-dev on a debian-based system), +`zig build` is all you need. If you are using nix, you can `nix develop`, which +will install the necessary notmuch header/library, and the build system will +detect and use that. Again, `zig build` will work in that instance, but you must +`nix develop` first. + +More to come... diff --git a/mail/.notmuch/xapian/docdata.glass b/mail/.notmuch/xapian/docdata.glass new file mode 100644 index 0000000..5eee2d2 Binary files /dev/null and b/mail/.notmuch/xapian/docdata.glass differ diff --git a/mail/.notmuch/xapian/flintlock b/mail/.notmuch/xapian/flintlock new file mode 100644 index 0000000..e69de29 diff --git a/mail/.notmuch/xapian/iamglass b/mail/.notmuch/xapian/iamglass new file mode 100644 index 0000000..9a40db8 Binary files /dev/null and b/mail/.notmuch/xapian/iamglass differ diff --git a/mail/.notmuch/xapian/position.glass b/mail/.notmuch/xapian/position.glass new file mode 100644 index 0000000..85627a6 Binary files /dev/null and b/mail/.notmuch/xapian/position.glass differ diff --git a/mail/.notmuch/xapian/postlist.glass b/mail/.notmuch/xapian/postlist.glass new file mode 100644 index 0000000..2abf952 Binary files /dev/null and b/mail/.notmuch/xapian/postlist.glass differ diff --git a/mail/.notmuch/xapian/termlist.glass b/mail/.notmuch/xapian/termlist.glass new file mode 100644 index 0000000..b64c82d Binary files /dev/null and b/mail/.notmuch/xapian/termlist.glass differ diff --git a/mail/Inbox/cur/1721591945.R4187135327503631514.nucman:2,S b/mail/Inbox/cur/1721591945.R4187135327503631514.nucman:2,S new file mode 100644 index 0000000..1e77690 --- /dev/null +++ b/mail/Inbox/cur/1721591945.R4187135327503631514.nucman:2,S @@ -0,0 +1,77 @@ +Return-Path: +Delivered-To: lobo@lerch.org +Received: from mail.eler.ch + by mail.eler.ch with LMTP + id BJ9kLnLDm2aYDgQAyA9pPg + (envelope-from ) + for ; Sat, 20 Jul 2024 14:02:26 +0000 +Received: from youpharm.co (youpharm.co [5.101.65.218]) + by mail.eler.ch (Postfix) with ESMTP id E3C9F467AB + for ; Sat, 20 Jul 2024 14:02:25 +0000 (UTC) +DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=s1; d=youpharm.co; + h=Message-ID:From:To:Subject:Date:MIME-Version:Content-Type; i=abuse@youpharm.co; + bh=Lnj6s7dL4V4gz92hab9GJ0HxEL8=; + b=MLiLXLaBVkYaJuabi+DsOFUkjmqsYJ0hsfHW5JKX61Fal+1j2iFjFWrggCv+m0zruA+j6W+iJ7CV + nxcDpT5mZe0+e2bOu1f8YEGNj7DPVpkYjeB8esR4qo/LSot0TIOU7YojSk8HP/hQVYEwpC58f21C + sXgqEyMn1bV4+UHE1QnhoyZRP/lyadba4SCCSeyG5VQMZ4cIZtlcBFA+yd6I03lZ2f/Lh7tinFXj + HrOyPjQLpk4VNbVsbbpsI+sKOEGlmgpRVIatV+Hcwk8ZuhFsubdF/cSc1p3jFbUdhBa3TMcqFVS2 + L1UO4e13PWJVjHzAUXlGDF66PGIR2pHnBd1pew== +DomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=s1; d=youpharm.co; + b=fFAj5RzuUx0++wASk/u6T0GlBurb2y6h/1WEls22gWKfoEKHzChyXYJNyknfho2r/3Cw0DNrWXFI + nRIivnoNX4rOvc4hsCieljl9lt0fOaYzLgHKS083D8JIYLLySX0Qwj7xydC3nB3WmHhOlrz6eM7d + lPIOT14K1e5LxQTLox8PaUqknSUNrsBZ8tREcVLqb7Ud9SVvdHjyccjampV70XPOeKMd9NLt4a/H + sEeS184PGBo7/uAuHojS2y2LDkY6nRdZPmjPvA9ghNU8udr+biG3NEX8V2v2ZJy7w9H6FfJfCb2/ + MrxZmGPWgcYJ7cQ/pNMKcHM1QoAKYKMEG76V8g==; +Message-ID: <8afeb74dca321817e44e07ac4a2e040962e86e@youpharm.co> +From: Top Medications +To: emil@lerch.org +Subject: ***SPAM*** Tablets without a prescription +Date: Sat, 20 Jul 2024 16:02:18 +0200 +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="08044db730c8e2ed1eb4fd56b0d4fef3d9e86e" +X-Rspamd-Queue-Id: E3C9F467AB +X-Rspamd-Server: mail.eler.ch +X-Spamd-Result: default: False [10.00 / 11.00]; + ABUSE_SURBL(5.50)[youpharm.co:helo,youpharm.co:dkim,youpharm.co:rdns]; + RSPAMD_URIBL(4.50)[unmaskfauci.com:url]; + BAD_REP_POLICIES(0.10)[]; + MIME_GOOD(-0.10)[multipart/alternative,text/plain]; + DKIM_TRACE(0.00)[youpharm.co:+]; + ARC_NA(0.00)[]; + DMARC_POLICY_ALLOW(0.00)[youpharm.co,none]; + RCVD_COUNT_ZERO(0.00)[0]; + MIME_TRACE(0.00)[0:+,1:+,2:~]; + FROM_EQ_ENVFROM(0.00)[]; + RCPT_COUNT_ONE(0.00)[1]; + TO_DN_NONE(0.00)[]; + R_SPF_ALLOW(0.00)[+a]; + R_DKIM_ALLOW(0.00)[youpharm.co:s=s1]; + ASN(0.00)[asn:34665, ipnet:5.101.65.0/24, country:RU]; + FROM_HAS_DN(0.00)[]; + TO_MATCH_ENVRCPT_ALL(0.00)[]; + DWL_DNSWL_BLOCKED(0.00)[youpharm.co:dkim]; + MID_RHS_MATCH_FROM(0.00)[] +X-Rspamd-Action: rewrite subject +Content-Length: 789 + +--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable + + +--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: quoted-printable + + + + + +
+ +--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e-- diff --git a/src/main.zig b/src/main.zig index 6cb6afd..2002110 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,11 +1,9 @@ const std = @import("std"); -const c = @cImport({ - @cInclude("notmuch.h"); -}); +const notmuch = @import("notmuch.zig"); pub fn main() !void { // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) - std.debug.print("All your {s} are belong to us. Status: {s}\n", .{ "codebase", c.notmuch_status_to_string(0) }); + std.debug.print("All your {s} are belong to us. \n", .{"codebase"}); // stdout is for the actual output of your application, for example if you // are implementing gzip, then only the compressed bytes should be sent to @@ -20,5 +18,151 @@ pub fn main() !void { } test "can get status" { - try std.testing.expectEqualStrings("No error occurred", std.mem.span(c.notmuch_status_to_string(0))); + // 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(); + try std.testing.expectEqualStrings("No error occurred", status.statusString()); + } + { + var db = try notmuch.Db.open(db_path, null); + defer db.deinit(); + defer db.close(); + } + { + var status: notmuch.Status = undefined; + try std.testing.expectError(error.CouldNotOpenDatabase, notmuch.Db.open( + "NON-EXISTANT", + &status, + )); + defer status.deinit(); + try std.testing.expectEqualStrings( + "Path supplied is illegal for this function", + status.statusString(), + ); + } + // + // // This is the python that's executing + // // def get(self, thread_id): + // // threads = notmuch.Query( + // // get_db(), "thread:{}".format(thread_id) + // // ).search_threads() + // // thread = next(threads) # there can be only 1 + // // messages = thread.get_messages() + // // return messages_to_json(messages) + // try std.testing.expectEqualStrings("No error occurred", std.mem.span(c.notmuch_status_to_string(open_status))); +} + +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: notmuch.Status = undefined; + var db = try notmuch.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); + } + + // This is the json we're looking to match on api/query/ + // [ + // { + // "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 + // }, + // { + // "authors": "The Washington Post", + // "matched_messages": 1, + // "newest_date": 1721603115, + // "oldest_date": 1721603115, + // "subject": "Upcoming Virtual Programs", + // "tags": [ + // "inbox", + // "unread" + // ], + // "thread_id": "0000000000031712", + // "total_messages": 1 + // }, + // { + // "authors": "The Washington Post", + // "matched_messages": 1, + // "newest_date": 1721590157, + // "oldest_date": 1721590157, + // "subject": "Biden Steps Aside", + // "tags": [ + // "inbox" + // ], + // "thread_id": "000000000003170d", + // "total_messages": 1 + // } + // ] + // + // And on api/thread/ + // + // [ + // { + // "from": "The Washington Post ", + // "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" + // } + // ] } diff --git a/src/notmuch.zig b/src/notmuch.zig new file mode 100644 index 0000000..07308a8 --- /dev/null +++ b/src/notmuch.zig @@ -0,0 +1,199 @@ +const std = @import("std"); +const c = @cImport({ + @cInclude("notmuch.h"); +}); + +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, + &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 Message = struct { + message_handle: *c.notmuch_message_t, + + 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)); + } + + // 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; + } + }; +};