Send Emails using Microsoft Graph API and a desktop client

This article shows how to use Microsoft Graph API to send emails for a .NET Core Desktop WPF application. Microsoft.Identity.Client is used to authenticate using an Azure App registration with the required delegated scopes for the Graph API. The emails can be sent with text or html bodies and also with any file attachments uploaded in the WPF application.

Code: https://github.com/damienbod/EmailCalandarsClient

To send emails using Microsoft Graph API, you need to have an office license for the Azure Active Directory user which sends the email.

You can sign-in here to check this:

https://www.office.com

Setup the Azure App Registration

Before we can send emails using Microsoft Graph API, we need to create an Azure App registration with the correct delegated scopes. In our example, the URI http://localhost:65419 is used for the AAD redirect to the browser opened by the WPF application and this is added to the authentication configuration. Once created, the client ID of the Azure App registration is used in the app settings in the application as well as the tenant ID and the scopes.

You need to add the required scopes for the Graph API to send emails. These are delegated permissions, which can be accessed using the Add a permission menu.

The Mail.Send and the Mail.ReadWrite delegated scopes from the Microsoft Graph API are added to the Azure App registration.

To add these, scroll down through the items in the App a permission, Microsoft Graph API delegated scopes menu, check the checkboxes for the Mail.Send and the Mail.ReadWrite.

Desktop Application

The Microsoft.Identity.Client and the Microsoft.Identity.Web.MicrosoftGraphBeta Nuget packages are used to authenticate and use the Graph API. You probably could use the Graph API Nuget packages directly instead of Microsoft.Identity.Web.MicrosoftGraphBeta, I used this since I normally do web and it has everything required.

<ItemGroup>
  <PackageReference Include="Microsoft.Identity.Client" Version="4.39.0" />
  <PackageReference Include="Microsoft.Graph" Version="4.11.0" />
  <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

The PublicClientApplicationBuilder class is used to define the redirect URL which matches the URL from the Azure App registration. The TokenCacheHelper class is the same as from the Microsoft examples.

public void InitClient()
{
  _app = PublicClientApplicationBuilder.Create(ClientId)
            .WithAuthority(Authority)
            .WithRedirectUri("http://localhost:65419")
            .Build();

  TokenCacheHelper.EnableSerialization(_app.UserTokenCache);
}

The identity can authentication using the SignIn method. If a server session exists, a token is acquired silently otherwise an interactive flow is used.

public async Task<IAccount> SignIn()
{
	try
	{
		var result = await AcquireTokenSilent();
		return result.Account;
	}
	catch (MsalUiRequiredException)
	{
		return await AcquireTokenInteractive().ConfigureAwait(false);
	}
}

private async Task<IAccount> AcquireTokenInteractive()
{
	var accounts = (await _app.GetAccountsAsync()).ToList();

	var builder = _app.AcquireTokenInteractive(Scopes)
		.WithAccount(accounts.FirstOrDefault())
		.WithUseEmbeddedWebView(false)
		.WithPrompt(Microsoft.Identity.Client.Prompt.SelectAccount);

	var result = await builder.ExecuteAsync().ConfigureAwait(false);

	return result.Account;
}

public async Task<AuthenticationResult> AcquireTokenSilent()
{
	var accounts = await GetAccountsAsync();
	var result = await _app.AcquireTokenSilent(Scopes, accounts.FirstOrDefault())
			.ExecuteAsync()
			.ConfigureAwait(false);

	return result;
}

The SendEmailAsync method uses a message object and Graph API to send the emails. If the identity has the permissions, the licenses and is authenticated, then an email will be sent using the definitions from the Message class.

public async Task SendEmailAsync(Message message)
{
	var result = await AcquireTokenSilent();

	_httpClient.DefaultRequestHeaders.Authorization 
		= new AuthenticationHeaderValue("Bearer", result.AccessToken);
	_httpClient.DefaultRequestHeaders.Accept.Add(
		new MediaTypeWithQualityHeaderValue("application/json"));

	GraphServiceClient graphClient = new GraphServiceClient(_httpClient)
	{
		AuthenticationProvider = new DelegateAuthenticationProvider(async (requestMessage) =>
		{
			requestMessage.Headers.Authorization 
				= new AuthenticationHeaderValue("Bearer", result.AccessToken);
		})
	};

	var saveToSentItems = true;

	await graphClient.Me
		.SendMail(message, saveToSentItems)
		.Request()
		.PostAsync();
}

