The Send-MailMessage cmdlet has been around for a couple of years and is mostly used to send email messages from PowerShell. But with the deprecation and security flaws of legacy authentication it’s time for a better option which actually supports modern authentication. For this purpose we can use the Microsoft Graph API and the Microsoft Graph PowerShell SDK. The best thing is that this solution works without any service account and does not need any exclusions from conditional access.

Microsoft Graph resource

To send a mail we simply specify the user account from which we want to send the email:

1
POST: https://graph.microsoft.com/v1.0/users/[email protected]/sendMail

Create an app registration

Simply create a new app registration with the Mail.Send permissions and use a certificate for the authentication.

New App registration

We need to take additional steps to limit the permissions of the app registration. Otherwise the app can send mails on behalf of any user in your tenant. To limit the permissions we leverage exchange application access policies.

  1. Connect to Exchange Online with the ExchangeOnlineManagement PowerShell module Connect-ExchangeOnline

  2. Create a mail enabled security group which contains all the accounts you want to send mails from $restrictedGroup = New-DistributionGroup -Name "Mail service accounts" -Type "Security" -Members @("[email protected]")

  3. Optionally hide the group from the address list Set-DistributionGroup -Identity $restrictedGroup.Identity -HiddenFromAddressListsEnabled $true

  4. Create the application access policy to only allow sending the app mails for the specified distribution group

    1
    2
    3
    4
    5
    6
    7
    8
    
    $params = @{
        AccessRight        = "RestrictAccess"
        AppId              = "1f5ffbea-f13f-4f1a-af63-258ce4344daf"
        PolicyScopeGroupId = $restrictedGroup.PrimarySmtpAddress
        Description        = "Restrict app permissions to only allow access to service account"
    }
    
    New-ApplicationAccessPolicy @params
    
  5. Wait a couple of minutes / hours for the policy to take effect

  6. If you now try to send a mail from an account not within the referenced distribution list you get some kind of access denied message

Sending emails

To actually send an email message make sure to have the MSAL.PS Module installed to acquire an access token.

Afterwards we can send an email message with the following PowerShell code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# acquire an access token to interact with the app
# we use a certificate from the users personal store
$appRegistration = @{
    TenantId          = "dev.nicolonsky.ch"
    ClientId          = "1f5ffbea-f13f-4f1a-af63-258ce4344daf"
    ClientCertificate = Get-Item "Cert:\CurrentUser\My\164446192FE04237C206211D59730445CA892911"
}

$msalToken = Get-msaltoken @appRegistration -ForceRefresh

# get byte contents of attachement and encode it as base64
$attachementPath = Get-Item $MyInvocation.MyCommand.Path
$byteStream = Get-Content -Path $attachementPath.FullName -AsByteStream
$attachementBytes = [System.Convert]::ToBase64String($byteStream)

# request body which contains our message
$requestBody = @{
    "message"         = [PSCustomObject]@{
        "subject"      = "OAuth Mail Sent from PowerShell via App"
        "body"         = [PSCustomObject]@{
            "contentType" = "Text"
            "content"     = "Hello this is a test for my latest blog post! `nYou can find the PowerShell script I used in the attachements. `n`n Cheers, `n /nicola"
        }
        "toRecipients" = @(
            [PSCustomObject]@{
                "emailAddress" = [PSCustomObject]@{
                    "address" = "[email protected]"
                }
            }
        )
        "attachments"  = @(
            @{
                "@odata.type"  = "#microsoft.graph.fileAttachment"
                "name"         = "Send-OAuthMailMessage.ps1.txt"
                "contentType"  = "text/plain"
                "contentBytes" = $attachementBytes
            })
    }
    "saveToSentItems" = "true"
}

# make the graph request
$request = @{
    "Headers"     = @{Authorization = $msalToken.CreateAuthorizationHeader() }
    "Method"      = "Post"
    "Uri"         = "https://graph.microsoft.com/v1.0/users/[email protected]/sendMail"
    "Body"        = $requestBody | ConvertTo-Json -Depth 5
    "ContentType" = "application/json"
}

Invoke-RestMethod @request

And the result will look like this: