| type | Architecture | |||||
|---|---|---|---|---|---|---|
| title | ErmesMail Architecture Overview | |||||
| description | High-level architecture of ErmesMail covering package structure, class relationships, async execution model, and SMTP security modes. | |||||
| tags |
|
ErmesMail follows a straightforward layered design: a CLI entry point delegates to a service layer that manages async email delivery via a thread pool, using Apache Commons Email as the underlying transport.
All production code lives in a single package:
com.softinstigate.ermes.mail
This keeps the library small and embeddable. There are no sub-packages — the entire public API is eight classes.
┌─────────────────────────────────────────────────────────────────┐
│ Main (CLI entry point) │
│ - Parses args with picocli │
│ - Creates SMTPConfig + EmailModel │
│ - Calls EmailService.send() │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ EmailService │
│ - Owns an ExecutorService (fixed thread pool) │
│ - send() submits SendEmailTask → returns Future<List<String>> │
│ - sendSynch() calls SendEmailTask.call() directly │
│ - shutdown() terminates the executor │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SendEmailTask implements Callable<List<String>> │
│ - Configures HtmlEmail from SMTPConfig + EmailModel │
│ - Handles attachments, recipients, STARTTLS/SSL │
│ - Calls email.send() → returns error list │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Apache Commons Email (HtmlEmail) │
│ - javax.mail transport under the hood │
└─────────────────────────────────────────────────────────────────┘
- Input — Caller provides
SMTPConfig(server + security mode) andEmailModel(message + recipients). - Dispatch —
EmailService.send()wraps the task in aSendEmailTaskand submits to the thread pool. - Build —
SendEmailTask.call()creates anHtmlEmailviaHtmlEmailFactory, configures host/port/auth/security, sets HTML body, attaches files, adds recipients. - Send —
HtmlEmail.send()delegates to javax.mail for SMTP transport. - Result — Errors (if any) are collected in a
List<String>and returned through theFuture.
For synchronous usage, EmailService.sendSynch() bypasses the executor and calls SendEmailTask.call() directly on the calling thread.
sequenceDiagram
participant Caller
participant EmailService
participant Executor
participant Task as SendEmailTask
participant HtmlEmail
Caller->>EmailService: send(EmailModel)
Note over EmailService: Lazy: create pool on first send()
EmailService->>Executor: submit(SendEmailTask)
Executor->>Task: call()
Task->>HtmlEmail: create via HtmlEmailFactory
Task->>HtmlEmail: configure SMTP, security, content
HtmlEmail-->>Task: send() via transport
Task-->>Executor: errors list
Executor-->>EmailService: Future of errors
EmailService-->>Caller: Future of errors
EmailService async send flow with lazy thread pool initialization.
EmailService uses Executors.newFixedThreadPool(threadPoolSize) to parallelize email sends. Key behaviors:
- Thread pool size is configurable at construction (e.g.,
new EmailService(config, 3)uses 3 threads). The default constructor usesRuntime.getRuntime().availableProcessors(). - Lazy initialization — the
ExecutorServiceis created on the firstsend()call, not in the constructor. Applications that only usesendSynch()never create a thread pool. - poolSize = 0 — no internal pool is ever created;
send()executes synchronously and returns an already-completedFuture. This is designed for callers that manage concurrency externally (e.g., virtual threads in RestHeart).shutdown()is a no-op. - AutoCloseable —
EmailServiceimplementsAutoCloseable, so the pool (if created) is shut down automatically in try-with-resources blocks. - send() returns a
Future<List<String>>immediately; callers block onFuture.get()when they need the result. - shutdown() calls
executor.shutdown()followed byawaitTermination()with a 10-second default timeout. If the timeout elapses,shutdownNow()is called to force-terminate abandoned tasks. send()aftershutdown()throwsIllegalStateException.
The CLI (Main.java) uses a pool size of 1 and blocks on Future.get() immediately, so it behaves synchronously.
Introduced in v2.0.0, SMTPConfig.SecurityMode is an enum that expresses the transport security intent:
| Mode | Factory Method | Behavior |
|---|---|---|
PLAIN |
SMTPConfig.forPlain(...) |
No encryption (port 25/1025) |
SSL |
SMTPConfig.forSsl(...) |
Implicit TLS on connect (port 465) |
STARTTLS_OPTIONAL |
SMTPConfig.forStartTlsOptional(...) |
Upgrade to TLS if server supports it, otherwise plaintext |
STARTTLS_REQUIRED |
SMTPConfig.forStartTlsRequired(...) |
Fail if server doesn't advertise STARTTLS |
The CLI maps --sslon → SSL, --starttls → STARTTLS_OPTIONAL, --starttls-required → STARTTLS_REQUIRED. Flags --sslon and --starttls are mutually exclusive (validated in Main.call()).
HtmlEmailFactory is an interface that abstracts HtmlEmail creation. Production code uses DefaultHtmlEmailFactory; tests inject a Mockito mock to verify STARTTLS/SSL configuration without a live SMTP server.
This pattern was introduced in v2.0.0 specifically to make SendEmailTask testable.
Since v2.1.0, SMTPConfig.toString() redacts the username and EmailModel.toString() redacts the message body. Both classes have toSecureString() methods that omit credentials and content entirely, reporting only metadata (hostname, port, security mode, recipient counts).
EmailService and SendEmailTask use toSecureString() for their log lines, so default log output never exposes passwords or email content.