The goal of this tutorial is to send an order confirmation by email directly from SAP transaction VA03. A pushbutton calls a .NET method that retrieves the order data via BAPI and sends it as a formatted HTML email.

We first add a pushbutton to the SAP screen:

GuiXT
pushbutton (3,22) "@J8\QSend order as e-mail@Send as E-Mail " _
    process="send_email_order_va03.txt"

In the process script send_email_order_va03.txt we first set a variable for the CC address, then call the .NET method asynchronously via callvbasync. The current order number is passed as a GuiXT field value:

GuiXT
// CC copy of the email to
Set V[ccRef] "your@email.com"

// Send order summary by email
callvbasync tutorials.utilities.send_order_html "&F[Order]" _
    "your@email.com" "Synactive Employee" "ccRef" _
    "Order &F[Order]"

For GuiXT to call .NET methods, the guinet.dll must be included as a project reference in the class library:

The script fetchData is called by the .NET method via g.Process(). It retrieves the order header data, line items and address information via BAPI BAPI_SALESORDER_GETDETAILBOS. The three data areas are joined into a single return string using the delimiter <|>:

GuiXT (fetchData)
PARAMETER vbeln

// Order header
CreateStructure V[orderheader] _
  sold_to doc_date purch_no net_val_hd currency purch_date

// Items
CreateTable V[orderitems] _
  itm_number material short_text req_qty sales_unit subtot_pp1

// Addresses
CreateStructure V[orderaddress] name street country postl_code city
CreateTable V[orderaddresses] include=V[orderaddress]

Call "BAPI_SALESORDER_GETDETAILBOS" _
  export.SALESDOCUMENT="vbeln" _
  import.ORDERHEADER="orderheader" _
  import.ORDERADDRESS="orderaddresses" _
  import.ORDERITEMS="orderitems"

return "&V[orderheader]<|>&V[orderitems]<|>&V[orderaddresses]"

The Email class contains the SMTP configuration and the send call. send_order creates a MailMessage with an optional CC recipient, enables SSL over TLS 1.2/1.3 and sends the email via an SmtpClient:

using guinet;
using System;
using System.Globalization;
using System.Net;
using System.Net.Mail;
using System.Text.Json.Nodes;

namespace Emails
{
    public class Email
    {
        private readonly string smtpHost = "your.server.de";
        private readonly int smtpPort = 587;

        private readonly string smtpUser = "your@email.com";
        private readonly string smtpPass = "password";

        public void send_order(
            string toEmail,
            string toName,
            string ccRef,
            string subject,
            string html)
        {
            MailMessage mail = new MailMessage();

            mail.From = new MailAddress(smtpUser, "Shop");
            mail.To.Add(new MailAddress(toEmail, toName));

            if (!string.IsNullOrEmpty(ccRef))
                mail.CC.Add(ccRef);

            mail.Subject = subject;
            mail.Body = html;
            mail.IsBodyHtml = true;

            SmtpClient smtp = new SmtpClient(smtpHost, smtpPort);

            smtp.Credentials = new NetworkCredential(
                smtpUser,
                smtpPass
            );

            smtp.EnableSsl = true;

            System.Net.ServicePointManager.SecurityProtocol =
                SecurityProtocolType.Tls12 |
                SecurityProtocolType.Tls13;

            smtp.Send(mail);
        }
    }

    // ...

The SMTPUtils class handles the entire HTML email assembly. The helper method format normalizes SAP decimal numbers: SAP returns amounts with a comma as the decimal separator (e.g. 1234,56), while Single.Parse with CultureInfo.InvariantCulture expects a dot. The comma is therefore replaced, the value parsed, and returned with two fixed decimal places.

send_order_html is the entry point called by callvbasync. Via g.Process("fetchData", orderno), GuiXT executes the fetchData script in the context of the current SAP session and returns the result string directly. The delimiter <|> between the three JSON blocks was chosen because it cannot appear in any SAP field value. After splitting, header data, line items and address are parsed as separate JsonNode objects and the order summary is written to the HTML body:

    // ...

    public class SMTPUtils
    {
        // Normalizes SAP comma decimals to "0.00" format
        public string format(string input)
        {
            input = input.Replace(",", ".");
            float val = float.Parse(
                input, CultureInfo.InvariantCulture);
            return val.ToString(
                "0.00", CultureInfo.InvariantCulture);
        }

        public string send_order_html(
            string orderno,
            string toEmail,
            string toName,
            string ccRef,
            string subject)
        {
            guixt g = new guixt();

            // Build HTML document head
            string HTML =
                "<!doctype html><html><head>" +
                "<meta charset='utf-8'></meta>";

            try
            {
                HTML += table_add_css() + "</head>";
                HTML += "<body><hr>" +
                    "Order summary for order " +
                    orderno + "<br><hr><br><br>";

                // g.Process executes the fetchData GuiXT script
                string data = g.Process("fetchData", orderno);
                // <|> is used as separator — never appears in data
                string[] data_arr = data.Split(
                    new[] { "<|>" },
                    StringSplitOptions.None
                );

                // Parse three JSON blocks: header, items, address
                JsonNode oHeader = JsonNode.Parse(data_arr[0]);
                JsonArray oItems =
                    JsonNode.Parse(data_arr[1]).AsArray();
                JsonNode oAddr =
                    JsonNode.Parse(data_arr[2])?[0];

                // Build address string from first address entry
                string address =
                    $"{oAddr["name"]}/{oAddr["street"]}/" +
                    $"{oAddr["country"]}-" +
                    $"{oAddr["postl_code"]} {oAddr["city"]}";

                // Write order summary lines to HTML body
                HTML += "Net value: " +
                    format(oHeader["net_val_hd"].ToString()) + " ";
                HTML += oHeader["currency"] + "<br>";
                HTML += "Sold-to: " +
                    address + "<br>";
                HTML += "Ship-to: " +
                    oHeader["sold_to"] + "<br>";
                HTML += "PO number: " +
                    oHeader["purch_no"] + "<br>";
                HTML += "PO date: " +
                    oHeader["purch_date"] + "<br>";

                HTML += "<br>Line items:<br><br>";

                // ...

The items table is built entirely as an HTML string. SAP returns position numbers with leading zeros (e.g. 000010). The value is therefore converted to an integer via Integer.Parse and then back to a string — this reliably removes the leading zeros without string manipulation. The subtotal from BAPI field subtot_pp1 is normalized via format and written to the last column together with the currency from the header data:

                // ...

                // Build items table
                HTML += "<table cellspacing='0'><thead><tr>";
                HTML += "<th>Pos</th>";
                HTML += "<th>Mat.No.</th>";
                HTML += "<th>Qty</th>";
                HTML += "<th>Unit</th>";
                HTML += "<th>Description</th>";
                HTML += "<th>Subtotal</th>";
                HTML += "</tr></thead><tbody>";

                foreach (JsonObject item in oItems)
                {
                  // Integer.Parse removes SAP leading zeros e.g. "000010" -> "10"
                    string itmNum = item["itm_number"].ToString();
                    int pos = int.Parse(itmNum);
                    HTML += "<tr>";
                    HTML += "<td>" + pos + "</td>";
                    HTML += "<td>" +
                        item["material"] + "</td>";
                    HTML += "<td>" +
                        item["req_qty"] + "</td>";
                    HTML += "<td>" +
                        item["sales_unit"] + "</td>";
                    HTML += "<td>" +
                        item["short_text"] + "</td>";
                    HTML += "<td>" +
                        format(item["subtot_pp1"].ToString()) +
                        " " + oHeader["currency"] +
                        "</td>";
                    HTML += "</tr>";
                }

                HTML += "</tbody></table>";

            // ...

If an error occurs, send_order_html returns the exception message as a return value — GuiXT then displays this string as the result of callvbasync, making debugging easier.

table_add_css returns a <style> block embedded directly in the HTML body. This is necessary because most email clients — including Outlook and Gmail — ignore or strip CSS in the <head>. CSS in the body ensures the table is rendered correctly in all clients:

            // ...

            catch (Exception ex)
            {
                return ex.Message;
            }

            Email e = new Email();
            e.send_order(toEmail, toName, ccRef, subject, HTML);

            return HTML;
        }

        private string table_add_css()
        {
            return @"
<style>
table {
    width: 100%;
    border-collapse: collapse;
    font-family: Arial, sans-serif;
}
th, td {
    border: 1px solid #ddd;
    padding: 10px;
    text-align: left;
}
th {
    background-color: #f4f4f4;
    font-weight: bold;
}
tr:nth-child(even) {
    background-color: #fafafa;
}
tr:hover {
    background-color: #f1f1f1;
}
</style>";
        }
    }
}

The result:

Download e-mail scripts (ZIP)