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

📬🔐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.

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)