Annotation-driven SMTP mail client for the GuicedEE / Vert.x stack. Declare SMTP connections with annotations — everything is discovered at startup via ClassGraph, wired through Guice, and managed by the Vert.x Mail Client with connection pooling.
Built on Vert.x Mail Client · Google Guice · JPMS module com.guicedee.mailclient · Java 25+
<dependency>
<groupId>com.guicedee</groupId>
<artifactId>mail-client</artifactId>
</dependency>Gradle (Kotlin DSL)
implementation("com.guicedee:mail-client:2.0.2-SNAPSHOT")- Annotation-driven setup —
@MailConnectionOptionsdeclares the entire SMTP configuration - Auto-discovery —
MailClientPreStartupscans the classpath via ClassGraph to find all annotated connection declarations - Guice-managed mail service —
MailServiceinstances are injectable by@Named("connection-name")with convenience send methods - Connection pooling — shared Vert.x
MailClientinstances with configurable pool size - StartTLS & SSL — full TLS configuration with
DISABLED,OPTIONAL, orREQUIREDmodes - DKIM signing — enable DomainKeys Identified Mail signature signing
- Default from address — configurable per-connection default sender
- Attachments — full
MailMessageAPI for attachments, inline images, CC/BCC, HTML, and multipart - Environment variable overrides — every annotation attribute can be overridden via system properties or environment variables at runtime
- Graceful shutdown — all mail clients are closed on application shutdown
Step 1 — Define a connection. Place @MailConnectionOptions on a class or package-info.java:
@MailConnectionOptions(
value = "notifications",
hostname = "smtp.example.com",
port = 587,
startTls = MailConnectionOptions.StartTLSMode.REQUIRED,
login = MailConnectionOptions.LoginMode.REQUIRED,
username = "user@example.com",
password = "secret",
defaultFrom = "noreply@example.com"
)
package com.example.mail;Or on a class:
@MailConnectionOptions(
value = "notifications",
hostname = "smtp.example.com",
port = 587,
startTls = MailConnectionOptions.StartTLSMode.REQUIRED,
login = MailConnectionOptions.LoginMode.REQUIRED,
username = "user@example.com",
password = "secret",
defaultFrom = "noreply@example.com"
)
public class MailConfig {
}Step 2 — Inject and send:
public class NotificationService {
@Inject
@Named("notifications")
private MailService mailService;
public void sendWelcome(String to) {
mailService.sendHtml(to, "Welcome!", "<h1>Welcome aboard!</h1>");
}
public void sendReport(String to) {
mailService.sendText(to, "Daily Report", "Here is your daily report...");
}
}Step 3 — Configure module-info.java:
module my.app {
requires com.guicedee.mailclient;
opens my.app.mail to com.google.guice, com.guicedee.mailclient;
}Step 4 — Bootstrap GuicedEE:
IGuiceContext.instance().inject();That's it. MailClientPreStartup discovers the annotations, MailClientModule creates the Guice bindings with shared pooled mail clients, and your MailService is ready to send.
flowchart TD
n1["Startup"]
n2["IGuiceContext.instance()"]
n1 --> n2
n3["MailClientPreStartup<br/>IGuicePreStartup — annotation scanning"]
n2 --> n3
n4["Discovers @MailConnectionOptions<br/>classes and package-info.java"]
n3 --> n4
n5["Wraps with environment variable resolution"]
n3 --> n5
n6["MailClientModule<br/>IGuiceModule — Guice bindings"]
n2 --> n6
n7["Creates shared MailClient per connection<br/>pooled"]
n6 --> n7
n8["Binds MailClient as @Named('connection-name')"]
n6 --> n8
n9["Binds MailService as @Named('connection-name') singleton"]
n6 --> n9
n10["MailClientPostStartup<br/>IGuicePostStartup — initialization logging"]
n2 --> n10
n11["MailClientPreDestroy<br/>IGuicePreDestroy — shutdown"]
n2 --> n11
n12["Closes all mail clients"]
n11 --> n12
Send:
MailService.sendHtml(to, subject, html)
→ MailMessage (auto-fills from if defaultFrom set)
→ MailClient.sendMail(message)
→ Future<MailResult> (messageId, recipients)
Placed on a class or package-info.java to declare an SMTP connection:
| Attribute | Default | Purpose |
|---|---|---|
value |
"default" |
Connection name (used for @Named binding) |
hostname |
"localhost" |
SMTP server hostname |
port |
25 |
SMTP server port |
startTls |
OPTIONAL |
StartTLS mode: DISABLED, OPTIONAL, REQUIRED |
login |
NONE |
Login mode: DISABLED, NONE, REQUIRED |
username |
"" |
SMTP authentication username |
password |
"" |
SMTP authentication password |
ssl |
false |
Use SSL on connect (port 465) |
trustAll |
false |
Trust all server certificates |
ehloHostname |
"" |
EHLO hostname for message-id generation |
authMethods |
"" |
Space-separated allowed auth methods |
keepAlive |
true |
Enable connection pooling |
maxPoolSize |
10 |
Maximum pool connections |
allowRcptErrors |
false |
Continue if a recipient is rejected |
disableEsmtp |
false |
Disable ESMTP commands |
userAgent |
"" |
Mail user agent name |
enableDkim |
false |
Enable DKIM signature signing |
pipelining |
true |
Enable SMTP pipelining |
maxMailsPerConnection |
0 |
Max emails per connection (0 = unlimited) |
keepAliveTimeout |
300 |
Keep-alive timeout in seconds |
defaultFrom |
"" |
Default sender address |
@Inject
@Named("notifications")
private MailService mailService;
// Plain text (uses default from)
mailService.sendText("user@example.com", "Subject", "Body text");
// Plain text (explicit from)
mailService.sendText("from@example.com", "user@example.com", "Subject", "Body text");
// HTML (uses default from)
mailService.sendHtml("user@example.com", "Subject", "<h1>Hello</h1>");
// HTML (explicit from)
mailService.sendHtml("from@example.com", "user@example.com", "Subject", "<h1>Hello</h1>");
// Multipart (text + HTML)
mailService.sendMultipart("from@example.com", List.of("a@ex.com", "b@ex.com"),
"Subject", "Plain text fallback", "<h1>HTML body</h1>");For attachments, inline images, CC/BCC, custom headers:
MailMessage message = new MailMessage()
.setTo("user@example.com")
.setCc("cc@example.com")
.setBcc("bcc@example.com")
.setSubject("Report with attachment")
.setText("See attached report.")
.setHtml("<p>See attached <b>report</b>.</p>")
.setAttachment(List.of(
MailAttachment.create()
.setContentType("application/pdf")
.setName("report.pdf")
.setData(Buffer.buffer(pdfBytes))
));
mailService.send(message); // auto-fills from if defaultFrom is set| Method | Return | Description |
|---|---|---|
send(MailMessage) |
Future<MailResult> |
Send pre-built message (auto-fills from) |
sendText(to, subject, text) |
Future<MailResult> |
Plain text using default from |
sendText(from, to, subject, text) |
Future<MailResult> |
Plain text with explicit from |
sendHtml(to, subject, html) |
Future<MailResult> |
HTML using default from |
sendHtml(from, to, subject, html) |
Future<MailResult> |
HTML with explicit from |
sendMultipart(from, to, subject, text, html) |
Future<MailResult> |
Multipart (text + HTML) |
getMailClient() |
MailClient |
Access underlying Vert.x client |
getConnectionName() |
String |
Logical connection name |
getDefaultFrom() |
String |
Default from address |
All annotation attributes can be overridden at runtime via system properties or environment variables. Overrides are scoped by name — the lookup tries a name-specific key first, then falls back to a global key:
MAIL_{NORMALIZED_NAME}_{PROPERTY}— name-specific overrideMAIL_{PROPERTY}— global fallback- Annotation default
The name is normalized to uppercase with hyphens and dots replaced by underscores. For example, a connection named order-notifications checking the HOSTNAME property would resolve:
MAIL_ORDER_NOTIFICATIONS_HOSTNAME(name-specific)MAIL_HOSTNAME(global fallback)- The
hostnameattribute from the@MailConnectionOptionsannotation
| Property | Variable pattern |
|---|---|
value() |
MAIL_{name}_CONNECTION_NAME / MAIL_CONNECTION_NAME |
hostname() |
MAIL_{name}_HOSTNAME / MAIL_HOSTNAME |
port() |
MAIL_{name}_PORT / MAIL_PORT |
startTls() |
MAIL_{name}_START_TLS / MAIL_START_TLS |
login() |
MAIL_{name}_LOGIN / MAIL_LOGIN |
username() |
MAIL_{name}_USERNAME / MAIL_USERNAME |
password() |
MAIL_{name}_PASSWORD / MAIL_PASSWORD |
ssl() |
MAIL_{name}_SSL / MAIL_SSL |
trustAll() |
MAIL_{name}_TRUST_ALL / MAIL_TRUST_ALL |
ehloHostname() |
MAIL_{name}_EHLO_HOSTNAME / MAIL_EHLO_HOSTNAME |
authMethods() |
MAIL_{name}_AUTH_METHODS / MAIL_AUTH_METHODS |
keepAlive() |
MAIL_{name}_KEEP_ALIVE / MAIL_KEEP_ALIVE |
maxPoolSize() |
MAIL_{name}_MAX_POOL_SIZE / MAIL_MAX_POOL_SIZE |
allowRcptErrors() |
MAIL_{name}_ALLOW_RCPT_ERRORS / MAIL_ALLOW_RCPT_ERRORS |
disableEsmtp() |
MAIL_{name}_DISABLE_ESMTP / MAIL_DISABLE_ESMTP |
userAgent() |
MAIL_{name}_USER_AGENT / MAIL_USER_AGENT |
enableDkim() |
MAIL_{name}_ENABLE_DKIM / MAIL_ENABLE_DKIM |
pipelining() |
MAIL_{name}_PIPELINING / MAIL_PIPELINING |
maxMailsPerConnection() |
MAIL_{name}_MAX_MAILS_PER_CONNECTION / MAIL_MAX_MAILS_PER_CONNECTION |
keepAliveTimeout() |
MAIL_{name}_KEEP_ALIVE_TIMEOUT / MAIL_KEEP_ALIVE_TIMEOUT |
defaultFrom() |
MAIL_{name}_DEFAULT_FROM / MAIL_DEFAULT_FROM |
| Type | Named by | Example |
|---|---|---|
MailService |
Connection name | @Named("notifications") MailService mailService |
MailClient |
Connection name | @Named("notifications") MailClient client |
@Inject
@Named("notifications")
private MailClient mailClient;flowchart LR
com_guicedee_mailclient["com.guicedee.mailclient"]
com_guicedee_mailclient --> io_vertx_mail_client["io.vertx.mail.client<br/>Vert.x Mail Client"]
com_guicedee_mailclient --> com_guicedee_vertx["com.guicedee.vertx<br/>Vert.x lifecycle"]
com_guicedee_mailclient --> com_guicedee_client["com.guicedee.client<br/>GuicedEE SPI contracts"]
com_guicedee_mailclient --> io_github_classgraph["io.github.classgraph<br/>annotation scanning"]
com_guicedee_mailclient --> org_apache_commons_lang3["org.apache.commons.lang3<br/>StringUtils"]
Module name: com.guicedee.mailclient
The module:
- exports
com.guicedee.mailclient,com.guicedee.mailclient.implementations - provides
IGuicePreStartupwithMailClientPreStartup - provides
IGuiceModulewithMailClientModule - provides
IGuicePostStartupwithMailClientPostStartup - provides
IGuicePreDestroywithMailClientPreDestroy - opens
com.guicedee.mailclient,com.guicedee.mailclient.implementationstocom.google.guiceandcom.fasterxml.jackson.databind
| Class | Package | Role |
|---|---|---|
MailConnectionOptions |
mailclient |
Annotation declaring an SMTP connection with all configuration options |
MailService |
mailclient |
High-level mail service with convenience send methods (text, HTML, multipart) |
MailClientPreStartup |
implementations |
IGuicePreStartup — scans for all @MailConnectionOptions and registers metadata |
MailClientModule |
implementations |
IGuiceModule — creates shared MailClient instances and binds MailService |
MailClientPostStartup |
implementations |
IGuicePostStartup — initialization logging |
MailClientPreDestroy |
implementations |
IGuicePreDestroy — closes all mail clients on shutdown |
The test suite validates annotation scanning, Guice binding, and MailService injection without requiring a real SMTP server:
@BeforeAll
static void init() {
IGuiceContext.registerModule("com.guicedee.mailclient.test");
IGuiceContext.instance().inject();
}
@Test
void testMailServiceBound() {
var service = IGuiceContext.get(Key.get(MailService.class, Names.named("test-smtp")));
assertNotNull(service);
assertEquals("test-smtp", service.getConnectionName());
assertEquals("test@example.com", service.getDefaultFrom());
}Run tests:
mvn testIssues and pull requests are welcome — please add tests for new connection options or mail service features.