On this page
Choose who should own delivery.
Every path can avoid needless SMTP reconnects. Start with the programming model you want to keep; each card jumps to the performance benefit, ownership boundary, and complete example.
There is one underlying connection-pool implementation and several ways to orchestrate it. The right choice depends less on raw throughput than on
which layer already owns your messages, work queue, Jakarta Mail Session, and shutdown boundary.
Single physical-pool owner Exactly one component owns the physical connection pool. Never placebatch-moduleor a direct pool around ansmtppooltransport.
Follow the ownership lane
Each row is a complete lifecycle. Moving across rows mid-flow creates ambiguous release, failure, and shutdown behavior.
close()connect / closeCompare the six options
| 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 |
1. Reuse one connection without a pool
Use this when reconnecting is the only overhead and the work is naturally sequential. The scoped sender keeps one connection open and closes it when the callback ends. There is no 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 callback owns 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
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.
Invalid double-pooling combinations
batch-module → smtppool
Rejected during Session registration. Both layers would believe they own release and shutdown.
direct pool → smtppool
Do not allocate provider-owned pooled Transports inside a caller-owned direct pool.
Mailer pool → wrapped provider
Do not change a pooled Mailer's physical protocol to smtppool. Use one integration path.
A Spring application should normally choose the provider path if it wants to keep JavaMailSender ownership. A Simple Java Mail application
should normally choose the Mailer path. Wrapping either in the standalone facade adds a second owner without adding pooling capability.
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.