212 lines
6.4 KiB
Zig
212 lines
6.4 KiB
Zig
const std = @import("std");
|
|
const httpz = @import("httpz");
|
|
const root = @import("root.zig");
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
// Get notmuch database path from environment or use default
|
|
const db_path = std.posix.getenv("NOTMUCH_PATH") orelse "mail";
|
|
|
|
// Open notmuch database
|
|
var db = try root.openNotmuchDb(allocator, db_path, null);
|
|
defer db.close();
|
|
|
|
std.debug.print("Zetviel starting on http://localhost:5000\n", .{});
|
|
std.debug.print("Notmuch database: {s}\n", .{db.path});
|
|
|
|
// Create HTTP server
|
|
var server = try httpz.Server(*root.NotmuchDb).init(allocator, .{
|
|
.port = 5000,
|
|
.address = "127.0.0.1",
|
|
}, &db);
|
|
defer server.deinit();
|
|
|
|
// API routes
|
|
var security_headers = SecurityHeaders{};
|
|
const security_middleware = httpz.Middleware(*root.NotmuchDb).init(&security_headers);
|
|
var router = try server.router(.{ .middlewares = &.{security_middleware} });
|
|
router.get("/api/query/*", queryHandler, .{});
|
|
router.get("/api/thread/:thread_id", threadHandler, .{});
|
|
router.get("/api/message/:message_id", messageHandler, .{});
|
|
router.get("/api/attachment/:message_id/:num", attachmentHandler, .{});
|
|
|
|
// TODO: Static file serving for frontend
|
|
|
|
try server.listen();
|
|
}
|
|
|
|
const SecurityHeaders = struct {
|
|
pub fn execute(_: *SecurityHeaders, req: *httpz.Request, res: *httpz.Response, executor: anytype) !void {
|
|
res.header("X-Frame-Options", "deny");
|
|
res.header("X-Content-Type-Options", "nosniff");
|
|
res.header("X-XSS-Protection", "1; mode=block");
|
|
res.header("Referrer-Policy", "no-referrer");
|
|
_ = req;
|
|
return executor.next();
|
|
}
|
|
};
|
|
|
|
fn queryHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
|
const query = req.url.path[11..]; // Skip "/api/query/"
|
|
if (query.len == 0) {
|
|
res.status = 400;
|
|
try res.json(.{ .@"error" = "Query parameter required" }, .{});
|
|
return;
|
|
}
|
|
|
|
var threads = db.search(query) catch |err| {
|
|
res.status = 500;
|
|
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
|
return;
|
|
};
|
|
defer threads.deinit();
|
|
|
|
try res.json(threads, .{});
|
|
}
|
|
|
|
fn threadHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
|
const thread_id = req.param("thread_id") orelse {
|
|
res.status = 400;
|
|
try res.json(.{ .@"error" = "Thread ID required" }, .{});
|
|
return;
|
|
};
|
|
|
|
var thread = db.getThread(thread_id) catch |err| {
|
|
res.status = 404;
|
|
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
|
return;
|
|
};
|
|
defer thread.deinit();
|
|
|
|
try res.json(thread, .{});
|
|
}
|
|
|
|
fn messageHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
|
const message_id = req.param("message_id") orelse {
|
|
res.status = 400;
|
|
try res.json(.{ .@"error" = "Message ID required" }, .{});
|
|
return;
|
|
};
|
|
|
|
const msg = db.getMessage(message_id) catch |err| {
|
|
res.status = 404;
|
|
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
|
return;
|
|
};
|
|
defer msg.deinit(db.allocator);
|
|
|
|
try res.json(msg, .{});
|
|
}
|
|
|
|
fn attachmentHandler(db: *root.NotmuchDb, req: *httpz.Request, res: *httpz.Response) !void {
|
|
const message_id = req.param("message_id") orelse {
|
|
res.status = 400;
|
|
try res.json(.{ .@"error" = "Message ID required" }, .{});
|
|
return;
|
|
};
|
|
|
|
const num_str = req.param("num") orelse {
|
|
res.status = 400;
|
|
try res.json(.{ .@"error" = "Attachment number required" }, .{});
|
|
return;
|
|
};
|
|
|
|
const num = std.fmt.parseInt(usize, num_str, 10) catch {
|
|
res.status = 400;
|
|
try res.json(.{ .@"error" = "Invalid attachment number" }, .{});
|
|
return;
|
|
};
|
|
|
|
const msg = db.getMessage(message_id) catch |err| {
|
|
res.status = 404;
|
|
try res.json(.{ .@"error" = @errorName(err) }, .{});
|
|
return;
|
|
};
|
|
defer msg.deinit(db.allocator);
|
|
|
|
if (num >= msg.attachments.len) {
|
|
res.status = 404;
|
|
try res.json(.{ .@"error" = "Attachment not found" }, .{});
|
|
return;
|
|
}
|
|
|
|
const att = msg.attachments[num];
|
|
res.header("Content-Type", att.content_type);
|
|
res.header("Content-Disposition", try std.fmt.allocPrint(db.allocator, "attachment; filename=\"{s}\"", .{att.filename}));
|
|
|
|
// TODO: Actually retrieve and send attachment content
|
|
// For now, just send metadata
|
|
try res.json(.{ .filename = att.filename, .content_type = att.content_type }, .{});
|
|
}
|
|
|
|
test "queryHandler with valid query" {
|
|
const allocator = std.testing.allocator;
|
|
var db = try root.openNotmuchDb(allocator, "mail", null);
|
|
defer db.close();
|
|
|
|
var t = httpz.testing.init(.{});
|
|
defer t.deinit();
|
|
|
|
t.url("/api/query/tag:inbox");
|
|
try queryHandler(&db, t.req, t.res);
|
|
try std.testing.expect(t.res.status != 400);
|
|
}
|
|
|
|
test "queryHandler with empty query" {
|
|
const allocator = std.testing.allocator;
|
|
var db = try root.openNotmuchDb(allocator, "mail", null);
|
|
defer db.close();
|
|
|
|
var t = httpz.testing.init(.{});
|
|
defer t.deinit();
|
|
|
|
t.url("/api/query/");
|
|
try queryHandler(&db, t.req, t.res);
|
|
try std.testing.expectEqual(@as(u16, 400), t.res.status);
|
|
}
|
|
|
|
test "messageHandler with valid message" {
|
|
const allocator = std.testing.allocator;
|
|
var db = try root.openNotmuchDb(allocator, "mail", null);
|
|
defer db.close();
|
|
|
|
var threads = try db.search("*");
|
|
defer threads.deinit();
|
|
|
|
var maybe_thread = (try threads.next()).?;
|
|
defer maybe_thread.deinit();
|
|
|
|
var mi = try maybe_thread.thread.getMessages();
|
|
const msg_id = mi.next().?.getMessageId();
|
|
|
|
var t = httpz.testing.init(.{});
|
|
defer t.deinit();
|
|
|
|
t.param("message_id", msg_id);
|
|
try messageHandler(&db, t.req, t.res);
|
|
try std.testing.expect(t.res.status != 404);
|
|
}
|
|
|
|
test "threadHandler with valid thread" {
|
|
const allocator = std.testing.allocator;
|
|
var db = try root.openNotmuchDb(allocator, "mail", null);
|
|
defer db.close();
|
|
|
|
var threads = try db.search("*");
|
|
defer threads.deinit();
|
|
|
|
var maybe_thread = (try threads.next()).?;
|
|
defer maybe_thread.deinit();
|
|
|
|
const thread_id = maybe_thread.thread.getThreadId();
|
|
|
|
var t = httpz.testing.init(.{});
|
|
defer t.deinit();
|
|
|
|
t.param("thread_id", thread_id);
|
|
try threadHandler(&db, t.req, t.res);
|
|
try std.testing.expect(t.res.status != 404);
|
|
}
|