Build and configure

SMTP connection pooling and batch orchestration

Reuse SMTP connections without giving up the API or framework that already owns delivery. See how Simple Java Mail, batch-module, direct pooling, Jakarta Mail, Spring, and Camel share one physical-pool foundation.

§

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.

Connection pooling abstraction paths Your application can reuse one scoped connection without a pool, or enter the shared smtp-connection-pool through Simple Java Mail, batch-module, the direct API, the Jakarta provider, or the Camel component. Your application One scoped connection No pool simple-java-mail batch-module smtp-connection-pool smtp-connection-pool-jakarta-provider smtp-connection-pool-camel 01Reuse one connectionwith Simple Java Mail 02Simple Java Mail'sfully managedconnection pool 03Managed pooling forapplication-ownedMimeMessages 04Pooling for your owndelivery infrastructure 05Pooling for existingJakarta Mail or Springcode 06Pooling for existingCamel mail routes
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.

05

Add Connection Pooling to existing Jakarta Mail or Spring code

  • You get Keep your existing Transport.connect() / close() or JavaMailSender workflow 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-provider over smtp-connection-pool, using the smtppool protocol.
06

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-camel over the Jakarta provider and smtp-connection-pool, using smtppool: or smtppools:.
§

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

  1. Delivery work

    Caller and Mailer run one sequential operation.

  2. SMTP connection

    Mailer opens and closes one Transport.

  3. Shutdown

    The operation ends the connection. No pool remains.

§

1. Reuse one connection without a pool

Owner: Simple Java MailSequentialNo clustering

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.

This gives you

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

Owner: Simple Java MailAutomatic leasesClusters

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.

This gives you

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

Owner: BatchTransportExecutorCallbacksCompletableFuture

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.

This gives you

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

Owner: ApplicationExplicit leaseMaximum control

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.

This gives you

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

Owner: smtppool providerJakarta MailSpring

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.

This gives you

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

Owner: Camel + providerRoutesJava 17+

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.

This gives you

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

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.