📌 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
- Go to App registrations → New 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`
- Go to Certificates & secrets → New client secret
- Define an expiration and copy the *Value* (will not be shown again)
- API permissions → Add a permission → Microsoft Graph → Application 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"
Expected Response:
{
"token_type": "Bearer",
"expires_in": 3599,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGci..."
}
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
}
✅ 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
Merge if needed; multiple SPF records will cause failure.
4️⃣ Find Smart Host (MX)
nslookup -type=mx yourdomain.com
Use the result as the smart host (port 25), e.g.:
yourdomain-com.mail.protection.outlook.com
#️⃣ 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.")
}
Top comments (0)