feat: add STARTTLS support for SMTP proxy server (#876)

* 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>
This commit is contained in:
Dream Hunter
2026-03-06 15:05:29 +08:00
committed by GitHub
parent 8341cae28f
commit 8cf1150b15
14 changed files with 307 additions and 11 deletions

View File

@@ -1,6 +1,8 @@
proxy_url=https://temp-email-api.xxx.xxx
port=8025
imap_port=11143
# smtp_tls_cert=/path/to/cert.pem
# smtp_tls_key=/path/to/key.pem
# imap_tls_cert=/path/to/cert.pem
# imap_tls_key=/path/to/key.pem
# imap_cache_size=500

View File

@@ -15,6 +15,8 @@ class Settings(BaseSettings):
port: int = 8025
imap_port: int = 11143
basic_password: str = ""
smtp_tls_cert: str = ""
smtp_tls_key: str = ""
imap_tls_cert: str = ""
imap_tls_key: str = ""
imap_cache_size: int = 500

View File

@@ -1,6 +1,8 @@
import asyncio
import logging
import email
import ssl
import httpx
from aiosmtpd.controller import Controller
@@ -132,17 +134,35 @@ class CustomSMTPHandler:
def start_smtp_server():
handler = CustomSMTPHandler()
tls_context = None
has_cert = bool(settings.smtp_tls_cert)
has_key = bool(settings.smtp_tls_key)
if has_cert != has_key:
raise ValueError(
"Both smtp_tls_cert and smtp_tls_key must be set together"
)
if has_cert and has_key:
_logger.info("TLS enabled for SMTP (STARTTLS)")
tls_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
tls_context.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
tls_context.load_cert_chain(settings.smtp_tls_cert, settings.smtp_tls_key)
server = Controller(
handler,
hostname="",
port=settings.port,
auth_require_tls=False,
auth_require_tls=bool(tls_context),
decode_data=True,
authenticator=handler.authenticator,
auth_exclude_mechanism=["DONT"]
auth_exclude_mechanism=["DONT"],
tls_context=tls_context,
)
_logger.info("Starting SMTP server on port %s", settings.port)
_logger.info(
"Starting SMTP server on port %s tls=%s",
settings.port, bool(tls_context),
)
server.start()
loop = asyncio.new_event_loop()
@@ -156,7 +176,8 @@ def start_smtp_server():
if __name__ == "__main__":
_logger.info(
"Starting SMTP server proxy_url=%s port=%s",
"Starting SMTP server proxy_url=%s port=%s tls=%s",
settings.proxy_url, settings.port,
bool(settings.smtp_tls_cert and settings.smtp_tls_key),
)
start_smtp_server()