The EmailService class is used to added the recipient, header (subject) and the body to the message which represents the email. The attachments are added separately using the MessageAttachmentsCollectionPage class. The AddAttachment method is used to add as many attachments to the email as required which are uploaded as a base64 byte array. The service can send html bodies or text bodies.

public class EmailService
{
	MessageAttachmentsCollectionPage MessageAttachmentsCollectionPage 
		= new MessageAttachmentsCollectionPage();

	public Message CreateStandardEmail(string recipient, string header, string body)
	{
		var message = new Message
		{
			Subject = header,
			Body = new ItemBody
			{
				ContentType = BodyType.Text,
				Content = body
			},
			ToRecipients = new List<Recipient>()
			{
				new Recipient
				{
					EmailAddress = new EmailAddress
					{
						Address = recipient
					}
				}
			},
			Attachments = MessageAttachmentsCollectionPage
		};

		return message;
	}

	public Message CreateHtmlEmail(string recipient, string header, string body)
	{
		var message = new Message
		{
			Subject = header,
			Body = new ItemBody
			{
				ContentType = BodyType.Html,
				Content = body
			},
			ToRecipients = new List<Recipient>()
			{
				new Recipient
				{
					EmailAddress = new EmailAddress
					{
						Address = recipient
					}
				}
			},
			Attachments = MessageAttachmentsCollectionPage
		};

		return message;
	}

	public void AddAttachment(byte[] rawData, string filePath)
	{
		MessageAttachmentsCollectionPage.Add(new FileAttachment
		{
			Name = Path.GetFileName(filePath),
			ContentBytes = EncodeTobase64Bytes(rawData)
		});
	}

	public void ClearAttachments()
	{
		MessageAttachmentsCollectionPage.Clear();
	}

	static public byte[] EncodeTobase64Bytes(byte[] rawData)
	{
		string base64String = System.Convert.ToBase64String(rawData);
		var returnValue = Convert.FromBase64String(base64String);
		return returnValue;
	}
}

Azure App Registration settings

The app settings specific to your Azure Active Directory tenant and the Azure App registration values need to be added to the app settings in the .NET Core application. The Scope configuration is set to use the required scopes required to send emails.

<appSettings>
	<add key="AADInstance" value="https://login.microsoftonline.com/{0}/v2.0"/>
	<add key="Tenant" value="5698af84-5720-4ff0-bdc3-9d9195314244"/>
	<add key="ClientId" value="ae1fd165-d152-492d-b4f5-74209f8f724a"/>
	<add key="Scope" value="User.read Mail.Send Mail.ReadWrite"/>
</appSettings>

WPF UI

The WPF application provides an Azure AD login for the identity. The user of the WPF application can sign-in using a browser which redirects to the AAD authentication page. Once authenticated, the user can send a html email or a text email. The AddAttachment method uses the OpenFileDialog to upload a file in the WPF application, get the raw bytes and add these to the attachments which are sent with the next email message. Once the email is sent, the attachments are removed.

public partial class MainWindow : Window
{
	AadGraphApiDelegatedClient _aadGraphApiDelegatedClient = new AadGraphApiDelegatedClient();
	EmailService _emailService = new EmailService();

	const string SignInString = "Sign In";
	const string ClearCacheString = "Clear Cache";

	public MainWindow()
	{
		InitializeComponent();
		_aadGraphApiDelegatedClient.InitClient();
	}

	private async void SignIn(object sender = null, RoutedEventArgs args = null)
	{
		var accounts = await _aadGraphApiDelegatedClient.GetAccountsAsync();

		if (SignInButton.Content.ToString() == ClearCacheString)
		{
			await _aadGraphApiDelegatedClient.RemoveAccountsAsync();

			SignInButton.Content = SignInString;
			UserName.Content = "Not signed in";
			return;
		}

		try
		{
			var account = await _aadGraphApiDelegatedClient.SignIn();

			Dispatcher.Invoke(() =>
			{
				SignInButton.Content = ClearCacheString;
				SetUserName(account);
			});
		}
		catch (MsalException ex)
		{
			if (ex.ErrorCode == "access_denied")
			{
				// The user canceled sign in, take no action.
			}
			else
			{
				// An unexpected error occurred.
				string message = ex.Message;
				if (ex.InnerException != null)
				{
					message += "Error Code: " + ex.ErrorCode + "Inner Exception : " + ex.InnerException.Message;
				}

				MessageBox.Show(message);
			}

			Dispatcher.Invoke(() =>
			{
				UserName.Content = "Not signed in";
			});
		}
	}

