Compare commits
3 commits
0b804a0d63
...
df741c69ae
Author | SHA1 | Date | |
---|---|---|---|
df741c69ae | |||
52affcd587 | |||
5e4ae198a1 |
9 changed files with 133 additions and 83 deletions
79
PLAN.md
79
PLAN.md
|
@ -1,79 +0,0 @@
|
|||
# Zetviel Development Plan
|
||||
|
||||
## Project Rules
|
||||
1. **Always run `zig fmt .` after any change to a zig file**
|
||||
2. **Before considering a task complete: `zig build` must have no errors/output**
|
||||
3. **Before considering a task complete: all tests must pass with `zig build test`**
|
||||
|
||||
## Goal
|
||||
Create a netviel clone with improvements:
|
||||
- Visual indication that server is working
|
||||
- URL changes with UI state for deep linking
|
||||
- Custom frontend (not copying netviel's JavaScript)
|
||||
|
||||
## Phase 1: Upgrade Zig ✅ COMPLETE
|
||||
- [x] Update `build.zig.zon` to Zig 0.15.2
|
||||
- [x] Update `.mise.toml` to use Zig 0.15.2
|
||||
- [x] Fix breaking changes in `build.zig` (Module API, alignment issues)
|
||||
- [x] Fix breaking changes in `src/main.zig` (stdout API)
|
||||
- [x] Fix JSON API changes in `src/root.zig` (converted OutOfMemory to WriteFailed)
|
||||
- [x] Verify all tests pass
|
||||
- [x] Run `zig fmt .`
|
||||
|
||||
## Phase 2: Complete Email Parsing API ✅ COMPLETE
|
||||
- [x] Finish `Email.zig` implementation:
|
||||
- [x] Extract HTML/plain text content with preference (html > plain)
|
||||
- [x] Parse and list attachments (filename, content-type)
|
||||
- [x] Extract all standard headers (from, to, cc, bcc, date, subject)
|
||||
- [x] Add attachment retrieval by index (getAttachments method)
|
||||
- [x] Integrate Email parsing into `root.zig` Thread API
|
||||
- [x] Add tests for new functionality (existing tests pass)
|
||||
- [x] Run `zig fmt .`
|
||||
|
||||
## Phase 3: HTTP Server & REST API ✅ COMPLETE
|
||||
- [x] Research and choose HTTP framework (httpz)
|
||||
- [x] Add HTTP server dependency
|
||||
- [x] Implement REST endpoints:
|
||||
- [x] `GET /api/query/<query_string>` - search threads
|
||||
- [x] `GET /api/thread/<thread_id>` - get thread messages
|
||||
- [x] `GET /api/attachment/<message_id>/<num>` - download attachment
|
||||
- [x] `GET /api/message/<message_id>` - get message details
|
||||
- [x] Complete JSON serialization (extend existing in root.zig)
|
||||
- [x] Add security headers via httpz middleware
|
||||
- [x] Add tests for API endpoints
|
||||
- [x] Run `zig fmt .`
|
||||
|
||||
## Phase 4: Static File Serving ✅ COMPLETE
|
||||
- [x] Implement static file serving:
|
||||
- [x] Serve `index.html` at `/`
|
||||
- [x] Serve static assets (placeholder 404 handler)
|
||||
- [x] Handle SPA routing (all non-API paths ready)
|
||||
- [x] Add `--port` CLI argument
|
||||
- [x] Run `zig fmt .`
|
||||
|
||||
## Phase 5: Frontend Development
|
||||
- [ ] Design minimal UI (list threads, view messages, search)
|
||||
- [ ] Implement frontend features:
|
||||
- [ ] Thread list view
|
||||
- [ ] Message detail view
|
||||
- [ ] Search functionality
|
||||
- [ ] Visual server status indicator
|
||||
- [ ] URL-based routing for deep linking
|
||||
- [ ] Attachment download links
|
||||
- [ ] Ensure API compatibility
|
||||
|
||||
## Phase 6: Polish
|
||||
- [ ] Add proper error handling throughout
|
||||
- [ ] Add logging
|
||||
- [x] Update README with usage instructions
|
||||
- [x] Add configuration options (NOTMUCH_PATH env var)
|
||||
- [x] Security audit and warnings (local-only usage)
|
||||
- [ ] Run `zig fmt .`
|
||||
|
||||
## Notes
|
||||
- Frontend will be custom-built, not copied from netviel
|
||||
- HTTP framework choice deferred to Phase 3
|
||||
- HTML sanitization will use simple allowlist approach (not porting bleach)
|
||||
|
||||
## Current Status
|
||||
Ready to begin Phase 1: Zig upgrade to 0.15.2
|
|
@ -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
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
77
mail/Inbox/cur/test_broken.msg
Normal file
77
mail/Inbox/cur/test_broken.msg
Normal file
|
@ -0,0 +1,77 @@
|
|||
Return-Path: <mail@youpharm.co>
|
||||
Delivered-To: lobo@lerch.org
|
||||
Received: from mail.eler.ch
|
||||
by mail.eler.ch with LMTP
|
||||
id BJ9kLnLDm2aYDgQAyA9pPg
|
||||
(envelope-from <mail@youpharm.co>)
|
||||
for <lobo@lerch.org>; Sat, 20 Jul 2024 14:02:26 +0000
|
||||
Received: from youpharm.co (youpharm.co [5.101.65.218])
|
||||
by mail.eler.ch (Postfix) with ESMTP id E3C9F467AB
|
||||
for <emil@lerch.org>; Sat, 20 Jul 2024 14:02:25 +0000 (UTC)
|
||||
DKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=s1; d=youpharm.co;
|
||||
h=Message-ID:From:To:Subject:Date:MIME-Version:Content-Type; i=abuse@youpharm.co;
|
||||
bh=Lnj6s7dL4V4gz92hab9GJ0HxEL8=;
|
||||
b=MLiLXLaBVkYaJuabi+DsOFUkjmqsYJ0hsfHW5JKX61Fal+1j2iFjFWrggCv+m0zruA+j6W+iJ7CV
|
||||
nxcDpT5mZe0+e2bOu1f8YEGNj7DPVpkYjeB8esR4qo/LSot0TIOU7YojSk8HP/hQVYEwpC58f21C
|
||||
sXgqEyMn1bV4+UHE1QnhoyZRP/lyadba4SCCSeyG5VQMZ4cIZtlcBFA+yd6I03lZ2f/Lh7tinFXj
|
||||
HrOyPjQLpk4VNbVsbbpsI+sKOEGlmgpRVIatV+Hcwk8ZuhFsubdF/cSc1p3jFbUdhBa3TMcqFVS2
|
||||
L1UO4e13PWJVjHzAUXlGDF66PGIR2pHnBd1pew==
|
||||
DomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=s1; d=youpharm.co;
|
||||
b=fFAj5RzuUx0++wASk/u6T0GlBurb2y6h/1WEls22gWKfoEKHzChyXYJNyknfho2r/3Cw0DNrWXFI
|
||||
nRIivnoNX4rOvc4hsCieljl9lt0fOaYzLgHKS083D8JIYLLySX0Qwj7xydC3nB3WmHhOlrz6eM7d
|
||||
lPIOT14K1e5LxQTLox8PaUqknSUNrsBZ8tREcVLqb7Ud9SVvdHjyccjampV70XPOeKMd9NLt4a/H
|
||||
sEeS184PGBo7/uAuHojS2y2LDkY6nRdZPmjPvA9ghNU8udr+biG3NEX8V2v2ZJy7w9H6FfJfCb2/
|
||||
MrxZmGPWgcYJ7cQ/pNMKcHM1QoAKYKMEG76V8g==;
|
||||
Message-ID: <8afeb74dca321817e44e07ac4a2e040962e86e@youpharm.co>
|
||||
From: Top Medications <mail@youpharm.co>
|
||||
To: emil@lerch.org
|
||||
Subject: ***SPAM*** Tablets without a prescription
|
||||
Date: Sat, 20 Jul 2024 16:02:18 +0200
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="08044db730c8e2ed1eb4fd56b0d4fef3d9e86e"
|
||||
X-Rspamd-Queue-Id: E3C9F467AB
|
||||
X-Rspamd-Server: mail.eler.ch
|
||||
X-Spamd-Result: default: False [10.00 / 11.00];
|
||||
ABUSE_SURBL(5.50)[youpharm.co:helo,youpharm.co:dkim,youpharm.co:rdns];
|
||||
RSPAMD_URIBL(4.50)[unmaskfauci.com:url];
|
||||
BAD_REP_POLICIES(0.10)[];
|
||||
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
|
||||
DKIM_TRACE(0.00)[youpharm.co:+];
|
||||
ARC_NA(0.00)[];
|
||||
DMARC_POLICY_ALLOW(0.00)[youpharm.co,none];
|
||||
RCVD_COUNT_ZERO(0.00)[0];
|
||||
MIME_TRACE(0.00)[0:+,1:+,2:~];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
TO_DN_NONE(0.00)[];
|
||||
R_SPF_ALLOW(0.00)[+a];
|
||||
R_DKIM_ALLOW(0.00)[youpharm.co:s=s1];
|
||||
ASN(0.00)[asn:34665, ipnet:5.101.65.0/24, country:RU];
|
||||
FROM_HAS_DN(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
DWL_DNSWL_BLOCKED(0.00)[youpharm.co:dkim];
|
||||
MID_RHS_MATCH_FROM(0.00)[]
|
||||
X-Rspamd-Action: rewrite subject
|
||||
Content-Length: 789
|
||||
|
||||
--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e
|
||||
Content-Type: text/plain; charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
|
||||
--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e
|
||||
Content-Type: text/html; charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8">
|
||||
</head>
|
||||
<body><a href=3D"https://unmaskfauci.com/assets/images/chw.php"><img src=3D=
|
||||
"https://imgpx.com/dfE6oYsvHoYw.png"></a> <div><img width=3D1 height=3D1 =
|
||||
alt=3D"" src=3D"https://vnevent.net/wp-content/plugins/wp-automatic/awe.p=
|
||||
hp?QFYiTaVCm0ogM30sC5RNRb%2FKLO0%2FqO3iN9A89RgPbrGjPGsdVierqrtB7w8mnIqJug=
|
||||
BVA5TZVG%2F6MFLMOrK9z4D6vgFBDRgH88%2FpEmohBbpaSFf4wx1l9S4LGJd87EK6"></div=
|
||||
></body></html>
|
||||
|
||||
--08044db730c8e2ed1eb4fd56b0d4fef3d9e86e--
|
12
notmuch-config
Normal file
12
notmuch-config
Normal file
|
@ -0,0 +1,12 @@
|
|||
[database]
|
||||
path=/home/lobo/shared/zetviel/mail
|
||||
[user]
|
||||
name=Test User
|
||||
primary_email=test@example.com
|
||||
[new]
|
||||
tags=unread;inbox;
|
||||
ignore=
|
||||
[search]
|
||||
exclude_tags=deleted;spam;
|
||||
[maildir]
|
||||
synchronize_flags=true
|
44
src/main.zig
44
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();
|
||||
|
||||
// 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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue