Commit Graph
13 Commits
Author SHA1 Message Date
Karti TripathiandClaude Opus 5 c24f71518a CI: allow CDLA-Permissive-2.0 for the CA root bundle; drop MPL-2.0
cargo-deny correctly rejected webpki-root-certs (CDLA-Permissive-2.0),
reached via hickory-resolver -> rustls-platform-verifier. CDLA-Permissive-2.0
is a data licence on the Mozilla CA root list, permissive, with no
reciprocal obligation on code that uses the data. Allowed deliberately, with
the reasoning in deny.toml rather than as a silent entry.

MPL-2.0 removed: nothing needs it, and pre-authorising unused licences makes
the policy something nobody reads carefully. If a dependency pulls it in,
CI fails and someone decides on purpose.

Diagnostic step removed — PATH in the workflow env is what fixed the runner;
the .path/.env files were not being applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
2026-09-02 13:57:59 -07:00
Karti Tripathi bb8f32ee61 CI: set PATH in the workflow; add a toolchain diagnostic step 2026-09-02 13:55:35 -07:00
Karti TripathiandClaude Opus 5 7fba0ab0c3 CI: run every job on spark-1; drop rust-cache
Two failures, two causes.

GitHub-hosted jobs never started: 'recent account payments have failed or
your spending limit needs to be increased'. Not a config problem. Public
repos get hosted runners free, so this may resolve itself; meanwhile every
job runs on spark-1, which is also the deployment architecture (Oracle
Ampere A1 is aarch64), so this is an improvement rather than a workaround.

The aarch64 job failed with exit 127 — cargo not found — and kept failing
after each fix. Cause: Swatinem/rust-cache's post-step prunes ~/.cargo.
That is correct on a GitHub-hosted runner where ~/.cargo is disposable, and
destructive on a self-hosted one where it is the real toolchain. It deleted
~/.cargo/bin/rustup after the first successful run, leaving every shim a
dangling symlink. The action is removed and the toolchain moved to
/opt/rust, wired through the runner's .env and .path, so it is out of reach
even if someone re-adds it. Caching bought nothing here anyway — the runner
keeps its target dir between runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
2026-09-02 13:53:50 -07:00
Karti TripathiandClaude Opus 5 037e87bc60 Wire CI: match runner label case, commit Cargo.lock
The aarch64 job targets the spark-1 self-hosted runner, whose labels are
registered as 'Linux' not 'linux'. Cargo.lock is committed because the
Dockerfile builds with --locked and a lockfile-free reproducible build is
not reproducible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
2026-09-02 13:41:01 -07:00
Karti TripathiandClaude Opus 5 f0ec4ad691 Add ROADMAP; settle ADR 0005 with a measured OCI port test
cloud-1 (phx) and cloud-2 (us-sanjose-1) are both already OCI aarch64
instances, so the open question in ADR 0005 was answerable directly rather
than by reading forum posts. Measured from cloud-2:

  BLOCKED  25    gmail-smtp-in.l.google.com, aspmx.l.google.com
  OPEN     587/465/2525  gmail, sendgrid, SES, OCI Email Delivery

The community reports claiming 587 and 2525 are also blocked are wrong.
Only 25 is blocked, exactly as Oracle documents, so relay works and the
launch host is viable. Inbound :25 reachability is still unverified — that
is a VCN security-list change, not a platform limit.

ROADMAP.md carries the goal, three success signals that are deliberately
not v0.1, a definition-of-ready checklist, and five standing rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
2026-09-02 13:38:01 -07:00
Karti TripathiandClaude Opus 5 a42b798a0e Scaffold tier 2 and 3, the schema, the website, and self-hosted-first
Store: the full Postgres schema as an embedded migration. UUIDv7 keys so
`ORDER BY id` is a free chronological index; raw MIME and attachments live
in object storage with only a key in the row; `pods` present from day one
because retrofitting tenancy costs more than an unused column. API keys are
stored as a SHA-256 hash — a database dump must not be a set of live
credentials.

API: the v0 route table, including `ingest`, which closes the
receive→thread→extract loop with zero mail infrastructure and is what makes
the agent layer testable in CI. Scopes are a closed enum rather than
strings, so "can send mail" and "can mint keys" are not one typo apart.
Internal errors are logged in full and reported as a bare string.

MCP: the tool catalogue, six tools. Adding a row here is the only way an
agent gains a capability — a new REST route is invisible until someone opts
it in. Three tests guard the rule that no tool can ever reach key
management; CI fails rather than production.

ADR 0006: enterprise self-hosted first. A hosted offering comes only after
we have run this ourselves long enough to have a deliverability record
worth selling. `pods` stays in the schema as the thing that keeps that path
open — do not remove it as dead code.

Also: multi-stage Dockerfile running as a non-root system user with no
shell in the runtime image, and the openmail.karti.ai static page.

17 tests, zero clippy warnings, fmt clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
2026-09-02 13:20:51 -07:00
Karti TripathiandClaude Opus 5 36b15ddcaf Rewrite as a Rust, Apache-2.0 workspace
Supersedes the Go + embed-Mox design. The Go tree is removed; its
architecture doc is preserved at docs/archive/ARCHITECTURE-go-embed-mox.md
because its competitive analysis and data model still hold.

Five decisions recorded as ADRs:

  0001  Rust, not Go — accepting ~5,500 lines of protocol code that Mox
        would have given us free, to get the first permissively licensed
        Rust mail server. Costs stated plainly.
  0002  Apache-2.0, not MIT or AGPL — patent grant, trademark, CLA-free
        contribution. Public on GitHub; Gitea stays as the private fallback.
  0003  Stalwart's primitive crates (Apache-2.0/MIT) yes; its AGPL server
        crates never. DANE and MTA-STS sit on the AGPL side of that line,
        which is why we write our own.
  0004  Milestones, reordered: embedded inbound is required at launch.
  0005  Oracle Cloud blocks outbound :25, so direct-to-MX is impossible on
        the launch host. Split delivery is mandatory, not an on-ramp.

Twelve crates in three tiers. Tier 1 (mail-dane, mail-mta-sts, mail-dsn)
is standalone and publishable — no `dane` or `mta-sts` crate exists on
crates.io at all today.

openmail-relay ships the provider table as data, with SES and Oracle from
the start. Oracle's and Resend's SPF includes are deliberately None: a
guessed include turns the DNS check green against a mechanism the provider
does not honour, and mail still fails SPF silently.

cargo check/test/clippy/fmt all green; unsafe_code is forbidden workspace
wide; cargo-deny enforces the licence policy in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkyvfNJGTshJNE9FtwPLk7
2026-09-02 13:08:05 -07:00
karti-aiandClaude Opus 4.8 428040d964 Fix 13 findings from adversarial milestone-1 review
Blockers in the ingest path (all funnel real mail in milestone 2):
- H1: coerce body bytes to valid UTF-8 (ToValidUTF8) — non-UTF-8/mid-rune cuts
  no longer abort the ingest tx and drop the message.
- H2: EnsurePart's recoverable parse error is non-fatal — proceed with the
  guaranteed-usable Part so messy real-world mail is stored, not rejected.
- H3: extractBodies descends into message/rfc822 (Part.Message via
  SetMessageReaderAt) — forwarded/bounce bodies no longer lost.
- H4: GetMessage/GetThread/GetThreadMessages scoped to inbox_id — no cross-inbox
  access; reply no longer a confused deputy.

Hardening:
- M1: /healthz no longer leaks DB error to unauthenticated callers.
- M2: all DB errors funnel through handleErr; malformed UUID -> 404, dup -> 409,
  internal errors no longer echo the driver string.
- M3: index messages(inbox_id, message_id_hdr) for thread resolution.
- M4: pods UNIQUE(name) + ON CONFLICT (name) — no duplicate default pods.
- L1: skip empty-User/Host addresses (no literal "@").
- L2: skip attachment-disposition parts when picking the body.
- L3: case-insensitive, trimmed 'Re:' detection.

Verified e2e vs Postgres 16: latin1 body stored valid UTF-8; rfc822-only body
extracted; cross-inbox 404; malformed UUID 404; dup 409; threading regression OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:22:35 -07:00
karti-aiandClaude Opus 4.8 319a2fa689 Milestone 1: core services + Mox ingest + wired v0 API
- internal/mail: MailBackend interface (Send/Start/Capabilities) + NullBackend.
- internal/core: inbox/message/thread services over pgx; default-pod bootstrap.
- IngestRaw: real mox/message parse → body extraction (text/html walk) →
  quote-stripping (extracted_text) → reference-based threading → store, all in
  one tx; emits message.received event; populates FTS tsvector.
- core implements mail.InboundSink (Deliver) so any backend feeds the same path.
- internal/api: v0 routes wired live (was 501) — create/list/get inbox, ingest,
  list/get messages, list/get thread(+messages), send/reply (503 via NullBackend).
- cmd/openmail serve: builds core + NullBackend, ensures default pod.

Verified e2e vs Postgres 16: inbox create; ingest original+reply → same thread
via In-Reply-To/References; extracted_text strips quoted history; thread detail
ordered; FTS matches 'invoice'; events written; send → honest 503. vet clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:54:25 -07:00
karti-aiandClaude Opus 4.8 abce753ba8 Architecture v3: pluggable MailBackend; competitive positioning; relay-first
- Confirm stack: Go, single binary, embed Mox + pluggable backend (chosen after
  landscape research).
- Add MailBackend interface: relay (v1 default) / imap_smtp (BYO mailbox) /
  embedded (go-smtp + mox smtpclient/dane/mtasts, flagship). Deliverability
  becomes opt-in; useful on day 1.
- Add competitive positioning (§0.1): closest competitor agenticmail is TS +
  Stalwart(AGPL) Docker sidecar; OpenMail differentiates as single binary,
  all-MIT, in-process. agentic-inbox is Cloudflare-locked.
- Reorder milestones backend-first: relay loop before embedded SMTP.
- State scope: self-hostable app, not a SaaS — no billing/Stripe.
- README: pluggable backends + positioning. Repo layout: internal/mail/*.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:42:32 -07:00
karti-aiandClaude Opus 4.8 551739baa3 Milestone 1 scaffold: binary skeleton, schema, deploy, Mox spike
- Confirmed core bet via spike/mimecheck: mox/message parses a messy multipart
  message standalone (envelope, MIME tree, attachment, body) with only an
  io.ReaderAt — no mox store/config. Option B is viable.
- cmd/openmail single binary: serve|migrate|smtpd|sender|version subcommands.
- internal/api: chi router, constant-time bearer auth, /healthz, stubbed v0
  AgentMail-shaped routes (501 until core services land).
- internal/store: pgxpool + embedded, idempotent, tracked SQL migrations.
- internal/store/migrations/0001_init.sql: full native schema (pods, inboxes,
  threads, messages, attachments, drafts, api_keys, webhooks, outbox, events,
  domains) with FTS + GIN indexes. Validated end-to-end against Postgres 16.
- deploy/: docker-compose (postgres+minio+openmail+caddy), Caddyfile, DNS.md
  (MX/SPF/DKIM/DMARC/DANE/MTA-STS). Makefile, .gitignore.
- Verified: go vet clean; build OK; health/auth smoke tests; migrate idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:16:51 -07:00
karti-aiandClaude Opus 4.8 aab2a8558e Architecture v2: embed Mox (MIT) as a library; native Postgres agent layer
- Choose Option B: single binary embedding Mox's stateless protocol/crypto/
  delivery packages (dkim, spf, dmarc, dane, mtasts, message, smtpclient, junk,
  dsn, anti-abuse) while owning server loop (go-smtp) + Postgres data model.
- Boundary rule: never import mox smtpserver/imapserver/queue/store/config
  (bbolt/global-config coupled) — that's the line between embed and fork.
- Add MIT LICENSE; reframe README as a real MIT OSS product.
- Deliverability moat leans on mox smtpclient+dane+mtasts; relay fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:07:37 -07:00
karti-aiandClaude Opus 4.8 9812e25973 Architecture: self-hosted AI-native mailbox for agents
Design of record for OpenMail — one Go binary (smtpd/core/sender/mcp) + Postgres
+ object storage on a VPS. Covers inbound SMTP/MIME/threading, outbound
deliverability (relay-default, self-host opt-in), data model, AgentMail-shaped
v0 API + MCP server, and an MVP milestone path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:47:01 -07:00