SuperAPI AI Directory

mail-destinations

Deployment & operations

View repository

Updated 10-16 10:15

About this skill

Mail Session

Configure mail sessions via the Destination service.

Purpose

Replace Neo's javax.mail.Session JNDI resource with manual session creation using mail server configuration from the Destination service.

Detection

This skill applies if any of these patterns are found:

In web.xml

<resource-ref>
    <res-ref-name>mail/Session</res-ref-name>
    <res-type>javax.mail.Session</res-type>
</resource-ref>

In Java source files

import javax.mail.Session;
import javax.annotation.Resource;

@Resource(name = "mail/Session")
private Session mailSession;

// OR JNDI lookup
Session session = (Session) ctx.lookup("java:comp/env/mail/Session");

Prerequisites

Working directory: This skill must run inside the -cf-migration copy of your app, created by jakarta-java25-migration or neo-to-cf-migration-orchestrator. If your current directory does not end in -cf-migration, switch to it before proceeding.

Before invoking this skill, ensure you have invoked:

  1. sdk-replacement - Use the sdk-replacement skill

    • Sets up SAP Cloud SDK
    • REQUIRED before this skill
  2. destinations - Use the destinations skill

    • Configures Destination service
    • REQUIRED before this skill
  3. connectivity-onpremise - Use the connectivity-onpremise skill

    • Required for on-premise mail servers (if applicable)

Transformation Steps

Step 1: Remove Resource Reference from web.xml

Remove this from web.xml:

<resource-ref>
    <res-ref-name>mail/Session</res-ref-name>
    <res-type>javax.mail.Session</res-type>
</resource-ref>

Step 2: Add Mail Dependencies

Add to pom.xml:

🛑 HARD RULE — ship a Jakarta Mail implementation, not just the API. On Jakarta EE 10 / Java 25 (sap_java_buildpack_jakarta) the Jakarta Mail API (jakarta.mail:jakarta.mail-api, often pulled transitively by an EE 10 BOM) contains only interfaces. Without a runtime implementation on the classpath, the app builds and deploys fine but throws at the first send: java.lang.IllegalStateException: No provider of jakarta.mail.util.StreamProvider was found (HTTP 500). The implementation is Angus Mail (org.eclipse.angus:jakarta.mail) — it registers the provider via META-INF/services. Always declare the implementation explicitly (it brings the API transitively), so the provider is packaged into the WAR:

<properties>
    <!-- Angus Mail is the Jakarta Mail 2.1 (EE 10) reference implementation.
         It transitively provides the jakarta.mail-api and the StreamProvider. -->
    <angus-mail-version>2.0.3</angus-mail-version>
    <jakarta-activation-version>2.1.3</jakarta-activation-version>
</properties>

<dependencies>
    <!-- Jakarta Mail IMPLEMENTATION (Angus Mail) — provides the API + the
         runtime StreamProvider. Do NOT depend on jakarta.mail-api alone. -->
    <dependency>
        <groupId>org.eclipse.angus</groupId>
        <artifactId>jakarta.mail</artifactId>
        <version>${angus-mail-version}</version>
    </dependency>

    <!-- Jakarta Activation implementation (required for mail) -->
    <dependency>
        <groupId>org.eclipse.angus</groupId>
        <artifactId>angus-activation</artifactId>
        <version>${jakarta-activation-version}</version>
    </dependency>
</dependencies>

