Using Graph API to access Office 365 mailbox in non-interactive case

Office 365 email service will be gradually revoking IMAP access with login/password authentication. You’ll need to use IMAP via OAuth 2.0 instead.

MailBee.NET Objects, email library for .NET platform, allows for accessing Office 365 accounts via IMAP with OAuth 2.0 used. However, it will only work in an interactive scenario when the user has to explicitly grant consent for your app to access their mailbox. The user will see Office 365 popup in the browser asking for consent and must click Agree.

In a non-interactive case, you would need to access mailbox using credentials of an application in Azure portal rather than user’s credentials – and currently, Microsoft allows that via their proprietary Graph API only, not IMAP.

To create an app registration in Azure, see OAuth 2.0 for Office 365 Accounts (installed applications) article or OAuth 2.0 with IMAP/SMTP for Office 365 in ASP.NET Core 3.1 MVC applications version for web apps. The only difference would be that these articles describe how to set permissions for IMAP (IMAP.AccessAsUser.All permission). But we’ll set permissions for Graph instead. Your app in Azure portal must have Mail.Read and User.Read.All enabled in Application Permissions (NOT Delegated Permissions). Click the picture to enlarge:

Let’s suppose we need to download all the email messages from the particular user account. The idea of the following sample is to use Graph to download email source from the server, feed it to MailBee.NET library and then use its MailMessage object the same way as you do when getting email from IMAP – for instance, to save a message as .EML file, or to display message headers or attachments when looping through all the messages found in the account.

To build this sample, you’ll need to add Microsoft.Graph, Microsoft.Graph.Auth and MailBee.NET packages in Nuget.

using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using System.Collections.Generic;
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using MailBee.Mime;

namespace MsGraphWithMailBeeConsoleApp
{
	class Program
	{
		private static string Tenant = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; // Directory (tenant) ID, from Azure app.
		private static string Instance = "https://login.microsoftonline.com/";
		private static string ClientId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx"; // Application (client) ID, from Azure app.
		private static string ClientSecret = "secret"; // You create one in "Certificates & secrets" of Azure app registration.
		private static string email = "user@some.office365.domain";

		static async Task Main(string[] args)
		{
			// Confidential app does not require consent from the user. With public apps (which require consent) we can use IMAP but
			// with confidential apps Graph is the only option.

			// Your app in Azure portal must have Mail.Read and User.Read.All enabled in Application Permissions (not Delegated Permissions).
			IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(ClientId)
		   .WithTenantId(Tenant)
		   .WithClientSecret(ClientSecret)
		   .Build();

			List scopes = new List();
			scopes.Add(".default");

			AuthenticationResult result = null;

			result = await app.AcquireTokenForClient(scopes).ExecuteAsync();

			ClientCredentialProvider authProvider = new ClientCredentialProvider(app);
			GraphServiceClient graphClient = new GraphServiceClient(authProvider);

			// Profile is not needed for this sample but can be useful for your app.
			// Note that we cannot use Me here because there is no signed-in user ("Me" user).
			var profile = await graphClient.Users[email].Request().GetAsync();

			// We get IDs of messages here.
			var messages = await graphClient.Users[email].Messages
				.Request()
				.Select("sender,subject") // You can remove this line, the sample does not need it but it shows how to request extra info with messages.
				.GetAsync();

			if (messages.Count == 0)
			{
				Console.WriteLine("Mailbox is empty");
			}
			else
			{
				// Get raw data of the most recent message in the email account and save it to disk
				// (note that in Graph messages are sorted from newer to older by default, contrary to IMAP).
				MailMessage msg = new MailMessage();
				using (var msgStream = await graphClient.Users[email].Messages[messages[0].Id].Content.Request().GetAsync())
				{
					msg.LoadMessage(msgStream);
					msg.SaveMessage(@"C:\Temp\msg.eml");
				}

				// Print some info on the message.
				Console.WriteLine(msg.From.ToString());
				Console.WriteLine(msg.Subject);
				Console.WriteLine(msg.Date);
				foreach (MailBee.Mime.Attachment att in msg.Attachments)
				{
					Console.WriteLine(att.Filename);
				}
			}
		}
	}
}

Using Graph API to access Office 365 mailbox in non-interactive case

Sending mail to addresses containing non-ASCII characters

If you require sending messages to email addresses which contain non-ASCII characters like “äöü”, that’s how it’s done. However, important to determine whether those characters are in domain or username part of email address.

If international characters are a part of domain name, you’ll need to convert domain name into Punycode. The following example demonstrates how it’s done with MailBee.NET Objects:

MailBee.Global.LicenseKey = "Trial or permanent key";
Smtp mailer = new Smtp();
mailer.Log.Enabled = true;
mailer.Log.Filename = @"C:\Temp\log.txt";

mailer.SmtpServers.Add("mail.domain.com", "jdoe", "secret");
mailer.From.Email = "jdoe@domain.com";
mailer.From.DisplayName = "John Doe";
EmailAddress addr = new EmailAddress("jane.doe@äöü.com", "Jane Doe");
mailer.To = addr.ToIdnAddress();

