Emails mit dem SAP Cloud Application Programming Model Senden und Empfangen

December 11, 2023
Autor:in:
Prüfer:in:
Sam Agnew
Twilion

Hallo und danke fürs Lesen! Dieser Blogpost ist eine Übersetzung von How to Send and Receive Email with SAP CAP.

Das SAP Cloud Application Programming Model (CAP) beschreibt sich selbst als ein Framework aus Sprachen, Bibliotheken und Werkzeugen zum Erstellen von Services und Anwendungen für Geschäftsanwendungen. Seine primären Fähigkeiten sind Datenbankmodellierung über Core Data Services und andere unternehmenskritische Funktionen für Lokalisierung, Datenschutz, Autorisierung und Messaging. Das Framework bietet eine hohe Abstraktion für diese Services, um die Entwickler so weit wie möglich von der Erstellung von Boilerplate-Code zu entlasten. CAP bietet die Flexibilität, beliebige JavaScript-Bibliotheken in seinen Lebenszyklus einzubeziehen, um andere Features zu integrieren, z.B. um das Benutzererlebnis auf die nächste Stufe zu heben. Dieser Beitrag zeigt, wie man mit einer CAP-Anwendung E-Mails sendet und empfängt. Dazu verwenden wir den SendGrid JavaScript-Client und Webhooks (auch bekannt als “Inbound Parse”).

Was bauen wir heute?

In diesem Beitrag werden wir das Rad nicht neu erfinden und bei dem bekannten Hello-world-Szenario bleiben: Dem Buchladen (Bookshop). Machen Sie sich keine Sorgen, wenn Sie diesen noch nicht verwendet haben. Buchladen-Beispiele sind kleine CRUD-Webanwendungen, die Entitäten aus einem Buchladen verwenden, wie Bücher, Autoren, Bestellungen usw. Das Projekt, das Sie erstellen werden, ist minimal und hat nur eine einzige Entität: Bücher. Alle Datensätze dieser Entität werden über einen schreibgeschützten REST-Endpunkt zugänglich gemacht. Es handelt sich also eher um eine RU (Read and Update) Webanwendung, ohne Create- und Delete-Operationen. Darüber hinaus wird die fertige Anwendung HTTP POST-Anfragen akzeptieren, um auf eingehende Bestellungen zu reagieren, wodurch der Lagerbestand des bestellten Buchs reduziert wird. Mit Hilfe von Twilio SendGrid stellen wir sicher, dass die Geschäftsleiter benachrichtigt werden, wenn der Bestand eines bestimmten Buches niedrig wird und eine einfache Möglichkeit bieten, weitere Bücher von den Lieferanten zu bestellen.

Anforderungen

Um diesem Tutorial folgen zu können, müssen folgende Voraussetzungen erfüllt sein:

Initialisierung eines Buchladen-Projekts

Um zu beginnen, verwenden Sie den cds init Kommandozeilenbefehl, um neue Projekte zu starten. Es werden dann alle erforderlichen Dateien generiert.

cds init bookshop --add samples
cd bookshop
npm install

Als Nächstes starten Sie Ihren lokalen Webserver mit einem Befehl, der diese drei Argumente verwendet:

  • --in-memory stellt sicher, dass das Datenbankschema in SQLite bereitgestellt wird.
  • --to rest steuert das Webprotokoll, das verwendet wird, um die Datenservices freizugeben.
  • --watch startet den Server automatisch neu, wenn Dateiänderungen auftreten.

Für die letzte Option müssen Sie zuerst eine neue Abhängigkeit installieren:

npm add -D @sap/cds-dk
cds serve all --watch --in-memory --to rest

Testen Sie das Projekt, indem Sie eine HTTP GET-Anfrage an http://localhost:4004/catalog/Books senden. Dafür empfehle ich die Nutzung der REST Client-Erweiterung für Visual Studio Code und einer neuen Datei namens requests.http:

### Get book data from the catalog 
GET http://localhost:4004/rest/catalog/Books

Sie sollten eine Antwort sehen, nachdem Sie die Anfrage gesendet haben.

VS Code after sending an http request

Sie können auch curl verwenden, um diese Anfrage zu senden.

curl http://localhost:4004/rest/catalog/Books

Als Nächstes wird eine Aktion hinzugefügt, die einen POST-Endpunkt in Ihrem Webservice definiert. Fügen Sie die unten angegebene Zeile in die Katalogdefinition von srv/cat-services.cds ein.

using my.bookshop as my from '../db/data-model';

service CatalogService {
   @readonly
   entity Books as projection on my.Books;

   action submitOrder(book : Books:ID, quantity : Integer);
}

Diese Aktion wird die eingehende Anfrage verarbeiten und die Parameter nutzen, um den Lagerbestandswert zu reduzieren. Sollte dieses Feld negativ werden, muss die Anfrage fehlschlagen. Erstellen Sie eine neue Datei namens srv/cat-service.js:

const cds = require("@sap/cds");

class CatalogService extends cds.ApplicationService {
 init() {
const { Books } = cds.entities("my.bookshop");

   // Reduce stock of ordered books if available stock suffices
   this.on("submitOrder", async (req) => {
     const { book, quantity } = req.data;
     let { stock, title } = await SELECT`stock, title`.from(Books, book);
     const remaining = stock - quantity;
     if (remaining < 0) {
       return req.reject(409, `${quantity} exceeds stock for book #${book}`);
     }
     await UPDATE(Books, book).with({ stock: remaining });
     return { ID: book, stock: remaining };
   });

   return super.init();
 }
}

module.exports = { CatalogService };

Nach dem Speichern der Datei wird der Service automatisch dank der watch Option neu gestartet.

Fügen Sie nun die folgenden Zeilen zur Datei requests.http hinzu, um die zweite Anfrage zu definieren:

### Get stock data from the catalog 
GET http://localhost:4004/rest/catalog/Books

### Submit an order
POST http://localhost:4004/rest/catalog/submitOrder HTTP/1.1
Content-Type: application/json

{
   "book": 1,
   "quantity": 95
}

Sie können auch curl verwenden, um diese Anfrage über das Terminal zu senden.

curl -X POST -d '{"book":1,"quantity":95}'  \
  -H 'Content-Type: application/json' \
  http://localhost:4004/rest/catalog/submitOrder

Die erste Anfrage wird eine leere, aber erfolgreiche Antwort zurückgeben. Alle folgenden Anfragen werden fehlschlagen, weil der Lagerbestand zu niedrig ist.

VS Code with the expected error message in the console

Senden Sie eine E-Mail, wenn der Lagerbestand signifikant sinkt

Im nächsten Abschnitt werden Sie eine Benachrichtigungsfunktion via  E-Mail hinzufügen. Sie werden dazu den SendGrid-Client für Node.js verwenden. Dazu müssen Sie den SendGrid Node.js-Client als Abhängigkeit zum Projekt hinzufügen.

npm add @sendgrid/mail

Sie müssenden API-Key Ihres SendGrid-Kontos, eine E-Mail-Adresse (die Ihrem verifizierten Absender entspricht) und eine beliebige Empfängeradresse definieren. Stellen Sie sicher, dass diese Umgebungsvariablen in der Datei default-env.json gespeichert sind:

{
 "SENDGRID_API_KEY": "<Replace with API Key>",
 "SENDGRID_SENDER": "<Replace with Sender's Email Address>",
 "SENDGRID_RECEIVER": "<Replace with Receiver's Email Address>"
}

Initialisieren Sie jetzt den SendGrid-Client und senden Sie eine E-Mail, wenn der Lagerbestand den Schwellenwert erreicht. Fügen Sie die hervorgehobenen Zeilen zur Service-Implementierung srv/cat-service.js hinzu:

const cds = require("@sap/cds");
const sgMail = require("@sendgrid/mail");

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

class CatalogService extends cds.ApplicationService {
 init() {
   const { Books } = cds.entities("my.bookshop");

   // Reduce stock of ordered books if available stock suffices
   this.on("submitOrder", async (req) => {
     const { book, quantity } = req.data;
     let { stock, title } = await SELECT`stock, title`.from(Books, book);
     const remaining = stock - quantity;
     if (remaining < 0) {
       return req.reject(409, `${quantity} exceeds stock for book #${book}`);
     }
     await UPDATE(Books, book).with({ stock: remaining });

     if (remaining < 10) {
       const msg = {
         to: process.env.SENDGRID_RECEIVER,
         from: process.env.SENDGRID_SENDER,
         subject: 'Low Stock Alert',
         text: `A customer just ordered ${quantity}x "${title}" and there are only ${remaining} left in stock.`,
         html: `<strong>A customer just ordered ${quantity}x "${title}" and there are only ${remaining} left in stock.</strong>`,
       };
       
       sgMail
         .send(msg)
         .then(() => console.log('Email sent'))
         .catch((error) => console.error(error));
     }

     return { ID: book, stock: remaining };
   });

   return super.init();
 }
}

module.exports = { CatalogService };

Lösen Sie eine Bestellung aus, indem Sie die zwei HTTP-Anfrage ausführen.

Console output indicating the email has been sent

Jetzt sollten Sie eine E-Mail erhalten, die den aktuellen Lagerbestand anzeigt.

Email in the inbox

Hören Sie auf eingehende E-Mails

Der vorherige Abschnitt hat einen einseitigen Kommunikationskanal von Ihrem Projekt zum E-Mail-Postfach der Buchladenmanager eingerichtet. Dieser letzte Abschnitt wird ihn in einen beidseitigen Kommunikationskanal umwandeln, der Antworten lesen kann, die von den Managern zurückgesendet werden.

Im SendGrid-Dashboard können Sie steuern, was passiert, wenn SendGrid eine E-Mail erhält, die an eine Ihrer Domains adressiert ist. SendGrid wird dann eine Webhook aufrufen. Daher werden wir eine benutzerdefinierte Middleware einsetzen, um deise Webhook zu implementieren und die E-Mail zu verarbeiten.

Da die Anwendung derzeit auf localhost läuft, müssen Sie einen Tunnel öffnen, um Traffic vom SendGrid-Rechenzentrum zu Ihrem Rechner zu senden. Dafür verwenden Sie ngrok.

Zunächst teilen Sie dem Buchladenmanager mit, wie er auf die ursprüngliche E-Mail antworten kann. Ändern Sie dazu die folgende Zeile in der Dienstimplementierung srv/cat-service.js:

const msg = {
  to: process.env.SENDGRID_RECEIVER,
  from: process.env.SENDGRID_SENDER,
  subject: 'Low Stock Alert',
  text: `A customer just ordered ${quantity}x "${title}" and there are only ${remaining} left in stock. Please respond with "Yes" `+ `if you would like to restock now.`,
  html: `<strong>A customer just ordered ${quantity}x "${title}" and there are only ${remaining} left in stock. Please respond with "Yes" `+ `if you would like to restock now.</strong>`,
};

Als Nächstes richten Sie einen Inbound Parse / Webhook in SendGrid ein, der es Ihrer Anwendung ermöglicht, eingehende E-Mails von Buchladenmanagern zu empfangen. SendGrid wird alle HTTP-Anfragen mit dem Content-Type multipart/form-data senden. Um diese Anfragen in Ihrer App zu verarbeiten, installieren Sie multer.

npm add multer

Um eine benutzerdefinierte Middleware mit CAP zu erstellen, müssen Sie nur eine Datei srv/server.js erstellen und auf das bootstrap-Ereignis hören, bevor Sie den SendGrid-Client initialisieren können. Dieser Code wird den Inhalt der E-Mail auf der Konsole ausgeben und eine erfolgreiche Antwort zurücksenden.

const cds = require("@sap/cds");
const multer = require('multer');
const sgMail = require("@sendgrid/mail");

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const upload = multer({ dest: 'uploads/' })

cds.on("bootstrap", (app) => {

   app.post(
       "/sendgridInbound",
       upload.none(),
       async (req, res) => {
            console.log(`Received message ${req.body.text}.`)
           res.end("ok");
       }
   );
});

Sie können den Empfang eingehender Emails nun testen. Danach können wir die oben erklärte Funktion wie folgt implementieren.

hl_lines="9 10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40  41  42 44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72  73"
const cds = require("@sap/cds");
const multer = require("multer");
const sgMail = require("@sendgrid/mail");

sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const upload = multer({ dest: "uploads/" });

