One application, six delivery paths
Every option starts in your application. Follow a solid green path for the programming model you want to keep; dashed links reveal which modules
provide the physical pool underneath. Path 01 reuses one scoped connection without a pool. Paths 02 through 06 converge on the same
smtp-connection-pool foundation.
Choose one delivery path Each numbered path is a complete way to reuse connections. Pick the one that matches the programming model and lifecycle owner you want to keep. Combining paths adds no capability and makes lifecycle ownership unclear.
On this page
Choose who should own delivery.
With the architecture in view, choose the boundary that matches your code. Each card states what you gain, who owns the lifecycle, and the API or module that preserves that boundary.
Reuse one connection with Simple Java Mail
↓- You get Connection reuse for one bounded, sequential burst, without shared pool state.
- Lifecycle Simple Java Mail opens and closes one Transport for the bounded
Maileroperation; no lease or pool exists. - Use
withOpenConnection(...)orsendMailsInSimpleBatch(...)fromsimple-java-mail. No connection pool.
Use Simple Java Mail's fully managed Connection Pool
↓- You get Concurrent, pooled and clustered sends without leaving
EmailBuilderandMailer. - Lifecycle Simple Java Mail owns work, Session selection, lease release or invalidation, and shutdown through
Mailer.close(). - Use
simple-java-mailwithbatch-module, backed bysmtp-connection-pool.
Manage pooled delivery for application-owned MimeMessages
↓- You get An SMTP connection pool without giving up your
MimeMessagecode. Each submission gives the callback a scopedTransportand returns aCompletableFuture. - Lifecycle The executor owns workers, leases, and the pool; your application registers Sessions and closes it.
- Use
BatchTransportExecutor<K>frombatch-module, backed bysmtp-connection-pool.
Add Connection Pooling to your own delivery infrastructure
↓- You get Only the reusable SMTP pool. Keep your existing queue, executor, routing, retry, and observability code instead of adopting another orchestration layer.
- Lifecycle Your infrastructure selects the Session, claims each Transport, releases healthy connections, invalidates failed ones, and shuts down the pool.
- Use
smtp-connection-pooldirectly throughSmtpConnectionPool.claimTransport(...)andSmtpTransportLease.
Add Connection Pooling to existing Jakarta Mail or Spring code
↓- You get Keep your existing
Transport.connect()/close()orJavaMailSenderworkflow while pooled SMTP connections are reused behind it. No new message API or work queue. - Lifecycle Jakarta Mail or Spring still opens and closes logical Transports. The provider maps those calls to leases; your application or container shuts down the Session pool.
- Use
smtp-connection-pool-jakarta-provideroversmtp-connection-pool, using thesmtppoolprotocol.
Add Connection Pooling to existing Camel mail routes
↓- You get Keep your routes, endpoints, exchanges, error handling, and physical SMTP settings. Only Transport selection changes; pooled connections are reused underneath Camel.
- Lifecycle Camel keeps route and exchange ownership. The adapter selects the provider; the provider owns leases and pooling; stopping the component shuts down pools for Sessions it owns.
- Use
smtp-connection-pool-camelover the Jakarta provider andsmtp-connection-pool, usingsmtppool:orsmtppools:.
Who owns the delivery lifecycle?
Read one option from delivery work to SMTP connection to shutdown. Keep all three responsibilities in that lane.
01Reuse one connection with Simple Java Mail
- Delivery work
Caller and Mailer run one sequential operation.
- SMTP connection
Mailer opens and closes one
Transport. - Shutdown
The operation ends the connection. No pool remains.
02Use Simple Java Mail's fully managed Connection Pool
- Delivery work
Simple Java Mail runs and tracks sends.
- SMTP connection
Simple Java Mail selects Sessions and manages leases.
- Shutdown
Mailer drains and closes everything through
close().
03Manage pooled delivery for application-owned MimeMessages
- Delivery work
BatchTransportExecutor runs callbacks and completes futures.
- SMTP connection
BatchTransportExecutor selects registered Sessions and manages leases.
- Shutdown
BatchTransportExecutor performs graceful or forced close.
04Add Connection Pooling to your own delivery infrastructure
- Delivery work
Your application queues, executes, routes and retries.
- SMTP connection
Your application claims, releases or invalidates every lease.
- Shutdown
Your application drains work and closes the pool.
05Add Connection Pooling to existing Jakarta Mail or Spring code
- Delivery work
Jakarta Mail or Spring keeps its existing send flow.
- SMTP connection
The provider maps
connect()andclose()to leases. - Shutdown
Your application or container closes the provider registry.
06Add Connection Pooling to existing Camel mail routes
- Delivery work
Camel owns routes, exchanges and error handling.
- SMTP connection
The adapter and provider select and manage pooled Transports.
- Shutdown
The Camel component closes pools for the Sessions it owns.
1. Reuse one connection without a pool
Use this when reconnecting is the only overhead and the work is naturally sequential. sendMailsInSimpleBatch(...) sends a known
collection over one connection; withOpenConnection(...) keeps one connection open while the caller drives the callback. Neither API
creates a shared lease or pool to shut down.
Most of the connection-reuse speed-up for a sequential burst, without sizing, sharing, or draining a pool. Stop here when one bounded
Mailer operation can own one SMTP connection; choose a pooled option when sends must overlap or Sessions must cluster.
Dependency: org.simplejavamail:simple-java-mail:9.3.0
try (Mailer mailer = MailerBuilder
.withSMTPServer("smtp.example.com", 587, "user", "secret")
.buildMailer()) {
mailer.withOpenConnection(sender -> {
for (Email email : emails) {
sender.sendMail(email);
}
});
}
If you already have all messages, mailer.sendMailsInSimpleBatch(emails, false) is the shorter equivalent. Move to a pool only when
independent work must run concurrently or several SMTP Sessions must form a cluster.
2. Let Simple Java Mail orchestrate pooled sends
This is the normal choice when you use EmailBuilder and Mailer. The optional batch module sections off the upstream pool
dependency and lets the Mailer own message conversion, asynchronous work, selected Session, success release, failure invalidation, and shutdown.
High-throughput pooled delivery while you stay in Simple Java Mail's fluent EmailBuilder and Mailer model. This is the
best default when you want connection reuse, concurrent sends, and clusters without turning lease safety, Session selection, or shutdown into
application code.
Dependencies: simple-java-mail:9.3.0 plus batch-module:9.3.0
try (Mailer mailer = MailerBuilder
.withSMTPServer("smtp.example.com", 587, "user", "secret")
.withConnectionPoolCoreSize(2)
.withConnectionPoolMaxSize(10)
.withConnectionPoolExpireAfterMillis(30_000)
.buildMailer()) {
mailer.sendMail(email); // sync or async, as configured
}
- A default executor belongs to the Mailer; a supplied executor remains caller-owned.
- The Mailer chooses the actual clustered Session and builds the message for it.
Mailer.close()waits for accepted work and closes that Mailer's registered pool.- Retry, circuit breaking, and ambiguous-delivery policy remain application concerns.
3. Use batch-module without EmailBuilder or Mailer
New in 9.3.0, BatchTransportExecutor<K> is the middle path for applications that own Jakarta Mail message construction but want
callback-scoped transports, cluster selection, futures, and deterministic lifecycle. It orchestrates
smtp-connection-pool; it is not another pool implementation.
The same high-throughput clustered pool and automatic lease safety while your application keeps building MimeMessage objects and
owning each unit of work. Choose this boundary when callbacks and futures fit, but adopting EmailBuilder and Mailer does not.
Dependency: org.simplejavamail:batch-module:9.3.0. The main simple-java-mail facade is not required.
<dependency>
<groupId>org.simplejavamail</groupId>
<artifactId>batch-module</artifactId>
<version>9.3.0</version>
</dependency>
Properties properties = new Properties();
properties.setProperty("mail.smtp.host", "smtp.example.com");
properties.setProperty("mail.smtp.port", "587");
properties.setProperty("mail.smtp.auth", "true");
Session session = Session.getInstance(properties, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("user", "secret");
}
});
try (BatchTransportExecutor<String> batch =
BatchTransportExecutor.<String>builder()
.withMaxPoolSize(8)
.withClaimTimeoutMillis(30_000)
.withExpireAfterMillis(60_000)
.build()) {
batch.registerSession("transactional", session);
CompletableFuture<Void> sent = batch.submit("transactional",
(selectedSession, transport) -> {
MimeMessage message = new MimeMessage(selectedSession);
message.setFrom("sender@example.com");
message.setRecipients(Message.RecipientType.TO, "recipient@example.com");
message.setSubject("Queued report");
message.setText("The report is ready.");
transport.sendMessage(message, message.getAllRecipients());
return null;
});
sent.join();
}
A cluster-selected callback receives the Session that actually won selection. When a message was already created for one registered Session,
use the exact-Session overload: batch.execute(clusterKey, session, operation).
The callback must not connect or close the Transport. A normal return releases it. Any escaping checked exception, runtime exception, or error invalidates it before the same failure is propagated or placed on the future.
4. Claim leases from smtp-connection-pool directly
Choose the direct API when your application must decide exactly when to claim, release, invalidate, or drain a pool. This is also how the full Simple Java Mail Mailer integrates internally.
High-throughput clustered pooling with every lifecycle lever exposed to your code. This boundary fits infrastructure and library code that must define claim, invalidation, draining, and shutdown policy; application teams take on that responsibility too.
Dependency: org.simplejavamail:smtp-connection-pool:4.0.1
SmtpConnectionPool pool =
new SmtpConnectionPool(new SmtpClusterConfig<Session>());
try (SmtpTransportLease lease = pool.claimTransport(session)) {
try {
lease.getTransport().sendMessage(message, message.getAllRecipients());
} catch (MessagingException | RuntimeException failure) {
lease.invalidate();
throw failure;
}
}
pool.shutDown().get();
The try-with-resources close releases an active lease. Invalidate first when connection health is uncertain. Your code owns interruption policy, active-work tracking, shutdown waiting, and every path that could otherwise leak a lease.
5. Present the pool as a Jakarta Mail Transport
Use the provider when existing code already speaks the Jakarta Mail Transport.connect() / close() lifecycle. It is the
natural fit for plain Jakarta Mail and Spring's JavaMailSenderImpl. The provider maps that lifecycle onto an internal lease.
Connection reuse behind the Jakarta Mail Transport contract your code or Spring already understands. Choose this boundary when
connect() / close() must stay framework-owned; the provider does not add message building, a work queue, or futures.
Dependencies: smtp-connection-pool-jakarta-provider:4.0.1 and one physical provider such as angus-mail:2.0.5
Properties properties = new Properties();
properties.setProperty(SmtpPoolProperties.DELEGATE_PROTOCOL, "smtp");
Session session = Session.getInstance(properties);
Transport transport = session.getTransport("smtppool");
transport.connect("smtp.example.com", 587, "user", "secret");
try {
transport.sendMessage(message, message.getAllRecipients());
} finally {
transport.close(); // release healthy; invalidate unhealthy
}
SmtpPoolRegistry.shutdown(session).get();
Spring configures the same provider with JavaMailSenderImpl#setProtocol("smtppool"). Application/framework executors and queued work
remain outside the provider; the Session-scoped registry owns pool draining and forced escalation.
6. Let Camel select the pooled provider
Choose the Camel adapter when Camel already owns endpoints, exchanges, and component lifecycle. The adapter selects the same provider; it does not contain a second pool.
Pooled connection reuse inside existing Camel mail routes while Camel keeps ownership of endpoints, exchanges, and route lifecycle. Choose this boundary for Camel 4.21+ / Java 17+ integration, not as a general-purpose batch API.
Dependency: org.simplejavamail:smtp-connection-pool-camel:4.0.1
from("direct:mail")
.to("smtppool://smtp.example.com:587"
+ "?username=user"
+ "&password=secret"
+ "&to=recipient@example.com");
Camel 4.21.x and the adapter require Java 17+. The original direct pool, Jakarta provider, and Simple Java Mail batch facade remain Java 8 compatible.
Failure, OAuth2, and shutdown semantics
Do not combine pooling paths
Each pooled option assumes one layer owns the complete connection lifecycle. Do not point batch-module, a pooled Mailer, or a
direct pool at an smtppool Transport. The provider beneath Camel is already part of path 06.
Successful work releases; uncertain failure invalidates
The Mailer and standalone batch facade make this decision automatically around their callback boundary. Direct users make it on the lease.
Provider users express it through Transport health and close(). Catch a recipient-level failure inside a callback only when you know the
physical SMTP conversation remains synchronized and reusable.
OAuth2 belongs to the selected Session
The standalone facade bridges its fixed-token and supplier properties when each Session is registered. A clustered claim therefore resolves the supplier from the Session actually selected, not from whichever Session happened to submit the work. Suppliers run only when a physical Transport connects or reconnects; reusing an already-connected Transport does not request a new token.
session.getProperties().put(
BatchTransportExecutor.OAUTH2_TOKEN_PROVIDER_PROPERTY,
(Supplier<String>) tokenService::currentAccessToken);
JPMS names are stable through the full pool chain
Simple Java Mail 9.3.0 consumes the fixed chain: org.bbottema.genericobjectpool,
org.bbottema.clusteredobjectpool, org.simplejavamail.smtpconnectionpool, and
org.simplejavamail.batch. The optional provider and Camel adapter add
org.simplejavamail.smtpconnectionpool.jakarta and org.simplejavamail.smtpconnectionpool.camel.
Release builds compile real module-path consumers so these manifest names cannot silently regress.
Graceful and forced shutdown have different jobs
- Graceful: stop accepting work, let accepted callbacks finish, then drain physical pools and close connections.
- Forced: also reject pending claims, invalidate active leases, and attempt to cancel queued module-owned work.
- Default executor: module-owned and stopped by the facade or Mailer.
- Supplied executor: caller-owned and left running; it must keep progressing accepted work during graceful shutdown.
Executable examples and deeper references
- Direct pool, standalone batch, Jakarta Mail, Spring, Camel, and Simple Java Mail smoke demos
- Direct pool and provider reference
- Mailer pool configuration and cluster configuration
- Mailer lifecycle and resource ownership
The upstream BatchModuleDemo
runs the standalone path against a real dummy SMTP server. It uses only the public batch API; the callback above and the generated
batch-module Javadocs remain the complete API references.