Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
42 kB · 1100 lines
Zig
at zig-0.16
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101// SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie// SPDX-License-Identifier: GPL-3.0-or-later
//! A single email message in the notmuch database.//!//! Obtained from searches (`Query.searchMessages`), lookups//! (`Database.findMessage`), thread traversal, or indexing. Exposes the//! message's identity, headers, dates, and filenames, and supports mutating//! tags and properties, synchronizing maildir flags, and batching tag changes//! with `freeze`/`thaw`. The message is owned by the query or database it//! came from; `deinit` releases it sooner.const Message = @This();
const std = @import("std");
const c = @import("c");
const Database = @import("Database.zig");const Error = @import("error.zig").Error;const FilenamesIterator = @import("FilenamesIterator.zig");const IndexOpts = @import("IndexOpts.zig");const MaildirFlag = @import("enums.zig").MaildirFlag;const MessageFlag = @import("enums.zig").MessageFlag;const MessagesIterator = @import("MessagesIterator.zig");const PropertiesIterator = @import("PropertiesIterator.zig");const status = @import("enums.zig").status;const TagsIterator = @import("TagsIterator.zig");const wrap = @import("error.zig").wrap;
/// For a message returned by `Database.indexFileGetMessage`: whether the/// indexed file carried a message ID already present in the database (its/// index was merged into the existing message). `null` for messages obtained/// any other way.duplicate: ?bool = null,/// The underlying `notmuch_message_t` handle; only needed directly when/// calling libnotmuch C APIs these bindings do not wrap.message: *c.notmuch_message_t,
/// Get the database associated with this message.pub fn getDatabase(self: *const Message) Database { return .{ .database = c.notmuch_message_get_database(self.message) orelse unreachable, };}
test getDatabase { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// The message's owning database is the one it was found in. try std.testing.expectEqual(test_db.database.database, message.getDatabase().database);}
/// Get the message ID of `Message`.////// The returned string belongs to `Message` and as such, should not be modified/// or freed by the caller and will only be valid for as long as the message is/// valid, which is until the query from which it derived is destroyed.////// This function will return `null` if it triggers an unhandled Xapian exception.pub fn getMessageID(self: *const Message) ?[:0]const u8 { return std.mem.span(c.notmuch_message_get_message_id(self.message) orelse return null);}
test getMessageID { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// The message ID, without the angle brackets of the Message-ID header. try std.testing.expectEqualStrings("one@example.org", message.getMessageID() orelse "");}
/// Get the thread ID of `Message`.////// The returned string belongs to `Message` and as such, should not be modified/// or freed by the caller and will only be valid for as long as the message is/// valid, which is until the user calls `deinit` or until the query from which/// it derived is deinitialized.////// This function will return `null` if it triggers an unhandled Xapian exception.pub fn getThreadID(self: *const Message) ?[:0]const u8 { return std.mem.span(c.notmuch_message_get_thread_id(self.message) orelse return null);}
test getThreadID { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const one = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer one.deinit(); const two = try test_db.database.findMessage("two@example.org") orelse return error.TestUnexpectedResult; defer two.deinit();
// Messages joined by In-Reply-To share a thread ID. try std.testing.expectEqualStrings( one.getThreadID() orelse return error.TestUnexpectedResult, two.getThreadID() orelse return error.TestUnexpectedResult, );}
/// Get a `MessagesIterator` for all of the replies to `Message`.////// Note: This call only makes sense if `Message` was ultimately obtained from/// a `Thread` object, (such as by coming directly from the result of calling/// `Thread.getToplevelMessages` or by any number of subsequent calls to/// `getReplies`).////// If `Message` was obtained through some non-thread means, (such as by a/// call to `Query.searchMessages`), then this function will return an empty/// iterator.////// If there are no replies to `Message`, this function will return an empty/// iterator.////// This function also returns an empty iterator if it triggers a Xapian/// exception.////// The returned list will be deinitialized when the thread is deinitialized.pub fn getReplies(self: *const Message) MessagesIterator { return .{ .messages = c.notmuch_message_get_replies(self.message), };}
test getReplies { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const db = test_db.database;
// Replies are available for messages obtained through a thread. 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 toplevel = thread.getToplevelMessages(); const top = try toplevel.next() orelse return error.TestUnexpectedResult; var replies = top.getReplies(); const reply = try replies.next() orelse return error.TestUnexpectedResult; try std.testing.expectEqualStrings("two@example.org", reply.getMessageID() orelse "");
// For a message from a plain search, the iterator is empty. const searched = try db.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer searched.deinit(); var none = searched.getReplies(); try std.testing.expectEqual(@as(?Message, null), try none.next());}
pub const CountFilesError = error{ /// An error occurred while trying to count files. CountFilesError,};
/// Get the total number of files associated with a message.pub fn countFiles(self: *const Message) CountFilesError!usize { std.debug.assert(@typeInfo(c_int).int.bits <= @typeInfo(usize).int.bits); const count = c.notmuch_message_count_files(self.message); if (count < 0) return error.CountFilesError; return @intCast(@max(0, count));}
test countFiles { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// Each corpus message is backed by a single file. try std.testing.expectEqual(@as(usize, 1), try message.countFiles());}
pub const GetFilenameError = error{ /// A Xapian exception occurred. XapianException,};
/// Get a filename for the email corresponding to `Message`.////// The returned filename is an absolute filename (the initial component will/// match `Database.getPath`).////// The returned string belongs to the message so should not be modified or/// freed by the caller (nor should it be referenced after the message is/// deinitialized).////// Note: If this message corresponds to multiple files in the mail store,/// (that is, multiple files contain identical message IDs), this function will/// arbitrarily return a single one of those filenames. See `getFilenames` for/// returning the complete list of filenames.////// Returns `error.XapianException` if the call triggers a Xapian exception.pub fn getFilename(self: *const Message) GetFilenameError![:0]const u8 { return std.mem.span(c.notmuch_message_get_filename(self.message) orelse return error.XapianException);}
test getFilename { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
const filename = try message.getFilename(); try std.testing.expect(std.fs.path.isAbsolute(filename)); try std.testing.expectEqualStrings(test_db.filename(0), std.fs.path.basename(filename));}
pub const GetFilenamesError = error{ /// A Xapian exception occurred. XapianException,};
/// Get all filenames for the email corresponding to `Message`.////// Returns a `FilenamesIterator` listing all the filenames associated with/// `Message`. These files may not have identical content, but each will have/// the identical Message-ID.////// Each filename in the iterator is an absolute filename (the initial component/// will match `Database.getPath`).////// Returns `error.XapianException` if the call triggers a Xapian exception.pub fn getFilenames(self: *const Message) GetFilenamesError!FilenamesIterator { return .{ .filenames = c.notmuch_message_get_filenames(self.message) orelse return error.XapianException, };}
test getFilenames { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try std.testing.expectEqual(@as(usize, 1), try message.countFiles()); try std.testing.expect(std.mem.endsWith(u8, try message.getFilename(), test_db.filename(0)));
var filenames = try message.getFilenames(); defer filenames.deinit(); var count: usize = 0; while (filenames.next()) |_| count += 1; try std.testing.expectEqual(@as(usize, 1), count);}
/// Re-index the e-mail corresponding to `Message` using the supplied index/// options (or the database defaults when `indexopts` is `null`).////// After reindexing, discard this message object by calling `deinit`: it/// refers to the original message, not to the reindexed message.pub fn reindex(self: *const Message, indexopts: ?IndexOpts) Error!void { return switch (status(c.notmuch_message_reindex(self.message, if (indexopts) |i| i.indexopts else null))) { .success => {}, .duplicate_message_id => {}, .file_error => error.FileError, .file_not_email => error.FileNotEmail, .read_only_database => error.ReadOnlyDatabase, .upgrade_required => error.UpgradeRequired, else => error.Unexpected, };}
test reindex { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const db = test_db.database;
const message = try db.findMessage("one@example.org") orelse return error.TestUnexpectedResult; try message.addTag("keep"); try message.reindex(null);
// The pre-reindex object refers to the original message; discard it and // look the message up again. message.deinit(); const fresh = try db.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer fresh.deinit(); try std.testing.expectEqualStrings("Lunch plans", fresh.getHeader("Subject") orelse ""); var tags = fresh.getTags(); try std.testing.expect(fixture.hasTag(&tags, "keep"));}
pub const GetFlagError = error{ /// A Xapian exception occurred. XapianException, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Get a value of a flag for the email corresponding to `Message`.pub fn getFlag(self: *const Message, flag: MessageFlag) GetFlagError!bool { var is_set: c.notmuch_bool_t = undefined; return switch (status(c.notmuch_message_get_flag_st( self.message, @intFromEnum(flag), &is_set, ))) { .success => is_set != 0, .null_pointer => unreachable, .xapian_exception => error.XapianException, else => error.Unexpected, };}
test getFlag { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// An ordinary message is not a ghost. try std.testing.expect(!try message.getFlag(.ghost));
// Flags set with setFlag are read back. message.setFlag(.match, true); try std.testing.expect(try message.getFlag(.match));}
/// Set a value of a flag for the email corresponding to `Message`.pub fn setFlag(self: *const Message, flag: MessageFlag, value: bool) void { c.notmuch_message_set_flag(self.message, @intFromEnum(flag), @intFromBool(value));}
/// Get the date of `Message` as the number of seconds since the Unix epoch/// (1970-01-01 00:00:00 UTC).////// For the original textual representation of the `Date` header from the/// message call `getHeader` with a header value of "date".////// Returns `null` in case of error.pub fn getDate(self: *const Message) ?i64 { std.debug.assert(@typeInfo(c.time_t).int.bits <= @typeInfo(i64).int.bits); const time = c.notmuch_message_get_date(self.message); if (time == 0) return null; return @intCast(time);}
test getDate { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// Mon, 01 Apr 2024 09:00:00 +0000, as seconds since the Unix epoch. try std.testing.expectEqual(@as(?i64, 1711962000), message.getDate());}
/// Get the value of the specified header from `Message` as a UTF-8 string.////// Common headers are stored in the database when the message is indexed and/// will be returned from the database. Other headers will be read from the/// actual message file.////// The header name is case insensitive.////// The returned string belongs to the message so should not be modified or/// freed by the caller (nor should it be referenced after the message is/// deinitialized).////// Returns an empty string ("") if the message does not contain a header line/// matching 'header'. Returns `null` if any error occurs.pub fn getHeader(self: *const Message, header: [:0]const u8) ?[:0]const u8 { return std.mem.span(c.notmuch_message_get_header(self.message, header) orelse return null);}
test getHeader { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try std.testing.expectEqualStrings("one@example.org", message.getMessageID() orelse ""); try std.testing.expect(message.getThreadID() != null); // Mon, 01 Apr 2024 09:00:00 +0000 try std.testing.expectEqual(@as(?i64, 1711962000), message.getDate()); try std.testing.expectEqualStrings("Alice <alice@example.org>", message.getHeader("From") orelse ""); try std.testing.expectEqualStrings("", message.getHeader("X-Nonexistent") orelse "missing");}
/// Get the tags for `Message`, returning a `TagsIterator` object which can be/// used to iterate over all tags.////// The tags object is owned by the message and as such, will only be valid/// for as long as the message is valid, which is until the query from which it/// derived is deinitialized.pub fn getTags(self: *const Message) TagsIterator { return .{ .tags = c.notmuch_message_get_tags(self.message), };}
test getTags { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// A freshly indexed message carries no tags. var tags = message.getTags(); try std.testing.expectEqual(@as(usize, 0), fixture.countTags(&tags));
try message.addTag("inbox"); tags = message.getTags(); try std.testing.expect(fixture.hasTag(&tags, "inbox"));}
pub const AddTagError = error{ /// The length of `tag` is too long (exceeds NOTMUCH_TAG_MAX). TagTooLong, /// Database was opened in read-only mode so message cannot be modified. ReadOnlyDatabase, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Add a tag to the given message.pub fn addTag(self: *const Message, tag: [:0]const u8) AddTagError!void { return switch (status(c.notmuch_message_add_tag(self.message, tag))) { .success => {}, .null_pointer => unreachable, .tag_too_long => error.TagTooLong, .read_only_database => error.ReadOnlyDatabase, else => error.Unexpected, };}
test addTag { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addTag("foo"); try message.addTag("bar"); var tags = message.getTags(); try std.testing.expectEqual(@as(usize, 2), fixture.countTags(&tags));
try message.removeTag("foo"); tags = message.getTags(); try std.testing.expect(!fixture.hasTag(&tags, "foo")); tags = message.getTags(); try std.testing.expect(fixture.hasTag(&tags, "bar"));
try message.removeAllTags(); tags = message.getTags(); try std.testing.expectEqual(@as(usize, 0), fixture.countTags(&tags));}
pub const RemoveTagError = error{ /// The length of `tag` is too long (exceeds NOTMUCH_TAG_MAX). TagTooLong, /// Database was opened in read-only mode so message cannot be modified. ReadOnlyDatabase, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Remove a tag from the given message.pub fn removeTag(self: *const Message, tag: [:0]const u8) RemoveTagError!void { return switch (status(c.notmuch_message_remove_tag(self.message, tag))) { .success => {}, .null_pointer => unreachable, .tag_too_long => error.TagTooLong, .read_only_database => error.ReadOnlyDatabase, else => error.Unexpected, };}
test removeTag { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addTag("spam"); try message.removeTag("spam"); var tags = message.getTags(); try std.testing.expect(!fixture.hasTag(&tags, "spam"));
// Removing a tag the message does not carry is not an error. try message.removeTag("spam");}
pub const RemoveAllTagsError = error{ /// Database was opened in read-only mode so message cannot be modified. ReadOnlyDatabase, /// A Xapian exception occurred. XapianException, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Remove all tags from the given message.////// See `freeze` for an example showing how to safely replace tag values.pub fn removeAllTags(self: *const Message) RemoveAllTagsError!void { return switch (status(c.notmuch_message_remove_all_tags(self.message))) { .success => {}, .read_only_database => error.ReadOnlyDatabase, .xapian_exception => error.XapianException, else => error.Unexpected, };}
test removeAllTags { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addTag("alpha"); try message.addTag("beta"); try message.removeAllTags(); var tags = message.getTags(); try std.testing.expectEqual(@as(usize, 0), fixture.countTags(&tags));}
/// Add/remove tags according to maildir flags in the message filename(s).////// This function examines the filenames of `Message` for maildir flags, and/// adds or removes tags on `Message` as follows when these flags are present:////// Flag Action if present/// ---- -----------------/// 'D' Adds the "draft" tag to the message/// 'F' Adds the "flagged" tag to the message/// 'P' Adds the "passed" tag to the message/// 'R' Adds the "replied" tag to the message/// 'S' Removes the "unread" tag from the message////// For each flag that is not present, the opposite action (add/remove) is/// performed for the corresponding tags.////// Flags are identified as trailing components of the filename after a/// sequence of ":2,".////// If there are multiple filenames associated with this message, the flag/// is considered present if it appears in one or more filenames. (That is,/// the flags from the multiple filenames are combined with the logical OR/// operator.)////// A client can ensure that notmuch database tags remain synchronized/// with maildir flags by calling this function after each call to/// `Database.indexFile`. See also `tagsToMaildirFlags` for synchronizing tag/// changes back to maildir flags.pub fn maildirFlagsToTags(self: *const Message) Error!void { try wrap(c.notmuch_message_maildir_flags_to_tags(self.message));}
test maildirFlagsToTags { const fixture = @import("testing.zig");
const alloc = std.testing.allocator; var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const db = test_db.database;
// A message delivered straight into cur/ with the "flagged" flag set but // not "seen" — what a client saving an already-marked message does. const delivered = try test_db.deliver( \\From: Erin <erin@example.org> \\To: Alice <alice@example.org> \\Subject: Flagged mail \\Date: Thu, 04 Apr 2024 10:00:00 +0000 \\Message-ID: <five@example.org> \\ \\This one is flagged. \\ , .{ .to = .{ .cur = .{ .flagged = true } } });
const path = try test_db.pathOf(&delivered); defer alloc.free(path); const message = try db.indexFileGetMessage(path, null); defer message.deinit();
try std.testing.expect(try message.hasMaildirFlag(.F)); try std.testing.expect(!try message.hasMaildirFlag(.S)); try std.testing.expect(!try message.hasMaildirFlag(.D));
try message.maildirFlagsToTags();
// F present adds "flagged"; S absent performs the opposite of removing // "unread", i.e. adds it. var tags = message.getTags(); try std.testing.expect(fixture.hasTag(&tags, "flagged")); tags = message.getTags(); try std.testing.expect(fixture.hasTag(&tags, "unread"));}
pub const HasMaildirFlagError = error{ /// A Xapian exception occurred. XapianException, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Check message for maildir flag.pub fn hasMaildirFlag(self: *const Message, flag: MaildirFlag) HasMaildirFlagError!bool { var is_set: c.notmuch_bool_t = undefined; return switch (status(c.notmuch_message_has_maildir_flag_st(self.message, @intFromEnum(flag), &is_set))) { .success => is_set != 0, .null_pointer => unreachable, .xapian_exception => error.XapianException, else => error.Unexpected, };}
test hasMaildirFlag { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// The corpus is delivered into new/, where a name has no ":2," maildir // info part to carry a flag — which is what makes it new. try std.testing.expect(!try message.hasMaildirFlag(.S)); try std.testing.expect(!try message.hasMaildirFlag(.F));}
/// Rename message filename(s) to encode tags as maildir flags.////// Specifically, for each filename corresponding to this message:////// If the filename is not in a maildir directory, do nothing. (A maildir/// directory is determined as a directory named "new" or "cur".) Similarly, if/// the filename has invalid maildir info, (repeated or outof-ASCII-order flag/// characters after ":2,"), then do nothing.////// If the filename is in a maildir directory, rename the file so that its/// filename ends with the sequence ":2," followed by zero or more of the/// following single-character flags (in ASCII order):////// * flag 'D' iff the message has the "draft" tag/// * flag 'F' iff the message has the "flagged" tag/// * flag 'P' iff the message has the "passed" tag/// * flag 'R' iff the message has the "replied" tag/// * flag 'S' iff the message does not have the "unread" tag////// Any existing flags unmentioned in the list above will be preserved in the/// renaming.////// Also, if this filename is in a directory named "new", rename it to be within/// the neighboring directory named "cur".////// A client can ensure that maildir filename flags remain synchronized/// with notmuch database tags by calling this function after changing tags/// (after calls to `addTag`, `removeTag`, or `freeze`/`thaw`). See also/// `maildirFlagsToTags` for synchronizing maildir flag changes back to tags.pub fn tagsToMaildirFlags(self: *const Message) Error!void { try wrap(c.notmuch_message_tags_to_maildir_flags(self.message));}
test tagsToMaildirFlags { const fixture = @import("testing.zig");
const alloc = std.testing.allocator; const io = std.testing.io; var test_db = try fixture.corpusDatabase(); defer test_db.deinit(); const db = test_db.database;
const message = try db.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addTag("flagged"); try message.addTag("passed"); // No "unread" tag, so the seen flag is set as well. try message.tagsToMaildirFlags();
// The corpus message was delivered into new/, which has nowhere to write // a flag, so notmuch moved it into cur/ and appended the info part to the // name it arrived under. const flagged_name = try std.fmt.allocPrint(alloc, "{s}:2,FPS", .{test_db.filename(0)}); defer alloc.free(flagged_name); const filename = try message.getFilename(); try std.testing.expect(std.mem.endsWith(u8, filename, flagged_name)); try std.testing.expect(try message.hasMaildirFlag(.F)); try std.testing.expect(try message.hasMaildirFlag(.P)); try std.testing.expect(try message.hasMaildirFlag(.S));
// The file was renamed on disk, not copied. const in_cur = try std.fs.path.join(alloc, &.{ "mail", "cur", flagged_name }); defer alloc.free(in_cur); try test_db.tmp.dir.access(io, in_cur, .{}); const in_new = try std.fs.path.join(alloc, &.{ "mail", "new", test_db.filename(0) }); defer alloc.free(in_new); try std.testing.expectError(error.FileNotFound, test_db.tmp.dir.access(io, in_new, .{}));
// The message kept the identity it was delivered under, so a maildir // reader still finds it — and reads back the flags notmuch wrote. var moved = try test_db.mailbox.find(io, test_db.delivered[0].id()) orelse return error.TestUnexpectedResult; try std.testing.expect(moved.subdir == .cur); const flags = moved.flags(); try std.testing.expect(flags.flagged and flags.passed and flags.seen);}
/// Freeze the current state of `Message` within the database.////// This means that changes to the message state, (via `addTag`, `removeTag`,/// and `removeAllTags`), will not be committed to the database until the/// message is thawed with `thaw`.////// Multiple calls to `freeze`/`thaw` are valid and these calls will "stack"./// That is there must be as many calls to thaw as to freeze before a message is/// actually thawed.////// The ability to do `freeze`/`thaw` allows for safe transactions to change/// tag values. For example, explicitly setting a message to have a given set of/// tags might look like this:////// try message.freeze();/// defer message.thaw() catch {};////// try message.removeAllTags();////// for (tags) |tag|/// try message.addTag(tag);////// With `freeze`/`thaw` used like this, the message in the database is/// guaranteed to have either the full set of original tag values, or the full/// set of new tag values, but nothing in between.////// Imagine the example above without `freeze`/`thaw` and the operation somehow/// getting interrupted. This could result in the message being left with no/// tags if the interruption happened after `removeAllTags` but before `addTag`.pub fn freeze(self: *const Message) Error!void { try wrap(c.notmuch_message_freeze(self.message));}
test freeze { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("two@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.freeze(); try message.addTag("frozen"); try message.thaw();
var tags = message.getTags(); try std.testing.expect(fixture.hasTag(&tags, "frozen"));}
/// Thaw the current `Message`, synchronizing any changes that may have/// occurred while `Message` was frozen into the `notmuch` database.////// See `freeze` for an example of how to use this function to/// safely provide tag changes.////// Multiple calls to `freeze`/`thaw` are valid and these calls with "stack"./// That is there must be as many calls to `thaw` as to `freeze` before a/// message is actually thawed.pub fn thaw(self: *const Message) Error!void { try wrap(c.notmuch_message_thaw(self.message));}
/// Deinitialize a `Message` object.////// It can be useful to call this function in the case of a single query/// object with many messages in the result, such as iterating over the entire/// database. Otherwise, it's fine to never call this function and there will/// still be no memory leaks. (The memory from the messages get reclaimed when/// the containing query is deinitialized.)pub fn deinit(self: *const Message) void { c.notmuch_message_destroy(self.message);}
/// Retrieve the value for a single property key.////// Returns a string owned by the `Message` or `null` if there is no such/// key. In the case of multiple values for the given key, the first one is/// retrieved.pub fn getProperty(self: *const Message, key: [:0]const u8) ?[:0]const u8 { var value: [*c]const u8 = null; return switch (status(c.notmuch_message_get_property(self.message, key, &value))) { .success => std.mem.span(value orelse return null), .null_pointer => unreachable, else => null, };}
test getProperty { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
// An absent property reads as null. try std.testing.expectEqual(@as(?[:0]const u8, null), message.getProperty("zig.absent"));
try message.addProperty("zig.key", "value"); try std.testing.expectEqualStrings( "value", message.getProperty("zig.key") orelse return error.TestUnexpectedResult, );}
pub const AddPropertyError = error{ /// `key` may not contain an '=' character. IllegalArgument, /// Database was opened in read-only mode so message cannot be modified. ReadOnlyDatabase, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Add a (key,value) pair to a message.pub fn addProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) AddPropertyError!void { return switch (status(c.notmuch_message_add_property(self.message, key, value))) { .success => {}, .null_pointer => unreachable, .illegal_argument => return error.IllegalArgument, .read_only_database => return error.ReadOnlyDatabase, else => error.Unexpected, };}
test addProperty { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addProperty("test.key", "value1"); try message.addProperty("test.key", "value2"); try std.testing.expectEqualStrings( "value1", message.getProperty("test.key") orelse return error.TestUnexpectedResult, ); try std.testing.expectEqual(@as(usize, 2), try message.countProperties("test.key"));
var properties = message.getProperties("test.key", true); defer properties.deinit(); var count: usize = 0; while (properties.next()) |kv| { try std.testing.expectEqualStrings("test.key", kv.key); count += 1; } try std.testing.expectEqual(@as(usize, 2), count);
try message.removeProperty("test.key", "value1"); try std.testing.expectEqual(@as(usize, 1), try message.countProperties("test.key"));
try message.removeAllProperties(null); try std.testing.expectEqual(@as(usize, 0), try message.countProperties("test.key"));}
pub const RemovePropertyError = error{ /// `key` may not contain an '=' character. IllegalArgument, /// Database was opened in read-only mode so message cannot be modified. ReadOnlyDatabase, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Remove a (key,value) pair from a message.////// It is not an error to remove a non-existent (key,value) pairpub fn removeProperty(self: *const Message, key: [:0]const u8, value: [:0]const u8) RemovePropertyError!void { return switch (status(c.notmuch_message_remove_property(self.message, key, value))) { .success => {}, .null_pointer => unreachable, .illegal_argument => error.IllegalArgument, .read_only_database => error.ReadOnlyDatabase, else => error.Unexpected, };}
test removeProperty { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addProperty("zig.key", "first"); try message.addProperty("zig.key", "second"); try message.removeProperty("zig.key", "first"); try std.testing.expectEqual(@as(usize, 1), try message.countProperties("zig.key"));
// Removing a non-existent pair is not an error. try message.removeProperty("zig.key", "first");}
pub const RemoveAllPropertiesError = error{ /// Database was opened in read-only mode so message cannot be modified. ReadOnlyDatabase, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Remove all (key,value) pairs from the given message.pub fn removeAllProperties( /// The message to operate on. self: *const Message, /// key to delete properties for. If `null`, delete properties for all keys key: ?[:0]const u8,) RemoveAllPropertiesError!void { return switch (status(c.notmuch_message_remove_all_properties(self.message, key orelse null))) { .success => {}, .read_only_database => error.ReadOnlyDatabase, else => error.Unexpected, };}
test removeAllProperties { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addProperty("zig.a", "1"); try message.addProperty("zig.b", "2");
// With a key, only that key's properties are removed. try message.removeAllProperties("zig.a"); try std.testing.expectEqual(@as(usize, 0), try message.countProperties("zig.a")); try std.testing.expectEqual(@as(usize, 1), try message.countProperties("zig.b"));
// With null, all properties are removed. try message.removeAllProperties(null); try std.testing.expectEqual(@as(usize, 0), try message.countProperties("zig.b"));}
pub const RemoveAllPropertiesWithPrefixError = error{ /// Database was opened in read-only mode so message cannot be modified. ReadOnlyDatabase, /// libnotmuch returned an undocumented status code. Unexpected,};
/// Remove all (prefix*,value) pairs from the given messagepub fn removeAllPropertiesWithPrefix( /// The message to operate on. self: *const Message, /// Delete properties with keys that start with prefix. If `null`, delete all properties. prefix: ?[:0]const u8,) RemoveAllPropertiesWithPrefixError!void { return switch (status(c.notmuch_message_remove_all_properties_with_prefix(self.message, prefix orelse null))) { .success => {}, .read_only_database => error.ReadOnlyDatabase, else => error.Unexpected, };}
test removeAllPropertiesWithPrefix { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addProperty("zig.a", "1"); try message.addProperty("zig.b", "2"); try message.addProperty("other.c", "3");
try message.removeAllPropertiesWithPrefix("zig."); try std.testing.expectEqual(@as(usize, 0), try message.countProperties("zig.a")); try std.testing.expectEqual(@as(usize, 0), try message.countProperties("zig.b")); try std.testing.expectEqual(@as(usize, 1), try message.countProperties("other.c"));}
/// Get the properties for `Message`, returning a `PropertiesIterator` object/// which can be used to iterate over all properties.////// The `PropertiesIterator` object is owned by the message and as such, will only/// be valid for as long as the message is valid, which is until the query from/// which it derived is deinitialized.pub fn getProperties( /// The message to examine. self: *const Message, /// Key or key prefix. key: [:0]const u8, /// If `true`, require exact match with key, otherwise treat as prefix. exact: bool,) PropertiesIterator { return .{ .properties = c.notmuch_message_get_properties(self.message, key, @intFromBool(exact)), };}
test getProperties { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try message.addProperty("zig.a", "1"); try message.addProperty("zig.ab", "2");
// With exact = true, only the exact key matches. var exact = message.getProperties("zig.a", true); defer exact.deinit(); var exact_count: usize = 0; while (exact.next()) |kv| : (exact_count += 1) try std.testing.expectEqualStrings("zig.a", kv.key); try std.testing.expectEqual(@as(usize, 1), exact_count);
// With exact = false, the key is treated as a prefix. var prefixed = message.getProperties("zig.a", false); defer prefixed.deinit(); var prefix_count: usize = 0; while (prefixed.next()) |_| prefix_count += 1; try std.testing.expectEqual(@as(usize, 2), prefix_count);}
/// Return the number of properties named "key" belonging to the specific message.pub fn countProperties(self: *const Message, key: [:0]const u8) Error!usize { std.debug.assert(@typeInfo(c_uint).int.bits <= @typeInfo(usize).int.bits); var count: c_uint = undefined; try wrap(c.notmuch_message_count_properties(self.message, key, &count)); return @intCast(count);}
test countProperties { const fixture = @import("testing.zig");
var test_db = try fixture.corpusDatabase(); defer test_db.deinit();
const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; defer message.deinit();
try std.testing.expectEqual(@as(usize, 0), try message.countProperties("zig.key")); try message.addProperty("zig.key", "first"); try message.addProperty("zig.key", "second"); try std.testing.expectEqual(@as(usize, 2), try message.countProperties("zig.key"));}
test { _ = std.testing.refAllDecls(@This());}