Migrating from Legacy SMTP to Microsoft Graph API for Email Delivery in .NET Applications

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.Send API 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="notifications@company.com">
  <network host="smtp.office365.com" port="587" 
           userName="notifications@company.com" 
           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="notifications@company.com" />
</appSettings>

Benefits: OAuth2 credentials, async operations, testable, future-proof


Azure AD Setup (Quick Steps)

  1. Register App: Azure Portal → App registrations → New registration
  2. Add Permission: API permissions → Microsoft Graph → Application → Mail.Send
  3. Grant Consent: Click "Grant admin consent"
  4. Create Secret: Certificates & secrets → New client secret → Copy value
  5. 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("user@example.com")
    .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.

← Quay lại Blog
Migrating from Legacy SMTP to Microsoft Graph API for Email Delivery in .NET Applications - Ginbok