From df741c69ae35be3c24268d6d58c540302e4f106b Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 16 Oct 2025 11:19:44 -0700 Subject: [PATCH] add mutt compatible processing to the query api --- build.zig.zon | 2 +- src/main.zig | 46 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index d701df7..c63642a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .zetviel, // This is a [Semantic Version](https://semver.org/). // In a future version of Zig it will be used for package deduplication. - .version = "0.0.0", + .version = "0.9.0", // This field is optional. // This is currently advisory only; Zig does not yet do anything diff --git a/src/main.zig b/src/main.zig index e0ee7bf..3577ac1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -204,8 +204,7 @@ fn queryHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) } // URL decode the query - const query_buf = try db.allocator.dupe(u8, encoded_query); - defer db.allocator.free(query_buf); + const query_buf = try req.arena.dupe(u8, encoded_query); const query = std.Uri.percentDecodeInPlace(query_buf); var threads = db.search(query) catch |err| { @@ -215,7 +214,48 @@ fn queryHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) }; defer threads.deinit(); - try res.json(threads, .{}); + // Check Accept header + const accept = req.header("accept") orelse "application/json"; + if (std.mem.startsWith(u8, accept, "text/plain")) { + // Parse parameters + const has_format = std.mem.indexOf(u8, accept, "format=message-ids") != null; + const separator_param = std.mem.indexOf(u8, accept, "separator="); + const has_mutt_escape = std.mem.indexOf(u8, accept, "escape=mutt") != null; + + if (!has_format) { + res.status = 400; + res.body = "Invalid Accept header: text/plain requires format=message-ids"; + return; + } + + // Collect message IDs + var msg_ids = std.ArrayList([]const u8){}; + + while (try threads.next()) |*thread| { + defer thread.deinit(); + var msg_iter = try thread.thread.getMessages(); + while (msg_iter.next()) |msg| { + const msg_id = msg.getMessageId(); + if (has_mutt_escape) { + const escaped = try std.mem.replaceOwned(u8, res.arena, msg_id, "+", "\\+"); + try msg_ids.append(res.arena, escaped); + } else { + try msg_ids.append(res.arena, msg_id); + } + } + } + + // Format output + const separator: []const u8 = if (separator_param) |s| accept[s + "separator=".len .. s + "separator=".len + 1] else "\n"; + const output = try std.mem.join(res.arena, separator, msg_ids.items); + res.header("Content-Type", "text/plain"); + res.body = output; + } else if (std.mem.startsWith(u8, accept, "application/json") or std.mem.eql(u8, accept, "*/*")) { + try res.json(threads, .{}); + } else { + res.status = 400; + res.body = "Invalid Accept header: must be application/json or text/plain; format=message-ids"; + } } fn threadHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {