DEV Community

Cover image for 📬🔐Sending Emails in Microsoft 365 using OAuth 2.0 (and SMTP Relay)
João Victor
João Victor

Posted on • Edited on

📬🔐Sending Emails in Microsoft 365 using OAuth 2.0 (and SMTP Relay)

📌 Change Summary

Since September 25, 2025, Microsoft has permanently disabled Basic Authentication (username/password) in Exchange Online (Microsoft 365).
Only OAuth 2.0 (Modern Authentication) — using temporary access tokens — is now supported for all apps, services, and devices. For more insights and to explore my other repositories or access this post in Portuguese, be sure to visit my GitHub profile at my GitHub.

Common error after the change:

550 5.7.30 Basic authentication is not supported for Client Submission

🔄 What Changed

  • Basic Authentication (user/password) is no longer supported
  • App Passwords have been discontinued
  • OAuth 2.0 (Modern Auth) is now mandatory

    • Tokens valid for about 1 hour
    • Issued by Microsoft Entra ID (Azure AD)
    • Supports MFA and Conditional Access

⚙️ Configuration — Microsoft Graph (OAuth 2.0 + Token)

1️⃣ Prerequisites

  • Access to Entra ID: https://entra.microsoft.com/
  • Valid mailbox (e.g., sender@yourdomain.com)
  • Verified domain in your tenant
  • Tools: cURL/Postman or Go

2️⃣ Register the App

  1. Go to App registrationsNew registration
  • Name: Email Relay OAuth
  • Supported account types: Single tenant
  • Redirect URI: not required
  • Click Register
Copy:

**Directory (tenant) ID** → `TENANT_ID`
**Application (client) ID** → `CLIENT_ID`
  1. Go to Certificates & secretsNew client secret
  • Define an expiration and copy the *Value* (will not be shown again)
  1. API permissionsAdd a permissionMicrosoft GraphApplication permissions
  • Select Mail.Send → Click Grant admin consent

3️⃣ Generate OAuth 2.0 Token

Client Credentials Flow:

curl -X POST https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token \
 -H "Content-Type: application/x-www-form-urlencoded" \
 -d "client_id=<CLIENT_ID>" \
 -d "client_secret=<CLIENT_SECRET>" \
 -d "scope=https://graph.microsoft.com/.default" \
 -d "grant_type=client_credentials"
Enter fullscreen mode Exit fullscreen mode

Expected Response:

{
  "token_type": "Bearer",
  "expires_in": 3599,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGci..."
}
Enter fullscreen mode Exit fullscreen mode

Use access_token in your header: Authorization: Bearer <token>.

#️⃣ Send Email Example (Go)

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
)

type tokenResp struct{ AccessToken string `json:"access_token"` }

func getToken(tenantID, clientID, clientSecret string) (string, error) {
    endpoint := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID)
    form := url.Values{
        "client_id":     {clientID},
        "client_secret": {clientSecret},
        "scope":         {"https://graph.microsoft.com/.default"},
        "grant_type":    {"client_credentials"},
    }
    req, _ := http.NewRequest("POST", endpoint, bytes.NewBufferString(form.Encode()))
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return "", err }
    defer resp.Body.Close()
    b, _ := io.ReadAll(resp.Body)
    if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("token fail: %s - %s", resp.Status, string(b)) }
    var tr tokenResp
    json.Unmarshal(b, &tr)
    return tr.AccessToken, nil
}

func sendMail(sender, token string) error {
    body := map[string]any{
        "message": map[string]any{
            "subject": "Test – Microsoft Graph API",
            "body": map[string]string{"contentType": "Text", "content": "Automatic email sent via OAuth 2.0 (Go)."},
            "toRecipients": []map[string]any{
                {"emailAddress": map[string]string{"address": "recipient@example.com"}},
            },
        },
        "saveToSentItems": false,
    }
    b, _ := json.Marshal(body)
    req, _ := http.NewRequest("POST", fmt.Sprintf("https://graph.microsoft.com/v1.0/users/%s/sendMail", url.PathEscape(sender)), bytes.NewReader(b))
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Content-Type", "application/json")
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return err }
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusAccepted { rb, _ := io.ReadAll(resp.Body); return fmt.Errorf("sendMail fail: %s - %s", resp.Status, string(rb)) }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

✅ Successful response: HTTP 202 Accepted


📨 SMTP Relay (Exchange Online)

1️⃣ Prerequisites

  • Fixed public IP (e.g., 191.209.58.228)
  • Verified domain in Microsoft 365
  • TCP Port 25 open (outbound/inbound)
  • PTR/rDNS configured to point to your domain hostname

2️⃣ Create Connector

Access: https://admin.cloud.microsoft/exchange?#/connectors

  • From: Your organization’s email server
  • To: Microsoft 365
  • Name: SMTP Relay – On‑prem
  • Identify by IP → add your public IP

⚠️ Mail must originate from the authorized IP; otherwise, relay will fail.

3️⃣ Configure SPF Record

Add/edit TXT (only one SPF per domain):

v=spf1 ip4:191.209.58.228 include:spf.protection.outlook.com -all
Enter fullscreen mode Exit fullscreen mode

Merge if needed; multiple SPF records will cause failure.

4️⃣ Find Smart Host (MX)

nslookup -type=mx yourdomain.com
Enter fullscreen mode Exit fullscreen mode

Use the result as the smart host (port 25), e.g.:

yourdomain-com.mail.protection.outlook.com
Enter fullscreen mode Exit fullscreen mode

#️⃣ Send Email Example (Go)

package main

import (
    "fmt"
    "log"
    "net/smtp"
    "os"
)

// Relay configuration
const (
    smtpHost = "yourdomain-com.mail.protection.outlook.com" // Smart host for your domain (MX)
    smtpPort = "25"                                         // Default relay port
)

func main() {
    // Sender address (must belong to the verified domain/tenant)
    from := "sender@yourdomain.com"
    to := []string{"recipient@example.com"}

    // Message body
    msg := []byte("To: recipient@example.com\r\n" +
        "Subject: SMTP Relay Test (Go)\r\n" +
        "Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
        "Hello!\nThis is a test message sent through SMTP Relay (IP-authenticated) in Microsoft 365.\n" +
        "Sent using Go and net/smtp.\r\n")

    // Server address
    addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)

    log.Printf("🔄 Connecting to %s to send email...", addr)

    // Send message without authentication (relay authorized by IP)
    err := smtp.SendMail(addr, nil, from, to, msg)
    if err != nil {
        log.Fatalf("❌ Failed to send email: %v", err)
    }

    fmt.Println("✅ Email successfully sent via SMTP Relay.")
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)