✅ After building the WAR, verify the impl is packaged: jar tf target/*.war | grep -i 'angus-mail\|jakarta.mail' must list the Angus Mail jar under WEB-INF/lib/. If only jakarta.mail-api-*.jar is present, the app will 500 at runtime with the StreamProvider error above.

Step 3: Copy Mail Session Helper Classes

Copy the mail session helper classes from assets/session/ to your project's source directory. These classes handle mail session creation from destination configuration.

Required files:

  • MailSession.java - Main session factory
  • MailAuthenticator.java - Password authentication
  • MailPropertiesHandler.java - Extracts mail properties from destination
  • OnPremiseSMTPProvider.java - Custom SMTP provider for on-premise
  • OnPremiseSMTPTransport.java - SMTP transport via Cloud Connector
  • ConnectivitySocks5ProxySocket.java - SOCKS5 proxy for on-premise connectivity

Copy these to src/main/java/com/example/document/ (the assets all declare package com.example.document;).

Note on the package: com.example.document is a placeholder. Rename to your project's package (e.g. com.acme.mail) when you copy the assets in, and update the imports below to match.

Alternative: Simple MailSessionFactory

For simpler scenarios (internet mail only), you can create a minimal MailSessionFactory.java:

package com.example.document;

import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import jakarta.mail.Authenticator;
import jakarta.mail.PasswordAuthentication;
import jakarta.mail.Session;

import java.util.Optional;
import java.util.Properties;

public class MailSessionFactory {

    /**
     * Create mail session from destination configuration
     */
    public static Session createSession(String destinationName) {
        Destination destination = DestinationAccessor.getDestination(destinationName);

        // Get mail server properties from destination
        String host = getProperty(destination, "mail.smtp.host")
            .orElseGet(() -> extractHost(destination));
        String port = getProperty(destination, "mail.smtp.port")
            .orElse("587");
        String user = getProperty(destination, "mail.user")
            .orElse(getProperty(destination, "User").orElse(null));
        String password = getProperty(destination, "mail.password")
            .orElse(getProperty(destination, "Password").orElse(null));

        boolean auth = getProperty(destination, "mail.smtp.auth")
            .map(Boolean::parseBoolean)
            .orElse(user != null);
        boolean starttls = getProperty(destination, "mail.smtp.starttls.enable")
            .map(Boolean::parseBoolean)
            .orElse(true);

        Properties props = new Properties();
        props.put("mail.smtp.host", host);
        props.put("mail.smtp.port", port);
        props.put("mail.smtp.auth", String.valueOf(auth));
        props.put("mail.smtp.starttls.enable", String.valueOf(starttls));

        // Store user/password in session properties for transport.connect() usage
        if (user != null) props.put("mail.smtp.user", user);
        if (password != null) props.put("mail.smtp.password", password);

        // Add any additional properties from destination
        addOptionalProperty(props, destination, "mail.smtp.ssl.enable");
        addOptionalProperty(props, destination, "mail.smtp.ssl.trust");
        addOptionalProperty(props, destination, "mail.smtp.ssl.checkserveridentity");
        addOptionalProperty(props, destination, "mail.smtp.connectiontimeout");
        addOptionalProperty(props, destination, "mail.smtp.timeout");
        addOptionalProperty(props, destination, "mail.smtp.from");
        addOptionalProperty(props, destination, "mail.from");

        if (auth && user != null && password != null) {
            final String finalUser = user;
            final String finalPassword = password;

            return Session.getInstance(props, new Authenticator() {
                @Override
                protected PasswordAuthentication getPasswordAuthentication() {
                    return new PasswordAuthentication(finalUser, finalPassword);
                }
            });
        } else {
            return Session.getInstance(props);
        }
    }

    private static String extractHost(Destination destination) {
        // Try to extract host from URL property
        return getProperty(destination, "URL")
            .map(url -> {
                try {
                    return new java.net.URL(url).getHost();
                } catch (Exception e) {
                    return url; // Return as-is if not a valid URL
                }
            })
            .orElseThrow(() -> new RuntimeException("Mail host not configured"));
    }

    private static Optional<String> getProperty(Destination destination, String key) {
        return destination.get(key).toJavaOptional()
            .map(Object::toString);
    }

    private static void addOptionalProperty(Properties props, Destination destination, String key) {
        getProperty(destination, key).ifPresent(value -> props.put(key, value));
    }
}

Step 4: Update Mail Sending Code

Before (Neo):

import javax.annotation.Resource;
import javax.mail.*;
import javax.mail.internet.*;
import javax.servlet.http.*;

public class MailServlet extends HttpServlet {

    @Resource(name = "mail/Session")
    private Session mailSession;

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String to = request.getParameter("to");
        String subject = request.getParameter("subject");
        String body = request.getParameter("body");

        try {
            Message message = new MimeMessage(mailSession);
            message.setFrom(new InternetAddress("sender@example.com"));
            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
            message.setSubject(subject);
            message.setText(body);

            Transport.send(message);

            response.getWriter().println("Email sent successfully");

        } catch (MessagingException e) {
            throw new ServletException("Failed to send email", e);
        }
    }
}

