Public/Send-ModernMailMessage.ps1
function Send-ModernMailMessage { <# .SYNOPSIS Sends an email message. .DESCRIPTION The Send-ModernMailMessage cmdlet sends an email message from within PowerShell. .EXAMPLE Send-ModernMailMessage -From "user01@fabrikam.com" -To "user02@fabrikam.com" -Subject "Test mail" Send-ModernMailMessage -From "user01@fabrikam.com" -To "user02@fabrikam.com" -Subject "Test mail" -SmtpServer smtp.contoso.com -UseSsl -Port 587 Send-ModernMailMessage -From "user01@fabrikam.com" -To "user02@fabrikam.com" -Subject "Test mail" -ClientId [AppId] -CertificateThumbprint [Thumbprint] -TenantId [TenantId] .INPUTS None .OUTPUTS System.Reflection.Assembly .NOTES Use "Enable-MailMessageAlias" to enable the command "Send-MailMessage". #> # Sends email messages from PowerShell using either Microsoft Graph API or SMTP with OAuth 2.0 (Modern Auth). Supports advanced features such as attachments, HTML body, CC/BCC, and flexible authentication methods, making it a modern replacement for the legacy Send-MailMessage cmdlet. [CmdletBinding(DefaultParameterSetName = 'GRAPH')] [OutputType([System.Reflection.Assembly])] #[OutputType([System.Reflection.Assembly], [string])] param ( # The path and file names of files to be attached to the email message. [Parameter(Position = 0)] [Alias("Attachments")] [String[]]$Attachment, # Email addresses that receive a copy of the mail but are not listed as recipients of the message. [Parameter(Position = 1)] #[Array] $Bcc, [String[]]$Bcc, # The body (content) of the email message. [Parameter(Position = 2)] #[string[]] $Text, [Alias("Message")] [String]$Body, # Indicates that the value of the Body parameter contains HTML. [Parameter(Position = 3)] #[Alias("Body")] #[string[]] $HTML, [Switch]$BodyAsHtml, # The encoding used for the body and subject. # Explicitly reference System.Text.Encoding [Parameter(Position = 4, ParameterSetName = 'SMTP')] [System.Text.Encoding]$Encoding = [System.Text.Encoding]::Default, # Email addresses to which a carbon copy (CC) of the email message is sent. [Parameter(Position = 5)] #[Array] $Cc, [String[]]$Cc, # Delivery notifications (if accepted by the recipient) [Parameter(Position = 6)] [Alias("Dno")] #[System.Net.Mail.DeliveryNotificationOptions]$DeliveryNotificationOption, # Explicitly reference System.Net.Mail [ValidateSet('None', 'OnSuccess', 'OnFailure', 'Delay', 'Never')] # Delivery notification options with validation [String[]]$DeliveryNotificationOption, # The address from which the mail is sent. [Parameter(Position = 7, ParameterSetName = 'GRAPH', Mandatory = $false)] [Parameter(Position = 7, ParameterSetName = 'SMTP', Mandatory = $false)] [Parameter(Position = 7, ParameterSetName = 'TEAMS', Mandatory = $true)] #[object] $From, [Alias("UserId")] [Alias("UserPrincipalName")] [ValidateNotNullOrEmpty()] [String]$From, # The name of the SMTP server that sends the email message (legacy). [Parameter(Position = 8, ParameterSetName = 'SMTP')] [Alias('Host')] [Alias('Server')] [String]$SmtpServer = "smtp.office365.com", # The priority of the email message. [Parameter(Position = 9)] #[System.Net.Mail.MailPriority]$Priority, # Explicitly reference System.Net.Mail.MailPriority [Alias('Importance')] [ValidateSet('Low', 'Normal', 'High')] [string]$Priority, # Specifies additional email addresses (other than the From address) to use to reply to this message [Parameter(Position = 10)] #[string] $ReplyTo, [String[]]$ReplyTo, # The subject of the email message. [Parameter(Position = 11)] #[string] $Subject, [String]$Subject, # The addresses to which the mail is sent [Parameter(Position = 12, Mandatory = $true)] #[Array] $To, #[Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String[]]$To, # A user account that has permission to perform this action (legacy). [Parameter(Position = 13)] [System.Management.Automation.Credential()] [PSCredential]$Credential, # Use the Secure Sockets Layer (SSL) protocol to establish a connection (legacy). # Default: StartTls, On: Ssl, Off: N/A [Parameter(Position = 14, ParameterSetName = 'SMTP', Mandatory = $false)] [Switch]$UseSsl, # SecureSocketOptions (use with -UseSsl) [ValidateSet("None", "Auto", "SslOnConnect", "StartTls", "StartTlsWhenAvailable")] [Parameter(ParameterSetName = 'SMTP', Mandatory = $false)] [Alias("Encryption")] [Alias("SslEncryption")] [Alias("SecureSocketOption")] [string]$SslMethod, # An alternate port on the SMTP server. # Default: 587, Custom: 25, Ssl: 465 [Parameter(Position = 15, ParameterSetName = 'SMTP', Mandatory = $false)] [Int32]$Port = 587, # Indicates whether to save the message in Sent Items. [Parameter(ParameterSetName = 'GRAPH')] [Switch]$SaveToSentItems, # Indicates whether a read receipt is requested for the message. #ToDevelop:IsReadReceiptRequested [Parameter(ParameterSetName = 'GRAPH')] [Switch]$RequestReadReceipt, # Indicates whether a delivery receipt is requested for the message. #ToDevelop: IsDeliveryReceiptRequested [Parameter(ParameterSetName = 'GRAPH')] [Switch]$RequestDeliveryReceipt, #ToDevelop: Message #ToDevelop: BodyParameter # Disable Telemetry # If set, telemetry information will not be logged. [Parameter(Mandatory = $false, HelpMessage = 'Disable telemetry')] [switch]$DisableTelemetry, # Modus defines the mode of operation for sending notifications. # Default: None # Fallback: Graph with Delegation (user-based interaction). # Fallback: User-based delegation for Microsoft Graph (interactive). # Recommended: # - 'GRAPH': App-only authentication (certificate-based) for Graph API. # - 'SMTP': SMTP-based email sending (e.g., OAuth2 authentication). # - 'TEAMS': Teams webhook notification (send notifications to a Teams channel). [Parameter(ParameterSetName = 'GRAPH', Mandatory = $false)] [Parameter(ParameterSetName = 'SMTP', Mandatory = $false)] [Parameter(ParameterSetName = 'TEAMS', Mandatory = $false)] [ValidateSet('GRAPH', 'SMTP', 'TEAMS')] [String]$Modus = 'GRAPH', #[String]$Auth = 'GRAPH', # Specifies the application ID of the service principal that is used in application-based authentication. [Parameter(ParameterSetName = 'GRAPH', Mandatory = $false)] [Parameter(ParameterSetName = 'TEAMS', Mandatory = $false)] [Alias('AppId')] [Alias('ApplicationId')] [string]$ClientId, # Specifies the ID of a tenant. [Parameter(ParameterSetName = 'GRAPH', Mandatory = $false)] [Parameter(ParameterSetName = 'TEAMS', Mandatory = $false)] # Organization [string]$TenantId, # Specifies the certificate thumbprint of a digital public key X.509 certificate of an application that has permission to perform this action. [Parameter(ParameterSetName = 'GRAPH', Mandatory = $false)] [Parameter(ParameterSetName = 'TEAMS', Mandatory = $false)] [Alias('Thumbprint')] [string]$CertificateThumbprint ) <# Reference: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/send-mailmessage?view=powershell-7.5 https://ss64.com/ps/send-mailmessage.html https://learn.microsoft.com/en-us/powershell/module/microsoft.graph.users.actions/send-mgusermail?view=graph-powershell-1.0 # With DisplayName Send-MailMessage -From "User01 <user01@fabrikam.com>"" -To "User02 <user02@fabrikam.com>" -Subject "Test mail" Send-MailMessage -To "User01 <user01@contoso.com>" -From "User02 <user02@contoso.com>" -Subject "Test mail" -SmtpServer smtp.contoso.com -UseSsl -Port 587 #> ## Defensive handling for Encoding parameter #if ($null -eq $Encoding) { # $Encoding = [System.Text.Encoding]::Default #} elseif ($Encoding -is [string]) { # $Encoding = [System.Text.Encoding]::GetEncoding($Encoding) #} Write-Debug "Active ParameterSet: $($PSCmdlet.ParameterSetName)" if (-not $Modus) { $Modus = $PSCmdlet.ParameterSetName; Write-Warning "Override Modus: $($Modus)"} if (-not $From) { throw "Parameter 'From' is required." } if (-not $To) { throw "Parameter 'To' is required." } # Compatibility values if ($SmtpServer) { Write-Verbose "Legacy: $($SmtpServer)" } if ($Credential) { Write-Verbose "Legacy: $($Credential)" } if ($UseSsl) { Write-Verbose "Legacy: $($UseSsl)" } if ($Port) { Write-Verbose "Legacy: $($Port)" } # Development values #if ($Attachments) { Write-Verbose "Development: $($Attachments)" } if ($Cc) { Write-Verbose "Development: $($Cc)" } if ($Bcc) { Write-Verbose "Development: $($Bcc)" } if ($DeliveryNotificationOption) { Write-Verbose "Development: $($DeliveryNotificationOption)" } # DNO if ($Priority) { Write-Verbose "Development: $($Priority)" } if ($ReplyTo) { Write-Verbose "Development: $($ReplyTo)" } if ($SaveToSentItems) { Write-Verbose "Development: $($SaveToSentItems)" } if ($RequestReadReceipt) { Write-Verbose "Development: $($RequestReadReceipt)" } # DNO if ($RequestDeliveryReceipt) { Write-Verbose "Development: $($RequestDeliveryReceipt)" } # DNO if ($Encoding) { Write-Verbose "Development: $($Encoding)" } if ($DisableTelemetry -or $Script:DisableTelemetry) { $Script:DisableTelemetry = $true } else { Write-Telemetry -EventName "SendModernMailMessage" } # -- Authentication switch ($Modus) { 'GRAPH' { # Graph - Application # Implement Microsoft Graph API logic using certificate for authentication (app-based) Write-Debug "Sending email via Microsoft Graph Application (certificate-based) to $To" # Implement Microsoft Graph API logic using certificate-based authentication (app-only) Write-Debug "Sending email via Microsoft Graph Application (app-only authentication) to $To" if ($ClientId -and $TenantId -and $CertificateThumbprint) { Connect-MgGraph ` -ClientId $ClientId ` -TenantId $TenantId ` -CertificateThumbprint $CertificateThumbprint } try { $user = Get-MgUser -UserId $From -ErrorAction Stop Write-Debug "Search From: $($user.DisplayName)" } catch { Write-Warning "The user specified in -From ('$From') does not exist in Microsoft 365." # Fallback to User Auth. if available if ((Get-EntraContext).Account) { $From = (Get-EntraContext).Account Write-Warning "Override From: $($From)" } } } 'SMTP' { # Implement SMTP with OAuth2 authentication here # Write-Debug "Sending email via SMTP to $To" # Implement SMTP with OAuth2 authentication here # Write-Debug "Sending email via SMTP to $To" # SMTP Import-Module EntraAuth ## During the Connectiong #$clientId = $application.AppId #$tenantId = $context.TenantId #$token = Connect-EntraService -ClientID $clientId -TenantID $tenantId -Service GraphBeta -PassThru #$token.Scopes ##Get-EntraService #$graphCfg = @{ # Name = 'Graph' # Office 365 Exchange Online # ServiceUrl = 'https://graph.microsoft.com/v1.0' # Resource = 'https://graph.microsoft.com' # https://outlook.office365.com/ # DefaultScopes = @() # HelpUrl = 'https://developer.microsoft.com/en-us/graph/quick-start' # Header = @{ } # NoRefresh = $false #} #Register-EntraService @graphCfg if (-not (Get-EntraContext)) { # Connect-EntraService -ClientID Graph -Scopes "SMTP.Send" -Service GraphBeta #Connect-EntraService -ClientID Graph -Scopes "SMTP.Send" -Service GraphBeta Connect-EntraService -ClientID Graph -Scopes "https://outlook.office365.com/SMTP.Send" -Service GraphBeta #Get-EntraService } $IsDelegated = $true $From = if ($IsDelegated) {(Get-EntraContext).Account} else {$From} # After already being connected $token = Get-EntraToken -Service GraphBeta #$token.Scopes # SMTP.Send #$token | fl * #$xauth2 = $token.AccessToken #$secure_xauth2 = ConvertTo-SecureString -AsPlainText $xauth2 -Force # Token #$secure_xauth2 = ConvertTo-SecureString -AsPlainText "[Password]" -Force # Pw (with Enabled MFA) ##[pscredential]$credential = New-Object System.Management.Automation.PSCredential("AutomateB@contoso.onmicrosoft.com", $secure_xauth2) # Get-CASMailbox user@domain.com | fl SmtpClientAuthenticationDisabled # Set-CASMailbox user@domain.com -SmtpClientAuthenticationDisabled $false } 'TEAMS' { # Implement Teams webhook notification logic here Write-Debug "Sending Teams notification to $To" if ($ClientId -and $TenantId -and $CertificateThumbprint) { Connect-MicrosoftTeams` -CertificateThumbprint $CertificateThumbprint ` -ApplicationId $ClientId ` -TenantId $TenantId } } default { # Handle invalid modus, if necessary Write-Debug "Invalid modus specified: $Modus" # You can also add additional handling for invalid modus, like logging or exit # Graph - Delegated (Fallback) # Implement Microsoft Graph API logic using delegated user consent (interactive) Write-Debug "Sending email via Microsoft Graph Delegation (user-based) to $To" # Implement Microsoft Graph API logic using delegated user consent (interactive) Write-Debug "Sending email via Microsoft Graph Delegation (user-based authentication) to $To" # Only connect if not already connected if (-not (Get-MgContext)) { Connect-MgGraph -Scopes "Mail.Send" -NoWelcome } $IsDelegated = $true $From = if ($IsDelegated) {(Get-MgContext).Account} else {$From} #$From = if ($Modus -eq 'GRAPH_DELEGATION') {(Get-MgContext).Account} else {$From} } } # -- Settings #$To = if ($To.Count -gt 1) {} else { $To[0] } # Handle Array $MessageBody = @{ contentType = if ($BodyAsHtml) { "HTML" } else { "Text" } #content = if ($Body) { $Body -join [System.Environment]::NewLine } else { "" } content = if ($Body) { $Body -join [System.Environment]::NewLine } else { "This email is sent via Microsoft Graph." } } if (!$Subject) { $Subject = "Test message from ModernMailTools (Modus: $Modus)" } if ($Attachment){ try { #$Attachment = "..\_readme.md" #Test-Path $Attachment # True #(Get-Item -Path $Attachment).Length -lt 3000000 # 3191 | True if ((Test-Path $Attachment) -and ((Get-Item -Path $Attachment).Length -lt 3000000)) { # Attachments are under 4MB or empty #Get File Name and Base64 string $FileName = (Get-Item -Path $Attachment).Name $FileBytes = [Convert]::ToBase64String([IO.File]::ReadAllBytes($Attachment)) Write-Verbose "Name: $($FileName)" Write-Verbose "Length: $($FileBytes.Length)" } else { <#Do this if attachments are over 4MB#> } } catch { Write-Verbose $_ #Write-Error $_.Exception.Message } } # -- Send if ($From) { Write-Verbose "Send-From: $($From)"} if ($To) { Write-Verbose "Send-To: $($To)"} if ($Subject) { Write-Verbose "Send-Subject: $($Subject)"} if ($MessageBody) { Write-Debug "Send-Body: $($MessageBody | Out-String)"} Write-Debug "Modus: $($Modus)" switch ($Modus) { 'GRAPH' { # --- MS Graph $params = [ordered] @{ # https://docs.microsoft.com/en-us/graph/api/resources/message?view=graph-rest-1.0 message = [ordered] @{ subject = $Subject body = $MessageBody #from = $From toRecipients = @( @{ emailAddress = @{ address = $To[0] } } ) #toRecipients = @( # @{ # emailAddress = @{ # address = "meganb@contoso.com" # } # } #) #ccRecipients = @() #bccRecipients = @() #replyTo = @() #importance = $Priority #isReadReceiptRequested = $RequestReadReceipt.IsPresent #isDeliveryReceiptRequested = $RequestDeliveryReceipt.IsPresent } #saveToSentItems = -not $DoNotSaveToSentItems.IsPresent #saveToSentItems = $false saveToSentItems = $SaveToSentItems.IsPresent } Write-Debug ($params.Values | Out-String) #Send-MgUserMail -UserId $From -Message $params try { Send-MgUserMail -UserId $From -BodyParameter $params } catch { throw "Failed to send mail: $($_.Exception.Message)" } } 'SMTP' { # --- SMTP #Send-MKMailMessage -To "admin@contoso.onmicrosoft.com" -From "AutomateB@contoso.onmicrosoft.com" -Subject "Test" -SmtpServer "smtp.office365.com" -Credential $credential -Port 25 # > Send-MKMailMessage: as8758.net SMTPBLOCKER ESMTP Service not available #Send-MKMailMessage -To "admin@contoso.onmicrosoft.com" -From "AutomateB@contoso.onmicrosoft.com" -Subject "Test" -SmtpServer "smtp.office365.com" -Credential $credential -Port 587 # > Send-MKMailMessage: 535: 5.7.139 Authentication unsuccessful, the request did not meet the criteria to be authenticated successfully # > Send-MKMailMessage: 535: 5.7.139 Authentication unsuccessful, the request did not meet the criteria to be authenticated successfully. If (!(Get-Package MailKit)){ # Find-Package -Name 'MailKit' -Source 'https://www.nuget.org/api/v2' | Install-Package #-Verbose } #Get-Package MailKit #(Get-Package -ProviderName NuGet -Name 'MailKit') | fl * $folderNameMail = Split-Path (Get-Package -ProviderName NuGet -Name 'MailKit').Source -Parent $folderNameMime = Split-Path (Get-Package -ProviderName NuGet -Name 'MimeKit').Source -Parent #C:\Program Files\PackageManagement\NuGet\Packages\MailKit.4.11.0\MailKit.4.11.0.nupkg # C:\Program Files\PackageManagement\NuGet\Packages\MailKit.4.11.0 [System.Reflection.Assembly]::LoadFile("$($folderNameMail)\lib\net48\MailKit.dll") > $null [System.Reflection.Assembly]::LoadFile("$($folderNameMime)\lib\net48\MimeKit.dll") > $null Add-Type -Path "$($folderNameMail)\lib\net48\MailKit.dll" Add-Type -Path "$($folderNameMime)\lib\net48\MimeKit.dll" #$smtpClient = New-Object MailKit.Net.Smtp.SmtpClient #[MailKit.Net.Smtp.SmtpClient] $smtpClient = [MailKit.Net.Smtp.SmtpClient]::new() # Set TLS to automatically negotiate security switch ($UseSSL) { $true { $SSLAuto = [MailKit.Security.SecureSocketOptions]::Auto Write-Verbose "Parameter -UseSSL is enabled." } $false { Write-Warning "Parameter -UseSSL is disabled." } } if ($UseSSL -and $SslMethod){ $SSLAuto = [MailKit.Security.SecureSocketOptions]::$($SslMethod) #} elseif ($UseSSL -and $SslMethod -eq "SslOnConnect" -and $Port -ne "587") { #} elseif {$UseSSL -and $SslMethod -eq "None" { } # $SSLAuto = [MailKit.Security.SecureSocketOptions]::SslOnConnect # $SSLAuto = [MailKit.Security.SecureSocketOptions]::StartTlsWhenAvailable # $SSLAuto = [MailKit.Security.SecureSocketOptions]::Auto ## https://mimekit.net/docs/html/T_MailKit_Security_SecureSocketOptions.htm If(Resolve-DnsName $SmtpServer) {Write-Debug "DNS Works"} #$smtpClient.Connect('smtp.gmail.com', 587, $False) $smtpClient.Connect($SmtpServer, $Port, $SSLAuto) # No such host is known. #$smtpClient.Authenticate($Username, ( Convert-SecureToPlaintext -String $channel.Definition.password)) #$smtpClient.Authenticate('myemail1@gmail.com', 'AppSpecificPassword' ) #$smtpClient.Authenticate($Username, $Password) # $token.AccessToken = OAuth2 Token (replace this with your actual OAuth2 token) #$username = "test" #$username = $token.TokenData.unique_name $username = $token.TokenData.upn # Create an OAuth2 mechanism for authentication ##$oAuth2 = [MailKit.Security.SaslMechanismOAuth2]::new($AuthToken.Account.Username, $AuthToken.AccessToken) #$oAuth2 = New-Object MailKit.Security.SaslMechanismOAuth2 -ArgumentList $channel.Definition.username, $token.AccessToken $oAuth2 = New-Object MailKit.Security.SaslMechanismOAuth2 -ArgumentList $username, $token.AccessToken #$oAuth2 = New-Object MailKit.Security.SaslMechanismOAuthBearer -ArgumentList $username, $token.AccessToken Write-Debug ($oAuth2.Credentials | Out-String) #Write-Verbose ($oAuth2.Credentials | fl * | Out-String) #$smtpClient.Authenticate($oauth2Mechanism) $smtpClient.Authenticate($oAuth2) #MethodInvocationException: Exception calling "Authenticate" with "1" argument(s): "451: 4.7.0 Temporary server error. Please try again later. PRX5 [ZR2P278CA0086.CHEP278.PROD.OUTLOOK.COM 2025-03-31T01:06:59.921Z 08DD6E37F3A552D5]" # Create the email message (MimeMessage) $message = [MimeKit.MimeMessage]::new() # Set the 'From' address if (!$From){$From = $username} $message_from = [MimeKit.MailboxAddress]::new('Your Name', $From) $message.From.Add($message_from) # Set the 'To' address if (!$To){$To = $username} $message_to = [MimeKit.MailboxAddress]::new('Recipient Name', $To) $message.To.Add($message_to) # Set the subject and body of the email #$message.Subject = "Your Email Subject" $message.Subject = $Subject $messageBody = [MimeKit.TextPart]::new("plain") #$messageBody.Text = "This is the email body content." $messageBody.Text = $Body $message.Body = $messageBody # Proceed with sending email try { $smtpClient.Send($message) } catch { # MethodInvocationException: Exception calling "Send" with "1" argument(s): "5.7.57 Client not authenticated to send mail. [ZR0P278CA0195.CHEP278.PROD.OUTLOOK.COM 2025-03-31T00:59:51.228Z 08DD6FAFE0C04FAE]" # MethodInvocationException: Exception calling "Send" with "1" argument(s): "5.1.3 Invalid address" # 2.0.0 OK <IBMO05GM0QU4.0T9V1MGPDDU03@tablet-sm30s97m> [Hostname=AM8P190MB0851.EURP190.PROD.OUTLOOK.COM] throw "Failed to send mail: $($_.Exception.Message)" } # Disconnect after sending the email $smtpClient.Disconnect($True) $smtpClient.Dispose() } 'TEAMS' { # --- MS Teams Write-Verbose "ToDevelop" } # EWS # SENDGRID default { Write-Debug "Invalid modus specified" } } } |