Sending an Order by Email

Summary

In this section we add a facility to send orders to suppliers by email. Or, rather more specifically, we will email a link to a page that will display the order.  We will be using a NuGet Package called MailKit and creating a separate page to test that we have MailKit setup correctly.

MailKit

Why MailKit?  My initial investigations led me in the direction of SendGrid.  For example, the free e-book published by Syncfusion, 'Blazor Succinctly' - which I would recommend - uses SendGrid for emails.  However, although SendGrid has a free option (that limits the user to 100 emails/day), it does not allow emails to be sent from GMail accounts, even with the GMail 'Enable Less secure apps' switched on.  For individuals this could be a problem.  I could find no obvious drawbacks to using MailKit.

YouTube Video

Install MailKit

In Visual Studio select 

  • Tools > NuGet Package Manager > Manage NuGet Packages for Solution
  • Select Browse and enter 'MailKit'
  • Select Mailkit by Jeffrey Stedfast and install.

Create a Test Page

Before attempting to send emails from the purchase orders page, we will test the basic email setup using a separate page, with hard-coded credentials.

In the Pages folder add a new razor component called MailKitTestPage and replace the default code with the following:

@page "/mailkittest/"

@using MailKit.Net.Smtp;
@using MailKit.Security;
@using MimeKit;
@using MimeKit.Text;

<h3>Email Test Page</h3>
<hr />
<div class="button-container">
    <button type="button" class="e-btn e-normal e-primary" @onclick="@SendEmail">Send Email</button>
</div>

<div>
    <p>
        //GMail<br />
        smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
    </p>
    <p>
        //Outlook<br />
        smtp.Connect("smtp.live.com", 587, SecureSocketOptions.StartTls);
    </p>
    <p>
        //Office 365<br />
        smtp.Connect("smtp.office365.com", 587, SecureSocketOptions.StartTls);
    </p>
</div>

@code {
    public MimeMessage email = new MimeMessage();

    public void SendEmail()
    {
        email.From.Add(MailboxAddress.Parse("FromEmailAddress"));
        email.To.Add(MailboxAddress.Parse("ToEmailAddress"));
        email.Subject = "Email Subject goes here";
        email.Body = new TextPart(TextFormat.Html) { Text = "This is the body of the email" };

        // send email
        using var smtp = new SmtpClient();
        smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
        smtp.Authenticate("FromUserName", "FromUserPassword");
        smtp.Send(email);
        smtp.Disconnect(true);
    }
}

This is the barebones code needed, and illustrates a form with a 'Send Email' button, using a GMail account.

To add this to the menu, edit 'NavMenu.razor' in the Shared folder and add the following (in a suitable 'Roles' section)

<li class="nav-item px-3">
    <NavLink class="nav-link" href="mailkittest">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Mailkit Test
    </NavLink>
</li>

If you are using a GMail account you will also need to set 'Less secure apps' to 'On'.  To do this:

  1. Log in to your GMail account
  2. Navigate to the 'Less secure apps' page
  3. Toggle to turn this feature 'ON'

For other common email providers substitute the smtp.Connect line with:

//GMail
smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls);
//Outlook
smtp.Connect("smtp.live.com", 587, SecureSocketOptions.StartTls);
//Office 365
smtp.Connect("smtp.office365.com", 587, SecureSocketOptions.StartTls);

To test, substitute valid data for the placeholders in the above code, (FromEmailAddress, ToEmailAddress, FromUserName and FromUserPassword) run the application and select the 'MailKit Test' option from the menu.  Click the 'Send Email'.  Nothing will appear to happen, but an email should be received at the 'ToEmailAddress'.

Setup an Email Service

