feat(auth): trusted senders table and API
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cc0adcd94d
commit
b536ed13e7
16 files changed
+20820
No files matched your search
@@ -0,0 +1,26 @@
|
|||||||
|
# OpenWolf: machine-local state (committed state: cerebrum.md, STATUS.md,
|
||||||
|
# memory.md, buglog.json, anatomy.md, anatomy-index.json, config.json, OPENWOLF.md)
|
||||||
|
hooks/
|
||||||
|
backups/
|
||||||
|
cache/
|
||||||
|
daemon.log
|
||||||
|
daemon.pid
|
||||||
|
dashboard-token
|
||||||
|
token-ledger.json
|
||||||
|
usage-report.json
|
||||||
|
updates/
|
||||||
|
handoff/
|
||||||
|
activity/
|
||||||
|
usage/
|
||||||
|
archive/
|
||||||
|
session-memory/
|
||||||
|
ledger-pending/
|
||||||
|
ledger-sessions/
|
||||||
|
bug-pending/
|
||||||
|
*.pending/
|
||||||
|
suggestions.json
|
||||||
|
cron-state.json
|
||||||
|
_scan-state.json
|
||||||
|
_*.json
|
||||||
|
*.lock
|
||||||
|
*.tmp
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# OpenWolf Operating Protocol
|
||||||
|
|
||||||
|
You are working in an OpenWolf-managed project. These rules apply every turn.
|
||||||
|
|
||||||
|
OpenWolf's hooks handle the bookkeeping: they maintain `.wolf/anatomy.md` and `.wolf/memory.md` after writes, track reads, and surface anatomy hints when you read files. Do not update those two files manually unless your agent has no OpenWolf hooks installed (Gemini CLI, Cursor).
|
||||||
|
|
||||||
|
## STATUS.md: read first, keep fresh
|
||||||
|
|
||||||
|
`.wolf/STATUS.md` is the handoff document. Read it FIRST when resuming a session; it replaces re-reading memory, plans, and code to reconstruct context.
|
||||||
|
|
||||||
|
Keep it fresh: when the user signals a quest is done ("done", "ship it", "next phase", "/clear", "wrap up"), move finished items to the done section, write the next quest (objective, files, decisions), and bump the date. Do this before responding "done" on any multi-file task and before suggesting `/clear`. A stale STATUS.md wastes the next session.
|
||||||
|
|
||||||
|
## File navigation
|
||||||
|
|
||||||
|
1. Before reading an unfamiliar file, grep `.wolf/anatomy.md` for its path to get a one-line description and token estimate. Do NOT read anatomy.md whole; it is an index, not a document.
|
||||||
|
2. If the description answers your question, skip the full read. For large files, prefer Read with offset/limit over whole-file reads.
|
||||||
|
3. If a file is not in anatomy.md, search with Grep/Glob. Regenerate the index with `openwolf scan`.
|
||||||
|
|
||||||
|
## Code generation
|
||||||
|
|
||||||
|
1. Before generating code, check `.wolf/cerebrum.md`: respect `## Do-Not-Repeat` (past mistakes), `## Key Learnings`, and `## User Preferences`.
|
||||||
|
2. Update cerebrum.md whenever you learn something: a user correction or preference, a project convention not obvious from code, an API surprise, a gotcha that would trip a fresh session, a significant decision and its why. The bar is LOW; a redundant entry costs nothing, a missing one repeats the discovery next session.
|
||||||
|
|
||||||
|
## Bug logging
|
||||||
|
|
||||||
|
Before fixing any bug: grep `.wolf/buglog.json` for the error message or filename; the fix may already be known.
|
||||||
|
|
||||||
|
After fixing any bug, failed test, failed build, or user-reported problem: append an entry with `id`, `timestamp`, `error_message`, `file`, `root_cause`, `fix`, `tags`, `occurrences`, `last_seen`. Also log when you edit a file more than twice to get it right. The threshold is LOW.
|
||||||
|
|
||||||
|
## Token discipline
|
||||||
|
|
||||||
|
- Never re-read a file already read this session unless it changed since.
|
||||||
|
- Prefer anatomy descriptions and targeted Grep over full file reads.
|
||||||
|
- If appending to a file, do not read the entire file first.
|
||||||
|
|
||||||
|
## Session end
|
||||||
|
|
||||||
|
Before wrapping up: update `.wolf/STATUS.md`, write a one-line session summary to `.wolf/memory.md` (`| HH:MM | description | file(s) | outcome | ~tokens |`), and record any learnings or bugs from the session in cerebrum.md / buglog.json.
|
||||||
|
|
||||||
|
## On-demand skills
|
||||||
|
|
||||||
|
- `/designqc`: screenshot-based design review of the running app (uses `openwolf designqc`).
|
||||||
|
- `/reframe`: UI framework selection, migration, and anti-generic design audits.
|
||||||
|
- `/security-audit`: security review of the project.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
description: session handoff, regenerate with /handoff when a quest finishes
|
||||||
|
budget_tokens: 1000
|
||||||
|
---
|
||||||
|
# STATUS — LambdaMail
|
||||||
|
|
||||||
|
> Single source of truth for resuming work. Read this FIRST when starting a session.
|
||||||
|
> Update this file at the end of every work phase so the next `/clear` resumes in 1 read.
|
||||||
|
> Last updated: 2026-10-06
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Done
|
||||||
|
|
||||||
|
<!-- Move items here from "🚀 Next phase" when finished. Group by area. -->
|
||||||
|
|
||||||
|
- (nothing yet — fill in as work completes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next phase
|
||||||
|
|
||||||
|
**Goal:** _<what we're building next, in 1 sentence>_
|
||||||
|
|
||||||
|
### Acceptance criteria
|
||||||
|
1. _<concrete user-visible outcome>_
|
||||||
|
2. _<...>_
|
||||||
|
|
||||||
|
### Files to create / edit
|
||||||
|
| Type | File | Content |
|
||||||
|
|---|---|---|
|
||||||
|
| new | `path/to/file.ts` | _what it does_ |
|
||||||
|
|
||||||
|
### Closed decisions
|
||||||
|
- _<choice + reasoning>_
|
||||||
|
|
||||||
|
### Open decisions
|
||||||
|
- _<question to ask the user before coding>_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Active architecture
|
||||||
|
|
||||||
|
- **Stack:** _<frameworks, libraries, runtime>_
|
||||||
|
- **Key tables / modules:** _<list>_
|
||||||
|
- **Patterns:** _<conventions enforced project-wide>_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ External blockers (don't block coding)
|
||||||
|
|
||||||
|
- _<env vars, secrets, external accounts, manual steps>_
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Useful commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# add the most-used commands here so the next session has them ready
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 References (read IF needed)
|
||||||
|
|
||||||
|
- `.wolf/cerebrum.md` — User Preferences + Do-Not-Repeat + Decision Log
|
||||||
|
- `.wolf/anatomy.md` — token-efficient file index
|
||||||
|
- `.wolf/buglog.json` — known bugs + fixes
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,648 @@
|
|||||||
|
# anatomy.md
|
||||||
|
|
||||||
|
> Auto-maintained by OpenWolf. Last scanned: 2026-10-22T17:41:17.634Z
|
||||||
|
> Files: 439 tracked | Anatomy hits: 0 | Misses: 0
|
||||||
|
|
||||||
|
> Project structure index. Auto-maintained by OpenWolf hooks and daemon.
|
||||||
|
> Run `openwolf scan` to generate, or wait for the first Claude Code session.
|
||||||
|
> Status: Pending initial scan
|
||||||
|
|
||||||
|
## ./
|
||||||
|
|
||||||
|
- `.gitignore` — Git ignore rules (~277 tok)
|
||||||
|
- `.trivyignore` — Findings that have been assessed and deliberately accepted. (~211 tok)
|
||||||
|
- `CLAUDE.md` — OpenWolf (~99 tok)
|
||||||
|
- `docker-compose.build.yaml` — Development overlay: builds the images from source instead of pulling them. (~260 tok)
|
||||||
|
- `docker-compose.yaml` — Docker Compose services (~5222 tok)
|
||||||
|
- `LICENSE` — Project license (~287 tok)
|
||||||
|
- `Makefile` — Make build targets (~1327 tok)
|
||||||
|
- `README.md` — Project documentation (~3456 tok)
|
||||||
|
|
||||||
|
## .github/workflows/
|
||||||
|
|
||||||
|
- `ci.yml` — CI: CI (~2438 tok)
|
||||||
|
- `release.yml` — CI: Release (~1863 tok)
|
||||||
|
|
||||||
|
## config/rspamd/
|
||||||
|
|
||||||
|
- `actions.conf` — Score thresholds from PLAN.md section 10.1. The principle behind them is (~108 tok)
|
||||||
|
- `logging.inc` (~10 tok)
|
||||||
|
- `options.inc` — DNS resolution is needed for RBL, SPF and DKIM lookups. (~33 tok)
|
||||||
|
- `ratelimit.conf` — Rate limiting of PLAN.md section 5.2. (~408 tok)
|
||||||
|
- `worker-normal.inc` — The protocols service reaches Rspamd over the internal network only; the (~40 tok)
|
||||||
|
|
||||||
|
## docs/
|
||||||
|
|
||||||
|
- `DEPLOYMENT.md` — Deploying LambdaMail on Coolify (~2605 tok)
|
||||||
|
- `OPERATIONS.md` — Operating LambdaMail (~2229 tok)
|
||||||
|
|
||||||
|
## migrations/
|
||||||
|
|
||||||
|
- `0001_init_schema.down.sql` — 0001_init_schema.down.sql (~225 tok)
|
||||||
|
- `0001_init_schema.up.sql` — 0001_init_schema.up.sql (~5730 tok)
|
||||||
|
- `0002_add_is_system_to_aliases.down.sql` — Revert is_system column from aliases table (~29 tok)
|
||||||
|
- `0002_add_is_system_to_aliases.up.sql` — Add is_system column to aliases table to track mandatory system security aliases (~45 tok)
|
||||||
|
- `0003_create_report_tables.down.sql` (~47 tok)
|
||||||
|
- `0003_create_report_tables.up.sql` — 0003_create_report_tables.up.sql (~575 tok)
|
||||||
|
- `0004_index_outbound_stale_locks.down.sql` (~14 tok)
|
||||||
|
- `0004_index_outbound_stale_locks.up.sql` — Delivery workers claim a job by moving it to SENDING with a lock deadline. (~105 tok)
|
||||||
|
- `0005_create_acme_tables.down.sql` (~34 tok)
|
||||||
|
- `0005_create_acme_tables.up.sql` — Self-managed ACME state (PLAN.md section 8.3, mode B). (~752 tok)
|
||||||
|
- `0006_add_user_settings_and_system_config.down.sql` — SQL: 2 alter(s) (~44 tok)
|
||||||
|
- `0006_add_user_settings_and_system_config.up.sql` — SQL: tables: system_config, 2 alter(s) (~135 tok)
|
||||||
|
- `0007_reconcile_folder_counters.down.sql` — Irreversible by nature: this migration replaced incorrect counter values and (~90 tok)
|
||||||
|
- `0007_reconcile_folder_counters.up.sql` — Reconcile folders.unread_count and folders.total_count with the messages (~819 tok)
|
||||||
|
- `0008_add_reports_folder.down.sql` — Only removes the folder where it is still empty. A Reports folder holding (~94 tok)
|
||||||
|
- `0008_add_reports_folder.up.sql` — Give every existing mailbox a Reports folder. (~185 tok)
|
||||||
|
- `0009_report_ingestion_idempotency.down.sql` (~30 tok)
|
||||||
|
- `0009_report_ingestion_idempotency.up.sql` — Make storing a report idempotent, keyed on the reporter and their own id (~425 tok)
|
||||||
|
- `0010_vacation_reply_log.down.sql` (~12 tok)
|
||||||
|
- `0010_vacation_reply_log.up.sql` — Who has already been told the mailbox is unattended. (~257 tok)
|
||||||
|
- `0011_backfill_markers.down.sql` (~10 tok)
|
||||||
|
- `0011_backfill_markers.up.sql` — Records one-shot data repairs so they run once rather than on every start. (~176 tok)
|
||||||
|
- `0012_mailbox_avatars.down.sql` (~11 tok)
|
||||||
|
- `0012_mailbox_avatars.up.sql` — The profile photo a mailbox shows inside LambdaMail. (~256 tok)
|
||||||
|
- `0013_domain_bimi.down.sql` (~10 tok)
|
||||||
|
- `0013_domain_bimi.up.sql` — The BIMI logo a domain publishes, and the certificate that vouches for it. (~188 tok)
|
||||||
|
|
||||||
|
## scripts/
|
||||||
|
|
||||||
|
- `dev-seed.sql` — Dev-only fixture data for local SMTP testing. Not a schema migration - (~533 tok)
|
||||||
|
- `lang-lint.mjs` — Fails the build if tracked source contains non-ASCII characters or common (~483 tok)
|
||||||
|
|
||||||
|
## services/auth/
|
||||||
|
|
||||||
|
- `Dockerfile` — Docker container definition (~408 tok)
|
||||||
|
- `package.json` — Node.js package manifest (~138 tok)
|
||||||
|
- `tsconfig.build.json` — TypeScript build configuration (~83 tok)
|
||||||
|
- `tsconfig.json` — TypeScript configuration (~88 tok)
|
||||||
|
- `vitest.config.ts` — Vitest test configuration (~153 tok)
|
||||||
|
|
||||||
|
## services/auth/src/
|
||||||
|
|
||||||
|
- `accounts.ts` — Several webmail accounts signed in at once, and a session that stays alive (~715 tok)
|
||||||
|
- `auth.test.ts` — Declares password (~3292 tok)
|
||||||
|
- `cli.ts` — Operator commands for a fresh deployment. (~2135 tok)
|
||||||
|
- `crypto.ts` — Derives the key-encryption key. Unlike the previous implementation there is (~1465 tok)
|
||||||
|
- `db.ts` — Lazily opens the shared connection pool. It is lazy so that unit tests which (~266 tok)
|
||||||
|
- `diagnostics.test.ts` — Declares UserFacts (~1317 tok)
|
||||||
|
- `diagnostics.ts` — Checks an operator can run by hand, against the whole server or one user. (~1557 tok)
|
||||||
|
- `health.test.ts` — Declares withServer (~470 tok)
|
||||||
|
- `health.ts` — Builds the /health/live and /health/ready handler shared by the auth and (~215 tok)
|
||||||
|
- `imageValidation.test.ts` — Declares png (~741 tok)
|
||||||
|
- `imageValidation.ts` — What an uploaded avatar is allowed to be. (~598 tok)
|
||||||
|
- `mfaHelpers.ts` — Generates single-use recovery codes. Only the hashes are ever stored, so a (~494 tok)
|
||||||
|
- `pagination.test.ts` — Declares at (~661 tok)
|
||||||
|
- `pagination.ts` — Page and filter parameters, read from a URL and made safe for SQL. (~510 tok)
|
||||||
|
- `repository.ts` — Decides whether this account must present a second factor now. (~14732 tok)
|
||||||
|
- `router.test.ts` — Enrolls a real second factor and returns the shared secret. (~16096 tok)
|
||||||
|
- `router.ts` — Cookie scoped to the whole origin: the API lives under /api, so a cookie (~15221 tok)
|
||||||
|
- `server.ts` — Declares port (~236 tok)
|
||||||
|
- `session.ts` — The signing secret. There is deliberately no default. (~1406 tok)
|
||||||
|
- `surfaceAccess.test.ts` — Which surface a login opens, and what it costs to cross between them. (~1640 tok)
|
||||||
|
- `surfaceAccess.ts` — Which surface a login opens, and what crossing between them costs. (~712 tok)
|
||||||
|
- `totp.ts` — Renders the otpauth:// URI an authenticator app scans. (~1037 tok)
|
||||||
|
|
||||||
|
## services/migrations/
|
||||||
|
|
||||||
|
- `Dockerfile` — Docker container definition (~241 tok)
|
||||||
|
|
||||||
|
## services/protocols/
|
||||||
|
|
||||||
|
- `.go-arch-lint.yml` — health: (~99 tok)
|
||||||
|
- `Dockerfile` — Docker container definition (~312 tok)
|
||||||
|
- `go.mod` — Go module definition (~326 tok)
|
||||||
|
- `go.sum` — Go dependency checksums (~2706 tok)
|
||||||
|
|
||||||
|
## services/protocols/cmd/lambdamail-protocols/
|
||||||
|
|
||||||
|
- `acme_mode.go` — tlsaPublisher (26 fields); methods: PublishTlsa (~1204 tok)
|
||||||
|
- `backfill_attachments.go` — Declares attachmentBackfillName (~717 tok)
|
||||||
|
- `backfill_reports.go` (~501 tok)
|
||||||
|
- `bimi_store.go` — bimiStore (4 fields); methods: Save, Load, Delete (~256 tok)
|
||||||
|
- `config.go` — config (52 fields) (~1251 tok)
|
||||||
|
- `disk_other.go` — go:build !unix (~98 tok)
|
||||||
|
- `disk_unix.go` — go:build unix (~183 tok)
|
||||||
|
- `dns_reconciler.go` — domainReconciler (14 fields); methods: ReconcileDomain (~961 tok)
|
||||||
|
- `main.go` — Command lambdamail-protocols is the entry point for the protocols service. (~8598 tok)
|
||||||
|
- `preflight.go` (~994 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/application/port/
|
||||||
|
|
||||||
|
- `arc_sealer.go` — ArcSealer adds an Authenticated Received Chain set to a message (~222 tok)
|
||||||
|
- `auth_repository.go` — Interface: AuthRepository (2 methods) (~205 tok)
|
||||||
|
- `blob_reader.go` — Interface: BlobReader (2 methods) (~65 tok)
|
||||||
|
- `blob_storage.go` — Interface: BlobStorage (2 methods) (~152 tok)
|
||||||
|
- `cert_provider.go` — CertProvider supplies the TLS certificate for STARTTLS. Its signature (~104 tok)
|
||||||
|
- `certificate_store.go` — Interface: CertificateStore (8 methods) (~524 tok)
|
||||||
|
- `content_scanner.go` — Interface: ContentScanner (2 methods) (~93 tok)
|
||||||
|
- `copy_repository.go` — CopiedMessage records one message's UID before and after a COPY/MOVE. (~244 tok)
|
||||||
|
- `dkim_repository.go` — DkimSigningKey is one active signing key, already decrypted and ready to (~304 tok)
|
||||||
|
- `dns_provider.go` — Interface: DnsProvider (6 methods) (~168 tok)
|
||||||
|
- `dns_verifier.go` — Interface: DnsVerifier (2 methods) (~190 tok)
|
||||||
|
- `expunge_repository.go` — ExpungeRepository permanently marks messages as expunged (soft delete - (~208 tok)
|
||||||
|
- `flag_repository.go` — FlagOp mirrors imap.StoreFlagsOp without importing the imap package here (~222 tok)
|
||||||
|
- `imap_folder_repository.go` — ImapFolderRecord is the IMAP-relevant subset of a folder row. (~185 tok)
|
||||||
|
- `inbound_message_repository.go` — Interface: InboundMessageRepository (2 methods) (~626 tok)
|
||||||
|
- `mail_authenticator.go` — Interface: MailAuthenticator (2 methods) (~424 tok)
|
||||||
|
- `mailbox_repository.go` — declares the interfaces the application layer depends on. (~386 tok)
|
||||||
|
- `message_query_repository.go` — Interface: MessageQueryRepository (2 methods) (~211 tok)
|
||||||
|
- `mx_resolver.go` — Interface: MXResolver (2 methods) (~38 tok)
|
||||||
|
- `outbound_repository.go` — Interface: OutboundRepository (5 methods) (~200 tok)
|
||||||
|
- `outbox.go` — Interface: OutboxRepository (4 methods) (~237 tok)
|
||||||
|
- `report_repository.go` — Interface: ReportRepository (8 methods) (~617 tok)
|
||||||
|
- `sieve_repository.go` — SieveScriptRecord represents a persistent Sieve script row in PostgreSQL. (~238 tok)
|
||||||
|
- `system_alias_repository.go` — SystemAliasRepository manages mandatory system security aliases (postmaster, abuse, dmarc, tlsrpt). (~75 tok)
|
||||||
|
- `tls_policy_resolver.go` — Interface: TLSPolicyResolver (2 methods) (~203 tok)
|
||||||
|
- `webmail_repository.go` — Interface: WebmailRepository (13 methods) (~1306 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/application/usecase/
|
||||||
|
|
||||||
|
- `apply_rules_test.go` — stubSieveScripts (35 fields); methods: ActiveScriptFor (~2504 tok)
|
||||||
|
- `apply_rules.go` — ApplyRulesUseCase (20 fields); methods: For (~990 tok)
|
||||||
|
- `backfill_reports_test.go` — stubDeliveredReportStore (35 fields); methods: ListUningestedReports, MoveToReportsFolder, ReadByID (~1565 tok)
|
||||||
|
- `backfill_reports.go` — BackfillReportsUseCase (16 fields); methods: Run (~846 tok)
|
||||||
|
- `imap_session_test.go` — fakeAuthRepository (40 fields); methods: FindByAddress, ListFolders, FindByName, Expunge (~1828 tok)
|
||||||
|
- `imap_session.go` — ImapSessionUseCase (25 fields); methods: SetTrackerManager, GetTrackerManager, Login, ListFolders (~1085 tok)
|
||||||
|
- `inbound_auth_test.go` — stubAuthenticator (19 fields); methods: Authenticate (~1438 tok)
|
||||||
|
- `inbound_scan_headers_test.go` — capturingBlobStorage (9 fields); methods: Store, Scan (~882 tok)
|
||||||
|
- `inbound_system_message_test.go` — junkingScanner (7 fields); methods: Scan (~1144 tok)
|
||||||
|
- `ingest_delivered_reports_test.go` — recordingReportRepo (48 fields); methods: SaveDmarcReport, SaveTlsRptReport (~3334 tok)
|
||||||
|
- `ingest_delivered_reports.go` — IngestDeliveredReportsUseCase (34 fields); methods: Ingest (~1905 tok)
|
||||||
|
- `ingest_reports_test.go` — fakeReportRepository (8 fields); methods: SaveDmarcReport, SaveTlsRptReport (~612 tok)
|
||||||
|
- `ingest_reports.go` — IngestReportsUseCase (15 fields); methods: IngestDmarc, IngestTlsRpt (~347 tok)
|
||||||
|
- `mailbox_tracker_manager.go` — MailboxTrackerManager (9 fields); methods: GetTracker, NotifyNumMessages, NotifyMessageFlags, NotifyExpunge (~585 tok)
|
||||||
|
- `manage_folders_test.go` — stubFolderStore (47 fields); methods: ListFolders, CreateFolder, RenameFolder, DeleteFolder (~2128 tok)
|
||||||
|
- `manage_folders.go` — ManageFoldersUseCase (52 fields); methods: Create, Rename, Delete (~1304 tok)
|
||||||
|
- `managesieve_session_test.go` — fakeSieveRepository (27 fields); methods: GetScript, PutScript, ListScripts, SetActiveScript (~1114 tok)
|
||||||
|
- `managesieve_session.go` — ManageSieveSessionUseCase (45 fields); methods: Login, PutScript, GetScript, ListScripts (~1011 tok)
|
||||||
|
- `message_headers_test.go` — TestExtractMessageHeaders_ReadsSubjectSenderAndSnippet, TestExtractMessageHeaders_DecodesEncodedWords, TestExtractMessageHeaders_FlagsMultipartMixe... (~1604 tok)
|
||||||
|
- `message_headers.go` — MessageHeaders (33 fields) (~1388 tok)
|
||||||
|
- `message_render_test.go` — TestRenderMessage_ExtractsHeadersAndText, TestRenderMessage_LeavesDateEmptyWhenHeaderAbsent, TestRenderMessage_PrefersPlainTextAndListsAttachments,... (~1976 tok)
|
||||||
|
- `message_render.go` — RenderedMessage (43 fields) (~2160 tok)
|
||||||
|
- `outbound_dsn_test.go` — TestOutboundWorker_PermanentSmtpFailureBouncesImmediately, TestOutboundWorker_TemporarySmtpFailureIsDeferred, TestOutboundWorker_DsnToFanOutSenderD... (~1480 tok)
|
||||||
|
- `outbound_local_delivery_test.go` — countingMXResolver (9 fields); methods: LookupMX (~1114 tok)
|
||||||
|
- `outbound_tls_policy_test.go` — stubPolicyResolver (29 fields); methods: Resolve (~1582 tok)
|
||||||
|
- `outbound_worker_test.go` — fakeOutboundRepo (18 fields); methods: Enqueue, FetchNextReady, CountRecipientsSince, UpdateJob (~744 tok)
|
||||||
|
- `outbound_worker.go` — OutboundWorkerUseCase (72 fields); methods: SetRelay, SetPolicyResolver, ProcessBatch (~4121 tok)
|
||||||
|
- `outbox_relay.go` — OutboxRelay (16 fields); methods: Run, PublishBatch (~551 tok)
|
||||||
|
- `pop3_session_test.go` — fakeMessageQueryRepository (12 fields); methods: ListMessages (~706 tok)
|
||||||
|
- `pop3_session.go` — Pop3SessionUseCase (31 fields); methods: Login, GetInbox, ReadBlob, ExpungeMessages (~690 tok)
|
||||||
|
- `preflight_test.go` — TestPreflight_HealthyDeploymentDoesNotBlock, TestPreflight_BlockedPort25IsCriticalAndSuggestsSmarthost, TestPreflight_SmarthostSkipsPort25Check, Te... (~2796 tok)
|
||||||
|
- `preflight.go` — CheckResult (112 fields); methods: Blocking, String, Execute (~3892 tok)
|
||||||
|
- `process_inbound_email_test.go` — fakeMailboxRepository (36 fields); methods: FindActiveByAddress, ResolveDeliveryTargets, FindActiveByAddress, ResolveDeliveryTargets (~2332 tok)
|
||||||
|
- `process_inbound_email.go` — contains the application's use cases (Clean Architecture layer 2). (~4084 tok)
|
||||||
|
- `process_outbound_email_test.go` — fakeAuthRepo (31 fields); methods: FindByAddress, CountRecipientsSince, Sign (~1768 tok)
|
||||||
|
- `process_outbound_email.go` — ProcessOutboundEmailInput (50 fields); methods: Authenticate, Submit (~1739 tok)
|
||||||
|
- `provision_dkim_keys_test.go` — fakeDkimStore (26 fields); methods: FindPublicKey, Insert, FindPublicKey, Insert (~1089 tok)
|
||||||
|
- `provision_dkim_keys.go` — Interface: DkimKeyProvisioner (5 methods) (~1121 tok)
|
||||||
|
- `relay_test.go` — fakeRelay (59 fields) (~3001 tok)
|
||||||
|
- `relay.go` — RelayConfig (42 fields); methods: Configured, Address, SpfInclude (~1588 tok)
|
||||||
|
- `rollover_tlsa_test.go` — TestRollover_StableWhenRenewalIsFarAway, TestRollover_StartsWhenRenewalApproaches, TestRollover_PublishesBothAssociationsWhileInFlight, TestRollove... (~2109 tok)
|
||||||
|
- `rollover_tlsa.go` — RolloverConfig (50 fields); methods: WithClock, Decide, ShouldRenewNow (~2470 tok)
|
||||||
|
- `scanning_pipeline_test.go` — mockScanner (9 fields); methods: Scan (~405 tok)
|
||||||
|
- `scanning_pipeline.go` — ScanningPipeline (13 fields); methods: Scan (~374 tok)
|
||||||
|
- `smtp_rejection.go` — SmtpRejection is a use case's decision to refuse a message, carrying the (~650 tok)
|
||||||
|
- `sync_dns_conflict_test.go` — stubDnsVerifier (32 fields); methods: VerifyRecord (~1548 tok)
|
||||||
|
- `sync_dns_records_test.go` — fakeDnsProvider (26 fields); methods: GetZoneID, ListRecords, CreateRecord, UpdateRecord (~1060 tok)
|
||||||
|
- `sync_dns_records.go` — SyncDnsRecordsInput (75 fields); methods: String, SetVerifier, Execute (~2481 tok)
|
||||||
|
- `vacation_reply_test.go` — TestReplyNamesTheMessageItAnswers, TestReplyCarriesTheConversationChain, TestReplyStartsTheChainWhenThereIsNone, TestReplyOmitsThreadingWhenThereIs... (~3030 tok)
|
||||||
|
- `vacation_reply.go` — pendingVacation (42 fields); methods: SetVacationSender, SetVacationSuppression (~2544 tok)
|
||||||
|
- `vacation_suppression_test.go` — stubVacationLog (18 fields); methods: LastRepliedAt, RecordReply (~1409 tok)
|
||||||
|
- `vacation_suppression.go` — VacationSuppression (19 fields); methods: ShouldReply, Record (~778 tok)
|
||||||
|
- `webmail_compose_test.go` — stubWebmailRepo (47 fields); methods: FindMailboxIDByAddress, ListFolders, ListMessages, GetMessageBlob (~4946 tok)
|
||||||
|
- `webmail_mail.go` — WebmailUseCase (81 fields); methods: WithLocalFiling, MailboxIDFor, ListFolders, ListMessages (~5136 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/domain/bimi/
|
||||||
|
|
||||||
|
- `svg_test.go` — TestValidate_AcceptsAConformingLogo, TestValidate_RequiresTheProfileReceiversLookFor, TestValidate_RequiresASquareViewBox, TestValidate_RequiresExa... (~1630 tok)
|
||||||
|
- `svg.go` — validates the logo a domain publishes for BIMI. (~1575 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/domain/entity/
|
||||||
|
|
||||||
|
- `dkim_key_pair_test.go` — TestNewDkimKeyPair_StartsPending, TestDkimKeyPair_Activate_SetsActive (~139 tok)
|
||||||
|
- `dkim_key_pair.go` — DkimStatus is the lifecycle state of a DkimKeyPair (PLAN.md section 9, dkim_keys.status). (~317 tok)
|
||||||
|
- `dmarc_report_test.go` — TestParseDmarcXmlReport_ValidXML, TestParseTlsRptReport_ValidJSON (~659 tok)
|
||||||
|
- `dmarc_report.go` — DmarcRecord (59 fields) (~1261 tok)
|
||||||
|
- `dmarc_zip_test.go` — TestDmarcReportParsesZippedPayload, TestDmarcReportStillParsesPlainXml, TestDmarcReportRejectsEmptyZip (~708 tok)
|
||||||
|
- `dns_record_idempotency_test.go` — TestBuildDnsRecordSpecs_MtaStsIdIsStableAcrossCalls, TestBuildDnsRecordSpecs_MtaStsIdChangesWithPolicy, TestDnsRecord_EqualsNormalized_DetectsPrior... (~740 tok)
|
||||||
|
- `dns_record_test.go` — TestBuildDnsRecordSpecs_GeneratesMandatoryRecords, TestDkimRecordsAreOmittedWhenThereIsNoKey, TestDkimRecordsArePublishedWhenKeysExist, TestOnlyThe... (~2424 tok)
|
||||||
|
- `dns_record.go` — DnsRecord (47 fields); methods: EqualsNormalized (~2951 tok)
|
||||||
|
- `domain_test.go` — TestNewDomain_StartsActiveWithNoMailboxes, TestDomain_Deactivate_SucceedsWithNoActiveMailboxes, TestDomain_Deactivate_FailsWithActiveMailboxes (~308 tok)
|
||||||
|
- `domain.go` — contains the domain's aggregate roots and entities (Clean Architecture layer 1). (~398 tok)
|
||||||
|
- `folder_test.go` — TestFolder_NextUID_StartsAtOneAndIsMonotonic (~141 tok)
|
||||||
|
- `folder.go` — Folder is an IMAP folder entity. uidNext is strictly monotonic (RFC 3501 section 2.3.1.1), (~168 tok)
|
||||||
|
- `mailbox_test.go` — TestNewMailbox_StartsWithZeroUsage, TestMailbox_RecordUsage_AllowsUpToQuota, TestMailbox_RecordUsage_RejectsOverQuota (~329 tok)
|
||||||
|
- `mailbox.go` — Mailbox (10 fields); methods: Address, UsedBytes, Quota, RecordUsage (~324 tok)
|
||||||
|
- `outbound_job_test.go` — TestCalculateNextBackoff_FollowsRetryTable, TestCalculateNextBackoff_ExpiredJobIsPermanent (~532 tok)
|
||||||
|
- `outbound_job.go` — OutboundJob (35 fields) (~705 tok)
|
||||||
|
- `sieve_script.go` — SieveScript represents a user's Sieve filtering script entity. (~68 tok)
|
||||||
|
- `sieve_validator_test.go` — TestValidateSieveScript_ValidScripts, TestValidateSieveScript_InvalidScripts (~396 tok)
|
||||||
|
- `sieve_validator.go` — ValidateSieveScript (~644 tok)
|
||||||
|
- `tls_policy_enforcement_test.go` — TestTLSPolicy_DaneTakesPrecedenceOverMtaSts, TestTLSPolicy_OpportunisticDoesNotRequireValidation, TestTLSPolicy_MtaStsTestingDoesNotEnforce, TestTL... (~1571 tok)
|
||||||
|
- `tls_policy_test.go` — TestTLSPolicy_Effective_DANETakesPrecedenceOverMTASTS, TestTLSPolicy_Effective_FallsBackToMTASTSWithoutDANE, TestTLSPolicy_Effective_FallsBackToOpp... (~232 tok)
|
||||||
|
- `tls_policy.go` — TLSPolicy (35 fields); methods: Effective, RequiresValidation, CoversHost, MatchesCertificate (~1537 tok)
|
||||||
|
- `tls_rpt_report_test.go` — TestTlsRptReportCarriesThePolicyDomain, TestTlsRptReportParsesGzippedPayload (~603 tok)
|
||||||
|
- `tls_rpt_report.go` — TlsRptPolicy (34 fields) (~718 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/domain/event/
|
||||||
|
|
||||||
|
- `events_test.go` — TestEmailReceived_ImplementsDomainEvent, TestEmailBounced_ImplementsDomainEvent (~138 tok)
|
||||||
|
- `events.go` — contains the domain events listed in PLAN.md section 3. (~1066 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/domain/sieve/
|
||||||
|
|
||||||
|
- `parser.go` — Interface: statement (12 methods) (~2127 tok)
|
||||||
|
- `sieve_test.go` — TestRuleFilesTheMessageInItsFolder, TestRuleThatDoesNotMatchLeavesTheMessageAlone, TestHeaderNameIsCaseInsensitive, TestContainsIsCaseInsensitiveOn... (~4735 tok)
|
||||||
|
- `sieve.go` — evaluates the mail-filtering rules a mailbox owner writes. (~926 tok)
|
||||||
|
- `test.go` — Interface: test (6 methods) (~1741 tok)
|
||||||
|
- `vacation.go` — shouldAutoReply decides whether a message may be answered automatically. (~701 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/domain/valueobject/
|
||||||
|
|
||||||
|
- `autoconfig_xml_test.go` — TestBuildThunderbirdAutoconfigXML, TestBuildOutlookAutodiscoverXML (~220 tok)
|
||||||
|
- `autoconfig_xml.go` — BuildThunderbirdAutoconfigXML, BuildOutlookAutodiscoverXML (~735 tok)
|
||||||
|
- `delivery_status_code_test.go` — TestNewDeliveryStatusCode_AcceptsSuccess, TestNewDeliveryStatusCode_AcceptsPermanentFailure, TestNewDeliveryStatusCode_RejectsInvalidClass (~220 tok)
|
||||||
|
- `delivery_status_code.go` — DeliveryStatusCode (6 fields); methods: String (~206 tok)
|
||||||
|
- `domain_name_test.go` — TestNewDomainName_AcceptsValidFQDN, TestNewDomainName_RejectsEmpty, TestNewDomainName_RejectsLabelWithoutDot, TestNewDomainName_RejectsTrailingHyph... (~398 tok)
|
||||||
|
- `domain_name.go` — DomainName (13 fields); methods: String, IsPunycode (~403 tok)
|
||||||
|
- `dsn_bounce_test.go` — TestBuildDsnReport_PreventsBounceLoopForEmptySender, TestBuildDsnReport_GeneratesValidMIME, TestBuildDsnReport_UsesTheConfiguredMailHost, TestBuild... (~721 tok)
|
||||||
|
- `dsn_bounce.go` — BuildDsnReport (~1106 tok)
|
||||||
|
- `email_address_test.go` — TestNewEmailAddress_AcceptsValidAddress, TestNewEmailAddress_RejectsMissingAtSign, TestNewEmailAddress_RejectsEmptyLocalPart, TestNewEmailAddress_R... (~411 tok)
|
||||||
|
- `email_address.go` — contains the domain's immutable, self-validating value objects. (~430 tok)
|
||||||
|
- `message_id_test.go` — TestNewMessageID_AcceptsAngleBracketForm, TestNewMessageID_RejectsEmpty, TestNewMessageID_RejectsMissingAngleBrackets (~202 tok)
|
||||||
|
- `message_id.go` — MessageID (6 fields); methods: String (~186 tok)
|
||||||
|
- `mta_sts_policy_test.go` — TestMtaStsPolicy_Format (~178 tok)
|
||||||
|
- `mta_sts_policy.go` — MtaStsPolicy (14 fields); methods: Format (~260 tok)
|
||||||
|
- `password_hash_test.go` — TestNewPasswordHash_AcceptsArgon2idPHCString, TestNewPasswordHash_RejectsNonArgon2idScheme, TestNewPasswordHash_RejectsEmpty (~208 tok)
|
||||||
|
- `password_hash.go` — PasswordHash (5 fields); methods: String (~223 tok)
|
||||||
|
- `protocol_security_level_test.go` — TestProtocolSecurityLevel_StringRoundTrip (~120 tok)
|
||||||
|
- `protocol_security_level.go` — ProtocolSecurityLevel is the effective TLS requirement for a listener (see PLAN.md section 4). (~190 tok)
|
||||||
|
- `quota_limit_test.go` — TestNewQuotaLimit_AcceptsPositiveBytes, TestNewQuotaLimit_RejectsZero, TestNewQuotaLimit_RejectsNegative, TestQuotaLimit_Exceeds (~261 tok)
|
||||||
|
- `quota_limit.go` — QuotaLimit is a strictly positive byte quota (see PLAN.md section 3, Mailbox invariant). (~187 tok)
|
||||||
|
- `scan_result_test.go` — TestNewCleanScanResult, TestNewVirusScanResult (~130 tok)
|
||||||
|
- `scan_result.go` — ScanResult (16 fields) (~268 tok)
|
||||||
|
- `spam_verdict_test.go` — TestSpamVerdictFromScore_MatchesThresholds (~164 tok)
|
||||||
|
- `spam_verdict.go` — SpamVerdict classifies a message by Rspamd score (thresholds from PLAN.md section 10.1). (~136 tok)
|
||||||
|
- `tlsa_record_test.go` — TestNewTLSARecord_AcceptsUsage3Selector1MatchingType1, TestNewTLSARecord_RejectsUsage2, TestNewTLSARecord_RejectsWrongLengthHash (~328 tok)
|
||||||
|
- `tlsa_record.go` — TLSARecord models a DANE TLSA RR (RFC 6698/7671). Usage 2 (PKIX-CA) is rejected (~344 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/e2e/
|
||||||
|
|
||||||
|
- `antispam_antivirus_e2e_test.go` — TestAntispamAntivirusEndToEnd (~1692 tok)
|
||||||
|
- `dkim_rotation_e2e_test.go` — TestDkimRotationLeavesAUsableKey (~1352 tok)
|
||||||
|
- `dns_sync_e2e_test.go` — e2eFakeDnsProvider (21 fields); methods: GetZoneID, ListRecords, CreateRecord, UpdateRecord (~1052 tok)
|
||||||
|
- `events_ws_e2e_test.go` — TestEventStreamPushesDeliveredMail, TestEventStreamRequiresUserSession (~1752 tok)
|
||||||
|
- `f2_complete_e2e_test.go` — TestF2CompleteSuiteEndToEnd (~4535 tok)
|
||||||
|
- `f3_complete_e2e_test.go` — fakeDnsProvider (56 fields); methods: GetZoneID, ListRecords, CreateRecord, UpdateRecord (~4487 tok)
|
||||||
|
- `http_services_e2e_test.go` — TestHttpServicesEndToEnd (~1425 tok)
|
||||||
|
- `imap_core_e2e_test.go` — TestImapCoreEndToEnd (~3244 tok)
|
||||||
|
- `imap_idle_condstore_e2e_test.go` — TestImapIdleAndCondStoreEndToEnd (~2089 tok)
|
||||||
|
- `managesieve_e2e_test.go` — TestManageSieveEndToEnd (~1814 tok)
|
||||||
|
- `migrations_test.go` (~595 tok)
|
||||||
|
- `outbound_queue_e2e_test.go` — staticMXResolver (27 fields); methods: LookupMX (~1791 tok)
|
||||||
|
- `pop3_e2e_test.go` — TestPop3EndToEnd (~1926 tok)
|
||||||
|
- `ports_test.go` — TestEmbeddedPostgresPortsAreOutsideTheEphemeralRange, TestEmbeddedPostgresPortsAreUnique, TestEachTestConnectsToThePortItStarted (~989 tok)
|
||||||
|
- `smtp_inbound_e2e_test.go` — contains end-to-end validation of the F0/F1 scope declared in (~2808 tok)
|
||||||
|
- `submission_e2e_test.go` — plainAuthClient (68 fields); methods: Start, Next (~3375 tok)
|
||||||
|
- `webmail_api_e2e_test.go` — TestWebmailApiEndToEnd, TestWebmailApiRejectsCrossMailboxAccess, TestWebmailApiRequiresUserSession (~3572 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/health/
|
||||||
|
|
||||||
|
- `health_test.go` — TestHandler_Live_AlwaysReturns200, TestHandler_Ready_Returns200WhenPingSucceeds, TestHandler_Ready_Returns503WhenPingFails (~334 tok)
|
||||||
|
- `health.go` — exposes liveness/readiness endpoints (PLAN.md section 11.5). (~205 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/acme/
|
||||||
|
|
||||||
|
- `issuer.go` — implements mode B of PLAN.md section 8.3: LambdaMail obtains (~2133 tok)
|
||||||
|
- `manager.go` — Interface: TlsaPublisher (6 methods) (~2176 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/arc/
|
||||||
|
|
||||||
|
- `canonical.go` — implements Authenticated Received Chain (RFC 8617). (~1334 tok)
|
||||||
|
- `lazy_sealer_test.go` — stubKeyRepo (15 fields); methods: FindActiveKeys (~811 tok)
|
||||||
|
- `lazy_sealer.go` — LazySealer (18 fields); methods: Seal (~682 tok)
|
||||||
|
- `sealer_test.go` — TestSealer_StartsChainAtInstanceOneWithCvNone, TestSealer_WritesHeadersInPrescribedOrder, TestSealer_ProducesVerifiableChain, TestSealer_ExtendsExi... (~2324 tok)
|
||||||
|
- `sealer.go` — Sealer (56 fields); methods: WithClock, Seal (~2407 tok)
|
||||||
|
- `verifier.go` — VerifyChain (~1289 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/clamav/
|
||||||
|
|
||||||
|
- `clamav_adapter_test.go` — TestClamAVAdapter_Scan_CleanAndVirus (~480 tok)
|
||||||
|
- `clamav_adapter.go` — ClamAVAdapter (28 fields); methods: Scan (~745 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/cloudflare/
|
||||||
|
|
||||||
|
- `cloudflare_adapter_test.go` — TestCloudflareAdapter_GetZoneID_List_Create_Update_Delete (~835 tok)
|
||||||
|
- `cloudflare_adapter.go` — CloudflareAdapter (115 fields); methods: GetZoneID, ListRecords, CreateRecord, UpdateRecord (~3382 tok)
|
||||||
|
- `cloudflare_hardening_test.go` — TestCloudflareAdapter_CreateRecord_SendsProxiedFalseExplicitly, TestCloudflareAdapter_ReportsHttpErrorStatus, TestCloudflareAdapter_ListRecords_Fol... (~2754 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/diskstorage/
|
||||||
|
|
||||||
|
- `local_disk_blob_reader_test.go` — TestLocalDiskBlobReader_ReadByID_ReturnsStoredContent, TestLocalDiskBlobReader_ReadByID_ReturnsErrorForUnknownID (~327 tok)
|
||||||
|
- `local_disk_blob_reader.go` — LocalDiskBlobReader (7 fields); methods: ReadByID (~248 tok)
|
||||||
|
- `local_disk_blob_storage_test.go` — TestLocalDiskBlobStorage_Store_WritesFileAndInsertsBlobRow, TestLocalDiskBlobStorage_Store_DedupsIdenticalContent (~676 tok)
|
||||||
|
- `local_disk_blob_storage.go` — implements port.BlobStorage on the local filesystem. (~992 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/dkim/
|
||||||
|
|
||||||
|
- `keygen.go` — generates, stores and applies the DKIM signing keys described (~981 tok)
|
||||||
|
- `signer_test.go` — fakeKeyRepo (20 fields); methods: FindActiveKeys (~1127 tok)
|
||||||
|
- `signer.go` — Signer (16 fields); methods: Sign (~732 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/mailauth/
|
||||||
|
|
||||||
|
- `authenticator_test.go` — fakeZone (34 fields); methods: LookupTXT, LookupIPAddr, LookupMX, LookupAddr (~2301 tok)
|
||||||
|
- `authenticator.go` — runs the inbound authentication chain of PLAN.md section (~1950 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/netdns/
|
||||||
|
|
||||||
|
- `mx_resolver.go` — NetMXResolver (10 fields); methods: LookupMX (~197 tok)
|
||||||
|
- `public_verifier_match_test.go` — Comparing a desired record against what a resolver actually answers. (~1121 tok)
|
||||||
|
- `public_verifier.go` — PublicVerifier (35 fields); methods: VerifyRecord (~1718 tok)
|
||||||
|
- `tlsa_resolver.go` — TlsaResolver (14 fields); methods: LookupTLSA (~648 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/postgres/
|
||||||
|
|
||||||
|
- `acme_repository.go` — AcmeRepository (64 fields); methods: LoadCertificate, SaveCertificate, LoadAccountKey, SaveAccountKey (~1995 tok)
|
||||||
|
- `alias_repository_test.go` — TestAliasRepository_EnsureSystemAliases (~342 tok)
|
||||||
|
- `alias_repository.go` — AliasRepository (15 fields); methods: EnsureSystemAliases (~388 tok)
|
||||||
|
- `attachment_backfill.go` — AttachmentBackfill (24 fields); methods: Done, MarkDone, Candidates, SetHasAttachments (~749 tok)
|
||||||
|
- `auth_repository_test.go` — TestAuthRepository_FindByAddress_ReturnsHashForActiveMailbox, TestAuthRepository_FindByAddress_ReturnsNilForUnknownAddress (~475 tok)
|
||||||
|
- `auth_repository.go` — AuthRepository (13 fields); methods: FindByAddress (~299 tok)
|
||||||
|
- `bimi_repository.go` — BimiRepository (24 fields); methods: Save, Load, Delete (~613 tok)
|
||||||
|
- `copy_repository_test.go` — TestCopyRepository_CopyMessages_AllocatesNewUIDsAndPreservesFlags (~1214 tok)
|
||||||
|
- `copy_repository.go` — CopyRepository (54 fields); methods: CopyMessages (~1610 tok)
|
||||||
|
- `delivered_report_store.go` — DeliveredReportStore (65 fields); methods: ListUningestedReports, MoveToReportsFolder (~1366 tok)
|
||||||
|
- `dkim_repository.go` — DkimRepository (73 fields); methods: FindActiveKeys, FindPublicKey, Insert, ListKeys (~2032 tok)
|
||||||
|
- `domain_repository.go` — DomainRepository (18 fields); methods: ActiveDomainNames, SaveDnsStatus (~711 tok)
|
||||||
|
- `domain_status_test.go` — dns_status is constrained by the schema to a fixed set. The verifier answers (~281 tok)
|
||||||
|
- `expunge_repository_test.go` — TestExpungeRepository_Expunge_SetsExpungedAtAndDecrementsCounters (~1063 tok)
|
||||||
|
- `expunge_repository.go` — ExpungeRepository (34 fields); methods: Expunge (~828 tok)
|
||||||
|
- `flag_repository_test.go` — TestFlagRepository_SetFlags_AddThenDelThenSet (~1329 tok)
|
||||||
|
- `flag_repository.go` — FlagRepository (45 fields); methods: SetFlags (~897 tok)
|
||||||
|
- `imap_folder_repository_test.go` — TestImapFolderRepository_FindByName_ReturnsFolderWithUIDData, TestImapFolderRepository_FindByName_ReturnsNilForUnknownFolder, TestImapFolderReposit... (~956 tok)
|
||||||
|
- `imap_folder_repository.go` — ImapFolderRepository (22 fields); methods: FindByName, ListFolders (~466 tok)
|
||||||
|
- `inbound_message_repository_test.go` — TestInboundMessageRepository_Persist_AllocatesSequentialUIDs, TestInboundMessageRepository_Persist_ReturnsErrorWhenNoInboxFolderExists, TestInbound... (~4003 tok)
|
||||||
|
- `inbound_message_repository.go` — InboundMessageRepository (55 fields); methods: PersistAll (~2136 tok)
|
||||||
|
- `mailbox_repository_test.go` — TestMailboxRepository_FindActiveByAddress_ReturnsNilForUnknownAddress, TestMailboxRepository_FindActiveByAddress_ReturnsRecordForSeededMailbox, Tes... (~3391 tok)
|
||||||
|
- `mailbox_repository.go` — MailboxRepository (33 fields); methods: FindActiveByAddress, ResolveDeliveryTargets (~699 tok)
|
||||||
|
- `message_query_repository_test.go` — TestMessageQueryRepository_ListMessages_ReturnsRowsOrderedByUID (~657 tok)
|
||||||
|
- `message_query_repository.go` — MessageQueryRepository (16 fields); methods: ListMessages (~366 tok)
|
||||||
|
- `outbound_repository.go` — OutboundRepository (41 fields); methods: Enqueue, FetchNextReady, CountRecipientsSince, UpdateJob (~1237 tok)
|
||||||
|
- `outbox_repository.go` — OutboxRepository (18 fields); methods: FetchUnpublished, MarkPublished (~415 tok)
|
||||||
|
- `pool.go` — contains pgx-based implementations of the application's (~120 tok)
|
||||||
|
- `report_ingestion_test.go` — dbBlobStore (59 fields); methods: Store (~3519 tok)
|
||||||
|
- `report_repository_test.go` — TestReportRepository_SaveDmarcAndTlsRpt (~406 tok)
|
||||||
|
- `report_repository.go` — ReportRepository (36 fields); methods: SaveDmarcReport, SaveTlsRptReport (~1023 tok)
|
||||||
|
- `sieve_repository_test.go` — TestSieveRepository_CRUD (~627 tok)
|
||||||
|
- `sieve_repository.go` — SieveRepository (55 fields); methods: GetScript, PutScript, ListScripts, SetActiveScript (~1245 tok)
|
||||||
|
- `vacation_log.go` — VacationLog (16 fields); methods: LastRepliedAt, RecordReply (~436 tok)
|
||||||
|
- `webmail_repository_test.go` — TestUnreadCountFollowsWhatWasActuallyRead, TestUnreadCountSelfHealsAfterDrift, TestOwnSentCopyIsNotCountedAsUnread, TestDeleteMovesToTrashThenExpun... (~2027 tok)
|
||||||
|
- `webmail_repository.go` — WebmailRepository (145 fields); methods: FindMailboxIDByAddress, ListFolders, ListMessages, GetMessageBlob (~4616 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/rspamd/
|
||||||
|
|
||||||
|
- `rspamd_adapter_test.go` — TestRspamdAdapter_Scan_MapsActions (~482 tok)
|
||||||
|
- `rspamd_adapter.go` — RspamdResponse (27 fields); methods: Scan (~882 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/tls/
|
||||||
|
|
||||||
|
- `acme_cert_watcher_test.go` — domain (38 fields) (~3205 tok)
|
||||||
|
- `acme_cert_watcher.go` — AcmeCertWatcher (73 fields); methods: Watch, EarliestExpiry, GetCertificate, LastReload (~2987 tok)
|
||||||
|
- `local_file_cert_provider_test.go` — TestNewLocalFileCertProvider_LoadsValidCertAndKey, TestNewLocalFileCertProvider_FailsFastOnMissingFiles, TestNewEphemeralSelfSignedCertProvider_Gen... (~874 tok)
|
||||||
|
- `local_file_cert_provider.go` — implements port.CertProvider by loading a static (~606 tok)
|
||||||
|
- `provider_status_test.go` — TestProviderStatus_ReportsAnEphemeralSelfSignedCertificate, TestProviderStatus_ReportsTheSpkiDigestOfTheServedCertificate (~846 tok)
|
||||||
|
- `provider_status.go` — ProviderStatus (22 fields); methods: HasCertificateFor, EarliestExpiry, LastReload, LastChange (~1114 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/tlspolicy/
|
||||||
|
|
||||||
|
- `resolver_test.go` — rewriteTransport (31 fields); methods: RoundTrip, LookupTLSA (~2177 tok)
|
||||||
|
- `resolver.go` — resolves the outbound transport security policy of (~1949 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/infrastructure/vault/
|
||||||
|
|
||||||
|
- `secret_vault_test.go` — TestSecretVault_SealOpenRoundTrip, TestSecretVault_SealIsNonDeterministic, TestSecretVault_DerivationIsStableAcrossInstances, TestSecretVault_Wrong... (~762 tok)
|
||||||
|
- `secret_vault.go` — seals the reusable secrets the system stores in Postgres - (~998 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/presentation/http/
|
||||||
|
|
||||||
|
- `admin_dkim_api.go` — adminDkimAPI (33 fields); methods: SetAdminDkimAPI (~1675 tok)
|
||||||
|
- `admin_dns_api.go` — Interface: DnsRecordVerifier (21 methods) (~2650 tok)
|
||||||
|
- `admin_dns_reconcile_test.go` — stubReconciler (42 fields); methods: ReconcileDomain, SaveDnsStatus (~2445 tok)
|
||||||
|
- `admin_tls_api_test.go` — TestAcmeCertWatcherSatisfiesTlsStatusSource (~155 tok)
|
||||||
|
- `admin_tls_api.go` — Interface: TlsStatusSource (14 methods) (~1376 tok)
|
||||||
|
- `admin_tls_status_test.go` — stillSource (4 fields) (~513 tok)
|
||||||
|
- `attachment_test.go` — TestAttachmentDownload_DowngradesRenderableContentTypes, TestAttachmentDownload_KeepsHarmlessContentTypes, TestAttachmentDownload_NeutralisesHostil... (~937 tok)
|
||||||
|
- `bimi_api.go` — Interface: BimiStore (24 methods) (~2025 tok)
|
||||||
|
- `events_ws_test.go` — TestEventsReachOnlyTheirOwnMailbox, TestASlowSubscriberIsDroppedRatherThanBlocking, TestSubscriberCountFollowsAddAndRemove, TestPublishWithNoSubscr... (~1693 tok)
|
||||||
|
- `events_ws.go` — EventMessage (30 fields); methods: Publish, SubscriberCount, ServeHTTP, SetEventHub (~1490 tok)
|
||||||
|
- `mail_api_test.go` — fakeMailStore (58 fields); methods: FindMailboxIDByAddress, ListFolders, ListMessages, GetMessageBlob (~5364 tok)
|
||||||
|
- `mail_api.go` — HTTP handlers: writeJSON, writeError (~4631 tok)
|
||||||
|
- `router_test.go` — TestRouter_MtaSts_Autoconfig_SecurityTxt_Ingest, TestRouter_HealthReportsDegradedWithoutFailing, TestRouter_HealthFailsWhenDependencyIsDown (~1153 tok)
|
||||||
|
- `router.go` — Router (45 fields); methods: SetMtaStsMode, ServeHTTP, SetDegradedCheck, SetMailAPI (~3672 tok)
|
||||||
|
- `websession_test.go` — TestWebSessionVerifier_AcceptsTokenIssuedByTheAuthService, TestWebSessionVerifier_RejectsChallengeToken, TestWebSessionVerifier_RejectsWrongSecret,... (~1697 tok)
|
||||||
|
- `websession.go` — WebSession (29 fields); methods: Verify, RequireSurface (~934 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/presentation/imap/
|
||||||
|
|
||||||
|
- `backend_test.go` — fakeAuthRepository (64 fields); methods: FindByAddress, ListFolders, FindByName, ListMessages (~6125 tok)
|
||||||
|
- `backend.go` — session (112 fields); methods: Close, Login, Select, Unselect (~4188 tok)
|
||||||
|
- `search_test.go` — TestSession_Search_MatchesByFlag, TestSession_Search_MatchesByUIDSetIntersection, TestSession_Search_MatchesByHeaderField, TestSession_Search_NotCr... (~1457 tok)
|
||||||
|
- `search.go` — Package imappresentation: this file implements IMAP SEARCH (RFC 3501 (~2395 tok)
|
||||||
|
- `stubs.go` — adapts github.com/emersion/go-imap/v2's (~373 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/presentation/managesieve/
|
||||||
|
|
||||||
|
- `conn_test.go` — fakeAuthRepository (42 fields); methods: FindByAddress, GetScript, PutScript, ListScripts (~1681 tok)
|
||||||
|
- `conn.go` — conn (74 fields) (~2789 tok)
|
||||||
|
- `server.go` — Server (14 fields); methods: ListenAndServe, Serve, Close (~299 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/presentation/pop3/
|
||||||
|
|
||||||
|
- `conn_test.go` — fakeAuthRepository (52 fields); methods: FindByAddress, ListFolders, FindByName, ListMessages (~2304 tok)
|
||||||
|
- `conn.go` — pop3Msg (82 fields) (~2806 tok)
|
||||||
|
- `server.go` — Server (14 fields); methods: ListenAndServe, Serve, Close (~293 tok)
|
||||||
|
|
||||||
|
## services/protocols/internal/presentation/smtp/
|
||||||
|
|
||||||
|
- `backend_test.go` — fakeMailboxRepository (58 fields); methods: FindActiveByAddress, ResolveDeliveryTargets, Store, Scan (~3106 tok)
|
||||||
|
- `backend.go` — adapts github.com/emersion/go-smtp's Backend/ (~1399 tok)
|
||||||
|
- `sasl_login_test.go` — TestLoginServer_PromptsThenAuthenticates, TestLoginServer_AcceptsInitialResponse, TestLoginServer_PropagatesAuthFailure, TestLoginServer_RefusesExt... (~614 tok)
|
||||||
|
- `sasl_login.go` — loginServer (15 fields); methods: Next (~561 tok)
|
||||||
|
- `submission_backend_test.go` — TestSubmissionSession_RefusesCommandsBeforeAuth, TestSubmissionSession_RefusesForeignSender, TestSubmissionSession_AdvertisesPlainAndLogin, TestSub... (~816 tok)
|
||||||
|
- `submission_backend.go` — SubmissionBackend (29 fields); methods: NewSession, AuthMechanisms, Auth, Reset (~1300 tok)
|
||||||
|
|
||||||
|
## services/rspamd/
|
||||||
|
|
||||||
|
- `Dockerfile` — Docker container definition (~147 tok)
|
||||||
|
|
||||||
|
## services/storage/
|
||||||
|
|
||||||
|
- `Dockerfile` — Docker container definition (~466 tok)
|
||||||
|
- `package.json` — Node.js package manifest (~131 tok)
|
||||||
|
- `tsconfig.build.json` — TypeScript build configuration (~83 tok)
|
||||||
|
- `tsconfig.json` — TypeScript configuration (~88 tok)
|
||||||
|
- `vitest.config.ts` — Vitest test configuration (~33 tok)
|
||||||
|
|
||||||
|
## services/storage/src/
|
||||||
|
|
||||||
|
- `health.test.ts` — Declares withServer (~470 tok)
|
||||||
|
- `health.ts` — Builds the /health/live and /health/ready handler shared by the auth and (~215 tok)
|
||||||
|
- `server.ts` — Declares port (~212 tok)
|
||||||
|
|
||||||
|
## services/webmail/
|
||||||
|
|
||||||
|
- `Dockerfile` — Docker container definition (~395 tok)
|
||||||
|
- `next-env.d.ts` — / <reference types="next" /> (~75 tok)
|
||||||
|
- `next.config.ts` — Next.js configuration (~601 tok)
|
||||||
|
- `package.json` — Node.js package manifest (~263 tok)
|
||||||
|
- `postcss.config.js` — PostCSS configuration (~24 tok)
|
||||||
|
- `tailwind.config.js` — Tailwind CSS configuration (~1064 tok)
|
||||||
|
- `tsconfig.json` — TypeScript configuration (~174 tok)
|
||||||
|
- `vitest.config.ts` — Vitest test configuration (~93 tok)
|
||||||
|
|
||||||
|
## services/webmail/messages/
|
||||||
|
|
||||||
|
- `en.json` (~5338 tok)
|
||||||
|
- `es.json` (~5774 tok)
|
||||||
|
- `pt-BR.json` (~5763 tok)
|
||||||
|
|
||||||
|
## services/webmail/public/
|
||||||
|
|
||||||
|
- `.gitkeep` (~0 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/
|
||||||
|
|
||||||
|
- `compose-draft-integrity.test.ts` — The composer's draft handling, and the delete action the mail list was (~1189 tok)
|
||||||
|
- `emailBody.test.ts` — The reader pane showed broken image icons for every message written by a (~1334 tok)
|
||||||
|
- `i18n.test.ts` — loadJson: getLeafKeys (~885 tok)
|
||||||
|
- `initials.test.ts` (~293 tok)
|
||||||
|
- `interface-integrity.test.ts` — Guards the class of defect this interface kept shipping: text that cannot be (~2690 tok)
|
||||||
|
- `mail-state-integrity.test.ts` — The wiring behind the counters, the reader pane and the two surfaces. (~2149 tok)
|
||||||
|
- `mailCounts.test.ts` — The counters the interface shows. (~4397 tok)
|
||||||
|
- `middleware.ts` — Gate for the two web surfaces. (~862 tok)
|
||||||
|
- `remoteImages.test.ts` — Whether a message's remote images load. (~1007 tok)
|
||||||
|
- `rules.test.ts` — Rules, as a person describes them, turned into Sieve and back. (~2014 tok)
|
||||||
|
- `sanitizer.test.ts` — These run under jsdom. Under the default node environment DOMPurify was (~1002 tok)
|
||||||
|
- `sidebarBadge.test.ts` — What the rail actually puts on screen, folder by folder. (~639 tok)
|
||||||
|
- `signature.test.ts` — The signature is edited in one place and injected into the composer in (~1022 tok)
|
||||||
|
- `surfaceGate.test.ts` — Which session, if any, a page needs before it may render. (~606 tok)
|
||||||
|
- `wellknown-routing.test.ts` — The well-known documents senders fetch over HTTPS. (~729 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/
|
||||||
|
|
||||||
|
- `globals.css` — Nocturne - the one visual language for both surfaces. (~2171 tok)
|
||||||
|
- `layout.tsx` — The locale is resolved on the server so the first paint is already in the (~356 tok)
|
||||||
|
- `page.tsx` — The root sends everyone straight into the mail client. (~213 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/
|
||||||
|
|
||||||
|
- `layout.tsx` — AdminLayout — uses useState (~2246 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/dashboard/
|
||||||
|
|
||||||
|
- `page.tsx` — Exactly the fields /api/v1/admin/dashboard returns - see dashboardStats(). (~2425 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/dmarc/
|
||||||
|
|
||||||
|
- `page.tsx` — AdminDmarcPage — renders table — uses useEffect (~1215 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/domains/
|
||||||
|
|
||||||
|
- `page.tsx` — Domains, read from the database. (~7639 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/mailboxes/
|
||||||
|
|
||||||
|
- `page.tsx` — The envelope every server-paged list answers with. (~12626 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/queue/
|
||||||
|
|
||||||
|
- `page.tsx` — The outbound queue, read from the database. (~2956 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/security/
|
||||||
|
|
||||||
|
- `page.tsx` — Live certificate state from the protocols service, which owns the cert (~6267 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(admin)/admin/tests/
|
||||||
|
|
||||||
|
- `page.tsx` — Checks an operator runs by hand, against the server or one user. (~2274 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(auth)/admin/login/
|
||||||
|
|
||||||
|
- `page.tsx` — AdminLoginPage — renders form — uses useState (~2803 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(auth)/admin/step-up/
|
||||||
|
|
||||||
|
- `page.tsx` — Crossing from the webmail into the console. (~2187 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(auth)/user/login/
|
||||||
|
|
||||||
|
- `page.tsx` — One step, because the mailbox is one step. (~1486 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(user)/user/
|
||||||
|
|
||||||
|
- `layout.tsx` — Creates, renames or deletes one of the user's own folders. (~4684 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(user)/user/compose/
|
||||||
|
|
||||||
|
- `page.tsx` — Splits a comma or semicolon separated field, dropping empty entries. (~5433 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(user)/user/mail/[folder]/
|
||||||
|
|
||||||
|
- `page.tsx` — The initials shown in the sender avatar, from a display name or an address. (~10230 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/(user)/user/settings/
|
||||||
|
|
||||||
|
- `page.tsx` — Writes the whole rule list to the server as one script. (~12580 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/api/v1/[...path]/
|
||||||
|
|
||||||
|
- `route.ts` — Proxies /api/v1/* to the auth service. (~1250 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/app/health/
|
||||||
|
|
||||||
|
- `route.ts` — Liveness probe for the web container. (~210 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/components/
|
||||||
|
|
||||||
|
- `Avatar.tsx` — Rendered size in pixels; the square the circle is cut from. (~542 tok)
|
||||||
|
- `AvatarEditor.tsx` — The frame is square; the mask over it is what makes the photo a circle. (~2510 tok)
|
||||||
|
- `MessageReader.tsx` — The document the body frame renders, or null for a text-only message. (~4736 tok)
|
||||||
|
- `RecipientInput.tsx` — Entries as they will be sent: `Name <addr>` or a bare address. (~1486 tok)
|
||||||
|
- `TotpEnrolment.tsx` — The scannable half of TOTP enrolment, shared by the webmail settings page and (~1644 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/components/ui/
|
||||||
|
|
||||||
|
- `Badge.tsx` — A tag: a small tinted label from the ramps. (~375 tok)
|
||||||
|
- `Button.tsx` — Nocturne buttons are outlined, not filled. (~584 tok)
|
||||||
|
- `Card.tsx` — A panel: a solid ground with a hairline inset ring. (~421 tok)
|
||||||
|
- `Pagination.tsx` — The pager under a server-paged list. (~672 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/i18n/
|
||||||
|
|
||||||
|
- `config.ts` — Picks the best supported locale from an Accept-Language header. (~993 tok)
|
||||||
|
- `LanguageSwitcher.tsx` — Language selector, rendered in both surface layouts (PLAN.md section 21.2). (~275 tok)
|
||||||
|
- `provider.tsx` — Shorthand for components that only need to translate. (~598 tok)
|
||||||
|
|
||||||
|
## services/webmail/src/lib/
|
||||||
|
|
||||||
|
- `avatarCrop.test.ts` — Declares VIEWPORT (~1187 tok)
|
||||||
|
- `avatarCrop.ts` — The geometry behind the circular photo editor. (~1132 tok)
|
||||||
|
- `emailBody.ts` — Turns a message's sanitized HTML into something a frame can actually show. (~1181 tok)
|
||||||
|
- `formatBytes.test.ts` (~434 tok)
|
||||||
|
- `formatBytes.ts` — A byte count as a person reads it. (~398 tok)
|
||||||
|
- `initials.ts` — The two letters an avatar shows. (~236 tok)
|
||||||
|
- `mailCounts.ts` — The arithmetic behind every number the mail screens display. (~2786 tok)
|
||||||
|
- `paging.test.ts` — Declares w (~464 tok)
|
||||||
|
- `paging.ts` — The page numbers a pager should offer. (~273 tok)
|
||||||
|
- `remoteImages.ts` — Which senders may load their remote images. (~863 tok)
|
||||||
|
- `roleLabel.ts` — A role as a person reads it. (~200 tok)
|
||||||
|
- `rules.ts` — Rules as a person describes them, and the Sieve the server runs. (~1672 tok)
|
||||||
|
- `sanitizer.ts` — Sanitizes message HTML with DOMPurify (PLAN.md section 14.2). (~1142 tok)
|
||||||
|
- `signature.ts` — The signature, on its way from the settings screen into a message. (~1107 tok)
|
||||||
|
- `surfaceGate.ts` — Which session a page needs before it can usefully render. (~445 tok)
|
||||||
|
- `useAccount.ts` — Roles that may open the admin console. (~429 tok)
|
||||||
|
- `useDebounced.ts` — Holds a value still until typing stops. (~170 tok)
|
||||||
|
- `useFolders.ts` — The folder list, kept current. (~635 tok)
|
||||||
|
- `useMailEvents.ts` — Subscribes to the server's event stream and calls onEvent for each push. (~641 tok)
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"bugs": [
|
||||||
|
{
|
||||||
|
"error_message": "Email layout flattened: width/height/colspan/align/bgcolor stripped; background images never restored after 'show images'",
|
||||||
|
"root_cause": "Custom DOMPurify ALLOWED_URI_REGEXP replaced the default that accepts non-URL values, so every non-URL attribute was rejected; blockRemoteImages deleted url()/background irreversibly",
|
||||||
|
"fix": "Added DOMPurify's default non-URL alternatives to the regexp; block/unblock now rewrite backgrounds reversibly (lm-blocked- scheme, data-blocked-background)",
|
||||||
|
"tags": [
|
||||||
|
"webmail",
|
||||||
|
"sanitizer",
|
||||||
|
"dompurify",
|
||||||
|
"remote-images"
|
||||||
|
],
|
||||||
|
"date": "2026-10-13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"error_message": "Domain admin dashboard showed server-wide totals (all domains, queue, DMARC, storage)",
|
||||||
|
"root_cause": "dashboardStats, queueSummary, dmarcSummary, preflightSummary and the diagnostics queueByStatus took no AdminScope and queried every tenant; only listDomains/listMailboxes were scoped",
|
||||||
|
"fix": "Each now takes the session's AdminScope; mailboxScope() reaches outbound_jobs/email_messages through mailboxes.domain_id; DMARC matches by domain_id or name. Covered by services/auth/src/adminScope.test.ts against a real Postgres",
|
||||||
|
"tags": [
|
||||||
|
"auth",
|
||||||
|
"authz",
|
||||||
|
"tenant-isolation",
|
||||||
|
"dashboard"
|
||||||
|
],
|
||||||
|
"date": "2026-10-13"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
description: learned preferences, project conventions, and Do-Not-Repeat rules
|
||||||
|
budget_tokens: 2000
|
||||||
|
---
|
||||||
|
# Cerebrum
|
||||||
|
|
||||||
|
> OpenWolf's learning memory. Updated automatically as the AI learns from interactions.
|
||||||
|
> Do not edit manually unless correcting an error.
|
||||||
|
> Last updated: 2026-10-06
|
||||||
|
|
||||||
|
## User Preferences
|
||||||
|
|
||||||
|
<!-- How the user likes things done. Code style, tools, patterns, communication. -->
|
||||||
|
|
||||||
|
## Key Learnings
|
||||||
|
|
||||||
|
- **Project:** LambdaMail
|
||||||
|
- **Description:** A self-hosted mail server with an integrated webmail and admin console, built to
|
||||||
|
|
||||||
|
## Do-Not-Repeat
|
||||||
|
|
||||||
|
<!-- Mistakes made and corrected. Each entry prevents the same mistake recurring. -->
|
||||||
|
<!-- Format: [YYYY-MM-DD] Description of what went wrong and what to do instead. -->
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
<!-- Significant technical decisions with rationale. Why X was chosen over Y. -->
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"openwolf": {
|
||||||
|
"enabled": true,
|
||||||
|
"reads": {
|
||||||
|
"duplicate_mode": "warn",
|
||||||
|
"skeleton_hints": true
|
||||||
|
},
|
||||||
|
"bash": {
|
||||||
|
"filter_mode": "suggest",
|
||||||
|
"governor": {
|
||||||
|
"mode": "replace",
|
||||||
|
"threshold_tokens": 2000,
|
||||||
|
"families": {
|
||||||
|
"grep_flood": "replace",
|
||||||
|
"file_print": "replace",
|
||||||
|
"git_show": "replace",
|
||||||
|
"test": "suggest",
|
||||||
|
"build": "suggest",
|
||||||
|
"unknown": "suggest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anatomy": {
|
||||||
|
"auto_scan_on_init": true,
|
||||||
|
"rescan_interval_hours": 6,
|
||||||
|
"max_description_length": 100,
|
||||||
|
"max_files": 500,
|
||||||
|
"exclude_patterns": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"build",
|
||||||
|
".next",
|
||||||
|
".nuxt",
|
||||||
|
".turbo",
|
||||||
|
".vercel",
|
||||||
|
".netlify",
|
||||||
|
".output",
|
||||||
|
"coverage",
|
||||||
|
"__pycache__",
|
||||||
|
".venv",
|
||||||
|
"venv",
|
||||||
|
"site-packages",
|
||||||
|
".pytest_cache",
|
||||||
|
".mypy_cache",
|
||||||
|
".ruff_cache",
|
||||||
|
".tox",
|
||||||
|
"*.pyc",
|
||||||
|
".gradle",
|
||||||
|
"target",
|
||||||
|
".m2",
|
||||||
|
".cargo",
|
||||||
|
".git",
|
||||||
|
".wolf",
|
||||||
|
".cache",
|
||||||
|
".vscode",
|
||||||
|
".idea",
|
||||||
|
".DS_Store",
|
||||||
|
"Thumbs.db",
|
||||||
|
"*.min.js",
|
||||||
|
"*.min.css",
|
||||||
|
"*.map"
|
||||||
|
],
|
||||||
|
"respect_gitignore": true
|
||||||
|
},
|
||||||
|
"token_audit": {
|
||||||
|
"enabled": true,
|
||||||
|
"report_frequency": "weekly",
|
||||||
|
"waste_threshold_percent": 15,
|
||||||
|
"chars_per_token_code": 3.5,
|
||||||
|
"chars_per_token_prose": 4
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_retry_attempts": 3,
|
||||||
|
"dead_letter_enabled": true,
|
||||||
|
"heartbeat_interval_minutes": 30
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"consolidation_after_days": 7,
|
||||||
|
"max_entries_before_consolidation": 200
|
||||||
|
},
|
||||||
|
"cerebrum": {
|
||||||
|
"max_tokens": 2000,
|
||||||
|
"reflection_frequency": "weekly"
|
||||||
|
},
|
||||||
|
"daemon": {
|
||||||
|
"port": 18810,
|
||||||
|
"log_level": "info"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"enabled": true,
|
||||||
|
"port": 18811,
|
||||||
|
"host": "127.0.0.1"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"session_digest_budget_tokens": 1500,
|
||||||
|
"reinjection_interval": 25,
|
||||||
|
"state_budgets": {
|
||||||
|
".wolf/cerebrum.md": 2000,
|
||||||
|
".wolf/STATUS.md": 1000
|
||||||
|
},
|
||||||
|
"budgets": {
|
||||||
|
"claude": 1500,
|
||||||
|
"codex": 1200,
|
||||||
|
"gemini": 1200,
|
||||||
|
"opencode": 1200,
|
||||||
|
"cursor": 800
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"mode": "quiet"
|
||||||
|
},
|
||||||
|
"updates": {
|
||||||
|
"mode": "compatible"
|
||||||
|
},
|
||||||
|
"agents": [
|
||||||
|
"claude"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"id": "anatomy-rescan",
|
||||||
|
"name": "Full anatomy rescan",
|
||||||
|
"schedule": "0 */6 * * *",
|
||||||
|
"description": "Re-scans project filesystem and reconciles anatomy.md",
|
||||||
|
"action": {
|
||||||
|
"type": "scan_project"
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"max_attempts": 3,
|
||||||
|
"backoff": "exponential",
|
||||||
|
"base_delay_seconds": 30
|
||||||
|
},
|
||||||
|
"failsafe": {
|
||||||
|
"on_failure": "log_and_continue",
|
||||||
|
"alert_after_consecutive_failures": 2,
|
||||||
|
"dead_letter": true
|
||||||
|
},
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "memory-consolidation",
|
||||||
|
"name": "Consolidate old memory",
|
||||||
|
"schedule": "0 2 * * *",
|
||||||
|
"description": "Compress memory.md entries older than 7 days",
|
||||||
|
"action": {
|
||||||
|
"type": "consolidate_memory",
|
||||||
|
"params": {
|
||||||
|
"older_than_days": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"max_attempts": 2,
|
||||||
|
"backoff": "exponential",
|
||||||
|
"base_delay_seconds": 60
|
||||||
|
},
|
||||||
|
"failsafe": {
|
||||||
|
"on_failure": "skip_and_retry_next_cycle",
|
||||||
|
"dead_letter": false
|
||||||
|
},
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "token-audit",
|
||||||
|
"name": "Token audit report",
|
||||||
|
"schedule": "0 0 * * 1",
|
||||||
|
"description": "Weekly waste pattern detection",
|
||||||
|
"action": {
|
||||||
|
"type": "generate_token_report"
|
||||||
|
},
|
||||||
|
"retry": {
|
||||||
|
"max_attempts": 2,
|
||||||
|
"backoff": "linear",
|
||||||
|
"base_delay_seconds": 60
|
||||||
|
},
|
||||||
|
"failsafe": {
|
||||||
|
"on_failure": "log_and_continue",
|
||||||
|
"dead_letter": true
|
||||||
|
},
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
description: chronological action log per session, consolidated weekly
|
||||||
|
---
|
||||||
|
# Memory
|
||||||
|
|
||||||
|
> Chronological action log. Hooks and AI append to this file automatically.
|
||||||
|
> Old sessions are consolidated by the daemon weekly.
|
||||||
|
|
||||||
|
## Session: 2026-10-06 08:07
|
||||||
|
|
||||||
|
| Time | Action | File(s) | Outcome | ~Tokens |
|
||||||
|
|------|--------|---------|---------|--------|
|
||||||
|
|
||||||
|
## Session: 2026-10-13 09:32
|
||||||
|
|
||||||
|
| Time | Action | File(s) | Outcome | ~Tokens |
|
||||||
|
|------|--------|---------|---------|--------|
|
||||||
|
| 09:40 | Created services/webmail/src/components/MessageReader.tsx | — | ~4736 |
|
||||||
|
| 09:51 | Created services/webmail/src/components/RecipientInput.tsx | — | ~1486 |
|
||||||
|
<!-- session:MWJiNDRiYmMtNDk5NC00MTgwLThlNjQtZmFhMmNiYTcwMjdj -->
|
||||||
|
| 2026-10-13T13:08:21.421Z | Webmail: MessageReader redesign (Outlook-style), sanitizer fix for layout attrs and reversible background blocking, chip recipients, reply-all, starred/important flag (Go /mail/flagged), high-importance send, resizable list/reader divider. Auth: domain-admin views scoped (dashboard, queue, DMARC, preflight, diagnostics). | services/webmail/src/components/MessageReader.tsx,services/webmail/src/components/RecipientInput.tsx,services/webmail/src/lib/recipients.ts,services/webmail/src/lib/paneWidth.ts,services/auth/src/repository.ts,services/auth/src/adminScope.test.ts,services/protocols/internal/infrastructure/postgres/webmail_repository.go | go, auth (incl. DB) and webmail suites pass; Coolify API token in .env returns 401 |
|
||||||
|
| 10:47 | Session end: 2 writes across 2 files (MessageReader.tsx, RecipientInput.tsx) | 2 reads | ~6222 tok |
|
||||||
|
|
||||||
|
## Session: 2026-10-20 19:06
|
||||||
|
|
||||||
|
| Time | Action | File(s) | Outcome | ~Tokens |
|
||||||
|
|------|--------|---------|---------|--------|
|
||||||
|
|
||||||
|
## Session: 2026-10-22 14:28
|
||||||
|
|
||||||
|
| Time | Action | File(s) | Outcome | ~Tokens |
|
||||||
|
|------|--------|---------|---------|--------|
|
||||||
|
| 14:41 | Created services/auth/src/accounts.ts | — | ~715 |
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# OpenWolf
|
||||||
|
|
||||||
|
This project uses OpenWolf for context management. The always-on rules live in `.claude/rules/openwolf.md`; the hooks handle bookkeeping (anatomy index, memory log, read tracking) automatically.
|
||||||
|
|
||||||
|
For the full operating protocol (session handoff, memory discipline, bug logging), load the `openwolf` skill, or read `.wolf/OPENWOLF.md`. Regenerate the session handoff with `/handoff`.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS trusted_senders;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- Senders a mailbox has said it trusts.
|
||||||
|
--
|
||||||
|
-- Two things hang off one decision: mail from a trusted sender is not filed as
|
||||||
|
-- spam, and its remote images load without asking. Both used to be forgotten:
|
||||||
|
-- moving a message out of Junk taught nothing, so the next reply from the same
|
||||||
|
-- person went to Junk again, and the choice to show images lived in one
|
||||||
|
-- browser's storage.
|
||||||
|
CREATE TABLE trusted_senders (
|
||||||
|
mailbox_id UUID NOT NULL REFERENCES mailboxes(id) ON DELETE CASCADE,
|
||||||
|
-- Lowercased, so one person is one row however they spell their address.
|
||||||
|
address VARCHAR(320) NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (mailbox_id, address)
|
||||||
|
);
|
||||||
@@ -1365,3 +1365,41 @@ export async function traceLogsByQueueId(queueId: string): Promise<unknown[]> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------- trusted senders
|
||||||
|
|
||||||
|
/** A mailbox cannot grow this list without bound. */
|
||||||
|
export const MAX_TRUSTED_SENDERS = 5000;
|
||||||
|
|
||||||
|
/** The address an input resolves to, or "" when it is not one. */
|
||||||
|
export function normaliseSender(raw: string): string {
|
||||||
|
const angled = raw.match(/<([^>]+)>/);
|
||||||
|
const address = (angled ? angled[1] : raw).trim().toLowerCase();
|
||||||
|
return address.length <= 320 && /^[^\s@<>]+@[^\s@<>]+$/.test(address) ? address : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTrustedSenders(mailboxId: string): Promise<string[]> {
|
||||||
|
const rows = await query<{ address: string }>(
|
||||||
|
`SELECT address FROM trusted_senders WHERE mailbox_id = $1 ORDER BY address`,
|
||||||
|
[mailboxId],
|
||||||
|
);
|
||||||
|
return rows.map((row) => row.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns false when the list is full; trusting someone already trusted is a no-op. */
|
||||||
|
export async function addTrustedSender(mailboxId: string, address: string): Promise<boolean> {
|
||||||
|
const rows = await query<{ n: string }>(
|
||||||
|
`SELECT COUNT(*) AS n FROM trusted_senders WHERE mailbox_id = $1`,
|
||||||
|
[mailboxId],
|
||||||
|
);
|
||||||
|
if (Number(rows[0]?.n ?? 0) >= MAX_TRUSTED_SENDERS) return false;
|
||||||
|
await query(
|
||||||
|
`INSERT INTO trusted_senders (mailbox_id, address) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
||||||
|
[mailboxId, address],
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeTrustedSender(mailboxId: string, address: string): Promise<void> {
|
||||||
|
await query(`DELETE FROM trusted_senders WHERE mailbox_id = $1 AND address = $2`, [mailboxId, address]);
|
||||||
|
}
|
||||||
@@ -1194,6 +1194,47 @@ describeDb("moving between the surfaces", () => {
|
|||||||
expect(first.body.token).not.toBe(second.body.token);
|
expect(first.body.token).not.toBe(second.body.token);
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
|
describe("trusted senders", () => {
|
||||||
|
let token = "";
|
||||||
|
beforeAll(async () => {
|
||||||
|
token = (await post("/api/v1/auth/user/login", { email: plainEmail(), password: PASSWORD })).body.token as unknown as string;
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
const send = async (method: string, path: string, body?: unknown) => {
|
||||||
|
const res = await fetch(`${base}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
||||||
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||||
|
});
|
||||||
|
return { status: res.status, body: (await res.json().catch(() => ({}))) as Record<string, any> };
|
||||||
|
};
|
||||||
|
|
||||||
|
it("remembers a sender once however it is spelled, and forgets it on request", async () => {
|
||||||
|
expect((await send("PUT", "/api/v1/user/trusted-senders", { address: "Jenny <Jenny@Example.test>" })).status).toBe(200);
|
||||||
|
expect((await send("PUT", "/api/v1/user/trusted-senders", { address: "jenny@example.test" })).status).toBe(200);
|
||||||
|
expect((await send("GET", "/api/v1/user/trusted-senders")).body.senders).toEqual(["jenny@example.test"]);
|
||||||
|
|
||||||
|
expect((await send("DELETE", "/api/v1/user/trusted-senders?address=JENNY@example.test")).status).toBe(200);
|
||||||
|
expect((await send("GET", "/api/v1/user/trusted-senders")).body.senders).toEqual([]);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
it("refuses something that is not an address", async () => {
|
||||||
|
expect((await send("PUT", "/api/v1/user/trusted-senders", { address: "not an address" })).status).toBe(400);
|
||||||
|
expect((await send("PUT", "/api/v1/user/trusted-senders", {})).status).toBe(400);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
it("keeps one mailbox's list from another's", async () => {
|
||||||
|
await send("PUT", "/api/v1/user/trusted-senders", { address: "private@example.test" });
|
||||||
|
const other = (await post("/api/v1/auth/user/login", { email: userEmail(), password: PASSWORD })).body.token as unknown as string;
|
||||||
|
const res = await fetch(`${base}/api/v1/user/trusted-senders`, { headers: { Authorization: `Bearer ${other}` } });
|
||||||
|
expect(((await res.json()) as { senders: string[] }).senders).not.toContain("private@example.test");
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
it("needs a session", async () => {
|
||||||
|
expect((await get("/api/v1/user/trusted-senders")).status).toBe(401);
|
||||||
|
}, 60000);
|
||||||
|
});
|
||||||
|
|
||||||
// Several webmail accounts at once, and a session that renews itself.
|
// Several webmail accounts at once, and a session that renews itself.
|
||||||
describe("several accounts and sliding sessions", () => {
|
describe("several accounts and sliding sessions", () => {
|
||||||
const emailOf = (local: string) => `${local}@authtest-${DOMAIN_ID}.invalid`;
|
const emailOf = (local: string) => `${local}@authtest-${DOMAIN_ID}.invalid`;
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ async function route(req: IncomingMessage, res: ServerResponse, url: string): Pr
|
|||||||
}
|
}
|
||||||
if (url === "/api/v1/user/preferences" && method === "GET") return userGetPreferences(res, session!);
|
if (url === "/api/v1/user/preferences" && method === "GET") return userGetPreferences(res, session!);
|
||||||
if (url === "/api/v1/user/preferences" && method === "POST") return userUpdatePreferences(req, res, session!);
|
if (url === "/api/v1/user/preferences" && method === "POST") return userUpdatePreferences(req, res, session!);
|
||||||
|
if (url === "/api/v1/user/trusted-senders" && method === "GET") return trustedSendersList(res, session!);
|
||||||
|
if (url === "/api/v1/user/trusted-senders" && method === "PUT") return trustedSenderAdd(req, res, session!);
|
||||||
|
if (url === "/api/v1/user/trusted-senders" && method === "DELETE") return trustedSenderRemove(req, res, session!);
|
||||||
if (url === "/api/v1/user/sieve" && method === "GET") return userGetSieve(res, session!);
|
if (url === "/api/v1/user/sieve" && method === "GET") return userGetSieve(res, session!);
|
||||||
if (url === "/api/v1/user/sieve" && method === "POST") return userSaveSieve(req, res, session!);
|
if (url === "/api/v1/user/sieve" && method === "POST") return userSaveSieve(req, res, session!);
|
||||||
if (url === "/api/v1/user/vacation" && method === "POST") return userSaveVacation(req, res, session!);
|
if (url === "/api/v1/user/vacation" && method === "POST") return userSaveVacation(req, res, session!);
|
||||||
@@ -1195,6 +1198,28 @@ async function userGetPreferences(res: ServerResponse, session: SessionTokenPayl
|
|||||||
sendJson(res, 200, await repo.getUserPreferences(session.sub));
|
sendJson(res, 200, await repo.getUserPreferences(session.sub));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function trustedSendersList(res: ServerResponse, session: SessionTokenPayload): Promise<void> {
|
||||||
|
sendJson(res, 200, { senders: await repo.listTrustedSenders(session.sub) });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trustedSenderAdd(req: IncomingMessage, res: ServerResponse, session: SessionTokenPayload): Promise<void> {
|
||||||
|
const body = await parseJsonBody(req);
|
||||||
|
const address = repo.normaliseSender(typeof body?.address === "string" ? body.address : "");
|
||||||
|
if (!address) return sendJson(res, 400, { error: "INVALID_INPUT", message: "A valid email address is required" });
|
||||||
|
if (!(await repo.addTrustedSender(session.sub, address))) {
|
||||||
|
return sendJson(res, 409, { error: "LIMIT_REACHED", message: "The trusted senders list is full" });
|
||||||
|
}
|
||||||
|
sendJson(res, 200, { ok: true, address });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function trustedSenderRemove(req: IncomingMessage, res: ServerResponse, session: SessionTokenPayload): Promise<void> {
|
||||||
|
const raw = new URL(req.url || "/", "http://localhost").searchParams.get("address") ?? "";
|
||||||
|
const address = repo.normaliseSender(raw);
|
||||||
|
if (!address) return sendJson(res, 400, { error: "INVALID_INPUT", message: "A valid email address is required" });
|
||||||
|
await repo.removeTrustedSender(session.sub, address);
|
||||||
|
sendJson(res, 200, { ok: true });
|
||||||
|
}
|
||||||
|
|
||||||
async function userUpdatePreferences(req: IncomingMessage, res: ServerResponse, session: SessionTokenPayload): Promise<void> {
|
async function userUpdatePreferences(req: IncomingMessage, res: ServerResponse, session: SessionTokenPayload): Promise<void> {
|
||||||
const body = await parseJsonBody(req);
|
const body = await parseJsonBody(req);
|
||||||
const signature = typeof body?.signature === "string" ? body.signature : "";
|
const signature = typeof body?.signature === "string" ? body.signature : "";
|
||||||
|
|||||||
Reference in new issue
Block a user