Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
14 kB · 366 lines
Zig
at zig-0.16
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367// SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie// SPDX-License-Identifier: GPL-3.0-or-later
//! A thread of messages matching a search, as returned by//! `Query.searchThreads`.//!//! Exposes the thread's subject, authors, dates, and message and file//! counts, plus traversal of its messages via `getToplevelMessages` and//! `getMessages`. The thread is owned by the query it derived from; `deinit`//! releases it sooner.const Thread = @This();
const std = @import("std");
const c = @import("c");
const Error = @import("error.zig").Error;const wrap = @import("error.zig").wrap;
const MessagesIterator = @import("MessagesIterator.zig");const TagsIterator = @import("TagsIterator.zig");
/// The underlying `notmuch_thread_t` handle; only needed directly when/// calling libnotmuch C APIs these bindings do not wrap.thread: *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` is called or until the query from which it derived/// is deinitialized).pub fn getThreadID(self: *const Thread) [:0]const u8 { return std.mem.span(c.notmuch_thread_get_thread_id(self.thread));}
test getThreadID { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// The thread's ID matches the thread ID of its messages. const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit(); try std.testing.expectEqualStrings( thread.getThreadID(), message.getThreadID() orelse return error.TestUnexpectedResult, );}
/// Get the total number of messages in `Thread`.////// This count consists of all messages in the database belonging to this/// thread. Contrast with `getMatchedMessages`.pub fn getTotalMessages(self: *const Thread) usize { std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); return @intCast(@max(0, c.notmuch_thread_get_total_messages(self.thread)));}
test getTotalMessages { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// The lunch thread holds the original message and its reply. try std.testing.expectEqual(@as(usize, 2), thread.getTotalMessages());}
/// Get the total number of files in 'thread'.////// This sums Message.countFiles over all messages in the thread.pub fn getTotalFiles(self: *const Thread) usize { std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); return @intCast(@max(0, c.notmuch_thread_get_total_files(self.thread)));}
test getTotalFiles { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// Each corpus message is backed by one file. try std.testing.expectEqual(@as(usize, 2), thread.getTotalFiles());}
/// Get a `MessagesIterator` for the top-level messages in `thread` in/// oldest-first order.////// This iterator will not necessarily iterate over all of the messages in the/// thread. It will only iterate over the messages in the thread which are not/// replies to other messages in the thread.////// The returned list will be deinitialized when the thread is deinitialized.pub fn getToplevelMessages(self: *const Thread) MessagesIterator { return .{ .messages = c.notmuch_thread_get_toplevel_messages(self.thread), };}
test getToplevelMessages { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// Only the message that started the thread is top-level; replies hang // off it via `Message.getReplies`. var toplevel = thread.getToplevelMessages(); const top = try toplevel.next() orelse return error.TestUnexpectedResult; try std.testing.expectEqualStrings("one@example.org", top.getMessageID() orelse ""); try std.testing.expect(try toplevel.next() == null);}
/// Get a `MessagesIterator` for all messages in `Thread` in oldest-first order.////// The returned list will be deinitialized when the thread is deinitialized.pub fn getMessages(self: *const Thread) MessagesIterator { return .{ .messages = c.notmuch_thread_get_messages(self.thread), };}
test getMessages { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// All messages in the thread, in oldest-first order. var messages = thread.getMessages(); const first = try messages.next() orelse return error.TestUnexpectedResult; try std.testing.expectEqualStrings("one@example.org", first.getMessageID() orelse ""); const second = try messages.next() orelse return error.TestUnexpectedResult; try std.testing.expectEqualStrings("two@example.org", second.getMessageID() orelse ""); try std.testing.expect(try messages.next() == null);}
/// Get the 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 `Query.addTagExclude`). Contrast/// with `getTotalMessages`.pub fn getMatchedMessages(self: *const Thread) usize { std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); return @intCast(@max(0, c.notmuch_thread_get_matched_messages(self.thread)));}
test getMatchedMessages { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
// Only Alice's half of the lunch thread matches this search, so one of // the thread's two messages is a match. const query = try test_db.database.queryCreate("from:alice@example.org"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
try std.testing.expectEqual(@as(usize, 2), thread.getTotalMessages()); try std.testing.expectEqual(@as(usize, 1), thread.getMatchedMessages());}
/// Get the authors of `Thread` as a UTF-8 string.////// 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.////// The string contains authors of messages matching the query first, then/// non-matched authors (with the two groups separated by `|`). Within each/// group, authors are ordered by date.////// 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` is called or until the query from which it derived/// is deinitialized).////// TODO: Create an iterator object that makes dealing with this list easier.pub fn getAuthors(self: *const Thread) [:0]const u8 { return std.mem.span(c.notmuch_thread_get_authors(self.thread));}
test getAuthors { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// Both participants matched the search, so both are listed. try std.testing.expectEqualStrings("Alice, Bob", thread.getAuthors());}
/// Get the subject of `Thread` as a UTF-8 string.////// The subject is taken from the first message (according to the query/// order—see `Query.setSort`) in the query results that belongs to this 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` is called or until the query from which it derived/// is deinitialized.pub fn getSubject(self: *const Thread) [:0]const u8 { return std.mem.span(c.notmuch_thread_get_subject(self.thread));}
test getSubject { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
try std.testing.expectEqualStrings("Lunch plans", thread.getSubject());}
/// Get the date of the oldest message in `Thread` as the number of seconds/// since the Unix epoch (1970-01-01 00:00:00 UTC).pub fn getOldestDate(self: *const Thread) i64 { std.debug.assert(@typeInfo(c_long).int.bits <= @typeInfo(i64).int.bits); return c.notmuch_thread_get_oldest_date(self.thread);}
test getOldestDate { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// Mon, 01 Apr 2024 09:00:00 +0000: the date of the first message. try std.testing.expectEqual(@as(i64, 1711962000), thread.getOldestDate());}
/// Get the date of the newest message in `Thread` as the number of seconds/// since the Unix epoch (1970-01-01 00:00:00 UTC).pub fn getNewestDate(self: *const Thread) i64 { std.debug.assert(@typeInfo(c_long).int.bits <= @typeInfo(i64).int.bits); return c.notmuch_thread_get_newest_date(self.thread);}
test getNewestDate { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const query = try test_db.database.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
// Mon, 01 Apr 2024 09:30:00 +0000: the date of the reply. try std.testing.expectEqual(@as(i64, 1711963800), thread.getNewestDate());}
/// Get the tags for `Thread`, returning a `TagsIterator` object which can be/// used to iterate over all tags.////// Note: In the `notmuch` database, tags are stored on individual messages, not/// on threads. So the tags returned here will be all tags of the messages which/// matched the search and which belong to this thread.////// The tags object is owned by the thread and as such, will only be valid for/// as long as the thread is valid, which is until `deinit` is called or until/// the query from which it derived is destroyed.pub fn getTags(self: *const Thread) TagsIterator { return .{ .tags = c.notmuch_thread_get_tags(self.thread), };}
test getTags { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const db = test_db.database;
// Tag the two thread members differently; the thread's tags are the // union of its messages' tags. const one = try db.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer one.deinit(); try one.addTag("red"); const two = try db.findMessage("two@example.org") orelse return error.TestUnexpectedResult; defer two.deinit(); try two.addTag("blue");
const query = try db.queryCreate("subject:lunch"); defer query.deinit(); var threads = try query.searchThreads(); defer threads.deinit(); const thread = try threads.next() orelse return error.TestUnexpectedResult; defer thread.deinit();
var tags = thread.getTags(); try std.testing.expect(fixture.hasTag(&tags, "red")); tags = thread.getTags(); try std.testing.expect(fixture.hasTag(&tags, "blue"));}
pub fn deinit(self: *const Thread) void { c.notmuch_thread_destroy(self.thread);}