ModernMailTools.psm1


function Register-ModernMailMessageEntraIDApp {
    <#
    .SYNOPSIS
        Registers an Entra ID App.
    .DESCRIPTION
        Registers an app in Entra ID, assigning necessary permissions and outputting relevant information.
    .PARAMETER ApplicationName
        The name of the application to register.
    .EXAMPLE
        Register-ModernMailMessageEntraIDApp -ApplicationName "M365 ModernMailTools"
    .INPUTS
        None
    .OUTPUTS
    .NOTES
    #>

    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationName
        #[string]$Tenant,
        #[string]$TenantID,
        #[string]$TenantName
    )
    # Reference:
    # https://pnp.github.io/powershell/cmdlets/Register-PnPAzureADApp.html

    #Import-Module Microsoft.Graph.Authentication
    #Import-Module Microsoft.Entra.Authentication

    #$ApplicationName = "Test M365 ModernMailTools"
    ##$ApplicationName = "Test PS ModernMailTools"
    ##$ApplicationName = "Test M365 SendMail"
    ##$ApplicationName = "Test PS SendMail"

    #Connect-MgGraph
    Connect-Entra -Scopes 'Application.ReadWrite.All' -NoWelcome #-TenantId

    #$application = Get-EntraApplication -Filter "DisplayName eq 'My new application'"
    $application = Get-EntraApplication -Filter "DisplayName eq '$ApplicationName'"
    Write-Verbose "VAR: $($application.DisplayName)" -Verbose

    if(!$application) {
        #$application = New-EntraApplication -DisplayName 'My new application'
        $application = New-EntraApplication -DisplayName $ApplicationName
                    #New-EntraServicePrincipal -AppId $myApp.AppId -DisplayName 'My new service principal'
        Write-Verbose "APP: $($application.DisplayName)" -Verbose
    }

    $requiredResourceAccess = @(
        @{resourceAppId    = '00000003-0000-0000-c000-000000000000' # Microsoft Graph
            resourceAccess = @(
                #@{
                # id = 'c79f8feb-a9db-4090-85f9-90d820caa0eb' # Application.Read.All (Delegate) - Read applications
                # type = 'Scope'
                #}
                #@{
                # id = '9a5d68dd-52b0-4cc2-bd40-abcf44ac3a30' # Application.Read.All (Application) - Read all applications
                # type = 'Role'
                #}
                @{
                    id   = 'e383f46e-2787-4529-855e-0e479a3ffac0' # Mail.Send (Delegate) - Send mail as a user
                    type = 'Scope'
                    # Role - AADSTS650051: Claim is invalid: Mail.Send does not exist in client application's RequiredResourceAccess.
                }
                #@{
                # id = 'a367ab51-6b49-43bf-a716-a1fb06d2a174' # Mail.Send.Shared (Delegate) - Send mail on behalf of others
                # type = 'Scope'
                #}
                @{
                    id   = 'b633e1c5-b582-4048-a93e-9f11b44c7e96' # Mail.Send (Application) - Send mail as any user
                    type = 'Role'
                }
                @{
                    id   = '258f6531-6087-4cc4-bb90-092c5fb3ed3f' # SMTP.Send (Delegate) - Send emails from mailboxes using SMTP AUTH
                    type = 'Scope'
                    # Role - AADSTS650051: Claim is invalid: SMTP.Send does not exist on resource application 00000003-0000-0000-c000-000000000000.
                }
                )
        }
        @{resourceAppId    = '00000002-0000-0ff1-ce00-000000000000' # Office 365 Exchange Online
            resourceAccess = @(
                @{
                    id   = 'b633e1c5-b582-4048-a93e-9f11b44c7e96' # Mail.Send (Application) - Send mail as any user
                    type = 'Role'
                }
            )
        }
        )

    $applicationEdited = Set-EntraApplication -ApplicationId $application.Id -RequiredResourceAccess $requiredResourceAccess
    # AADSTS500113: No reply address is registered for the application.

    #Set-EntraApplication -ApplicationId $application.Id -ReplyUrls "" # N/A
    # https://login.microsoftonline.com/common/oauth2/nativeclient + http://localhost
    # Ensure you have a 'Mobile and desktop applications' platform with redirect to 'http://localhost' configured (and not a 'Web' Platform).

    # Grant-EntraAdminConsent -AppId "your-application-id" # N/A
    # https://login.microsoftonline.com/{organization}/adminconsent?client_id={client-id}

    if($applicationEdited){
        $context = Get-EntraContext
        $clientId = $application.AppId
        $tenantId = $context.TenantId
        $consentUrl = "https://login.microsoftonline.com/$tenantId/adminconsent?client_id=$clientId"

        #Write-host -ForegroundColor Gray "Opening: $consentUrl"
        Write-Output "Opening: $consentUrl"
        #Write-Information "Opening: $consentUrl"
        #Write-Verbose "Opening: $consentUrl"
        Start-Process $consentUrl
    }
    Write-Verbose "EDIT: $($application.DisplayName)" -Verbose
}


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
    .INPUTS
        None
    .OUTPUTS
    .NOTES
        Use "Enable-MailMessageAlias" to enable the command "Send-MailMessage".
    #>

    param (
        [Parameter(Position = 0)]
        # The path and file names of files to be attached to the email message.
        [alias("Attachments")]
        [String[]]$Attachment,

        [Parameter(Position = 1)]
        # Email addresses that receive a copy of the mail but are not listed as recipients of the message.
        #[Array] $Bcc,
        [String[]]$Bcc,

        [Parameter(Position = 2)]
        # The body (content) of the email message.
        [alias("Message")]
        #[string[]] $Text,
        [String]$Body,

        [Parameter(Position = 3)]
        # Indicates that the value of the Body parameter contains HTML.
        #[alias("Body")]
        #[string[]] $HTML,
        [Switch]$BodyAsHtml,

        [Parameter(Position = 4)]
        # The encoding used for the body and subject.
        # Explicitly reference System.Text.Encoding
        [System.Text.Encoding]$Encoding,

        [Parameter(Position = 5)]
        # Email addresses to which a carbon copy (CC) of the email message is sent.
        #[Array] $Cc,
        [String[]]$Cc,

        [Parameter(Position = 6)]
        # Delivery notifications (if accepted by the recipient)
        [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,

        [Parameter(Position = 7, Mandatory = $true)]
        # The address from which the mail is sent.
        #[Parameter(Mandatory = $true)]
        #[object] $From,
        [alias("UserId")]
        [ValidateNotNullOrEmpty()]
        [String]$From,

        [Parameter(Position = 8)]
        # Deprecated??
        [String]$SmtpServer,

        [Parameter(Position = 9)]
        # The priority of the email message.
        #[System.Net.Mail.MailPriority]$Priority, # Explicitly reference System.Net.Mail.MailPriority
        [alias('Importance')]
        [ValidateSet('Low', 'Normal', 'High')]
        [string] $Priority,

        [Parameter(Position = 10)]
        # Specifies additional email addresses (other than the From address) to use to reply to this message
        #[string] $ReplyTo,
        [String[]]$ReplyTo,

        [Parameter(Position = 11)]
        # The subject of the email message.
        #[string] $Subject,
        [String]$Subject,

        [Parameter(Position = 12, Mandatory = $true)]
        # The addresses to which the mail is sent
        #[Array] $To,
        #[Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String[]]$To,

        [Parameter(Position = 13)]
        # Deprecated?
        [PSCredential]$Credential,

        [Parameter(Position = 14)]
        # Deprecated?
        [Switch]$UseSsl,

        [Parameter(Position = 15, Mandatory = $false)]
        # Deprecated?
        #[Parameter(Position = 15)]
        #[Parameter(Mandatory = $false)]
        [Int32]$Port,

        # Indicates whether to save the message in Sent Items.
        [Switch]$SaveToSentItems,

        # Indicates whether a read receipt is requested for the message.
        #ToDevelop:IsReadReceiptRequested
        [Switch]$RequestReadReceipt,

        # Indicates whether a delivery receipt is requested for the message.
        #ToDevelop: IsDeliveryReceiptRequested
        [Switch]$RequestDeliveryReceipt

        #ToDevelop: Message
        #ToDevelop: BodyParameter
    )
    <#
    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
 
        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
    #>


    # Deprecated values
    if ($SmtpServer) { Write-Verbose "Deprecated: $($SmtpServer)" }
    if ($Credential) { Write-Verbose "Deprecated: $($Credential)" }
    if ($UseSsl) { Write-Verbose "Deprecated: $($UseSsl)" }
    if ($Port) { Write-Verbose "Deprecated: $($Port)" }

    # Development values
    #if ($Attachments) { Write-Verbose "Development: $($Attachments)" }
    if ($Bcc) { Write-Verbose "Development: $($Bcc)" }
    if ($Encoding) { Write-Verbose "Development: $($Encoding)" }
    if ($Cc) { Write-Verbose "Development: $($Cc)" }
    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

    # -- Authentication
    #Get-MgContext
    #Get-EntraContext

    if ($clientId -and $tenantId -and $certificateThumbprint) {
        # Graph - Application
        Connect-MgGraph `
            -ClientId $clientId `
            -TenantId $tenantId `
            -CertificateThumbprint $certificateThumbprint

    } else {
        # Graph - Delegated
        Connect-MgGraph -Scopes "Mail.Send" -NoWelcome
        #Find-MgGraphCommand Send-MgUserMail
        $IsDelegated = $true

    }
    <#
    } else {
        # 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
        # After already being connected
        $token = Get-EntraToken -Service GraphBeta
        #$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)
    }
    #>


    # -- Settings

    $From = if ($IsDelegated) {(Get-MgContext).Account} else {$From}

    #$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." }
    }
    $Subject = if ($Subject) { $Subject } else { "Test message from ModernMailTools" }


    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 v1: $($From)"}
    if ($MessageBody) { Write-Verbose "Send v1: $($MessageBody | Out-String)"}
    if ($Subject) { Write-Verbose "Send v1: $($Subject)"}
    if ($To) { Write-Verbose "Send v1: $($To)"}

    #If ($false) {
    If ($true) {
        # 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
        $result = Send-MgUserMail -UserId $From -BodyParameter $params; $result

    } else {
        # --- 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.
    }
}


function Enable-MailMessageAlias {
    <#
    .SYNOPSIS
        Enables alias for Send-MailMessage command.
    .DESCRIPTION
        Enables Send-MailMessage command alias in the current PowerShell session.
    .EXAMPLE
        Enable-MailMessageAlias
    .INPUTS
        None
    .OUTPUTS
    .NOTES
    #>


    #Reference:
    # https://learn.microsoft.com/en-us/powershell/module/microsoft.entra/enable-entraazureadalias?view=entra-powershell

    Set-Alias -Name Send-MailMessage -Value Send-ModernMailMessage -Scope Global -Force
    Set-Alias -Name Register-MailMessageEntraIDApp -Value Register-ModernMailMessageEntraIDApp -Scope Global -Force
    Set-Alias -Name Register-MailMessageApp -Value Register-ModernMailMessageEntraIDApp -Scope Global -Force
}

Export-ModuleMember -Function @("Send-ModernMailMessage", "Register-ModernMailMessageEntraIDApp","Enable-MailMessageAlias")