Specification: Ballerina Email Library
Owners: @Maninda @wggihan @shafreenAnfar
Reviewers: @shafreenAnfar @Maninda
Created: 2020/04/24
Updated: 2026/09/03
Edition: Swan Lake
Introduction
This is the specification for the Email standard library of Ballerina language, which provides functionalities related to sending/receiving emails via SMTP, POP3, and IMAP protocols.
The Email library specification has evolved and may continue to evolve in the future. The released versions of the specification can be found under the relevant GitHub tag.
If you have any feedback or suggestions about the library, start a discussion via a GitHub issue or in the Discord server. Based on the outcome of the discussion, the specification and implementation can be updated. Community feedback is always welcome. Any accepted proposal which affects the specification is stored under /docs/proposals. Proposals under discussion can be found with the label type/proposal in GitHub.
The conforming implementation of the specification is released and included in the distribution. Any deviation from the specification is considered a bug.
Contents
- Overview
- SMTP, POP3 and IMAP Protocols
- Client
- 3.1. SMTP Client
- 3.1.1.
initfunction- 3.1.1.1. Authenticated Mode
- 3.1.1.2. Unauthenticated Mode
- 3.1.1.3. OAuth2 Mode
- 3.1.2.
sendMessagefunction - 3.1.3.
sendfunction
- 3.1.1.
- 3.2. POP3 Client
- 3.2.1.
initfunction - 3.2.2.
receiveMessagefunction - 3.2.3.
closefunction
- 3.2.1.
- 3.3. IMAP Client
- 3.3.1.
initfunction - 3.3.2.
receiveMessagefunction - 3.3.3.
closefunction
- 3.3.1.
- 3.1. SMTP Client
- Service
- 4.1. POP3 Listener
- 4.2. IMAP Listener
- 4.3. POP3/IMAP Service
- Samples
- 5.1. Clients
- 5.1.1. SMTP Client
- 5.1.1.1. With Authentication
- 5.1.1.2. Without Authentication
- 5.1.1.3. With OAuth2 (Client Credentials Grant)
- 5.1.1.4. With OAuth2 (Direct Token)
- 5.1.2. POP3 Client
- 5.1.3. IMAP Client
- 5.1.1. SMTP Client
- 5.2. Services
- 5.2.1. POP3 Service
- 5.2.2. IMAP Service
- 5.1. Clients
- Static Code Rules
1. Overview
This specification elaborates on Basic SMTP, POP3, IMAP4 clients and services/listeners.
2. SMTP, POP3 and IMAP Protocols
SMTP is an application layer protocol used for sending an email to a recipient across a network. POP3 and IMAP are application layer protocols used for receiving emails from an email server across a network.
Ballerina Email library has the capability of sending and receiving email via SMTP, POP3 and IMAP protocols using both clients and services.
3. Client
Introduce three different clients, SMTP for sending emails and POP3/IMAP clients for receiving emails.
3.1 SMTP Client
Sends an email to with SMTP protocol.
Either this client can be used to send an email from a pre-defined email:Message record or directly sending the email by passing all the parameters as arguments.
3.1.1 init function
If there are certificates to be added or in the case where the port number is different from 465 with SSL a configuration has to be passed.
The SMTP client supports both authenticated and unauthenticated modes.
3.1.1.1 Authenticated Mode
For authenticated SMTP servers, provide the host address, username, and password:
email:SmtpClient smtpClient = check new ("smtp.email.com", "sender@email.com" , "pass123");
3.1.1.2 Unauthenticated Mode
For SMTP servers that don't require authentication (e.g., internal relay servers), omit the username and password or explicitly pass ():
email:SmtpClient smtpClient = check new ("smtp.email.com");
Note:
- Both
usernameandpasswordmust either be provided together or both omitted. Providing only one will result in an error.- When using the unauthenticated mode, the
fromfield is mandatory in the email message, as there is no authenticated username to fall back on.
3.1.1.3 OAuth2 Mode
For SMTP servers that support OAuth2 (XOAUTH2 SASL mechanism), the auth field of SmtpConfiguration accepts either:
The password parameter must be omitted in all OAuth2 modes.
Direct token:
email:SmtpConfiguration smtpConfig = { port: 587, security: email:START_TLS_ALWAYS, auth: "ya29.access-token-value" }; email:SmtpClient smtpClient = check new ("smtp.gmail.com", "sender@gmail.com", clientConfig = smtpConfig);
Client Credentials Grant:
import ballerina/oauth2; oauth2:ClientCredentialsGrantConfig grantConfig = { tokenUrl: "https://oauth2.provider.com/token", clientId: "client-id", clientSecret: "client-secret", scopes: ["https://mail.google.com/"] }; email:SmtpConfiguration smtpConfig = { port: 587, security: email:START_TLS_ALWAYS, auth: grantConfig }; email:SmtpClient smtpClient = check new ("smtp.gmail.com", "sender@gmail.com", clientConfig = smtpConfig);
Resource Owner Password Credentials Grant:
import ballerina/oauth2; oauth2:PasswordGrantConfig grantConfig = { tokenUrl: "https://oauth2.provider.com/token", username: "resource-owner@example.com", password: "resource-owner-password", clientId: "client-id", clientSecret: "client-secret" }; email:SmtpConfiguration smtpConfig = { port: 587, security: email:START_TLS_ALWAYS, auth: grantConfig }; email:SmtpClient smtpClient = check new ("smtp.example.com", "sender@example.com", clientConfig = smtpConfig);
With OAuth2GrantConfig, the token is fetched at SmtpClient initialization. If the token endpoint is unreachable or rejects the credentials, init fails immediately (fail-fast). Subsequent sends use the cached token, which the ballerina/oauth2 module refreshes automatically on expiry. For direct string tokens, the caller is fully responsible for token lifecycle management.
Note:
usernameis required when using OAuth2. Thepasswordparameter must not be provided alongsideauth.
3.1.2 sendMessage function
The email:Message record has to be defined first as follows.
email:Message emailMessage = { to: ["receiver1@email.com", "receiver2@email.com"], cc: ["receiver3@email.com", "receiver4@email.com"], bcc: ["receiver5@email.com"], subject: "Sample Email Subject", body: "This is a sample email.", 'from: "author@email.com", sender: "sender@email.com", replyTo: ["replyTo1@email.com", "replyTo2@email.com"] };
Then the above record can be used to send using the SMTP client.
check smtpClient->sendMessage(emailMessage);
If body, htmlBody, and attachments are empty then the client will send CRLF token which denotes end-of-message according to RFC 5322.
3.1.3 send function
Instead of defining a record related to the email message the email can be sent directly. This method can be in simple cases.
check smtpClient->send("receiver@email.com", "Sample Email Subject", "author@email.com", "This is a sample email.");
3.2 POP3 Client
3.2.1 init function
If there are certificates to be added or in the case where the port number is different from 995 with SSL a configuration has to be passed.
Otherwise, only the host address, username, and the password has to be provided.
email:PopClient popClient = check new ("pop.email.com", "reader@email.com", "pass456");
3.2.2 receiveMessage function
A email:Message record can to be received as follows.
email:Message? emailResponse = check popClient->receiveMessage();
This method will return a single email that was already received if any such email exists. Otherwise, returns a nil.
A timeout value can be passed as an argument to the function required to be blocked on the method invocation till an email is received.
3.2.3 close function
Closes the email folder and the store from the POP3 server.
check popClient->close();
3.3 IMAP Client
3.3.1 init function
If there are certificates to be added or in the case where the port number is different from 993 with SSL a configuration has to be passed.
Otherwise, only the host address, username, and the password has to be provided.
email:ImapClient imapClient = check new ("imap.email.com", "reader@email.com", "pass456");
3.3.2 receiveMessage function
A email:Message record can to be received as follows.
email:Message? emailResponse = check imapClient->receiveMessage();
This method will return a single email that was already received if any such email exists. Otherwise, returns a nil.
A timeout value can be passed as an argument to the function required to be blocked on the method invocation till an email is received.
3.3.3 close function
Closes the email folder and the store from the IMAP server.
check imapClient->close();
4. Service
A service can subscribe to a listener to read data from the POP3/IMAP server.
4.1 POP3 Listener
A POP3 listener is initialized by passing email:PopListenerConfiguration to the constructor of the email:PopListener.
email:PopListenerConfiguration popListenerConfiguration = { host: "pop.email.com", username: "reader@email.com", password: "pass456", pollingInterval: 2, port: 995 } listener email:PopListener emailListener = check new (popListenerConfiguration);
4.2 IMAP Listener
A IMAP listener is initialized by passing email:ImapListenerConfiguration to the constructor of the email:ImapListener.
email:ImapListenerConfiguration imapListenerConfiguration = { host: "imap.email.com", username: "reader@email.com", password: "pass456", pollingInterval: 2, port: 993 } listener email:ImapListener emailListener = check new (imapListenerConfiguration);
pollingInterval specifies the polling interval time period from number of seconds.
4.3 POP3/IMAP Service
Both POP3 and IMAP services can be defined as follows.
email:Service emailObserver = service object { remote function onMessage(email:Message emailMessage) { io:println("Email Body: ", emailMessage?.body); } remote function onError(email:Error emailError) { io:println("Error while polling for the emails: " + emailError.message()); } remote function onClose(email:Error? closeError) { } };
When a new email is received onMessage function get called with the received email given as the argument.
If an error occurred during the listening onError method get called.
When the listener is getting closed onClose method get called.
5 Samples
5.1 Clients
5.1.1 SMTP Client
5.1.1.1 With Authentication
public function main() returns error? { email:SmtpClient smtpClient = check new ("smtp.email.com", "sender@email.com" , "pass123"); email:Message email = { to: ["receiver1@email.com", "receiver2@email.com"], subject: "Sample Email", body: "This is a sample email.", 'from: "author@email.com", replyTo: ["replyTo1@email.com", "replyTo2@email.com"] }; check smtpClient->sendMessage(email); }
5.1.1.2 Without Authentication
public function main() returns error? { email:SmtpConfiguration smtpConfig = { port: 25, security: email:START_TLS_AUTO }; email:SmtpClient smtpClient = check new ("smtp.relay.com", clientConfig = smtpConfig); email:Message email = { to: ["receiver1@email.com", "receiver2@email.com"], subject: "Sample Email", body: "This is a sample email.", 'from: "author@email.com", replyTo: ["replyTo1@email.com", "replyTo2@email.com"] }; check smtpClient->sendMessage(email); }
5.1.1.3 With OAuth2 (Client Credentials Grant)
import ballerina/email; import ballerina/oauth2; public function main() returns error? { oauth2:ClientCredentialsGrantConfig grantConfig = { tokenUrl: "https://oauth2.provider.com/token", clientId: "client-id", clientSecret: "client-secret", scopes: ["https://mail.google.com/"] }; email:SmtpConfiguration smtpConfig = { port: 587, security: email:START_TLS_ALWAYS, auth: grantConfig }; email:SmtpClient smtpClient = check new ("smtp.gmail.com", "sender@gmail.com", clientConfig = smtpConfig); email:Message emailMsg = { to: ["receiver@email.com"], subject: "Sample Email", body: "This is a sample email sent via OAuth2.", 'from: "sender@gmail.com" }; check smtpClient->sendMessage(emailMsg); }
5.1.1.4 With OAuth2 (Direct Token)
import ballerina/email; public function main() returns error? { email:SmtpConfiguration smtpConfig = { port: 587, security: email:START_TLS_ALWAYS, auth: "ya29.access-token-value" }; email:SmtpClient smtpClient = check new ("smtp.gmail.com", "sender@gmail.com", clientConfig = smtpConfig); email:Message emailMsg = { to: ["receiver@email.com"], subject: "Sample Email", body: "This is a sample email sent via OAuth2 with a direct token.", 'from: "sender@gmail.com" }; check smtpClient->sendMessage(emailMsg); }
5.1.2 POP3 Client
public function main() returns error? { email:PopClient popClient = check new ("pop.email.com", "reader@email.com", "pass456"); email:Message? emailResponse = check popClient->receiveMessage(); if (emailResponse is email:Message) { io:println("POP client received an email."); io:println("Email Subject: ", emailResponse.subject); io:println("Email Body: ", emailResponse?.body); } else { io:println("There are no emails in the INBOX."); } check popClient->close(); }
5.1.3 IMAP Client
public function main() returns error? { email:ImapClient imapClient = check new ("imap.email.com", "reader@email.com", "pass456"); emailResponse = check imapClient->receiveMessage(); if (emailResponse is email:Message) { io:println("IMAP client received an email."); io:println("Email Subject: ", emailResponse.subject); io:println("Email Body: ", emailResponse?.body); } else { io:println("There are no emails in the INBOX."); } check imapClient->close(); }
5.2 services
5.2.1 POP3 Service
import ballerina/email; import ballerina/io; listener email:PopListener emailListener = check new ({ host: "pop.email.com", username: "reader@email.com", password: "pass456", pollingInterval: 2, port: 995 }); service "emailObserver" on emailListener { remote function onMessage(email:Message emailMessage) { io:println("POP Listener received an email."); io:println("Email Subject: ", emailMessage.subject); io:println("Email Body: ", emailMessage?.body); } remote function onError(email:Error emailError) { io:println("Error while polling for the emails: " + emailError.message()); } remote function onClose(email:Error? closeError) { io:println("Closed the listener."); } }
5.2.2 IMAP Service
import ballerina/email; import ballerina/io; listener email:ImapListener emailListener = check new ({ host: "imap.email.com", username: "reader@email.com", password: "pass456", pollingInterval: 2, port: 993 }); service "emailObserver" on emailListener { remote function onMessage(email:Message emailMessage) { io:println("IMAP Listener received an email."); io:println("Email Subject: ", emailMessage.subject); io:println("Email Body: ", emailMessage?.body); } remote function onError(email:Error emailError) { io:println("Error while polling for the emails: " + emailError.message()); } remote function onClose(email:Error? closeError) { io:println("Closed the listener."); } }
6. Static Code Rules
The following static code rules are applied to the Email module.
| Id | Kind | Description |
|---|---|---|
| ballerina/email:1 | VULNERABILITY | Avoid unverified server hostnames during SSL/TLS connections |
| ballerina/email:2 | VULNERABILITY | Avoid connecting to mail servers without TLS |
| ballerina/email:3 | VULNERABILITY | Avoid falling back to cleartext when TLS is unavailable |
| ballerina/email:4 | VULNERABILITY | Avoid using weak TLS protocol versions |
6.1. Avoid unverified server hostnames during SSL/TLS connections
Disabling host name verification keeps the encryption but removes the check that the certificate belongs to the server being contacted.
| Property | Description |
|---|---|
| Rule ID | ballerina/email:1 |
| Rule Kind | Vulnerability |
| CWE | CWE-297, CWE-295 |
| OWASP Top 10:2025 | A07 Authentication Failures |
6.1.1. Why this is an issue?
A TLS certificate proves two things: that it was issued by a trusted authority, and that it was issued for the host the client is talking to. Setting verifyHostName to false keeps the first check and drops the second, so any party holding a certificate the client trusts, for any host at all, can terminate the connection.
6.1.2. What is the potential impact?
An attacker positioned on the network path can present a valid certificate for a host they control and the client will accept it, giving them the mailbox credentials and the message contents while the connection still appears encrypted.
6.1.3. How can I fix this?
Leave verifyHostName at its default of true. Where a certificate genuinely does not match the host, correct the certificate rather than the check.
Non-compliant code:
email:SmtpClient smtpClient = check new ("smtp.example.com", "sender@example.com", "password", clientConfig = { secureSocket: { cert: "/path/to/public.crt", verifyHostName: false } });
Compliant code:
email:SmtpClient smtpClient = check new ("smtp.example.com", "sender@example.com", "password", clientConfig = { secureSocket: { cert: "/path/to/public.crt" } });
6.1.4. Additional Resources
- CWE-295: Improper Certificate Validation
- CWE-297: Improper Validation of Certificate with Host Mismatch
- OWASP Top 10:2025 A07 Authentication Failures
6.2. Avoid connecting to mail servers without TLS
START_TLS_NEVER keeps the connection in plaintext for its whole life.
| Property | Description |
|---|---|
| Rule ID | ballerina/email:2 |
| Rule Kind | Vulnerability |
| CWE | CWE-319 |
| OWASP Top 10:2025 | A04 Cryptographic Failures |
6.2.1. Why this is an issue?
The security field decides whether the connection is protected at all. START_TLS_NEVER disables the upgrade entirely, so the session, including the mailbox credentials sent during authentication, crosses the network in the clear. The field defaults to SSL, so plaintext is only ever reached by asking for it.
6.2.2. What is the potential impact?
Anyone on the network path reads the mailbox credentials and every message the client sends or retrieves, and can modify them in transit.
6.2.3. How can I fix this?
Use SSL for an implicitly encrypted connection, or START_TLS_ALWAYS where the protocol requires the upgrade form.
Non-compliant code:
email:SmtpClient smtpClient = check new ("smtp.example.com", "sender@example.com", "password", clientConfig = { port: 25, security: email:START_TLS_NEVER });
Compliant code:
email:SmtpClient smtpClient = check new ("smtp.example.com", "sender@example.com", "password", clientConfig = { port: 587, security: email:START_TLS_ALWAYS });
6.2.4. Additional Resources
- CWE-319: Cleartext Transmission of Sensitive Information
- OWASP Top 10:2025 A04 Cryptographic Failures
6.3. Avoid falling back to cleartext when TLS is unavailable
START_TLS_AUTO lets the server decide whether the connection is encrypted.
| Property | Description |
|---|---|
| Rule ID | ballerina/email:3 |
| Rule Kind | Vulnerability |
| CWE | CWE-319, CWE-757 |
| OWASP Top 10:2025 | A04 Cryptographic Failures |
6.3.1. Why this is an issue?
START_TLS_AUTO upgrades the connection when the server advertises STARTTLS and continues in plaintext when it does not. Whether the credentials are encrypted is therefore decided by the server's greeting, which an attacker positioned on the network path can rewrite. Stripping the advertisement is enough to have the client send everything in the clear, and nothing in the client indicates that anything went wrong.
6.3.2. What is the potential impact?
An attacker who can modify traffic downgrades the connection silently and reads the mailbox credentials and message contents, with the same result as never attempting TLS.
6.3.3. How can I fix this?
Use START_TLS_ALWAYS, which fails the connection instead of downgrading it, or SSL for an implicitly encrypted connection.
Non-compliant code:
email:ImapClient imapClient = check new ("imap.example.com", "reader@example.com", "password", clientConfig = { port: 143, security: email:START_TLS_AUTO });
Compliant code:
email:ImapClient imapClient = check new ("imap.example.com", "reader@example.com", "password", clientConfig = { port: 143, security: email:START_TLS_ALWAYS });
6.3.4. Additional Resources
- CWE-319: Cleartext Transmission of Sensitive Information
- CWE-757: Selection of Less-Secure Algorithm During Negotiation
- OWASP Top 10:2025 A04 Cryptographic Failures
6.4. Avoid using weak TLS protocol versions
TLS 1.0, TLS 1.1 and the SSL family are withdrawn and should not be named.
| Property | Description |
|---|---|
| Rule ID | ballerina/email:4 |
| Rule Kind | Vulnerability |
| CWE | CWE-326, CWE-327 |
| OWASP Top 10:2025 | A04 Cryptographic Failures |
6.4.1. Why this is an issue?
TLS 1.0 and TLS 1.1 depend on MD5 and SHA-1 in the handshake and support no AEAD cipher suites, and RFC 8996 deprecates both for those reasons. Whether a particular connection is also open to a padding-oracle attack depends on the CBC construction negotiated and on the implementation, so that is a risk these versions leave available rather than one they guarantee. The SSL family is broken outright. Naming any of them in protocol.versions pins the connection to a version a current server should refuse.
6.4.2. What is the potential impact?
An attacker able to influence the negotiation can hold the connection at a weak version and attack the encryption itself, rather than having to defeat a current one.
6.4.3. How can I fix this?
Name only TLS 1.2 and TLS 1.3, or leave protocol unset and take the runtime's defaults.
Non-compliant code:
email:SmtpClient smtpClient = check new ("smtp.example.com", "sender@example.com", "password", clientConfig = { secureSocket: { cert: "/path/to/public.crt", protocol: { name: email:TLS, versions: ["TLSv1.2", "TLSv1.1"] } } });
Compliant code:
email:SmtpClient smtpClient = check new ("smtp.example.com", "sender@example.com", "password", clientConfig = { secureSocket: { cert: "/path/to/public.crt", protocol: { name: email:TLS, versions: ["TLSv1.2", "TLSv1.3"] } } });