mailer.Subject = "Sending mail to addresses containing non-ASCII characters";
mailer.BodyHtmlText = "This is a sample mail message.";

mailer.Send();

It is perfectly safe to use this method for addresses which don’t contain any international characters, you’ll just get the new object which has the same values as the current one.

And if those characters are in username, not just in domain name – then you need to set Smtp.RequestEncoding property to UTF8. Bear in mind that many SMTP servers are unable to deal with international email addresses, so you may wish to check if particular SMTP server returns UTF8SMTP or SMTPUTF8 capability in EHLO response.

Sending mail to addresses containing non-ASCII characters

Troubleshoot SSL/TLS errors by disabling .NET troubleshooting tools

Logging is the immense tool for troubleshooting of .NET apps, especially when it comes to debugging network-related code. However, it turns out that sometimes these troubleshooting tools can be the root of the problem!

A client reported that they experience the strange [NotSupportedException: The certificate key algorithm is not supported.] exception when connecting to GMail’s IMAP over SSL with our MailBee.NET Objects email library. First thought was that TLS 1.2 was not in place (it’s required by Google) but further testing showed that this wasn’t true.

With extra research, we found that the client’s app had System.Diagnostics logging enabled for network connections. Normally tracing network connections with System.Diagnostics helps detect and eliminate various network issues but this time it caused the issue itself. Removing <system.diagnostics> section from app.config or web.config fixed this error.

Troubleshoot SSL/TLS errors by disabling .NET troubleshooting tools

Use TLS 1.3 for IMAP and SMTP connections

To take advantage of the most secure TLS 1.3 connections in e-mail .NET applications, you don’t necessarily need the latest .NET Core 3.0 or .NET 5 (even though the corresponding enum values are not defined in earlier versions).

The below shows how you can establish an IMAP connection using TLS 1.3. As an example, we’ll use GMail as the mail server and MailBee.NET Objects email component as the client. The key is setting ServicePointManager.SecurityProtocol static property prior to connecting to a mail server (be it IMAP, SMTP or POP3):

ServicePointManager.SecurityProtocol=SecurityProtocolType.Tls12 | (SecurityProtocolType)12288;

By default, MailBee.NET checks the value of ServicePointManager.SecurityProtocol, and if it’s set to non-default value, MailBee.NET will use all the flags listed in it as the allowed security protocols. In particular, SecurityProtocolType.Tls12 | (SecurityProtocolType)12288 means a combination of TLS 1.2 and TLS 1.3 (the best will be selected during the connection procedure, depending on what the mail server supports). .NET Framework may not have the corresponding value for TLS 1.3 in SecurityProtocolType enum yet so we use its int value which we cast to SecurityProtocolType type.

Here’s the complete sample with TLS 1.3 over Gmail’s IMAP:

using System;
using System.Net;
using System.Net.Security;
using MailBee;
using MailBee.ImapMail;

namespace ConsoleApplicationNet45
{
	class Program
	{
		static void Main(string[] args)
		{
			MailBee.Global.LicenseKey = "your key";
			Imap imp = new Imap();
			imp.Log.Enabled = true;
			imp.Log.Filename = @"C:\Temp\log.txt";
			imp.Log.Clear();
			ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 |(SecurityProtocolType)12288; // Request TLS 1.3 but allow TLS 1.2 fallback
			imp.Connect("imap.gmail.com", 993);
			SslStream s = (SslStream)imp.GetStream();
			Console.WriteLine(s.SslProtocol.ToString()); // See that it's actually TLS 1.3
			imp.Login("user@gmail.com", "password");
			imp.Disconnect();
		}
	}
} 

In my tests, the technique above works with .NET 4.5 and newer and .NET Core 2.1 and newer. TLS 1.3 support must also be installed and activated in Windows.

Use TLS 1.3 for IMAP and SMTP connections

Using MailBee.NET Objects in Azure cloud

MailBee.NET Objects library is fully compatible with Microsoft Azure. You can use it just the same way you do on a normal PC.

For instance, let’s assume you have an ASP.NET MVC5 web app which you publish on Azure. You can modify Controllers/HtmlController.cs as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.IO;
using MailBee;
using MailBee.ImapMail;

namespace WebApplicationAzure.Controllers
{
	public class HomeController : Controller
	{
		public ActionResult Index()
		{
			return View();
		}

		public ActionResult About()
		{
			ViewBag.Message = "Your application description page.";
			MailBee.Global.LicenseKey = "your key";
			Imap imp = new Imap();
			string logPath = Path.Combine(System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath, "log.txt");
			imp.Log.Filename = logPath;
			imp.Log.Enabled = true;
			imp.Log.Clear();
			try
			{
				imp.Connect("mail.domain.com");
				imp.Login("your@account.com", "password");
				FolderCollection col = imp.DownloadFolders();
				imp.Disconnect();
				ViewBag.Message = $"Number of folders: {col.Count}, log path is {logPath}";
			}
			catch (MailBeeException ex)
			{
				ViewBag.Message = ex.ToString();
			}
			ViewBag.Log = System.IO.File.ReadAllText(logPath);
			return View();
		}

