smtp-connection-pool keeps Jakarta Mail Transport connections open and leases each physical connection exclusively to one caller at a time. It supports lazy or eager allocation, bounded waiting, expiration, and clusters of SMTP servers.
It does not build messages or decide how SMTP works. The selected Jakarta Mail provider still owns authentication, TLS, EHLO, PIPELINING, CHUNKING, response parsing, and the actual send.
Version 4.0.0 expands the project beyond its direct API with optional Jakarta Mail and Camel integrations. Existing users can keep the same org.simplejavamail:smtp-connection-pool dependency and direct API.
The best introduction is the non-published demo project. Every example runs against a real dummy SMTP server on a random port and asserts message delivery, physical connection reuse, and clean shutdown.
| Runnable example | Integration shown | Verified result |
|---|---|---|
| DirectPoolDemo | Direct leases, release, forced failure, invalidation, and recovery | 3 messages over 1 connection; then replacement after a dropped connection |
| SimpleJavaMailDemo | Simple Java Mail as a higher-level library built directly on the pool | 3 messages over 1 connection |
| BatchModuleDemo | Standalone Simple Java Mail batch callbacks over caller-created Jakarta Mail messages | 3 messages over 1 connection |
| JakartaMailDemo | Plain Jakarta Mail with smtppool |
3 messages over 1 connection |
| SpringDemo | Spring JavaMailSenderImpl with smtppool |
3 messages over 1 connection |
| CamelDemo | Camel with the separate smtppool: adapter |
3 messages over 1 connection |
Run the complete executable suite with JDK 21:
mvn -pl smtp-connection-pool-demo -am testOr run DemoLauncher or any individual demo directly from IntelliJ. The demo is built and tested with the rest of the project, but is deliberately excluded from Maven Central. BatchModuleDemo uses the supported standalone path-2 API published in Simple Java Mail 9.3.0 through #698.
Start by deciding whether you need a pool at all. Then choose the one layer that should own it:
| Option | Best when | Orchestration owner | Lease handling | Pooling/clustering |
|---|---|---|---|---|
No pool: withOpenConnection / simple batch |
One sequential unit of work | Application / Simple Java Mail | Not applicable | No |
Simple Java Mail Mailer + batch-module |
Using EmailBuilder and Mailer |
Simple Java Mail | Automatic | Yes |
Standalone batch-module facade |
Creating MimeMessage objects while wanting managed callbacks and futures |
Batch facade | Automatic | Yes |
Direct smtp-connection-pool |
Needing exact claim, failure, and shutdown control | Application | Explicit lease | Yes |
Jakarta smtppool provider |
Plain Jakarta Mail or Spring owns Transport calls |
Provider | Mapped to connect / close |
Yes |
Camel smtppool: adapter |
Camel owns endpoints and component lifecycle | Camel adapter / provider | Automatic | Yes |
Exactly one component owns the physical connection pool. Never place
batch-moduleor a direct pool around ansmtppooltransport.
The complete chooser, ownership model, and examples live in Simple Java Mail's SMTP connection pooling and batch orchestration guide.
The pooled choices reduce to three integration paths, not three abstraction levels inside this repository.
| Path | Choose it when | Who manages the pool | Status |
|---|---|---|---|
| 1. Use the pool directly | Your application or a higher-level library needs clustering, explicit leases, and complete failure/shutdown control. Simple Java Mail itself belongs here. | Your application or library | Available; explicit SmtpTransportLease is available since 4.0.0 |
2. Use Simple Java Mail's batch-module directly |
You create Jakarta Mail messages yourself but want Simple Java Mail's asynchronous batch engine and a safe callback API without adopting EmailBuilder and Mailer. |
Simple Java Mail's batch API | Available since Simple Java Mail 9.3.0 through #698 |
3. Use it as a Jakarta Mail Transport |
Plain Jakarta Mail, Spring, or Camel already obtains and closes Transport instances. |
PooledTransport |
Available since 4.0.0 through #10 |
Simple Java Mail stays on path 1 internally. Path 2 is a narrower public API over part of its batch engine. Path 3 presents the pool as a normal Jakarta Mail transport protocol.
The architecture, ownership rules, and five flow diagrams are in PRODUCT-VISION.md.
All three published modules are released together at one version. Maven Central also receives the shared smtp-connection-pool-parent POM required by Maven; it is build metadata, not a fourth application dependency.
| Artifact | Purpose | Java |
|---|---|---|
org.simplejavamail:smtp-connection-pool |
Direct and clustered pool APIs plus SmtpTransportLease |
8+ |
org.simplejavamail:smtp-connection-pool-jakarta-provider |
Discoverable smtppool Jakarta Mail provider and Session-scoped lifecycle registry |
8+ |
org.simplejavamail:smtp-connection-pool-camel |
Optional Camel Mail selection adapter; pooling remains in the provider module | 17+ (Camel 4.22) |
Starting with 4.0.1, every published JAR declares a stable Automatic-Module-Name:
| Artifact | Module name |
|---|---|
smtp-connection-pool |
org.simplejavamail.smtpconnectionpool |
smtp-connection-pool-jakarta-provider |
org.simplejavamail.smtpconnectionpool.jakarta |
smtp-connection-pool-camel |
org.simplejavamail.smtpconnectionpool.camel |
The transitive object-pool chain is stable as well: generic-object-pool 2.5.0 declares
org.bbottema.genericobjectpool, and clustered-object-pool 4.1.0 declares
org.bbottema.clusteredobjectpool. The build inspects every packaged manifest and compiles a real
module-path consumer requiring all five names.
The repository also contains smtp-connection-pool-demo. It is an example project tested with every build, not a fourth published module, and is explicitly excluded from Maven Central.
The provider module lets Jakarta Mail, Spring, and Camel obtain pooled Transport instances; it does not speak SMTP itself. Applications still supply Angus Mail or another compatible SMTP Transport provider. Each underlying Transport must represent one reusable physical connection—do not hide a second connection pool beneath this one.
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>smtp-connection-pool</artifactId>
<version>4.1.0</version>
</dependency>Create a Session normally, then claim one exclusive lease. Closing an active lease releases it; invalidate it first when a failure makes the connection uncertain.
SmtpConnectionPool pool = new SmtpConnectionPool(new SmtpClusterConfig<Session>());
try (SmtpTransportLease lease = pool.claimTransport(session)) {
try {
Session selectedSession = lease.getSession();
Transport transport = lease.getTransport();
transport.sendMessage(message, message.getAllRecipients());
} catch (MessagingException | RuntimeException failure) {
lease.invalidate();
throw failure;
}
}
// Application shutdown: wait until active leases return and connections close.
pool.shutDown().get();claimTransport can block and throws InterruptedException. Preserve the thread's interruption policy. The default pool is lazy, has a maximum of four physical connections per Session, and makes an available transport eligible for expiration ten seconds after its last claim.
For partial-recipient failures, an advanced integration may release rather than invalidate only when the delegate is demonstrably still connected. Unknown failures should be treated conservatively.
Use the original claimTransport(session) unless the job needs cancellation or its own acquisition budget. For example,
a user stops an export while every SMTP connection is busy: the export should leave the queue without closing another
job's connection or shutting down the pool.
ClaimControl control = new ClaimControl();
ClaimOptions options = ClaimOptions.withTimeout(30, TimeUnit.SECONDS)
.withClaimControl(control);
// On the job's worker:
try (SmtpTransportLease lease = pool.claimTransport(session, options)) {
// Send using this lease; invalidate it on an uncertain transport failure, as above.
}
// From the job's Stop handler, on another thread:
control.requestCancellation();Both types come from org.bbottema.genericobjectpool. Creating a control does not request cancellation. A cancelled
claim throws CancellationException; budget expiry throws IllegalStateException, matching the original SMTP-pool
timeout result. Thread interruption remains InterruptedException. The same options work with clustered keyed and
load-balanced claims.
The budget starts before registration and selection, includes waiting and connection preparation, and is capped by the configured cluster timeout. It is cooperative, not a hard socket deadline: a running credential supplier, authenticator, DNS lookup or unsupported provider operation must return before the claim can settle. Late transports are disposed, not handed out. Keep provider connection/read/write timeouts configured as well.
Once a lease is handed out, its acquisition control no longer owns it. Retaining or requesting that control cannot revoke the lease or affect its next borrower. The stopped-job demo runs this scenario against a local SMTP server and then reuses the same connection for a healthy send.
Physical abort needs provider support. The pool has no built-in Angus abort adapter. An integration that owns a capable
physical provider can supply a TransportCancellationSupport through SmtpClusterConfig.withTransportCancellationSupport(...).
Its createAbortAction(transport) is called before connect and returns Optional<Runnable>: empty for unsupported
transports, or a quick, thread-safe, latched abort action covering this transport's current and replacement sockets.
Configure this before building the pool; it replaces the allocator factory. The caller-owned Session is never modified.
Optional<TransportCancellation> cancellation = lease.getCancellation();
if (cancellation.isPresent() && cancellation.get().request()) {
// Observe the actual send/connect operation exiting, and then its cleanup:
lease.getDisposalCompletion().toCompletableFuture().get();
}An effective request atomically marks the lease unusable before invoking the provider. A racing close() cannot release
that connection as healthy, and an old lease's handle cannot abort a new borrower. An unsupported capability is absent,
not a successful no-op. This API does not mean a message was unsent: keep the actual sending operation's result, including
an accepted final reply that arrived before a late request.
Disposal acknowledgement is separate from operation completion and preserves cleanup failures. It may remain pending
after a healthy release until the physical object is eventually retired. Cancelling the returned stage cannot cancel the
pool's cleanup. A raw borrowed Transport does not let the pool infer when arbitrary caller code has stopped using it.
| Provider / route | Cancel pending pool waiting | Abort physical connect/send |
|---|---|---|
| Angus plain SMTP | Yes, with claim options | Not built in; observed when the provider returns |
| Angus STARTTLS or implicit TLS | Yes, with claim options | Not built in; TLS/socket replacement needs a provider-owned hook |
| Angus HTTP/SOCKS proxy routing | Yes, with claim options | Not built in; routing and proxy authentication remain provider-owned |
| Custom provider with configured cancellation support | Yes | Only within that provider's documented capability; must cover replacement sockets |
| Unsupported custom provider | Yes | No; the optional lease capability is absent |
Jakarta smtppool, Spring and Camel integration |
Existing interruptible waiting | No per-send control property; existing provider ownership stays unchanged |
See the Angus integration boundary for why Transport.close() is not an abort adapter.
SmtpClusterConfig<UUID> config = new SmtpClusterConfig<>();
config.getConfigBuilder()
.defaultCorePoolSize(0)
.defaultMaxPoolSize(10)
.loadBalancingStrategy(new RandomAccessLoadBalancing<>())
.claimTimeout(new Timeout(30, SECONDS));
SmtpConnectionPoolClustered<UUID> pool = new SmtpConnectionPoolClustered<>(config);
ResourceClusterAndPoolKey<UUID, Session> server =
new ResourceClusterAndPoolKey<>(clusterId, session);
try (SmtpTransportLease lease = pool.claimTransport(server)) {
lease.getTransport().sendMessage(message, message.getAllRecipients());
}Clusters and pools are created on demand. Use registerResourceCluster or registerResourcePool when a cluster or server needs different sizing, expiration, or load-balancing behavior.
The direct allocator resolves a thread-safe token supplier only when it opens or reconnects a physical transport:
session.getProperties().put(
SmtpConnectionPool.OAUTH2_TOKEN_PROVIDER_PROPERTY,
(Supplier<String>) tokenProvider::getAccessToken);The supplier owns caching and refresh. A fixed token remains available through OAUTH2_TOKEN_PROPERTY for short-lived use.
This path is deliberately delivered in Simple Java Mail, not in this repository. Simple Java Mail #698 added a public callback API in Simple Java Mail 9.3.0 for applications that already create Jakarta Mail messages but want asynchronous execution, clustering, and safe release/invalidate handling without adopting the full EmailBuilder/Mailer API.
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>batch-module</artifactId>
<version>9.3.0</version>
</dependency>try (BatchTransportExecutor<String> batch =
BatchTransportExecutor.<String>builder().build()) {
batch.registerSession("outbound", session);
batch.execute("outbound", (selectedSession, transport) -> {
MimeMessage message = new MimeMessage(selectedSession);
// populate recipients, subject, and content
transport.sendMessage(message, message.getAllRecipients());
return null;
});
}BatchTransportExecutor uses SmtpTransportLease internally and remains separate from smtppool. It releases after a successful callback, invalidates after an escaping failure, and owns only the default executor it creates. BatchModuleDemo runs this exact path against a real dummy SMTP server.
Add the provider plus a physical Jakarta Mail implementation:
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>smtp-connection-pool-jakarta-provider</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>org.eclipse.angus</groupId>
<artifactId>angus-mail</artifactId>
<version>2.0.5</version>
</dependency>The provider registers only smtppool; it never replaces or pretends to be smtp or smtps.
Properties properties = new Properties();
properties.setProperty(SmtpPoolProperties.DELEGATE_PROTOCOL, "smtp"); // default
Session session = Session.getInstance(properties);
Transport transport = session.getTransport("smtppool");
transport.connect(host, port, username, password); // claims and connects if needed
try {
transport.sendMessage(message, message.getAllRecipients());
} finally {
transport.close(); // releases a healthy lease; invalidates an unhealthy one
}
Future<?> shutdown = SmtpPoolRegistry.shutdown(session); // graceful
shutdown.get();Graceful shutdown stops new claims and waits for active leases. If a bounded wait expires, shutdownNow(session) invalidates active leases and returns the same completion handle, which completes only after physical Transport.close() calls finish. A Session can be reused only after that shutdown completes and SmtpPoolRegistry.restart(session) is called explicitly.
When credentials or OAuth tokens rotate for the same endpoint, a new credential-isolated pool becomes current and the superseded pool drains. Its retained credential material is cleared as soon as its last active lease finishes; inactive credential generations do not accumulate.
Spring uses the same provider by configuring JavaMailSenderImpl with protocol smtppool. Camel uses the separate adapter and smtppool: or smtppools: endpoint schemes:
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>smtp-connection-pool-camel</artifactId>
<version>4.1.0</version>
</dependency>to("smtppool://smtp.example.com:587"
+ "?username=user&password=secret&to=recipient@example.com");See the provider reference and Camel reference for configuration, programmatic delegate selection, credential rotation, and shutdown semantics.
Build the complete project with JDK 21 and Maven. The original pool and Jakarta provider still run on Java 8; the Camel and demo modules require Java 17.
mvn clean verifyVerification runs all module tests, the real-server demo smoke tests, SpotBugs, Javadocs, and a checksum-pinned japicmp comparison with the preceding published 4.0.2 library. CircleCI also compiles and tests the original pool plus Jakarta provider on an actual JDK 8. Its JDK 21 release job versions and publishes all public modules together while leaving out the demo.
Simple Java Mail #699 may produce a faster physical transport. If it implements Jakarta Mail's synchronous Transport contract with one reusable physical session per instance, it can be selected beneath these paths just like Angus. If it instead uses Simple Java Mail's CustomMailer, it owns its own lifecycle and must not be stacked on this pool.
4.1.0 (8 September 2026)
- #31: opt-in acquisition cancellation and total budgets, using
clustered-object-pool 4.1.0andgeneric-object-pool 2.5.0. Existing calls, Java baselines and provider selection remain supported. - Optional provider-neutral physical abort with lease-scoped ownership and separate disposal acknowledgement. Angus physical abort remains unsupported; pending acquisition is independently useful.
- Failed or cancelled connection preparation closes partially created transports. Cleanup failures remain observable, and the stopped-job demo shows subsequent healthy connection reuse.
- #27: update the optional Camel adapter to Camel 4.22.0, fixing CVE-2026-59230 in MIME multipart unmarshalling with
headersInline=true. The Java 17 baseline is unchanged.
Older releases are recorded in RELEASE.txt.