After (Cloud Foundry):

import com.example.document.MailSessionFactory;
import jakarta.mail.*;
import jakarta.mail.internet.*;
import jakarta.servlet.http.*;

public class MailServlet extends HttpServlet {

    private static final String MAIL_DESTINATION = "mail-destination";

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String to = request.getParameter("to");
        String subject = request.getParameter("subject");
        String body = request.getParameter("body");

        try {
            // Create session from destination
            Session mailSession = MailSessionFactory.createSession(MAIL_DESTINATION);

            Message message = new MimeMessage(mailSession);

            // Use mail.smtp.from from destination instead of hardcoded address
            String from = mailSession.getProperty("mail.smtp.from");
            if (from == null) from = mailSession.getProperty("mail.from");
            message.setFrom(new InternetAddress(from != null ? from : "sender@example.com"));

            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
            message.setSubject(subject);
            message.setText(body);

            // Important: use Transport instance with explicit credentials.
            // Transport.send(message) does NOT pass credentials from session properties.
            Transport transport = mailSession.getTransport();
            String user = mailSession.getProperty("mail.smtp.user");
            String password = mailSession.getProperty("mail.smtp.password");
            try {
                if (user != null) {
                    transport.connect(
                        mailSession.getProperty("mail.smtp.host"),
                        Integer.parseInt(mailSession.getProperty("mail.smtp.port")),
                        user, password);
                } else {
                    transport.connect();
                }
                transport.sendMessage(message, message.getAllRecipients());
            } finally {
                transport.close();
            }

            response.getWriter().println("Email sent successfully");

        } catch (MessagingException e) {
            throw new ServletException("Failed to send email", e);
        }
    }
}

Important: Do not use Transport.send(message) — it creates a new transport internally and does NOT pass user/password from session properties. Always use transport.connect(host, port, user, password) explicitly.

Step 5: Create Mail Destination

Option A: Internet Mail Server

Create destination in BTP Cockpit:

Property Value
Name mail-destination
Type HTTP
URL smtp://smtp.example.com
Proxy Type Internet
Authentication BasicAuthentication
User your-email@example.com
Password your-password

Additional Properties:

Property Value
mail.smtp.host smtp.example.com
mail.smtp.port 587
mail.smtp.auth true
mail.smtp.starttls.enable true
mail.smtp.from sender@example.com
mail.smtp.ssl.checkserveridentity true

Important: Use mail.smtp.from to set the sender address. Do NOT hardcode sender addresses like noreply@mail.hana.ondemand.com in your code — these Neo-era addresses won't work on CF. The mail.smtp.from address must be verified with your SMTP provider.

Option B: On-Premise Mail Server

For on-premise mail servers via Cloud Connector:

Property Value
Name mail-destination
Type HTTP
URL smtp://mail-virtual-host:25
Proxy Type OnPremise
Authentication NoAuthentication (or as needed)

Cloud Connector Configuration:

  1. Add TCP resource mapping:
    • Protocol: TCP
    • Internal Host: mail.internal.company.com
    • Internal Port: 25
    • Virtual Host: mail-virtual-host
    • Virtual Port: 25

Step 6: Update MTA Descriptor

modules:
  - name: ${app-name}
    type: java.tomcat
    path: target/<artifactId>.war
    parameters:
      buildpack: sap_java_buildpack_jakarta
      disk-quota: 1024M
      memory: 1024M
    properties:
      ENABLE_SECURITY_JAVA_API_V2: true
      SET_LOGGING_LEVEL: 'ROOT: INFO'
    requires:
      - name: ${app-name}-destination
      - name: ${app-name}-connectivity  # Only needed for on-premise

resources:
  - name: ${app-name}-destination
    type: org.cloudfoundry.managed-service
    parameters:
      service: destination
      service-plan: lite

  - name: ${app-name}-connectivity
    type: org.cloudfoundry.managed-service
    parameters:
      service: connectivity
      service-plan: lite

Configuration Files

No new configuration files required. Mail configuration is stored in destinations.

CF Services