We will eventually be adding the ability to send an email from the Index page, and could simply copy and paste most of the above code into the Index page.  However, in most systems there will be a need to send emails from a variety of pages, so it makes much more sense to create an email service that contains the bulk of the code that can be called from any number of different pages, passing the required parameters to the service.  To create an email service we need to take the following steps:

  • We will need to record some information, such as email user name and password, however we don't want to 'hardcode' this type of data into the actual C# code - both from a security perspective (uploading the code to GitHub, for example, and also if user names and passwords are changed we don't need to edit and recompile the application.  Instead we will save it in appsettings.json and read from that file when we need to.  Add the following to appsettings.json, anyware will do, but I suggest at the top, before "ConnectionStrings":
  "SmtpHost": "smtp.hostname.com",
  "SmtpPort": 587,
  "SmtpUserFriendlyName": "Your name here",  //E.g John Smith
  "SmtpUserEmailAddress": "SmtpUserEmailAddress",
  "SmtpPass": "EmailPassword",

Leave SmtpPort as '587', but you will need to substitute your own information, such 'smpt.gmail.com' for SmtpHost, etc. 

  • Open the Data folder and add a new class, call it 'EmailSenderMailKit.cs'. Copy and paste the following code, overwriting the default code.
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Configuration;
using MimeKit;
using MimeKit.Text;
using System;

namespace BlazorPurchaseOrders.Data
{
    public interface IEmailService
    {
        bool Send(string to, string subject, string body);
    }

    public class EmailService : IEmailService
    {
        private readonly IConfiguration configuration;

        public EmailService(IConfiguration Configuration)
        {
            configuration = Configuration;
        }

        public bool Send(string to, string subject, string body)
        {
            //Get settings from appsettings
            var SmtpHost = configuration["SmtpHost"];
            var SmtpPort = configuration["SmtpPort"];
            var SmtpUserFriendlyName = configuration["SmtpUserFriendlyName"];
            var SmtpUserEmailAddress = configuration["SmtpUserEmailAddress"];
            var SmtpPass = configuration["SmtpPass"];
            // create message
            var email = new MimeMessage();
            email.From.Add(new MailboxAddress(SmtpUserFriendlyName, SmtpUserEmailAddress));
            email.To.Add(new MailboxAddress(to,to));
            email.Subject = subject;
            email.Body = new TextPart(TextFormat.Html) { Text = body };

            try
            {
                // send email
                using var smtp = new SmtpClient();                

                smtp.Connect(SmtpHost, Int32.Parse(SmtpPort), SecureSocketOptions.StartTls);
                smtp.Authenticate(SmtpUserEmailAddress, SmtpPass);
                smtp.Send(email);
                smtp.Disconnect(true);

                return true;
            }
            catch
            {
                return false;
            }

        }
    }
}

I won't try to explain the 'configuration' code, other than to say it gets the data from appsettings.json.  The IEmailService interface and Send method are declared as a boolean (to indicate whether they succeeded or not) and have passed to them variables for the 'To' address, the email subject and the email body.  The 'Send' method then collects the data from the configuration and creates the MailKit 'MimeMessage'.  There is then an error-trapping 'try-catch' statement, the Try block attempts to send the email and returns 'true' if it succeeds; if the Send fails the code proceeds to the Catch block, where the return is set to false.  We will use this true/false status later to notify the user of success or failure.  In my experience failure is normally caused by a mistyped to or from email address, or password.

  • Because we are using a service we also need to edit 'Startup.cs' and add the following line to the 'ConfigureServices' method.
services.AddScoped<IEmailService, EmailService>();

Emailing an order

In summary, the changes we want to make to Index.razor are:

  • Inject the IEmailService
  • Add an item to the toolbar with the on-click event triggering the sending of an email.
  • Including a suitable 'email' icon in the toolbar button
  • Adding the toolbar event method to send the email
  • Handle the success or failure of the 'send email' by using the Syncfusion 'Toast' control.

To use the EmailService we need to inject it.  Add the following line of code at the top of the page.

@inject IEmailService EmailService

Next, we'll add the item to the toolbar.  In the 'OnInitializedAsync' method add the following line to the Toolbaritems list.

