* feat(mail): support gzip compressed email storage in D1 raw_blob column
Add ENABLE_MAIL_GZIP env var to optionally gzip-compress incoming emails
into a new raw_blob BLOB column, saving D1 storage space. Reading is
backward-compatible: prioritizes raw_blob (decompress) with fallback to
plaintext raw field. Includes DB migration v0.0.7, docs, and changelogs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: gzip fallback on missing column + decouple resolve from handleListQuery
- email/index.ts: gzip INSERT failure now falls back to plaintext INSERT
instead of silently losing the email (P1: data loss prevention)
- common.ts: add handleMailListQuery for raw_mails-specific list queries
with resolveRawEmailList, keeping handleListQuery generic
- Replace handleListQuery → handleMailListQuery in mails_api, admin_mail_api,
user_mail_api (only raw_mails callers)
- Add e2e test infrastructure: worker-gzip service, wrangler.toml.e2e.gzip,
api-gzip playwright project, mail-gzip.spec.ts with 4 test cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address CodeRabbit review feedback for gzip feature
- Use destructuring in resolveRawEmailRow to truly remove raw_blob key
- Narrow fallback scope: only fallback to plaintext on compression failure
or missing raw_blob column, re-throw other DB errors
- Clean unused imports in e2e gzip test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add try-catch in resolveRawEmail to prevent single corrupt blob from failing entire list
A corrupted raw_blob would cause decompressBlob to throw, which with
Promise.all in resolveRawEmailList would reject the entire batch query.
Now catches decompression errors and falls back to row.raw field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(mail): align sendAdminInternalMail with gzip storage path
sendAdminInternalMail now respects ENABLE_MAIL_GZIP: compresses to
raw_blob when enabled, with fallback to plaintext on failure.
Added e2e test verifying admin internal mail is readable under gzip.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(e2e): match admin internal mail by body content instead of encoded subject
mimetext base64-encodes the Subject header, so the raw MIME string
does not contain the literal subject text. Match on body content
(balance: 99) which is plaintext.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(e2e): add WORKER_GZIP_URL guard and length assertions in gzip tests
Address CodeRabbit feedback:
- Skip gzip tests when WORKER_GZIP_URL is not set to prevent false positives
- Assert results array length before accessing [0] for clearer error messages
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(mail): narrow gzip fallback scope and fix webhook query compatibility
- sendAdminInternalMail: separate compress vs DB error handling, only
fallback to plaintext on compression failure or missing raw_blob
column, rethrow other DB errors (aligns with email/index.ts)
- Webhook test endpoints: use SELECT * instead of explicit raw_blob
column reference, so pre-migration databases don't 500
- Docs/changelog: clarify that db_migration must run before enabling
ENABLE_MAIL_GZIP
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(telegram): use generic Record type for raw_mails query result
Align with other query sites — avoid hardcoding raw_blob in the
TypeScript type annotation so the query works with or without the
column after migration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(models): add RawMailRow type and unify raw_mails query typing
Add RawMailRow type to models with raw_blob as optional field, replacing
ad-hoc Record<string, unknown> and inline type annotations across
webhook test endpoints, telegram API, and gzip utilities.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* chore: upgrade dependencies
- dompurify 3.3.1 → 3.3.2
- naive-ui 2.43.2 → 2.44.0
- vue-i18n 11.2.8 → 11.3.0
- @cloudflare/workers-types 4.20260305.1 → 4.20260307.1
- @types/node 25.3.3 → 25.3.5
- wrangler 4.70.0 → 4.71.0 (all subprojects)
* feat: upgrade @simplewebauthn packages from v10 to v13
Breaking changes addressed:
- [v11] startRegistration/startAuthentication now take object param
- [v11] registrationInfo.credential replaces flat destructuring
- [v11] authenticator param renamed to credential in verifyAuthenticationResponse
- [v13] @simplewebauthn/types removed, types imported from @simplewebauthn/server
Packages:
- @simplewebauthn/server: 10.0.1 → 13.2.3
- @simplewebauthn/browser: 10.0.0 → 13.2.2
- @simplewebauthn/types: removed (deprecated)
* test: add passkey API E2E tests
- User registration and login flow
- register_request/authenticate_request return valid WebAuthn options
- authenticate_response with invalid credential returns 404
- register_response with invalid credential returns error
- Passkey list empty for new user
- Rename/delete operations with validation
* fix: use UI login instead of localStorage injection in browser passkey test
The localStorage approach doesn't work with VueUse's useStorage because
it doesn't detect external changes during page navigation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: hash password before registration to match frontend login behavior
The frontend hashes passwords with SHA-256 before sending to the API.
Registration via API must use the same hashed password so that UI login
matches the stored value.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: allow crypto.subtle in Docker browser tests
The frontend uses crypto.subtle for password hashing, which requires
a secure context (HTTPS or localhost). In Docker, the frontend runs
at http://frontend:5173 which is not a secure context. Add Chromium
flag to treat this origin as secure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: serve frontend over HTTPS in Docker for WebAuthn secure context
WebAuthn (navigator.credentials) and crypto.subtle both require a
secure context (HTTPS or localhost). The Docker frontend was serving
over HTTP, making passkey operations impossible.
Changes:
- Generate self-signed cert in Dockerfile.frontend
- Configure Vite to serve over HTTPS
- Update FRONTEND_URL to https://
- Add ignoreHTTPSErrors to Playwright browser config
- Use localStorage injection for passkey test login
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add Vite proxy to avoid mixed-content blocking in HTTPS Docker frontend
HTTPS pages cannot make HTTP API requests (mixed content). Add a Vite
proxy for all API paths so the browser makes same-origin HTTPS requests,
which Vite proxies to the HTTP worker server-to-server.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: store userJwt without JSON.stringify in localStorage
VueUse's useStorage with a string default uses raw string serialization
(no JSON wrapping). Using JSON.stringify added double quotes around the
JWT token, causing 401 Unauthorized from the worker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: clean up passkey API test per review feedback
Remove unused variables and rename test to match actual behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add STARTTLS support for SMTP proxy server
Add smtp_tls_cert and smtp_tls_key environment variables to enable
STARTTLS on the SMTP proxy server, matching existing IMAP TLS support.
Closes#249
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add E2E tests for SMTP/IMAP STARTTLS
- Add smtp-proxy-tls service with self-signed certs in docker-compose
- Add smtp-tls.spec.ts: SMTP STARTTLS send plain/HTML/auth tests
- Add imap-tls.spec.ts: IMAP STARTTLS login/list/select/fetch tests
- Register smtp-proxy project in playwright.config.ts
- Wait for TLS proxy readiness in docker-entrypoint.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enforce auth over TLS when STARTTLS is configured
- Set auth_require_tls conditionally based on tls_context presence
- Disable insecure SSLv2/SSLv3 protocols in TLS context
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace cert-gen service with inline cert generation
The cert-gen one-shot container was exiting immediately after
generating certificates, triggering --abort-on-container-exit
and stopping all services before tests could run.
Replace with an entrypoint script in smtp-proxy-tls that generates
the self-signed cert before starting the proxy server.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
refactor: modularize IMAP server with fixes and E2E tests
- Modularize IMAP server into imap_server, imap_mailbox, imap_message,
imap_http_client, parse_email, config, models
- Support dual login: JWT token and address+password via backend
- Add STARTTLS support with configurable TLS cert/key
- Fix FETCH/STORE returning UID instead of sequence number (RFC 3501)
- Implement IMessageFile.open() for correct BODY[] raw MIME delivery
- Add UIDNEXT to SELECT response via _cbSelectWork override
- Use per-restart UIDVALIDITY to force client resync
- Pass raw MIME to SimpleMessage for accurate RFC822.SIZE
- Fix SENT mailbox returning empty source
- Handle CREATE command gracefully for Thunderbird compatibility
- Add IMAP E2E tests: auth, LIST, SELECT, STATUS, FETCH, SEARCH,
STORE, UID FETCH, BODY[] integrity, size, seq numbers, SENT mailbox
- Add SMTP E2E tests using nodemailer: send plain/HTML, auth failure,
sendbox verification
- Add sendTestMail helper using admin/send_mail
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>