da9b803bdb5564fbc64ea99cf6ed86d0849a7c0d
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c5c756a775 | update migration flow | ||
|
|
c1441cedd5 |
feat(jmap): Email/import for MIME-composing clients, and the header query filter (#46)
* fix(jmap): answer the header filter in Email/query instead of ignoring it conditionToSql() had no `header` branch, so a condition it did not recognise was dropped and the query matched every message in the mailbox. A client that de-duplicates drafts by Message-ID before importing therefore either adopted an unrelated draft or refused to send because "multiple remote drafts" came back. Handle `header` per RFC 8621 §4.4.1 from the columns Mailflare already stores — Message-ID, In-Reply-To and References — and make every other header name an `unsupportedFilter` error, which is what RFC 8620 §5.5 requires of a filter the server cannot apply. Message-IDs compare without angle brackets, since inbound rows store them and outbound rows do not, and header names are matched case-insensitively. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(jmap): Email/import so MIME-composing clients can send Clients that build MIME locally — Flectar Mail and anything else on the jmap-client crate — send by uploading the raw message, importing it into Drafts and submitting it. Email/import answered `unknownMethod`, so reading worked but every send failed. Implement it for the Drafts mailbox (RFC 8621 §4.8). The blob must be an upload this account owns, the target exactly one Drafts mailbox, and the parsed From has to pass the same getAuthorizedSenderAddress() check a draft created through Email/set does; anything else comes back in notCreated as blobNotFound, invalidProperties, invalidEmail, forbidden or tooLarge. Importing into arbitrary mailboxes is deliberately left out: delivered mail belongs to the inbound pipeline, which owns threading, spam scoring and inbound state. The parsed headers fill the same columns Email/set create writes — Email/set create and Email/import now share one insert and one target rule — plus providerMessageId, which is what lets the client find its own draft again with a Message-ID filter. The uploaded bytes are kept verbatim under `drafts/<messageId>.eml`, so downloading the message's blob returns the client's own MIME instead of the rebuilt minimal message, and the jmap-uploads object is released once claimed. Email/copy and Email/parse stay unsupported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(jmap): cover Email/import validation and the header filter tests/jmap-email-import.test.mjs is a node:test file over the pure pieces — the mailbox rule, keyword and receivedAt parsing, and the SQL Email/query builds for header conditions, including the unsupportedFilter throw and the regression that a Message-ID filter narrows the mailbox instead of matching every draft. The modules are TypeScript behind the `@/*` alias, so they are bundled with the esbuild the repo already uses for the self-hosted server rather than loaded directly; nothing here needs a Workers binding. scripts/jmap-import-check.mjs is the end-to-end pass against `npm run dev`: upload a multipart message with an attachment, import it, read it back, download the blob byte-for-byte, query it by Message-ID, submit it, and check every rejection path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(jmap): document Email/import and the header query filter Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(jmap): clean up imported draft objects --------- Co-authored-by: egamer24 <25360766+egamer24@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com> |
||
|
|
f0dc5a16fc |
feat(self-host): run Mailflare as a Docker container on any host
Adds a second runtime next to Cloudflare Workers. One Node process serves the Next app, a realtime WebSocket hub, the job queues, the backup schedule and an SMTP listener; SQLite and local files replace D1 and R2. The application code is unchanged: the runtime builds an object with the same binding interfaces (`server/runtime/`) and publishes it before Next starts, so `getEnv()` hands route handlers the same shape on both platforms. Runtime (server/) - `sqlite-database.ts`: the D1 API over better-sqlite3, so Drizzle's D1 driver, the raw `env.DB.prepare` callers, the FTS5 index and its triggers all work as-is. Migrations are applied from drizzle/migrations at start, recorded in `d1_migrations` like Wrangler does. - `file-bucket.ts`: the R2 API over a directory, with metadata sidecars and byte ranges. `mailer.ts`: the send_email builder over nodemailer (any SMTP relay) or Cloudflare's Email Sending REST API; generates the Message-ID so threading matches Workers. `queue.ts`: in-process queues with the same retry policy. `realtime.ts`: per-user WebSocket fan-out in place of the Durable Object. `misc.ts`: rate limiter and ASSETS fetcher. - `smtp.ts`: inbound SMTP on port 25 via smtp-server; reject rules answer 550, forward rules relay the raw message through the outbound SMTP. - `index.ts`: the custom server; `scripts/build-server.mjs` bundles it with esbuild to dist/server.mjs. Application changes - `src/lib/runtime.ts` + `getEnv()` prefer the Node env when present. - `src/lib/email/intake.ts` factors the Worker email handler's logic so the SMTP listener and the new signed `/api/inbound` webhook share it. - Domains without Cloudflare credentials are recorded with zone id "manual": Cloudflare calls no-op, the domain is active at once, and the DNS page lists MX/SPF/DMARC records to create. Setup checks, the domains page copy and the self-update button adapt to the runtime; `/api/auth/me` exposes `runtime` and `managesDns`. - `APP_URL` overrides the origin for reset links and the JMAP session behind a proxy. The direct `getCloudflareContext` import in the update route is replaced with `getEnv()`. Deployment - Dockerfile (multi-stage, 1.1 GB, Workers toolchain pruned), compose file, `.env.docker.example`, `docs/self-hosting.md`, README section. - `deploy/cloudflare-email-relay`: a Worker that keeps MX on Cloudflare Email Routing and posts messages to the server with an HMAC signature, acting on the reject/forward decision it returns. Verified on a fresh data directory and again inside the built image: migrations apply, first-run setup and registration work with no Cloudflare credentials, mail over SMTP lands in the inbox with the raw copy on disk, a reject rule answers 550, a send through SMTP_URL loops back and is stored as sent and received, an inbound reply threads with the sent copy, FTS search finds body text, a backup completes to the volume, the WebSocket delivers a push, the relay webhook rejects a bad signature and returns store/reject decisions, JMAP serves a session with the APP_URL origin. Container healthcheck reports healthy. tsc (11) and eslint (105) unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGeWDVSkXtVEmDAN2AtB9h |