From 5d6a7779655164067863a137a280f34e2da19931 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 30 Jul 2024 10:41:32 -0700 Subject: [PATCH] basic search functionality --- README.md | 21 ++ mail/.notmuch/xapian/docdata.glass | Bin 0 -> 8192 bytes mail/.notmuch/xapian/flintlock | 0 mail/.notmuch/xapian/iamglass | Bin 0 -> 101 bytes mail/.notmuch/xapian/position.glass | Bin 0 -> 8192 bytes mail/.notmuch/xapian/postlist.glass | Bin 0 -> 24576 bytes mail/.notmuch/xapian/termlist.glass | Bin 0 -> 24576 bytes ...1721591945.R4187135327503631514.nucman:2,S | 77 +++++++ src/main.zig | 154 +++++++++++++- src/notmuch.zig | 199 ++++++++++++++++++ 10 files changed, 446 insertions(+), 5 deletions(-) create mode 100644 README.md create mode 100644 mail/.notmuch/xapian/docdata.glass create mode 100644 mail/.notmuch/xapian/flintlock create mode 100644 mail/.notmuch/xapian/iamglass create mode 100644 mail/.notmuch/xapian/position.glass create mode 100644 mail/.notmuch/xapian/postlist.glass create mode 100644 mail/.notmuch/xapian/termlist.glass create mode 100644 mail/Inbox/cur/1721591945.R4187135327503631514.nucman:2,S create mode 100644 src/notmuch.zig 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 0000000000000000000000000000000000000000..5eee2d2ea739b0a0a2f66b2f0f3e78cbb7e4714f GIT binary patch literal 8192 zcmeIuu?fOZ7)H_0M6gKZ1pf%m;TDhBCm;ke=vyHZSPOzvq&T;`?MZS;_3F9Tw?1`5 z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfWRpQ+BD?)T;_Fuyta3{4H^H} Fk{^q_3*i6& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9a40db83bbfe130bf32299ae606be07d9b12bab3 GIT binary patch literal 101 zcmd<*jYurWOw3bo&q*vUX35jsS^TVggOB!tS!Vx?k6-j>Vq$P)VPIooW@KVyW?*1q m5n^LvWB>vt1`ZG#DlP<40~TjsW?&IwfRHQ_SqU+VvF>P zj_8m+&>K3Smjn+4AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2teRp z0z5zMwp&qhXnsuxyjUT8p}IDcX- zI)T(Z6F)n=JeR)Ri$!R%Cx(H(;%GsNwhl@bVq${*K zk*0T_;pLLMzTIk`NB!6riM@Kx!q`klu_?{Y)o~fh1Lc{?sB4$b@R{OIEXHD4>u0CD J$aJSTA>U?3bHV@s literal 0 HcmV?d00001 diff --git a/mail/.notmuch/xapian/postlist.glass b/mail/.notmuch/xapian/postlist.glass new file mode 100644 index 0000000000000000000000000000000000000000..2abf95238ee1bd86841c056c76cefd7105e19397 GIT binary patch literal 24576 zcmeI4%WoS+9LHzvHmRM5owuKx6$c(siM_THCstJz@<0Jhg5yd_4s^%6<9NaLu6EZ= zEQC}Esc-`%E^r}#00JR;;J_i`!hr)lA%s*^2_z&A6*q)f00jQ)1Qfpy9bYGwtsC{Ua^gqFWrCfCMeJIm>gZ;b zNG5hFwqz1ZsuA19`^0A*_$^Y!j((j;nr;!*F|1~59Mj5{QA^m3(u-6`wC6m&u`20x zNy82f z00e*l5C8%|00{gq3DB?pJ!s~&&^&tMwEzDL@m}~ad^tQB`Y!ZM=#$VZp+ZOsO^cRz zRa_A>;;iV4{}lfyeiVN-UWpgPx5W43!Pw8S?_*!aK90Q?yBRCT^07p0D*9XWhv?_g z52J5Ko6&N#5WNz;7@dxsM1G9ij@*j88+kdRMAjq8$awgd@VDX5!XJd+3Eyo0B?8=l z01yBIKmZ5;0U!VbfB+Bx0zd!=00AHX1b_e#00KY&2mpb9pMWxW9({cd3Br+*pif>T zJT^Bsx4rd5p5CM)E>#I_Q#{a}iqUW+ESa=}QnhrGH6bdG(it8kJXADHDNnR^Z=x+- zVqK0`nnuH{V5>H-8kdzNI#fVHY6+J~DVxz$oKDG0a+YK=B$>r(2B%0elU!a*k)=h_ zJy)5fQ|bRM9=Q90{^{Zo5^f;&_|@xgx_4#3i*BD|TbQR0-Ur#iBXol2Z*3q;M+*O_ z{vi*m?nbL-Wn^$~r?_cY`+|UEBm`un1bi%1CA3A92S+ckAgSq99-9vC?QCCt^69me zV%rg}O$9m{ZAGQ7acN?tBiSadaUnF`5jwtdHR$iCm`5i^_jXuYYH@dK%^`=5;Gx^_ zux+pkT*a(nz0N0vC%Dw=zr|Kpf!#SPPOWdpZcbcvh}D&p%~~&P=G}9~mt#Pg;ut0m zjt#YfHFi*WnC+5{Wk;4r8GRO24IUdF=r?Zk!dc%&2G7_w75EF+T&ZQ6&6gV=cBNj^ zB9m^s;~D*30rwn{%+xk=swT-M(Ow4#unUsc6F_g+o+d}LjlqAir9I*w79drx=~nru2|U3 zylSDF|u{K;{+`sF*nDB zpq~po8t}EF-9vret?nO(2cFS;AG7 literal 0 HcmV?d00001 diff --git a/mail/.notmuch/xapian/termlist.glass b/mail/.notmuch/xapian/termlist.glass new file mode 100644 index 0000000000000000000000000000000000000000..b64c82d88a0d5ef2c8828570cd3596aea30105e7 GIT binary patch literal 24576 zcmeI)TTGL87zXfM;NnT5C{Yy009U<00Izz00bZa0SG_<0uX=z1Rwwb2tWV=5P$##AOHaf zKmY=7oB;K5Vshp6;HwA(AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=Z=}H5>YpT7#a2%VEA|UkKTE-$V%=cNS#4|ytL62-02Blu009U<00Izz00bZa z0SG_<0uX=z1Rwwb2tWV=5P$##AOL~?Nr0m15;TJNr*vNxxmNnRYlqBVW>i2DItT8C zgmv#WrBj76f3?_Vj{$>gf8p%z@5*FdN~c>Dc2P;>Y;J5(&~k8z%SvVlx z!Nr9$S2L+6PJeRCF1P2W-b^|WxX?XP8`;lV|J?0Lu=7&ud5$9D%scr)1FvkpjYCzg zJD16*91P#Ainu*fV40peHY|D^a6`&yjhnhUJZZ6fG8a(;~x;a>plnWy&DGM)O5+-`hC}wo|qK_92mh`S)k77c)1MS!Zyfsq&22 zR{3^+;ZeDTBl=Zd~n<>gTra%qvaptRH@Ce|=5)oW;HA!*x%3N8;;`^m+O&%=fy) z%MWfmEiNG^%+Hhivjc1#uI0FM$x++iis@C#vDqHu4!U+`%cr&pg9<5ATRku{J(O1& z7OUq(iiawUFV(Eit|LBR(s>Vg(KKe(K1cbEl$`;B5so=0&AVg6apgB07Uk2&|LWFh zPalt}nrYgAk*K3keN0wlxu7TUuu4=^y#f|)Qp@TR{iL;%w&gP4zJMD0mLj)AXYbG3 zzff5loyvH$P?1*O^tGXcZ$$8`NYNEAHHX_B(1u|{yZgbP!@}d?>EOZU6w?ZChw@evrDCsM4St} z{=ktWXaeeC!N;Fyj8Ya24K8@eg12prGhxzF%Jx1#WB1^S0qJxS;aC}#e@ tFPpW!A<7>qlTrFQ-!K2tepEp#SIGQ?TrWvk0vOB_#1lPV#fdg literal 0 HcmV?d00001 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; + } + }; +};