		public ActionResult Contact()
		{
			ViewBag.Message = "Your contact page.";

			return View();
		}
	}
} 

In Views/Home/About.cshtml, make sure you have Message and Log blocks.
Don’t forget to add a reference to MailBee.NET library via Nuget.
Now, when you publish the app and navigate to About page, this will display the number of folders in an IMAP account and the contents of the log file produced by MailBee.

Using MailBee.NET Objects in Azure cloud

Fix IMAP errors with imap.sina.net service

If you’re getting “The envelope data is corrupted or incorrect” when downloading e-mails from sina.net with MailBee.NET Objects, you may face the issue that their mail server returns IMAP envelopes incorrectly in case if a message becomes read (Seen) during downloading.

You can avoid this by keeping message flags intact during downloading (so that download process won’t trigger Seen flag). This code downloads last e-mail in the current folder in sina.net-compatible way:

imp.SetSeenForEntireMessages = false; // Do not set Seen flag on download
MailMessage msg = imp.DownloadEntireMessage(imp.MessageCount, false);


			
Fix IMAP errors with imap.sina.net service

Workaround for intermittent “NO [ALERT]” errors in IMAP

Sometimes (a customer faced this with imap.orange.fr provider), you may be getting intermittent NO responses which cause MailBee to throw an exception (MailBee doesn’t know if this response is intermittent or not). This usually happens when selecting a folder.

For instance, the following can be found in the log:

[16:35:00.52] [RECV] * NO [ALERT] Mailbox is at 83% of quota\r\n [Total 41 bytes received.]

To workaround this problem, just repeat select folder operation in case if the previous one failed due to a negative response from the IMAP server.

try
{
  imap.SelectFolder("INBOX");
}
catch (MailBeeImapNegativeResponseException)
{
  imap.SelectFolder("INBOX");
}
Workaround for intermittent “NO [ALERT]” errors in IMAP

Base64 embedding of related images

In EMBEDDED PICTURES NOT DISPLAYED? BASE TAG CAN BE THE CAUSE. post I explained how to fix the problem of displaying e-mails with embedded pictures.

MailBee.NET Objects v11 now supports another method of preparing related images so that you can display them regardless of tag and avoid the necessity to save them locally on your server. With this method you can embed images directly in the HTML message body as base64-encoded “data” blocks.

// msg is a MailMessage instance
html = msg.GetHtmlWithBase64EncodedRelatedFiles();

So, if you have a message which contains some related images which are embedded into the message as inline attachments, this method will embed them directly in the message body. You don’t need these attachments any longer.

Just to avoid confusion. There are two types of embedding. An image can be embedded into the message at inline attachment (rather than be referenced by an external link) or be embedded directly in the HTML body as “data” blocks. What we’re talking about is converting inline attachments into “data” blocks.

The method above is mostly intended for parsing and viewing existing e-mails. However, if you’re building a new message and want to base64-embed inline attachments, it can also be used:

// msg is a MailMessage instance
msg.LoadBodyText(@"C:\Temp\body.html", MessageBodyType.Html, null, ImportBodyOptions.ImportRelatedFiles);
html = msg.GetHtmlWithBase64EncodedRelatedFiles();
msg.Attachments.Clear(); // Remove attachments just added, we don't need them anymore.
msg.BodyHtmlText = html;

Please note that it’s recommended to use base64 embedding only if your images are relatively small. Older browsers may have issues with displaying images larger than 32KB in size. Also, Gmail currently does not support base64 encoded images! This means you cannot send an email with base64 encoded images to Gmail users.

Base64 embedding of related images

Accessing Office 365 shared mailboxes via IMAP

Our fellow customer and Office 365 user Bob Sledge kindly shared with the community his findings on which settings must be used in order to access Office 365 accounts via IMAP (and therefore with MailBee.NET), including shared mailboxes.

Server hostname: outlook.office365.com
Port: 993 (for encrypted IMAP)
Authentication: AuthenticationMethods.Auto or AuthenticationMethods.Regular
IMAP4 UserName: Email address of the Office 365 mailbox, e.g. bob@mydomain.com
IMAP4 Password: Domain password for the user assigned to that mailbox

Office 365 has the concept of a normal mailbox, and a “shared” mailbox. The above works for a normal mailbox. If you want to fetch mail from a “shared” mailbox, this is the trick to make it work.

You have to use the credentials of a user who has been assigned to a normal mailbox and has been delegated access to the shared mailbox. Let’s say the normal mailbox address is bob@mydomain.com. And let’s assume that the shared mailbox is SharedMailbox@mydomain.com. In order to fetch mail from this shared mailbox, you use these credentials (and bob@mydomain.com must have been delegated access to SharedMailbox@mydomain.com):

IMAP4 UserName: bob@mydomain.com\SharedMailbox
IMAP4 Password: Domain password for the user assigned to bob@mydomain.com

Accessing Office 365 shared mailboxes via IMAP