Service Plan Purpose
destination lite Store mail server configuration
connectivity lite Required for on-premise mail servers

Verification

1. Compile Check

mvn clean compile

2. Verify Destination

In BTP Cockpit, test the destination connection.

3. Send Test Email

curl -X POST "https://${app-url}/mail" \
  -d "to=test@example.com" \
  -d "subject=Test Email" \
  -d "body=Hello from Cloud Foundry!"

4. Check Logs

cf logs ${app-name} --recent | grep -i mail

Common Issues

Common runtime errors → root cause

Runtime error Root cause Fix
DestinationNotFoundException for mail destination Missing connectivity-destination-service runtime dep Add to pom.xml with <scope>runtime</scope>
MessagingException: Could not connect to SMTP host Destination URL or port wrong, or mail service not reachable from CF Verify destination properties in BTP Cockpit; check that the SMTP port is open
AuthenticationFailedException Credentials not set in destination, or Transport.connect() not passed explicit user/password See issue below — pass credentials explicitly

Issue: "failed to connect, no password specified?" with transport.connect()

Cause: Transport.connect() without arguments does not read credentials from the mail session properties. The destination service provides credentials as mail.user and mail.password (with mail. prefix), but transport.connect() only reads mail.smtp.user from session properties and never reads any password property automatically. Solution: Extract user/password from the destination properties and pass them explicitly:

String user = destProperties.getOrDefault("User", destProperties.get("mail.user"));
String password = destProperties.getOrDefault("Password", destProperties.get("mail.password"));
if (user != null && !user.isBlank()) {
    String host = session.getProperty("mail.smtp.host");
    int port = Integer.parseInt(session.getProperty("mail.smtp.port"));
    transport.connect(host, port, user, password);
} else {
    transport.connect();
}

Issue: "535 5.7.139 Authentication unsuccessful, basic authentication is disabled" (Office 365)

Cause: Microsoft has disabled basic authentication (username + password) for SMTP on Office 365 / Outlook.com accounts by default. Solution:

  • Enable SMTP AUTH for the specific mailbox: Microsoft 365 Admin Center > Users > select user > Mail > "Manage email apps" > enable "Authenticated SMTP"
  • Or via PowerShell: Set-CASMailbox -Identity "user@domain.com" -SmtpClientAuthenticationDisabled $false
  • Alternative: Switch to a mail provider that supports basic auth (e.g., AWS SES, SendGrid), or implement OAuth2 XOAUTH2 authentication

Issue: "554 Message rejected: Email address is not verified" (hardcoded From address)

Cause: Neo applications often hardcode a sender address like noreply@mail.hana.ondemand.com in the message builder. After migration, the SMTP service rejects this unverified address. Solution: Use mail.smtp.from from the destination configuration instead of a hardcoded address:

String fromAddress = mailSession.getProperty("mail.smtp.from");
if (fromAddress == null || fromAddress.isBlank()) {
    fromAddress = DEFAULT_MAIL_FROM; // fallback
}
message.setFrom(new InternetAddress(fromAddress));

Make sure to configure mail.smtp.from in the BTP destination with a verified sender address.

Issue: Destination properties have mail.* prefix

Cause: The SAP Cloud SDK returns destination properties with their original keys. Mail destinations in BTP Cockpit use mail.user and mail.password (with mail. prefix), not the SDK's typical User/Password keys. Solution: When extracting credentials, check both key formats:

String user = destProperties.getOrDefault("User", destProperties.get("mail.user"));
String password = destProperties.getOrDefault("Password", destProperties.get("mail.password"));

Issue: Authentication failed

Cause: Invalid credentials or app password required. Solution:

  • For Gmail/O365: Use app-specific passwords
  • Verify credentials in destination

Issue: Connection timed out

Cause: Port blocked or wrong server. Solution:

  • Verify mail server host and port
  • Check if port 587/465/25 is allowed

Issue: SSL/TLS errors

Solution: Add destination properties:

mail.smtp.ssl.trust=*
mail.smtp.ssl.enable=true

Issue: On-premise mail not reachable

Cause: Cloud Connector not configured for TCP. Solution: Add TCP protocol mapping in Cloud Connector.

Next Steps

After completing this skill, proceed to other applicable skills: