OAuth2 for the 100th time, sorry, but I'm on the verge of despair here. I need to send authenticated e-mails from a PowerShell script to my own Exchange Online Mailbox (I'm also the tenant admin, if this matters). So I registered an App in AzureAD.
Display name: MyApp
Supported account types: My organization only
App permissions:
Microsoft Graph\User.Read (Delegated, Granted for MyDomain)
Office 365 Exchange Online\IMAP.AccessAsApp (Application, Granted for MyDomain)
Probably the Microsoft Graph
API isn't needed here. The app has a secret with an expiration period of 730 days.
I created a new Exchange Service Principal and granted FullAccess to my mailbox:
$app = Get-AzureADServicePrincipal -SearchString MyApp
New-ServicePrincipal -AppId $app.AppId -ObjectId $app.ObjectId -DisplayName "MyServicePrincipalName"
Add-MailboxPermission -Identity $azureUserName -User $app.ObjectId -AccessRights FullAccess
Here's the result of Get-MailboxPermission -Identity $azureUserName | fl
:
IsOwner : False
AccessRights : {FullAccess, ReadPermission}
Deny : False
InheritanceType : All
User : NT AUTHORITY\SELF
UserSid : S-X-Y-Z
Identity : admin
IsInherited : False
IsValid : True
ObjectState : Unchanged
IsOwner : False
AccessRights : {FullAccess}
Deny : False
InheritanceType : All
User : MyServicePrincipalName
UserSid : S-X-Y-Z-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxx
Identity : admin
IsInherited : False
IsValid : True
ObjectState : Unchanged
With this configured, I tried to add an e-mail to my inbox. Here's the script I used:
Import-Module AzureAD
Import-Module ExchangeOnlineManagement
$tenantID = "asdf1234"
$appID = "asdf1234"
$secretValue = "asdf1234"
$userName = "myazureuser@mydomain.onmicrosoft.com"
$senderAddress = $userName
$recipientAddress = $userName
$scope = "https://outlook.office365.com/.default"
$tokenURL = "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token"
# Define the OAuth 2.0 token request body
$tokenRequestBody = @{
client_id = $appID
scope = $scope
client_secret = $secretValue
grant_type = "client_credentials"
}
# Request an access token
$response = Invoke-RestMethod -Uri $tokenURL -Method POST -ContentType "application/x-www-form-urlencoded" -Body $tokenRequestBody
# Access the access token
$accessToken = $response.access_token
# Connect to Office 365 IMAP service
$server = "outlook.office365.com"
$port = 993
$tcpClient = [System.Net.Sockets.TcpClient]::new($server, $port)
$sslStream = [System.Net.Security.SslStream]::new($tcpClient.GetStream())
$sslStream.AuthenticateAsClient($server, $null, [System.Security.Authentication.SslProtocols]::Tls12, $false)
$reader = [System.IO.StreamReader]::new($sslStream)
$writer = [System.IO.StreamWriter]::new($sslStream)
# Read server response
$reader.ReadLine()
# Compose the email
$emailSubject = "Test e-mail"
$emailBody = "This is a test e-mail"
$email = "To: $recipientAddress`nFrom: $senderAddress`nSubject: $emailSubject`n`n$emailBody"
# build XOAUTH2 login string with accesstoken and username
$accessString ="user=" + $userName + "$([char]0x01)auth=Bearer " + $accessToken + "$([char]0x01)$([char]0x01)"
$Bytes = [System.Text.Encoding]::ASCII.GetBytes($accessString)
$loginString = [Convert]::ToBase64String($Bytes)
# Authenticate using XOAUTH2
$command = "A01 AUTHENTICATE XOAUTH2 $loginString"
$writer.WriteLine($command)
# Capture the response
$ResponseStr = $reader.ReadLine()
# Check if the response indicates success
if ($ResponseStr -like "*OK AUTHENTICATE completed.") {
Write-Host "Authentication successful."
} else {
Write-Host "Authentication failed: $ResponseStr"
}
# Send an email
$command = "A01 APPEND INBOX (\Seen) $(" + $email.Length + ")"
$writer.WriteLine($command)
$writer.WriteLine($email)
$writer.WriteLine()
# Logout and clean up sessions
$writer.WriteLine("A01 Logout")
$writer.Close()
$reader.Close()
$sslStream.Close()
$tcpClient.Close()
I get an access token, the $response
looks like this:
token_type | expires_in | ext_expires_in | access_token |
---|---|---|---|
Bearer | 3599 | 3599 | myaccesstokenstring |
But after I execute $writer.WriteLine($command)
, it hangs and after a few seconds I get the error:
Authentication failed: * BYE Connection is closed. 13
I don't really understand, what I did wrong. Maybe it's completely stupid for those who know what they are doing. I'm just trying to send e-mail notifications to myself and my understanding of the whole concept here is very basic.
Any help is greatly appreciated!
Alternatively, you can make use of Microsoft Graph API to send an authenticated email from PowerShell.
I registered one Azure AD application and granted Mail.Send
Application permission as below:
Now, I ran below modified script to send mail by installing Microsoft.Graph
module like this:
#Install-Module -Name Microsoft.Graph -Scope CurrentUser
#Import-Module Microsoft.Graph.Users.Actions
$tenantID = "tenantID"
$appID = "appID"
$secretValue = "secret"
$ClientSecretPass = ConvertTo-SecureString -String $secretValue -AsPlainText -Force
$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $appID, $ClientSecretPass
# Connect to Microsoft Graph with Client Secret
Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential
$userName = "sri@xxxxxxxxx.onmicrosoft.com"
$senderAddress = $userName
$recipientAddress = $userName
$emailSubject = "Test e-mail"
$emailBody = "This is a test e-mail"
$type = "Text"
$params = @{
Message = @{
Subject = $emailSubject
Body = @{
ContentType = $type
Content = $emailBody
}
ToRecipients = @(
@{
EmailAddress = @{
Address = $recipientAddress
}
}
)
}
}
Send-MgUserMail -UserId $senderAddress -BodyParameter $params
Response:
When I checked the same in Inbox
folder of user, mail added successfully as below:
Note that, The Graph API is not replacing the Exchange Online API. But you can access multiple Microsoft 365 services including Exchange Online, SharePoint, OneDrive, Teams, etc. via Graph API.
As Microsoft is moving towards consolidating its APIs under the Microsoft Graph API, it's possible that the Exchange Online API may eventually be deprecated in favor of the Graph API. So, it is recommended to use the Graph API for new development that simplifies the complexity.