Tag: Azure Active Directory

webTiger Logo Wide
  • Sending Emails Using Microsoft Graph

    Microsoft Graph

    With Microsoft iminently shuttering the capability to send emails programmatically via MS Exchange in Azure, developers need to switch to a mechanism that will be supported moving forwards. Enter Microsoft Graph. This article describes how the MS Graph API can be leveraged to replace MS Exchange based mailing for the programmatic sending of emails.

    In this article:

    Preamble

    Microsoft Graph is a REST API originally developed as the officially supported API for Office 365 (now Microsoft 365 (MS365)). Since then, it has been massively expanded to cater for SharePoint Online, MS365’s Power Platform, Azure, Azure Active Directory (Azure Entra ID), and other targets.

    This diagram is from Microsoft’s developer center website to give you an idea how it is organised:

    Microsoft Graph Nodes

    Microsoft distribute NuGet packages to support the development of MS Graph interactions in .NET so we don’t have to resort to the raw REST API calls most of the time.

    Design Overview

    In this walkthrough we’ll design the solution as a abstracted service contract and vendor specific service implementation, so that Microsoft Graph doesn’t have to be the only solution available (although we won’t develop any alternatives) to the consuming code – the specific mailing service instance can be dependency injected at runtime.

    Microsoft Graph Based Bespoke Email Service Design

    Defining the Entity Classes and Service Contracts

    • Create a new C# Class Library project and solution in Visual Studio, calling it EmailService.
    • Create two folders in the code project: Entities and Services.
    • In the new code project, rename Class1.cs file to Credentials.cs and accept the renaming of the class name when prompted. Move the file to the Entities folder, and accept the namespace changes when prompted.
    • Add two other classes to the project in the Entities folder, called Attachment.cs and Email.cs.
    • Add a new interface to the project in the Services folder, calling the file IEmailService.cs.
    • Populate the classes and interface as described below.
    using System.Security;
    
    namespace EmailService.Entities
    {
        /// <summary>
        /// Specifies the credentials that the email service will use.
        /// </summary>
        public class Credentials
        {
            /// <summary>
            /// Represents the type of authentication the email service expects.
            /// </summary>
            public enum AuthenticationType
            {
                /// <summary>
                /// The email service doesn't require authentication.
                /// <para>(This can be the case for some localised SMTP servers.)</para>
                /// </summary>
                None,
    
                /// <summary>
                /// The email service requires an Azure app registration based client ID and secret.
                /// </summary>
                AzureClientSecret,
    
                /// <summary>
                /// The email service requires simple username and password based authentication.
                /// </summary>
                UsernameAndPassword
            }
    
            /// <summary>
            /// Creates a new instance of the class, specifying that authentication is not required.
            /// </summary>
            public Credentials()
            {
                AuthMode = AuthenticationType.None;
                ClientIdOrUsername = "";
                ClientSecretOrPassword = null;
                TenantId = null;
                Scopes = null;
                Domain = null;
            }
    
            /// <summary>
            /// Creates a new instance of the class, configuring the credentials to target an Azure App Registration.
            /// </summary>
            /// <param name="clientId">The client ID of the app registration.</param>
            /// <param name="secret">The client secret associated with the app registration.</param>
            /// <param name="tenantId">The ID of the tenant the app registration is specified on.</param>
            /// <param name="scopes">The authentication scopes to request, or an empty collection or null to ignore.</param>
            /// <param name="domain">The tenant's domain name, or an empty string or null to ignore.</param>
            public Credentials(
                string clientId,
                SecureString secret,
                string tenantId,
                IList<string>? scopes = null,
                string? domain = null)
            {
                AuthMode = AuthenticationType.AzureClientSecret;
                ClientIdOrUsername = clientId;
                ClientSecretOrPassword = secret;
                TenantId = tenantId;
                Scopes = scopes;
                Domain = domain;
            }
    
            /// <summary>
            /// Creates a new instance of the class, configuring the credentials to target a simple username and password based login.
            /// </summary>
            /// <param name="username">The account username to use.</param>
            /// <param name="password">The account password to use.</param>
            /// <param name="domain">The domain the account is associated with, if any, or an empty string or null to ignore.</param>
            public Credentials(string username, SecureString password, string? domain = null)
            {
                AuthMode = AuthenticationType.UsernameAndPassword;
                ClientIdOrUsername = username;
                ClientSecretOrPassword = password;
                TenantId = null;
                Scopes = null;
                Domain = domain;
            }
    
            public AuthenticationType AuthMode { get; }
    
            public string ClientIdOrUsername { get; }
    
            public SecureString? ClientSecretOrPassword { get; }
    
            public string? TenantId { get; }
    
            public IList<string>? Scopes { get; }
    
            public string? Domain { get; }
        }
    }
    Code language: JavaScript (javascript)
    namespace EmailService.Entities;
    
    /// <summary>
    /// Specifies an attachment that can be added to an email message.
    /// </summary>
    public class Attachment
    {
        /// <summary>
        /// Creates a new instance of the class.
        /// </summary>
        public Attachment()
        {
            Filename = "";
            Data = [];
        }
    
        /// <summary>
        /// Creates a new instance of the class, initialising the filename and raw data properties.
        /// </summary>
        /// <param name="name">The filename to set.</param>
        /// <param name="data">The attachment's raw data.</param>
        public Attachment(string name, byte[] data)
        {
            Filename = name;
            Data = data;
        }
    
        public string Filename { get; set; }
    
        public byte[] Data { get; set; }
    }
    Code language: JavaScript (javascript)
    using System.Collections.Generic;
    
    namespace EmailService.Entities;
    
    /// <summary>
    /// Describes an email message being sent.
    /// </summary>
    public class Email
    {
        /// <summary>
        /// Gets or sets the main recipients of the message.
        /// <para>At least one of <see cref="To"/>, <see cref="CarbonCopy"/>, or <see cref="BlindCopy"/> must contain a recipient.</para>
        /// </summary>
        public IList<string> To { get; set; } = new List<string>();
    
        /// <summary>
        /// Gets or sets the recipients, if any, that should be carbon-copied (CC'd) the message.
        /// <para>At least one of <see cref="To"/>, <see cref="CarbonCopy"/>, or <see cref="BlindCopy"/> must contain a recipient.</para>
        /// </summary>
        public IList<string> CarbonCopy { get; set; } = new List<string>();
    
        /// <summary>
        /// Gets or sets the recipients, if any, that should be blind-copied (BCC'd) the message.
        /// <para>At least one of <see cref="To"/>, <see cref="CarbonCopy"/>, or <see cref="BlindCopy"/> must contain a recipient.</para>
        /// </summary>
        public IList<string> BlindCopy { get; set; } = new List<string>();
    
        /// <summary>
        /// Gets or sets the email address from which the message is being sent.
        /// <para>This may be used to identify the sender's mailbox by some email provider implementations.</para>
        /// </summary>
        public string From { get; set; } = "";
    
        /// <summary>
        /// Gets or sets the subject (title) of the email.
        /// </summary>
        public string Subject { get; set; } = "";
    
        /// <summary>
        /// Gets or sets the message body content being sent.
        /// </summary>
        public string Message { get; set; } = "";
    
        /// <summary>
        /// Gets or sets a value indicating if the message is HTML formatted. 
        /// <para>(If not HTML, the email will be sent as plain text.)</para>
        /// </summary>
        public bool IsHtml { get; set; } = false;
    
        /// <summary>
        /// Gets or sets a collection of zero or more attachments being included with the email.
        /// </summary>
        public IList<Attachment> Attachments { get; set; } = new List<Attachment>();
    
        /// <summary>
        /// Gets or sets a collection of zero or more recipients any replies should be directed to.
        /// <para>If omitted, the sender will be set as the recipient of any replies.</para>
        /// </summary>
        public IList<string>? ReplyTo { get; set; }
    }Code language: HTML, XML (xml)
    using EmailService.Entities;
    
    namespace EmailService.Services;
    
    /// <summary>
    /// Defines the contract for a service that can be used for sending emails.
    /// </summary>
    public interface IEmailService
    {
        /// <summary>
        /// Gets or sets the authentication credentials the email service requires.
        /// <para>This must be set before calling <see cref="SendAsync(Email, bool)"/>.</para>
        /// </summary>
        Credentials Auth { get; set; }
    
        /// <summary>
        /// Sends a specified email message.
        /// </summary>
        /// <param name="message">The email message to send.</param>
        /// <param name="saveInSentFolder">(Optional, default: false) if asserted, saves the email message to the Sent emails folder of the mailbox being used.</param>
        Task SendAsync(Email message, bool saveInSentFolder = false);
    }
    Code language: HTML, XML (xml)

    Implementing the Microsoft Graph Based Email Service

    Now that the entities and service contract has been defined, let’s get on with developing the Microsoft Graph based mailing service implementation.

    • Add a new C# Class Library project to the solution, and call it EmailService.MicrosoftGraph.
    • Add the EmailService project as a dependency. (Right click Dependencies in Solution Explorer, choose ‘Add Project Reference’ and then select the EmailService project.)
    • Add the following NuGet packages to the project:
      • Microsoft.Graph.
      • Azure.Identity.
    • Rename the Class1.cs file to MicrosoftGraphEmailService.cs and accept the class name change when prompted.
    • Update the service class to inherit from IEmailService, and implement the members.
    • Implement the service code as follows:
    using System.Runtime.InteropServices;
    using System.Security;
    using Azure.Identity;
    using EmailService.Entities;
    using EmailService.Services;
    using Microsoft.Graph;
    using Microsoft.Graph.Models;
    using Microsoft.Graph.Users.Item.SendMail;
    
    namespace EmailService.MicrosoftGraph;
    
    /// <summary>
    /// Provides an implementation of <see cref="IEmailService"/> allowing 
    /// consuming code to send emails via the Microsoft Graph API.
    /// </summary>
    public class MicrosoftGraphEmailService : IEmailService
    {
        /// <inheritdoc />
        public Credentials Auth { get; set; } = new();
    
        /// <inheritdoc />
        public async Task SendAsync(Email message, bool saveInSentFolder = false)
        {
            GraphServiceClient client = CreateClient();
            Message email = ConvertEmailToGraphMessage(message);
    
            try
            {
                await client.Users[message.From].SendMail.PostAsync(
                    new SendMailPostRequestBody
                    {
                        Message = email,
                        SaveToSentItems = saveInSentFolder
                    });
            }
            catch (AuthenticationFailedException authEx)
            {
                throw new Exception("Failed to authenticate against MS Graph API.", authEx);
            }
            catch (Exception ex)
            {
                throw new Exception("An error occurred while attempting to send an email via the MS Graph API.", ex);
            }
        }
    
        private Message ConvertEmailToGraphMessage(Email email)
        {
            List<Recipient> to = new();
            List<Recipient> carbonCopy = new();
            List<Recipient> blindCopy = new();
            List<Recipient> replyTo = new();
    
            if (email.To.Count > 0)
            {
                to.AddRange(
                    email.To
                    .Where(x => !string.IsNullOrWhiteSpace(x))
                    .Select(address => new Recipient
                    {
                        EmailAddress = new EmailAddress { Address = address.Trim() }
                    }));
            }
    
            if (email.CarbonCopy.Count > 0)
            {
                carbonCopy.AddRange(
                    email.CarbonCopy
                    .Where(x => !string.IsNullOrWhiteSpace(x))
                    .Select(address => new Recipient
                    {
                        EmailAddress = new EmailAddress { Address = address.Trim() }
                    }));
            }
    
            if (email.BlindCopy.Count > 0)
            {
                blindCopy.AddRange(
                    email.BlindCopy
                    .Where(x => !string.IsNullOrWhiteSpace(x))
                    .Select(address => new Recipient
                    {
                        EmailAddress = new EmailAddress { Address = address.Trim() }
                    }));
            }
    
            if (email.ReplyTo != null)
            {
                replyTo.AddRange(
                    email.ReplyTo
                    .Where(x => !string.IsNullOrWhiteSpace(x))
                    .Select(address => new Recipient
                    {
                        EmailAddress = new EmailAddress { Address = address.Trim() }
                    }));
            }
    
            if (!to.Any() && !carbonCopy.Any() && !blindCopy.Any())
            {
                throw new ArgumentException("At least one email recipient must be specified.", nameof(email));
            }
    
            Message message = new()
            {
                ToRecipients = to,
                CcRecipients = carbonCopy,
                BccRecipients = blindCopy,
                ReplyTo = replyTo,
                Subject = email.Subject,
                Body = new ItemBody
                {
                    ContentType = email.IsHtml ? BodyType.Html : BodyType.Text,
                    Content = email.Message
                }
            };
    
            if (email.Attachments.Count > 0)
            {
                List<Microsoft.Graph.Models.Attachment> attachments = new();
    
                foreach (Attachment attachment in email.Attachments)
                {                
                    FileAttachment file = new()
                    {
                        Name = attachment.Filename,
                        ContentBytes = attachment.Data                    
                    };
    
                    attachments.Add(file);
                }
    
                message.Attachments = attachments;
            }
    
            return message;
        }
    
        private GraphServiceClient CreateClient()
        {
            ValidateAuth(Auth);
    
            GraphServiceClient client;
    
            try
            {
                ClientSecretCredential credentials = new(
                    Auth.TenantId,
                    Auth.ClientIdOrUsername,
                    MicrosoftGraphEmailService.FromSecureString(Auth.ClientSecretOrPassword)
                    );
                client = new GraphServiceClient(credentials);
            }
            catch (ServiceException ex)
            {
                throw new Exception("An error occurred while initialising a new MS Graph client instance.", ex);
            }
    
            return client;
        }
    
        private void ValidateAuth(Credentials? credentials)
        {
            if (credentials == null) throw new ArgumentException("No credentials were supplied.", nameof(credentials));
    
            if (credentials.AuthMode != Credentials.AuthenticationType.AzureClientSecret)
            {
                throw new InvalidOperationException(
                    "This email service implementation requires Azure authentication settings.");
            }
    
            if (string.IsNullOrWhiteSpace(credentials.ClientIdOrUsername))
            {
                throw new ArgumentException("A valid client ID must be supplied.", nameof(credentials));
            }
    
            if (string.IsNullOrWhiteSpace(credentials.TenantId))
            {
                throw new ArgumentException("A value tenant ID must be supplied.", nameof(credentials));
            }
    
            if (credentials.ClientSecretOrPassword == null || credentials.ClientSecretOrPassword.Length == 0)
            {
                throw new ArgumentException("A client secret must be supplied.", nameof(credentials));
            }
        }
        
        /// <summary>
        /// Returns a plain text representation of the supplied <see cref="SecureString"/> object.
        /// </summary>
        /// <param name="encrypted">The <see cref="SecureString"/> value to parse.</param>
        private static string FromSecureString(SecureString? encrypted)
        {
            if (encrypted == null) return "";
    
            IntPtr pointer = IntPtr.Zero;
            try
            {
                pointer = Marshal.SecureStringToGlobalAllocUnicode(encrypted);
                return Marshal.PtrToStringUni(pointer) ?? "";
            }
            finally
            {
                Marshal.ZeroFreeGlobalAllocUnicode(pointer);
            }
        }
    }Code language: HTML, XML (xml)

    That’s it. You can now add the code projects / assemblies to your solutions and send emails using Microsoft Graph.

    NOTE: this tutorial didn’t include details on how to configure an Azure App Registration with the correct permissions because that is fairly straightforward to do, Microsoft have articles on it, and it would be time consuming to write up here.

  • Convert DOCX to PDF Using Microsoft Graph API

    Microsoft Graph API Logo

    Converting a file from DOCX to another format (such as PDF) had long been a pain in SharePoint and usually led to resorting to using Word Automation Services (WAS). Fortunately, modern SharePoint (Online) and the Microsoft Graph API mean you can do it with a few Web API calls.

    (more…)