	private async void SendEmail(object sender, RoutedEventArgs e)
	{
		var message = _emailService.CreateStandardEmail(EmailRecipientText.Text, 
			EmailHeader.Text, EmailBody.Text);

		await _aadGraphApiDelegatedClient.SendEmailAsync(message);
		_emailService.ClearAttachments();
	}

	private async void SendHtmlEmail(object sender, RoutedEventArgs e)
	{
		var messageHtml = _emailService.CreateHtmlEmail(EmailRecipientText.Text,
			EmailHeader.Text, EmailBody.Text);

		await _aadGraphApiDelegatedClient.SendEmailAsync(messageHtml);
		_emailService.ClearAttachments();
	}

	private void AddAttachment(object sender, RoutedEventArgs e)
	{
		var dlg = new OpenFileDialog();
		if (dlg.ShowDialog() == true)
		{
			byte[] data = File.ReadAllBytes(dlg.FileName);
			_emailService.AddAttachment(data, dlg.FileName);
		}
	}

	private void SetUserName(IAccount userInfo)
	{
		string userName = null;

		if (userInfo != null)
		{
			userName = userInfo.Username;
		}

		if (userName == null)
		{
			userName = "Not identified";
		}

		UserName.Content = userName;
	}
}

Running the application

When the application is started, the user can sign-in using the Sign in button.

The standard Azure AD login is used in a popup browser. Once the authentication is completed, the browser redirect sends the tokens back to the application.

If a file attachment needs to be sent, the Add Attachment button can be used. This opens up a dialog and any single file can be selected.

When the email is sent successfully, the email and the file can be viewed in the recipients inbox. The emails are also saved to the senders sent emails. This can be disabled if required.

Links

https://docs.microsoft.com/en-us/graph/outlook-send-mail-from-other-user

https://stackoverflow.com/questions/43795846/graph-api-daemon-app-with-user-consent

https://winsmarts.com/managed-identity-as-a-daemon-accessing-microsoft-graph-8d1bf87582b1

https://cmatskas.com/create-a-net-core-deamon-app-that-calls-msgraph-with-a-certificate/

https://docs.microsoft.com/en-us/answers/questions/43724/sending-emails-from-daemon-app-using-graph-api-on.html

https://stackoverflow.com/questions/56110910/sending-email-with-microsoft-graph-api-work-account

https://docs.microsoft.com/en-us/graph/sdks/choose-authentication-providers?tabs=CS#InteractiveProvider

https://converter.telerik.com/

13 comments

  1. […] Send Emails using Microsoft Graph API and a desktop client – Damien Bowden […]

  2. […] Send Emails using Microsoft Graph API and a desktop client (Damien Bowden) […]

  3. […] in ASP.NET Web Forms (Bjoern Meyer) BabylonJS and Blazor – Getting Set Up (Cody Merritt Anhorn) Send Emails using Microsoft Graph API and a desktop client (Damien Bowden) ‘return await promise’ vs ‘return promise’ in JavaScript (Dmitri Pavlutin) […]

  4. […] Send Emails using Microsoft Graph API and a desktop client […]

  5. Anand N · · Reply

    is there any criteria for WithRedirectUri ?

    1. Hi Anand, yes this is the browser window used to login and only works with http between the desktop app and the browser.

      Greetings Damien

  6. Anand N · · Reply

    A configuration issue is preventing authentication – check the error message from the server for details. You can modify the configuration in the application registration portal. See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS7000218: The request body must contain the following parameter: ‘client_assertion’ or ‘client_secret’.
    Trace ID: edbfadae-e1f1-463a-b81e-7d542db63000
    Correlation ID: ef2f4d97-80c0-4910-ba72-592fe59de0be
    Timestamp: 2021-10-04 05:22:58Z

    This error is coming.

    1. Hi Anand, yes you need to add your secret from your azure App registristration. This can be added to your user secrets. I added a comment in the app.settings about this, Greetings Damien

  7. Josef · · Reply

    Hi Damienbod,

    is it possible to get an step by step guide for using your code for the Desktop program? In Azure is everything ok, but i’m not so I am not so familiar with programming.

    Thank you very much.

    1. Josef · · Reply

      Program is running now, but i get also a error:

      See https://aka.ms/msal-net-invalid-client for details. Original exception: AADSTS7000218: The request body must contain the following parameter: ‘client_assertion’ or ‘client_secret’.

      I can’t find an file app.settings .
      Where can i store the client secret?

      Thank you

  8. MessageAttachmentsCollectionPage is not recognised by Microsoft Graph 5.44
    Any thoughts on how to solve that?

    tnx

    frank

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.