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

  1. Overview
  2. SMTP, POP3 and IMAP Protocols
  3. Client
  4. Service
  5. Samples
  6. 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 username and password must either be provided together or both omitted. Providing only one will result in an error.
  • When using the unauthenticated mode, the from field 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:

  • A direct access token (string): use a pre-obtained token.
  • An email:OAuth2GrantConfig: let the library fetch the token from an authorization server.

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: username is required when using OAuth2. The password parameter must not be provided alongside auth.

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.

IdKindDescription
ballerina/email:1VULNERABILITYAvoid unverified server hostnames during SSL/TLS connections
ballerina/email:2VULNERABILITYAvoid connecting to mail servers without TLS
ballerina/email:3VULNERABILITYAvoid falling back to cleartext when TLS is unavailable
ballerina/email:4VULNERABILITYAvoid 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.

PropertyDescription
Rule IDballerina/email:1
Rule KindVulnerability
CWECWE-297, CWE-295
OWASP Top 10:2025A07 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

6.2. Avoid connecting to mail servers without TLS

START_TLS_NEVER keeps the connection in plaintext for its whole life.

PropertyDescription
Rule IDballerina/email:2
Rule KindVulnerability
CWECWE-319
OWASP Top 10:2025A04 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

6.3. Avoid falling back to cleartext when TLS is unavailable

START_TLS_AUTO lets the server decide whether the connection is encrypted.

PropertyDescription
Rule IDballerina/email:3
Rule KindVulnerability
CWECWE-319, CWE-757
OWASP Top 10:2025A04 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

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.

PropertyDescription
Rule IDballerina/email:4
Rule KindVulnerability
CWECWE-326, CWE-327
OWASP Top 10:2025A04 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"]
        }
    }
});

6.4.4. Additional Resources