async function collectBookDetails(sender, message) {
 if (message.includes("Yes")) {
   const restockPattern = /Yes (\d+)/i;
   const lastOrderPattern = /(\d+)x/;
   const titlePattern = /"(.*?)"/;

   const restock = message.match(restockPattern)
     ? +message.match(restockPattern)[1]
     : undefined;

   const originalMessage = message.match(/>(.*)/g).join("");

   try {
     const lastOrder = +originalMessage.match(lastOrderPattern)[1];
     const title = originalMessage.match(titlePattern)[1];
     const { Books } = cds.entities("my.bookshop");

     const books = await cds.read(Books).where({ title });

     return {
       valid: true,
       restock: restock || lastOrder,
       book: books[0],
     };
   } catch (err) {
     //regex didn't find a last order or book title
     return { valid: false };
   }
 }
 return {
   valid: false,
 };
}

cds.on("bootstrap", (app) => {
 app.post("/sendgridInboud", upload.none(), async (req, res) => {
   const { from } = JSON.parse(req.body.envelope),
     to = req.body.to,
     parsed = await collectBookDetails(from, req.body.text);
   if (
     from === process.env.SENDGRID_RECEIVER &&
     to === process.env.SENDGRID_SENDER &&
     parsed.valid
   ) {
     const newStock = parsed.book.stock + parsed.restock;

     const { Books } = cds.entities("my.bookshop");
     await UPDATE(Books, parsed.book.ID).with({ stock: newStock });
     sgMail.send({
       to: process.env.SENDGRID_RECEIVER,
       from: process.env.SENDGRID_SENDER,
       subject: `RE: ${req.body.subject}`,
       text: `Successfully restocked "${parsed.book.title}". Current stock: ${newStock}`,
       html: `<strong>Successfully restocked "${parsed.book.title}". Current stock: ${newStock}</strong>`,
     });
   } else {
     const msg = {
       to: process.env.SENDGRID_RECEIVER,
       from: process.env.SENDGRID_SENDER,
       subject: `RE: ${req.body.subject}`,
       text: `Failed to restock. Please reply with "Yes <additionalStock>"`,
       html: `<strong>Failed to restock. Please reply with "Yes <additionalStock>"</strong>`,
     };
     sgMail.send(msg);
   }
   res.end("ok");
 });
});

Nun können die Manager den Lagerbestand auffüllen, indem sie auf die E-Mail mit "Yes <additionalStock>" antworten. Danach sollten Sie eine E-Mail erhalten, die das Auffüllen bestätigt. Wenn das Auffüllen fehlschlägt, erhalten Sie eine E-Mail, die den Fehler anzeigt.

Verwenden Sie ngrok, um einen Tunnel von Ihrem lokalen Port 4004 ins Internet zu öffnen.

ngrok http 4004

Navigieren Sie auf Ihrem SendGrid-Dashboard zu Settings -> Inbound Parse. Wählen Sie Ihre Domain aus und geben Sie die ngrok-URL Ihrer Anwendung ein, um eingehende E-Mails zu bearbeiten.

Configuration of the inbound parse

Lassen Sie uns einen Versuch starten. Antworten Sie mit "Yes 2000" auf die Nachricht, die Sie vor ein paar Minuten erhalten haben. Erfragen Sie nun erneut die aktuellen Lagerbestandsinformationen über die erste HTTP-Anfrage.

Ist der Bestand wieder aufgefüllt?

Restocked supply after email response was sent

Was kommt als Nächstes?

Herzlichen Glückwunsch! Sie haben soeben einen beidseitigen Kommunikationskanal zu einer CAP-Buchladen-Anwendung hinzugefügt. Und damit SendGrid integriert und das Benutzererlebnis der Buchladenmanager verbessert!

Sie können diese Anwendung weiter ausbauen, indem Sie andere Kommunikationskanäle wie SMS, WhatsApp oder Sprachanrufe integrieren. Darüber hinaus können Sie auch Ihren Anmeldeprozess verbessern, um Ihre Webanwendung vor Bots zu schützen. Als Referenz können Sie den vollständigen Quellcode auf GitHub finden.

Gerne können Sie sich bei mir melden, wenn Sie Fragen zu dieser Anwendung oder einem verwandten Thema haben.