Why Migrate from SMTP?
Microsoft is deprecating Basic Authentication for Exchange Online, making SMTP obsolete for modern applications.
Why Graph API?
- ✅ OAuth 2.0 authentication (no passwords stored)
- ✅ Modern security with Azure AD
- ✅ Rich features (HTML, attachments, tracking)
- ✅ Better monitoring and error handling
- ✅ Future-proof Microsoft 365 integration
Prerequisites
- Azure AD tenant with admin access
- App Registration with
Mail.SendAPI permission (admin consented) - Client ID, Client Secret, Tenant ID, and sender UPN
- NuGet:
Microsoft.Graph(5.x),Azure.Identity(1.10+)
Before & After Comparison
❌ Old: SMTP with Password
<smtp deliveryMethod="Network" from="[email protected]">
<network host="smtp.office365.com" port="587"
userName="[email protected]"
password="SuperSecretPassword123!" />
</smtp>
Problems: Plain text passwords, no modern auth, synchronous calls, hard to test
✅ New: Graph API with OAuth2
<appSettings>
<add key="Email:ClientId" value="your-client-id" />
<add key="Email:ClientSecret" value="your-secret" />
<add key="Email:TenantId" value="your-tenant-id" />
<add key="Email:SenderMailbox" value="[email protected]" />
</appSettings>
Benefits: OAuth2 credentials, async operations, testable, future-proof
Azure AD Setup (Quick Steps)
- Register App: Azure Portal → App registrations → New registration
- Add Permission: API permissions → Microsoft Graph → Application →
Mail.Send - Grant Consent: Click "Grant admin consent"
- Create Secret: Certificates & secrets → New client secret → Copy value
- Save Values: Client ID, Tenant ID, Secret, Sender UPN
Implementation: Core Components
1. Authentication Factory
using Azure.Identity;
using Microsoft.Graph;
public static class GraphClientFactory
{
public static GraphServiceClient Create(EmailConfig config)
{
var credential = new ClientSecretCredential(
config.TenantId,
config.ClientId,
config.ClientSecret,
new ClientSecretCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
}
);
return new GraphServiceClient(credential,
new[] { "https://graph.microsoft.com/.default" });
}
}
2. Configuration Model
public class EmailConfig
{
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string TenantId { get; set; }
public string SenderMailbox { get; set; }
public static EmailConfig LoadFromConfig() => new()
{
ClientId = ConfigurationManager.AppSettings["Email:ClientId"],
ClientSecret = ConfigurationManager.AppSettings["Email:ClientSecret"],
TenantId = ConfigurationManager.AppSettings["Email:TenantId"],
SenderMailbox = ConfigurationManager.AppSettings["Email:SenderMailbox"]
};
}
3. Fluent Email Builder
using Microsoft.Graph.Models;
using Microsoft.Graph.Users.Item.SendMail;
public class EmailBuilder
{
private string _subject, _from, _bodyHtml;
private List<string> _recipients = new();
public static EmailBuilder Create() => new();
public EmailBuilder SetSubject(string subject)
{
_subject = subject;
return this;
}
public EmailBuilder SetFrom(string from)
{
_from = from;
return this;
}
public EmailBuilder SetBodyHtml(string html)
{
_bodyHtml = html;
return this;
}
public EmailBuilder AddRecipients(params string[] emails)
{
_recipients.AddRange(emails);
return this;
}
public SendMailPostRequestBody Build() => new()
{
Message = new Message
{
Subject = _subject,
Body = new ItemBody
{
ContentType = BodyType.Html,
Content = _bodyHtml
},
From = new Recipient
{
EmailAddress = new EmailAddress { Address = _from }
},
ToRecipients = _recipients.Select(e => new Recipient
{
EmailAddress = new EmailAddress { Address = e }
}).ToList()
},
SaveToSentItems = false
};
}
4. Email Service
public interface IEmailService
{
Task SendEmailAsync(SendMailPostRequestBody body, EmailConfig config);
}
public class GraphEmailService : IEmailService
{
public async Task SendEmailAsync(SendMailPostRequestBody body, EmailConfig config)
{
var client = GraphClientFactory.Create(config);
await client.Users[config.SenderMailbox].SendMail.PostAsync(body);
}
}
5. Usage Example
var config = EmailConfig.LoadFromConfig();
var email = EmailBuilder.Create()
.SetSubject("Welcome!")
.SetFrom(config.SenderMailbox)
.SetBodyHtml("<h1>Hello</h1><p>Welcome to our platform</p>")
.AddRecipients("[email protected]")
.Build();
var service = new GraphEmailService();
await service.SendEmailAsync(email, config);
Common Errors & Solutions
1. "Authorization_RequestDenied"
Cause: Missing API permissions
Fix: Azure Portal → API permissions → Add Mail.Send → Grant admin consent → Wait 5 mins
2. "MailboxNotFound"
Cause: Invalid UPN
Fix: Verify the sender email exists in Microsoft 365 tenant and is a mailbox (not distribution list)
3. "Client secret has expired"
Cause: Secrets expire (max 24 months)
Fix: Generate new secret in Azure Portal → Update configuration → Restart app
4. "InvalidAuthenticationToken"
Cause: Token acquisition failed
Fix: Verify ClientId, TenantId, Secret are correct GUIDs with no trailing spaces
Conclusion
Migrating from SMTP to Microsoft Graph API modernizes your email infrastructure with better security and features.
What We Accomplished: ✅ OAuth2 authentication (no passwords)
✅ Reusable, testable email service
✅ Fluent API for email composition
✅ Safe backward compatibility
✅ Production-ready implementation
Key Takeaways:
- Use Client Credentials Flow for server apps
- Implement builder pattern for clean code
- Add feature toggles for safe migration
- Never log secrets
- Plan secret rotation from day one
Your application is now future-proof and ready for modern Microsoft 365 integration.