basic search functionality
This commit is contained in:
parent
bfea6e15ef
commit
5d6a777965
10 changed files with 446 additions and 5 deletions
21
README.md
Normal file
21
README.md
Normal file
|
@ -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...
|
BIN
mail/.notmuch/xapian/docdata.glass
Normal file
BIN
mail/.notmuch/xapian/docdata.glass
Normal file
Binary file not shown.
0
mail/.notmuch/xapian/flintlock
Normal file
0
mail/.notmuch/xapian/flintlock
Normal file
BIN
mail/.notmuch/xapian/iamglass
Normal file
BIN
mail/.notmuch/xapian/iamglass
Normal file
Binary file not shown.
BIN
mail/.notmuch/xapian/position.glass
Normal file
BIN
mail/.notmuch/xapian/position.glass
Normal file
Binary file not shown.
BIN
mail/.notmuch/xapian/postlist.glass
Normal file
BIN
mail/.notmuch/xapian/postlist.glass
Normal file
Binary file not shown.
BIN
mail/.notmuch/xapian/termlist.glass
Normal file
BIN
mail/.notmuch/xapian/termlist.glass
Normal file
Binary file not shown.
77
mail/Inbox/cur/1721591945.R4187135327503631514.nucman:2,S
Normal file
77
mail/Inbox/cur/1721591945.R4187135327503631514.nucman:2,S
Normal file
|
@ -0,0 +1,77 @@
|
|||
Return-Path: <mail@youpharm.co>
|
||||
Delivered-To: lobo@lerch.org
|
||||
Received: from mail.eler.ch
|
||||
by mail.eler.ch with LMTP
|
||||
id BJ9kLnLDm2aYDgQAyA9pPg
|
||||
(envelope-from <mail@youpharm.co>)
|
||||
for <lobo@lerch.org>; 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 <emil@lerch.org>; 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 <mail@youpharm.co>
|
||||
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
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
|
||||
</head>
|
||||
<body><a href=3D"https://unmaskfauci.com/assets/images/chw.php"><img src=3D=
|
||||
"https://imgpx.com/dfE6oYsvHoYw.png"></a> <div><img width=3D1 height=3D1 =
|
||||
alt=3D"" src=3D"https://vnevent.net/wp-content/plugins/wp-automatic/awe.p=
|
||||
hp?QFYiTaVCm0ogM30sC5RNRb%2FKLO0%2FqO3iN9A89RgPbrGjPGsdVierqrtB7w8mnIqJug=
|
||||
BVA5TZVG%2F6MFLMOrK9z4D6vgFBDRgH88%2FpEmohBbpaSFf4wx1l9S4LGJd87EK6"></div=
|
||||
></body></html>
|
||||
|
||||
--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e--
|
154
src/main.zig
154
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/<term>
|
||||
// [
|
||||
// {
|
||||
// "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/<threadid>
|
||||
//
|
||||
// [
|
||||
// {
|
||||
// "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"
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
|
|
199
src/notmuch.zig
Normal file
199
src/notmuch.zig
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
Loading…
Add table
Reference in a new issue