Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
codebrokerad
Participant
 

Hello SAP Community.

This blog post will provide a solution for many faced when developing their business application.
How to connect On-premise SAP BTP destination through a NodeJS application to send email using Cloud Application Programming CAP?

Solutions to these problem were successful for scenarios by joachim.vanpraet either creating our own destination in BTP using SMTP providers like Mailtrap, which was not suitable for our use case because it is not on-premise solution.

Other solution provided by miroll is tackling the scenario through BTP Cloud Foundry using Java successfuly, but our tech stack was Javascript / NodeJS.

To find a solution, I have checked out the announcement from marika.marszalkowski and this section of the post about newest updates fascinated me.

 

Sending Emails


We recently released experimental functionality to send e-mails against SAP BTP destinations of type “MAIL”.
In version 3 we will make it available for productive use.
You can find more details in the mail documentation.



After I read the documentation and understand it, I have created a new project to test this feature. And with a simple configuration, you can now connect your on premise mail servers successfully thanks to the wonderful developers of SAP Cloud SDK Team.

 

Let's move step by step to show how to perform it in a simple CAP application from scratch.

 

1.Requirements - BTP Services Necessary In Setup Account


These are free services on BTP, so it means everyone can test what we perform with a Trial account:

  • Authorization and Trust Management Service

  • Destination Service

  • Connectivity Service


IDE to make our app work is running in SAP Business Application Studio. 

We also need to generate service instance keys for each of those services to test our application. Check image below how to create service instance key aft

I have generated those services and service keys with a naming convention as follows. First parameter is service instance name, second parameter is service instance key. I have created those in BTP as you can see Authorization and Trust Management service below, and complete the rest.

 

mail-xsuaa , mail-xsuaa-key


mail-destination , mail-destination-key


mail-connectivity, mail-connectivity-key


 


Figure 1 : Setting up BTP Service Instance and Service Key in BTP Cockpit - Click to Enlarge


 

Destination configuration is very important to handle, in this example we are using SMTP server for Gmail and its configuration is as below in our BTP Destination.

With the 'New Property' Tab, you can add more properties or delete the extra ones that are not necessary for your configuration.The destination you are calling should be type 'MAIL' and must be configured correctly.

Here is an example for you:


Figure 2 : SMTP Destination Configuration in BTP Subaccount - Click to Enlarge

 

Additional properties we set up in the image that are not clearly visible is as below for this usecase is as below. Please note the configuration may change for different SMTP Server Providers:
mail.smtp.from=<enter e-mail address to send email from here>
mail.smtp.ssl.checkserveridentity=true
mail.smtp.starttls.enable=true
mail.smtp.starttls.required=true
mail.transport.protocol=smtp9
tokenServiceURLType=Dedicated (optional field)


     2. Create a New Project


Open Business Application Studio. Click the top left pane red highlighted>File>New Project From Template.


Figure 3 : Initial Project Setup in SAP Business Application Studio - Click to Enlarge


 

Then choose CAP Project and click Start.


Figure 4 : Choosing CAP Project will Provide Necessary Development Runtime


 

Set your project configuration as below image and click Finish.


Figure 5 :Minimal Features Addition to Run Project


 

3) Fill In Project With Packages and Add Custom Logic



package.json file


We have an empty project with a backbone configuration. An empty db folder, an empty srv folder, a package.json file and mta.yaml file which are important. We will fill those step by step.

  • Go to package.json file. Open the terminal from Terminal > New Terminal and download dependencies generated by CAP Wizard with


npm install​


  • We have installed pregenerated packages, but we need to add 4 more packages to make our BTP services and mail service work. These packages can be installed consecutively through these commands


npm install @sap-cloud-sdk/mail-client
npm install @sap-cloud-sdk/connectivity
npm install @sap/xssec​
npm install passport

 

Mail client package is the newest package from Sap Cloud SDK that provides necessary configuration for on-premise mail handling. Connectivity package is necessary to handle connectivity with on-premise destination. Xssec and passport are necessary to handle authentication.

 

  • To call our destination, we need to add our destination details to the package.json file. This cds requires block should be added after rules block to the package.json, to the end of the package.json file. Here is the full package.json file also for convenience.


