diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml index c63ba93..ea255a3 100644 --- a/.forgejo/workflows/build.yaml +++ b/.forgejo/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Zig uses: https://codeberg.org/mlugg/setup-zig@v2.2.1 - - name: Install dependencies (gitea) + - name: Install dependencies if: env.GITEA_ACTIONS == 'true' run: apt-get update && apt-get install --no-install-recommends -y libnotmuch-dev libgmime-3.0-dev libglib2.0-dev - name: Build project diff --git a/src/Email.zig b/src/Email.zig index 557080f..bda0905 100644 --- a/src/Email.zig +++ b/src/Email.zig @@ -212,73 +212,6 @@ pub const Message = struct { return null; } - pub fn rawBody(self: Message, allocator: std.mem.Allocator) ![]const u8 { - // Get the message body using GMime - const body = gmime.g_mime_message_get_body(self.message); - if (body == null) return error.NoMessageBody; - - // Check if it's a multipart message - if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(body)), gmime.g_mime_multipart_get_type()) != 0) { - const multipart: *gmime.GMimeMultipart = @ptrCast(body); - - // Try to find HTML content in the multipart - if (try findHtmlInMultipart(multipart, allocator)) |html_content| { - // Trim trailing whitespace and newlines to match expected format - return html_content; - } - } - - // If it's not multipart or we didn't find HTML, check if it's a single text part - if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(body)), gmime.g_mime_text_part_get_type()) != 0) { - const text_part: *gmime.GMimeTextPart = @ptrCast(body); - const text = gmime.g_mime_text_part_get_text(text_part); - if (text != null) { - defer gmime.g_free(text); - const content = try allocator.dupe(u8, std.mem.span(text)); - return content; - } - } - - // Fallback: convert the entire body to string - const body_string = gmime.g_mime_object_to_string(body, null); - if (body_string == null) return error.BodyConversionFailed; - - defer gmime.g_free(body_string); - return try allocator.dupe(u8, std.mem.span(body_string)); - } - - pub fn getContent(self: Message, allocator: std.mem.Allocator) !struct { content: []const u8, content_type: []const u8 } { - const body = gmime.g_mime_message_get_body(self.message); - if (body == null) return error.NoMessageBody; - - // Check if it's a multipart message - if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(body)), gmime.g_mime_multipart_get_type()) != 0) { - const multipart: *gmime.GMimeMultipart = @ptrCast(body); - if (try findHtmlInMultipart(multipart, allocator)) |html_content| { - return .{ .content = html_content, .content_type = "text/html" }; - } - } - - // Check if it's a single text part - if (gmime.g_type_check_instance_is_a(@as(*gmime.GTypeInstance, @ptrCast(body)), gmime.g_mime_text_part_get_type()) != 0) { - const text_part: *gmime.GMimeTextPart = @ptrCast(body); - const text = gmime.g_mime_text_part_get_text(text_part); - if (text != null) { - defer gmime.g_free(text); - const content = try allocator.dupe(u8, std.mem.span(text)); - const content_type_obj = gmime.g_mime_object_get_content_type(body); - const mime_type = if (content_type_obj != null) - gmime.g_mime_content_type_get_mime_type(content_type_obj) - else - null; - const ct = if (mime_type != null) std.mem.span(mime_type) else "text/plain"; - return .{ .content = content, .content_type = ct }; - } - } - - return error.NoTextContent; - } - pub fn getTextAndHtmlBodyVersions(self: Message, allocator: std.mem.Allocator) !struct { text: []const u8, html: []const u8 } { const body = gmime.g_mime_message_get_body(self.message); if (body == null) return error.NoMessageBody; @@ -343,13 +276,6 @@ pub const Message = struct { }; } - pub fn getHeader(self: Message, name: []const u8) ?[]const u8 { - const name_z = std.mem.sliceTo(name, 0); - const header = gmime.g_mime_message_get_header(self.message, name_z.ptr); - if (header == null) return null; - return std.mem.span(header); - } - pub const AttachmentInfo = struct { filename: []const u8, content_type: []const u8, @@ -427,51 +353,6 @@ pub const Message = struct { } }; -fn testPath(allocator: std.mem.Allocator) ![:0]const u8 { - var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; - const cwd = try std.fs.cwd().realpath(".", cwd_buf[0..]); - return std.fs.path.joinZ(allocator, &[_][]const u8{ cwd, "mail", "Inbox", "cur", "1721591945.R4187135327503631514.nucman:2,S" }); -} -test "read raw body of message" { - var engine = Self.init(); - defer engine.deinit(); - const allocator = std.testing.allocator; - const message_path = try testPath(allocator); - defer allocator.free(message_path); - const msg = try engine.openMessage(message_path); - defer msg.deinit(); - const body = try msg.rawBody(allocator); - defer allocator.free(body); - try std.testing.expectEqualStrings( - \\ - \\ - \\ - \\ - \\
- , std.mem.trimRight(u8, body, "\r\n")); -} - -test "can get body from multipart/alternative html preferred" { - var engine = Self.init(); - defer engine.deinit(); - const allocator = std.testing.allocator; - const message_path = try testPath(allocator); - defer allocator.free(message_path); - const msg = try engine.openMessage(message_path); - defer msg.deinit(); - const body = try msg.rawBody(allocator); - defer allocator.free(body); - const b = "hi"; - _ = b; - try std.testing.expectEqualStrings( - \\ - \\ - \\ - \\ - \\
- , std.mem.trimRight(u8, body, "\r\n")); -} - test "can parse attachments" { var engine = Self.init(); defer engine.deinit(); diff --git a/src/notmuch.zig b/src/notmuch.zig index 791e006..e35e4a0 100644 --- a/src/notmuch.zig +++ b/src/notmuch.zig @@ -283,14 +283,6 @@ pub const Db = struct { 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, diff --git a/src/root.zig b/src/root.zig index 845ac36..f4ebd10 100644 --- a/src/root.zig +++ b/src/root.zig @@ -189,21 +189,25 @@ pub const NotmuchDb = struct { if (self.email_owned) self.email.deinit(); } - /// Run a threads query, recovering once from a stale database snapshot. + /// Run a database operation, recovering once from a stale snapshot. /// /// A read-only notmuch handle sees a snapshot of the database as of open - /// time. Mail delivery runs `notmuch new`, and a query issued against a - /// snapshot that has since been rewritten can fail with a Xapian error. + /// time. Mail delivery runs `notmuch new`, and an operation issued against + /// a snapshot that has since been rewritten can fail with a Xapian error. /// When that happens we reopen to the latest revision and retry once, /// rather than killing the whole process as the previous code did (it /// called std.process.exit and leaned on the container restart policy). If - /// the reopen itself fails we surface the original query error. + /// the reopen itself fails we surface the original error. /// - /// Callers must hold `self.mutex`. - fn openThreads(self: *NotmuchDb, query_z: [:0]const u8) !notmuch.Db.ThreadIterator { - return self.db.searchThreads(query_z) catch |first_err| { + /// `op` is a notmuch.Db method taking one [:0]const u8 argument (e.g. + /// searchThreads, findMessage). It's a comptime higher-order parameter + /// (anytype), which keeps the helper generic over each op's exact return + /// and error type -- naming a concrete fn type would force the shared + /// signature's error set to anyerror. Callers must hold `self.mutex`. + fn withReopenRetry(self: *NotmuchDb, comptime op: anytype, arg: [:0]const u8) @TypeOf(op(self.db, arg)) { + return op(self.db, arg) catch |first_err| { self.db.reopen() catch return first_err; - return self.db.searchThreads(query_z); + return op(self.db, arg); }; } @@ -212,7 +216,7 @@ pub const NotmuchDb = struct { const query_z = try std.fmt.bufPrintZ(&query_buf, "{s}", .{query}); const ti = try self.allocator.create(notmuch.Db.ThreadIterator); errdefer self.allocator.destroy(ti); - ti.* = try self.openThreads(query_z); + ti.* = try self.withReopenRetry(notmuch.Db.searchThreads, query_z); return Threads.init(self.allocator, ti); } @@ -221,7 +225,7 @@ pub const NotmuchDb = struct { const query_z = try std.fmt.bufPrintZ(&query_buf, "thread:{s}", .{thread_id}); const iter_ptr = try self.allocator.create(notmuch.Db.ThreadIterator); errdefer self.allocator.destroy(iter_ptr); - iter_ptr.* = try self.openThreads(query_z); + iter_ptr.* = try self.withReopenRetry(notmuch.Db.searchThreads, query_z); errdefer iter_ptr.deinit(); const thread = iter_ptr.next(); @@ -265,15 +269,6 @@ pub const NotmuchDb = struct { } }; - /// Find a message by its exact id, reopening the snapshot and retrying - /// once on failure (mirrors openThreads). Callers must hold self.mutex. - fn findMessage(self: *NotmuchDb, message_id: [:0]const u8) !?notmuch.Db.Message { - return self.db.findMessage(message_id) catch |first_err| { - self.db.reopen() catch return first_err; - return self.db.findMessage(message_id); - }; - } - pub fn getMessage(self: *NotmuchDb, message_id: []const u8) !MessageDetail { const message_id_z = try self.allocator.dupeZ(u8, message_id); defer self.allocator.free(message_id_z); @@ -281,7 +276,7 @@ pub const NotmuchDb = struct { // Look the message up by id directly. Previously this searched for the // thread containing the id and returned the thread's first message, so // every message in a reply thread rendered the same body. - const notmuch_msg = (try self.findMessage(message_id_z)) orelse return error.MessageNotFound; + const notmuch_msg = (try self.withReopenRetry(notmuch.Db.findMessage, message_id_z)) orelse return error.MessageNotFound; defer notmuch_msg.deinit(); const filename_z = try self.allocator.dupeZ(u8, notmuch_msg.getFilename());