- name
- mail-destinations
- description
- Invoke this skill to configure mail sessions via Destination service. Detects javax.mail.Session or jakarta.mail.Session resource-ref in web.xml, or mail session JNDI lookups. Replaces Neo JNDI mail with manual session creation.
- disable-model-invocation
- false
- allowed-tools
- Read, Edit, Write, Bash, Grep, Glob
# 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
```xml
<resource-ref>
<res-ref-name>mail/Session</res-ref-name>
<res-type>javax.mail.Session</res-type>
</resource-ref>
```
### In Java source files
```java
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:**
```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:
```xml
<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/](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`:
```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):**
```java
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):**
```java
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) |
Auf GitHub ansehen