"cds": {
"requires": {
"[hybrid]": {
"auth": {
"kind": "xsuaa"
},
"mailService": {
"kind": "rest",
"credentials": {
"destination": "mail_destination",
"forwardAuthToken": true
}
}
}
}
}

{
"name": "mail-on-premise",
"version": "1.0.0",
"description": "A simple CAP project.",
"repository": "<Add your repository here>",
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@sap-cloud-sdk/connectivity": "^2.14.0",
"@sap-cloud-sdk/mail-client": "^2.14.0",
"@sap/cds": "^6",
"@sap/xssec": "^3.2.17",
"express": "^4",
"passport": "^0.6.0"
},
"devDependencies": {
"sqlite3": "^5.0.4"
},
"scripts": {
"start": "cds run"
},
"engines": {
"node": "^16.15"
},
"eslintConfig": {
"extends": "eslint:recommended",
"env": {
"es2020": true,
"node": true,
"jest": true,
"mocha": true
},
"globals": {
"SELECT": true,
"INSERT": true,
"UPDATE": true,
"DELETE": true,
"CREATE": true,
"DROP": true,
"CDL": true,
"CQL": true,
"CXL": true,
"cds": true
},
"rules": {
"no-console": "off",
"require-atomic-updates": "off"
}
},
"cds": {
"requires": {
"[hybrid]": {
"auth": {
"kind": "xsuaa"
},
"mailService": {
"kind": "rest",
"credentials": {
"destination": "mail_destination",
"forwardAuthToken": true
}
}
}
}
}
}

 

srv folder



  • Create a mail.cds file inside srv folder. Call db file with this simple service


service MailService {
action sendmail();
}

 

  • Create mail.js file inside srv folder. We are adding our custom logic here and mail client configuration.


const { sendMail } = require('@sap-cloud-sdk/mail-client');

module.exports = srv =>
srv.on("sendmail", async (req) => {
const mailConfig = {
to: 'ahmet.demirlicakmak@aarini.com',
subject: 'Test On Premise Destination',
text: 'If you receive this e-mail, you are successful.'
};
sendMail({ destinationName: 'mail_destination' }, [mailConfig]);
});​

 

 

project root



  • Create a test.http file in the project root and fill it as below.


### // Check mail
POST http://localhost:4004/mail/sendmail
Content-Type: application/json
{}

 

4) Connect to Your BTP Services in Hybrid Profile



  • We will test sending e-mail in hybrid profile. Our reasoning is clearly defined in Capire documentation and the reasoning behind using it is as below:



  1. Saving time and effort: We can test new features locally

  2. Particularly selecting individual or combination of services: Although we may need to define many services in deployment, in hybrid approach we have flexibility to use only the ones we are interested.


To bind our service instances to our application, open the terminal and bind each of below service instancess step by step.
cds bind -2 mail-xsuaa:mail-xsuaa-key
cds bind -2 mail-destination:mail-destination-key
cds bind -2 mail-connectivity:mail-connectivity-key

 

5) Final Step - Test Your Application


Start your application with service instance bindings with command
cds watch --profile hybrid

Go to your test.http file and click Send Request shaded in gray.


Figure 6 : Simple CAP Tool to send HTTP Request


 

Now check your e-mail. We want to receive e-mail with subject and text you have filled in mail.js file instantly.


Figure 7 : Open the E-mail Address Provided in mail.js File




6) Conclusion


If you have successfully followed up, you have received an e-mail from your SAP BTP On-Premise destination using SAP Cloud SDK and CAP.

To summarize, you have learned how to set up a simple CAP Project, consuming necessary BTP Services in hybrid mode, how SAP Cloud SDK is used with CAP and connecting on-premise mail servers.

Feel free to share and comment on this post for troubleshooting when necessary. Find the resources about SAP CAP and SAP Cloud SDK in below topics and make sure to follow to be notified with latest use cases:

For Cloud Application Programming - Click Here


For SAP Cloud SDK - Click Here

14 Comments
Pmayank
Participant
0 Kudos
Hi Ahmet,

Great blog.

We have tried same way to trigger the mail from CAP project but our mails are going to Junk folder not in Inbox. Can you help with that.

Also can you share the destination properties in Details.

Regards,

Preeti
codebrokerad
Participant
0 Kudos
Hi Preeti.

I have updated the blog post with the additional parameters configuration for the destination to try and test. Please share the results then if it worked or not.

All the best,

Ahmet
miroll
Product and Topic Expert
Product and Topic Expert
0 Kudos

Hi Ahmet,

great use of the Cloud SDK functionalities! That would have saved me some work if it had been available 🙂

Since you mention my blog you might want to include a small notice regarding a major restriction: this only works for sending emails. Retrieving emails (which in my opinion is the trickier part) is not supported by the Cloud SDK. Since your title is "Connecting On Premise Mail Destination [...]" this might be misleading to some.

Best regards,
Niklas

miroll
Product and Topic Expert
Product and Topic Expert
0 Kudos
Hi Preeti,

regarding emails arriving in the junk folder: this presumably is not the fault of the SDK as the categorisation is determined by the receiving mail server. Usually factors such as subject and content length, email attributes (such as alt texts, return addresses, etc.) and sender address are used to decide whether an email might be junk or not.

If you know the recipient and you don't find another way to influence the receiving mail server's behaviour, they could probably also create a rule to not mark your sender email as junk.

Best regards,
Niklas
codebrokerad
Participant
0 Kudos
Hi Niklas.

Thank you for your necessary input. I am changing the headline based on your suggestion.

All the best...
Junjie
Advisor
Advisor
0 Kudos

Thanks adsapnl , for sharing your experience.

The SAP Cloud SDK for JavaScript has released the version 3 (here is the blog post ), and the mail client has been officially published.

vipin_Ojha
Participant
0 Kudos
Hi adsapnl,

Thanks for sharing the blog.

It has helped us to send emails from SAP CAPM projects.

I am facing one issue with this, It's able to send emails successfully to the same organization's email id's but when we are trying to send the email to external email id's or Gmail we are not getting emails.
Can you please help me with this?

Thanks and Regards,
Vipin Ojha
Ivan-Mirisola
Product and Topic Expert
Product and Topic Expert
Ivan-Mirisola
Product and Topic Expert
Product and Topic Expert
0 Kudos
Hi adsapnl,

Would you mind to add a Note about the requirements to send out e-mails to other e-mail domains thru the smtp service.

Please read my answer to Vipin:

https://answers.sap.com/answers/13842171/view.html

Best regards,
Ivan
martinstenzig
Contributor
0 Kudos
I tried the same approach and tried to specify an Office 365 mailbox, but fail miserably with the error: he destination 'name: [my name]' does not contain host or port information as properties. Please configure in the "Additional Properties" of the destination.

I used the process automation documentation (https://help.sap.com/docs/build-process-automation/sap-build-process-automation/configuring-smtp-mail-destination) to setup my destination.

The big difference to your example is that Office365 does not work with type: Basic Auth, but requires type: OAuth2Password

 

Any idea?
sampptv34
Member
0 Kudos
Hi Martin,

 

Did you manage to solve this issue? I'm facing the same issue. Port and Host details have been assigned under Additional Properties.

 

Thanks & Regards,

Sam.
martinstenzig
Contributor
Yes Sam, but it took me a while. A few things I encountered when I set it up that might have been fixed since then.

  1. When you make changes to the destinations, there are NOT necessarily immediately acknowledged by Process Automation. So you make a change and it still does not work does not mean it might not work 24 hours later. I have yet to understand how long the "caching" lasts.

  2. The destination dialog had some problems not allowing for some of the parameters that I had below, so I used the Destination REST API to make the changes. The configuration for me below works like a charm now... but remember, you make the change and you probably have to wait a while before it takes effect.


  {
"Name": "sap_process_automation_mail",
"Type": "MAIL",
"Authentication": "OAuth2Password",
"ProxyType": "Internet",
"User": "[Exchange email address]",
"clientId": "[Client ID from Azure AD]",
"mail.smtp.port": "587",
"tokenServiceURL": "https://login.microsoftonline.com/organizations/oauth2/v2.0/token",
"mail.smtp.auth": "true",
"mail.smtp.from": "[Exchange email address]",
"mail.smtp.starttls.enable": "true",
"mail.smtp.host": "smtp.office365.com",
"mail.smtp.auth.mechanisms": "XOAUTH2",
"mail.smtp.ssl.enable": "false",
"mail.smtp.ssl.checkserveridentity": "false",
"mail.transport.protocol": "smtp",
"mail.bpm.send.disable": "true",
"scope": "https://outlook.office.com/SMTP.Send https://outlook.office.com/Mail.Send",
"mail.smtp.starttls.required": "true",
"clientSecret": "[Client Secret from Azure AD]",
"mail.description": "Mail Server Configuration for SAP Process Automation",
"Password": "[Exchange email address passwords]"
}
ibibhu
Product and Topic Expert
Product and Topic Expert
0 Kudos

Recently we did face a similar issue using @sap-cloud-sdk/mail-client' (3.7.0)

It was failing with host name and port name not found

'@sap-cloud-sdk/mail-client' library tries to access the host and port from the below

https://github.com/SAP/cloud-sdk-js/blob/main/packages/mail-client/src/mail-client.ts#L54-L69

function getHostAndPort(
destination: Destination,
originalProperties: Record<string, any>
): { host: string; port: number } {
const host = originalProperties['mail.smtp.host'];
const port = originalProperties['mail.smtp.port'];

if (!(host && port)) {
throw Error(
`The destination '${toDestinationNameUrl(
destination
)}' does not contain host or port information as properties. Please configure in the "Additional Properties" of the destination.`
);
}
return { host, port: parseInt(port) };
}

But these properties are not present at the root, instead, it is inside "destinationConfiguration"

originalProperties

2. The mail-client only supports basic authentication, so "user" and "password" are required. ( an OAUTH2 auth it would not work)

For reference based on the @sap-cloud-sdk/mail-client  I have created a basic utility where I have fixed both

/**
* @module mailUtilClient
*/

// Import necessary modules and packages
const { toDestinationNameUrl } = require("@sap-cloud-sdk/connectivity");
const { resolveDestination } = require("@sap-cloud-sdk/connectivity/internal");
const cloudsdkUtil = require("@sap-cloud-sdk/util");
const logger = cloudsdkUtil.createLogger({
package: 'mail-util-client',
messageContext: 'mail-util-client'
});

const nodemailer = require("nodemailer");

/**
* Build a mail destination from a resolved destination.
*
* @param {Object} destination - The resolved destination object.
* @returns {Object} - The mail destination.
*/
function buildMailDestination(destination) {
const originalProperties = destination.originalProperties;
if (!originalProperties) {
throw Error(
`The destination '${toDestinationNameUrl(
destination
)}' does not contain any properties.`
);
}
const { host, port } = getHostAndPort(destination, originalProperties);
const from = originalProperties?.destinationConfiguration["mail.smtp.from"];
const user = originalProperties?.destinationConfiguration["User"];
const accessToken = originalProperties?.authTokens[0].value;

return {
...destination,
host,
port,
from,
user,
accessToken
};
}

/**
* Get host and port from the original properties of a destination.
*
* @param {Object} destination - The resolved destination object.
* @param {Object} originalProperties - The original properties of the destination.
* @returns {Object} - An object with host and port properties.
*/
function getHostAndPort(destination, originalProperties) {
const host = originalProperties?.destinationConfiguration["mail.smtp.host"];
const port = originalProperties?.destinationConfiguration["mail.smtp.port"];
if (!(host && port)) {
throw Error(
`The destination '${toDestinationNameUrl(
destination
)}' does not contain host or port information as properties. Please configure in the "Additional Properties" of the destination.`
);
}
return { host, port: parseInt(port) };
}

/**
* Send an email using Nodemailer.
*
* @param {Object} mailDestination - The mail destination object.
* @param {Object} mailConfigs - The email configuration object.
* @param {Object} mailClientOptions - Options for the mail client.
* @returns {Promise} - A promise that resolves with the response.
*/
async function sendMailWithNodemailer(
mailDestination,
mailConfigs,
mailClientOptions
) {
const transport = await createTransport(mailDestination, mailClientOptions);

const mailConfigsFromDestination =
buildMailConfigsFromDestination(mailDestination);

let response;
response = await sendMailInSequential(
transport,
mailConfigsFromDestination,
mailConfigs
);
return response;
}

/**
* Create a Nodemailer transport configuration.
*
* @param {Object} mailDestination - The mail destination object.
* @param {Object} mailClientOptions - Options for the mail client.
* @returns {Object} - The Nodemailer transport configuration.
*/
function createTransport(mailDestination, mailClientOptions) {
const baseOptions = {
pool: true,
auth: {
type: "OAuth2",
user: mailDestination.user,
accessToken: mailDestination.accessToken,
},
};
return nodemailer.createTransport({
...baseOptions,
host: mailDestination.host,
port: mailDestination.port,
...mailClientOptions,
});
}

/**
* Build mail configurations from a mail destination.
*
* @param {Object} mailDestination - The mail destination object.
* @returns {Object} - Mail configurations.
*/
function buildMailConfigsFromDestination(mailDestination) {
const from = mailDestination.from;
return { from };
}

/**
* Send multiple emails sequentially using Nodemailer.
*
* @param {Object} transport - The Nodemailer transport configuration.
* @param {Object} mailConfigsFromDestination - Mail configurations from the destination.
* @param {Object} mailConfigs - Email configurations.
* @returns {Promise} - A promise that resolves with an array of responses.
*/
async function sendMailInSequential(
transport,
mailConfigsFromDestination,
mailConfigs
) {
const response = [];
for (const mailConfigIndex in mailConfigs) {
logger.debug(
`Sending email ${mailConfigIndex + 1}/${mailConfigs.length}...`
);
response[mailConfigIndex] = await transport.sendMail({
...mailConfigsFromDestination,
...mailConfigs[mailConfigIndex],
});
logger.debug(
`...email ${mailConfigIndex + 1}/${mailConfigs.length} for subject "${
mailConfigs[mailConfigIndex].subject
}" was sent successfully.`
);
}
return response;
}

/**
* Transform a single element or an array into an array.
*
* @param {Array|Object} singleElementOrArray - An element or an array.
* @returns {Array} - An array containing the element(s).
*/
function transformToArray(singleElementOrArray) {
return Array.isArray(singleElementOrArray)
? singleElementOrArray
: [singleElementOrArray];
}

/**
* Send emails using a resolved destination, email configurations, and client options.
*
* @param {Object} destination - The resolved destination object.
* @param {Object} mailConfigs - The email configurations.
* @param {Object} mailClientOptions - Options for the mail client.
* @returns {Promise} - A promise that resolves with the response.
*/
module.exports = {
sendMail: async function (destination, mailConfigs, mailClientOptions) {
const resolvedDestination = await resolveDestination(destination);
if (!!resolvedDestination.type && resolvedDestination.type !== "MAIL") {
throw Error(
`The type of the destination '${toDestinationNameUrl(
destination
)}' has to be 'MAIL', but is '${destination.type}'.`
);
}
const mailDestination = buildMailDestination(resolvedDestination);

return sendMailWithNodemailer(
mailDestination,
transformToArray(mailConfigs),
mailClientOptions
);
},
};


 

satyaP1395
Discoverer
0 Kudos

Screenshot 2024-04-25 113932.pngHI any body kelp me i am facing this error : 

Uncaught Error: The destination 'name: mail_destination' does not contain host or port information as properties. Please configure in the "Additional Properties" of the destination.

how to resolve this error.

Labels in this area