Toolbaritems.Add(new ItemModel() { Text = "Email", TooltipText = "Email selected order" });

Notice that unlike the other items there is no PrefixIcon.  I had quite a bit of trouble before I found a suitable icon (I'm still not sure if there is a simpler answer to this problem).  In the end I discovered a library of icons on the Bootstrap website.  To use these icons enter the following at the top of the HTML section.  It is essentially pointing to a location from which icons can be downloaded as required.

<head>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css">
</head>

With this in place we can now add the PrefixIcon code to then end of the Toolbar item.  Here I show the whole line.

Toolbaritems.Add(new ItemModel() { Text = "Email", TooltipText = "Email selected order", PrefixIcon = "bi-envelope" });

We need the supplier email address (which we will get directly from the purchase order), and additionally I want to refer to the order number in the email. To do this we need to declare variables called 'selectedPOHeaderSupplierEmail' and 'selectedPOHeaderOrderNumber'.  Enter the declarations somewhere near to top of the code section (under the declarations of the other 'selected' items would be logical.)

private string selectedPOHeaderSupplierEmail {get; set;} = "";
private int selectedPOHeaderOrderNumber { get; set; } = 0;

To assign the correct value to these variables, add the following lines to the RowSelectHandler.

selectedPOHeaderSupplierEmail = args.Data.POHeaderSupplierEmail;
selectedPOHeaderOrderNumber = args.Data.POHeaderOrderNumber;

The last stage of preparation is to declare a variable that will be used to compose the body of the email that will be passed to the EmailService.  I am going to call this variable 'emailbody' (no prizes for originality!).  Enter the following line of code somewhere near the top of the code section.

private string emailbody = "";

The next stage is to handle the toolbar click event. Here is the almost complete code; it will require additional code to handle the 'Toast', which we will add shortly.

if (args.Item.Text == "Email")
{
    //Code for editing - Check that an Order has been selected from the grid
    if (selectedPOHeaderID == 0)
    {
        WarningHeaderMessage = "Warning!";
        WarningContentMessage = "Please select an Order from the grid.";
        Warning.OpenDialog();
    }
    else
    {
        //Create body of email using html
        emailbody = "<p>Dear Supplier</p>";
        emailbody = emailbody + "<p>Please click one of the following links (or copy and paste the link into your browser) to view our ";
        emailbody = emailbody + "order number " + selectedPOHeaderOrderNumber + " </p>";

        //without the anchor tag
        emailbody = emailbody + "<p>https://localhost:44377/previeworder/" + selectedPOHeaderGuid + "</p>";
                
        //with the anchor tag
        emailbody = emailbody + "<p><a href= &Qquot;https://localhost:44377/previeworder/" + selectedPOHeaderGuid + "&Qquot;>https://localhost:44377/previeworder/" + selectedPOHeaderGuid + "<a></p>";
                
        emailbody = emailbody + "<p>Kind regards</p>";
        emailbody = emailbody + "<p>BlazorCode</p>";

        //MailKit
        if (EmailService.Send(selectedPOHeaderSupplierEmail, "Purchase Order", emailbody))
        {
            //do something
        }
        else
        {
            //do something else
        };
    }
}

(If copying and pasting the above please replace '&Qquot;' with '&quot;')

As usual we are checking that the user has selected an order and issuing a warning message if one hasn't been selected.

The next part of the code builds the html text we are going to send as the body of the email.  You can amend 'emailbody' to suit your own requirements, but be aware that I have hardcoded the route to the 'previeworder' page to my development environment.  You will need to change this to point to your localhost, or IIS server (or better still, read this as a parameter from, say, appsettings.json).

I have included two versions of the link to preview the order, one without an anchor tag and one with.  The one with the anchor tag uses &quot; in place of double-quotes; this is because C# would otherwise interpret the double-quotes as the end (or beginning) of a string.

The reason I have included two versions is that I have discovered that some email clients and webmail need the anchor tag to make the link clickable, and bizarrely some others are only clickable without the anchor tag!  Since we cannot predict how the supplier is going to access the email, it would be much safer to include both types of link.  (I have also found email clients that refuse to make either version clickable.)

Finally, the 'EmailService.Send' method requires the email address of the recipient, a subject and the body of the email.  The email address is provided by 'selectedPOHeaderSupplierEmail', the subject is hardcoded as "Purchase Order" and the body is the html text built up as 'emailbody'.

The 'EmailService.send' is a boolean, so we can test to see if it returns true or false.  In the above code we just have placeholders, but we will replace these with an appropriate 'Toast' control to let the user know what has happened. The 'Toast' control we will use is the Syncfusion Toast control. To use this we need to do the following:

  • Add the Toast control to the HTML section:
<div class="col-lg-12 control-section toast-default-section">
    <SfToast @ref="ToastObj" Timeout="4000">
        <ToastPosition X="Right" Y="Top"></ToastPosition>
    </SfToast>
</div>

The 'class' is used for styling - see later.  The Timeout property is the time the Toast is displayed in milliseconds, here I have set it to 4 seconds, and the ToastPosition controls where the Toast is displayed.  The X options are 'Left', 'Center' and 'Right' and the Y options 'Top' or 'Bottom'.

  • Add the styling for the Toast.  (Most of this seems to be associated with the icons displayed on the Toast.  And the url really is that long!)
<style>
    @@font-face {
        font-family: 'Toast_icons';
        src: url(data:application/x-font-ttf;charset=utf-8;base64,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) format('truetype');
        font-weight: normal;
        font-style: normal;
    }

    #toast_types button {
        margin: 5px;
        min-width: 160px;
        max-width: 160px;
    }

    .toast-icons {
        font-family: 'Toast_icons' !important;
        speak: none;
        font-size: 55px;
        font-style: normal;
        font-weight: normal;
        font-variant: normal;
        text-transform: none;
        line-height: 1;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }

    #toast_type .e-toast-icon.e-icons {
        height: auto;
        font-size: 30px;
    }

    .toast-icons.e-success::before {
        content: "\e701";
    }

    .toast-icons.e-error::before {
        content: "\e700";
    }

    .toast-icons.e-info::before {
        content: "\e704";
    }

    .toast-icons.e-warning::before {
        content: "\e703";
    }

    #toast_types {
        text-align: center;
    }
</style>
  • Declare the Toast object.  To provide the Title, Content and Icons for the Toast add a List of possible values.  (The values shown here are taken from the Syncfusion documentation and include values that we will not be using, but are included here for completeness.)
SfToast ToastObj;

private List<ToastModel> Toast = new List<ToastModel>
{
    new ToastModel{ Title = "Warning!", Content="There was a problem with your network connection.", CssClass="e-toast-warning", Icon="e-warning toast-icons" },
    new ToastModel{ Title = "Success!", Content="Your message has been sent successfully.", CssClass="e-toast-success", Icon="e-success toast-icons" },
    new ToastModel{ Title = "Error!", Content="A problem has been occurred while submitting your email", CssClass="e-toast-danger", Icon="e-error toast-icons" },
    new ToastModel{ Title = "Information!", Content="Please read the comments carefully.", CssClass="e-toast-info", Icon="e-info toast-icons" }
};

Having these parts in place we can now replace the placeholders with code to display the appropriate Toast message:

//do something
await this.ToastObj.Show(Toast[1]);
//do something else
await this.ToastObj.Show(Toast[2]);

The result of this is that if the email is send is successfully the Toast is displayed with the title and content from index item 1 (remember that the index starts at 0, so 1 is the second entry).  Similarly, if the email fails to be sent the Toast with index 2 is displayed.

With these changes in place, it's time to test.  Select an order and make sure that the supplier email address on the order is one that you will receive.  Click the 'Email' button and wait for the Toast to be displayed and hopefully an incoming email message.

References

Project Code

The code for all the files changed in this post are shown here.