GraphEmailApp.psm1

#Region '.\Private\Get-AppSecret.ps1' 0
function Get-AppSecret {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "The application name.")]
        [string]$AppName,

        [Parameter(Mandatory = $true, HelpMessage = "The app registration object.")]
        [PSObject]$AppRegistration,

        [Parameter(Mandatory = $true, HelpMessage = "The certificate thumbprint.")]
        [string]$CertThumbprint,

        [Parameter(Mandatory = $true, HelpMessage = "The context object.")]
        [PSObject]$Context,

        [Parameter(Mandatory = $true, HelpMessage = "The user object.")]
        [PSObject]$User,

        [Parameter(Mandatory = $true, HelpMessage = "The mail enabled sending group.")]
        [string]$MailEnabledSendingGroup
    )

    # Begin Logging
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint }
    if (!(Get-SecretVault -Name GraphEmailAppLocalStore)) {
        try {
            Write-AuditLog -Message "Registering CredMan Secret Vault"
            Register-SecretVault -Name GraphEmailAppLocalStore -ModuleName "SecretManagement.JustinGrote.CredMan" -ErrorAction Stop
            Write-AuditLog -Message "Secret Vault: GraphEmailAppLocalStore registered."
        }
        catch {
            throw $_.Exception
        }
    }
    elseif ((Get-SecretInfo -Name "CN=$AppName" -Vault GraphEmailAppLocalStore) ) {
        Write-AuditLog -Message "Secret found! Would you like to delete the previous configuration for `"CN=$AppName.`"?" -Severity Warning
        try {
            Remove-Secret -Name "CN=$AppName" -Vault GraphEmailAppLocalStore -Confirm:$false -ErrorAction Stop
            Write-AuditLog -Message "Previous secret CN=$AppName removed."
        }
        catch {
            throw $_.Exception
        }
    }

    $output = [PSCustomObject] @{
        AppId                  = $AppRegistration.AppId
        CertThumbprint         = $CertThumbprint
        TenantID               = $Context.TenantId
        CertExpires            = ($Cert.NotAfter).ToString("yyyy-MM-dd HH:mm:ss")
        SendAsUser             = $($User.UserPrincipalName.Split("@")[0])
        AppRestrictedSendGroup = $MailEnabledSendingGroup
        Appname               = "CN=$AppName"
    }

    $delimiter = '|'
    $joinedString = ($output.PSObject.Properties.Value) -join $delimiter

    try {
        Set-Secret -Name "CN=$AppName" -Secret $joinedString -Vault GraphEmailAppLocalStore -ErrorAction Stop
    }
    catch {
        throw $_.Exception
    }

    Write-AuditLog -Message "Returning output. Save the AppName $("CN=$AppName"). The AppName will be needed to retreive the secret containing authentication info."

    Write-Host "You can use the following values as input into the email function!" -ForegroundColor Green
    Write-AuditLog -EndFunction
    $output | ForEach-Object {
        $hashTable = @{}
        $_.psobject.properties | ForEach-Object {
            $hashTable[$_.Name] = $_.Value
        }

        # Convert hashtable to script text
        $splatScript = "`$params = @{`n"
        $hashTable.Keys | ForEach-Object {
            $value = $hashTable[$_]
            if ($value -is [string]) {
                $value = "`"$value`""
            }
            $splatScript += " $_ = $value`n"
        }
        $splatScript += "}"

        Write-Output $splatScript
    }
}
#EndRegion '.\Private\Get-AppSecret.ps1' 96
#Region '.\Private\Get-GraphEmailAppConfig.ps1' 0
function Get-GraphEmailAppConfig {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "The App Registration object.")]
        $AppRegistration,

        [Parameter(Mandatory = $true, HelpMessage = "The Graph Service Principal Id.")]
        [string]$GraphServicePrincipalId,

        [Parameter(Mandatory = $true, HelpMessage = "The Azure context.")]
        $Context,

        [Parameter(Mandatory = $true, HelpMessage = "The Certificate.")]
        [string]$CertThumbPrint
    )

    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint }
        Write-AuditLog "###############################################"
        Write-AuditLog "Creating service principal for app with AppId $($AppRegistration.AppId)"
    }

    process {
        try {
            # Create a Service Principal for the app.
            New-MgServicePrincipal -AppId $AppRegistration.AppId -AdditionalProperties @{}

            # Get the client Service Principal for the created app.
            $ClientSp = Get-MgServicePrincipal -Filter "appId eq '$($AppRegistration.AppId)'"
            if (!($ClientSp)) {
                Write-AuditLog "Client service Principal not found for $($AppRegistration.AppId)" -Error
                throw "Unable to find Client Service Principal."
            }

            # Build the parameters for the New-MgOauth2PermissionGrant and create the grant.
            $Params = @{
                "ClientId"    = $ClientSp.Id
                "ConsentType" = "AllPrincipals"
                "ResourceId"  = $GraphServicePrincipalId
                "Scope"       = "Mail.Send"
            }
            New-MgOauth2PermissionGrant -BodyParameter $Params -Confirm:$false

            # Create the admin consent url:
            $adminConsentUrl = "https://login.microsoftonline.com/" + $Context.TenantId + "/adminconsent?client_id=" + $AppRegistration.AppId
            Write-Output "Please go to the following URL in your browser to provide admin consent"
            Write-Output $adminConsentUrl
            Write-Output "After providing admin consent, you can use the following values with Connect-MgGraph for app-only authentication:"

            # Generate graph command that can be used to connect later that can be copied and saved.
            $connectGraph = "Connect-MgGraph -ClientId """ + $AppRegistration.AppId + """ -TenantId """`
                + $Context.TenantId + """ -CertificateName """ + $Cert.SubjectName.Name + """"
                Write-Output $connectGraph
        }
        catch {
            throw $_.Exception
        }
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\Get-GraphEmailAppConfig.ps1' 67
#Region '.\Private\Initialize-GraphEmailApp.ps1' 0
function Initialize-GraphEmailApp {
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "The 2 to 4 character long prefix ID of the app, files and certs that are created.")]
        [ValidatePattern('^[A-Z]{2,4}$')]
        [string]$Prefix,

        [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")]
        [String] $UserId
    )

    process {
        # Begin Logging Check
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog "###############################################"

        # Step 5:
        # Get the MGContext
        $context = Get-MgContext
        # Step 6:
        # Instantiate the user variable.
        $user = Get-MgUser -Filter "Mail eq '$UserId'"
        # Step 7:
        # Define the application Name and Encrypted File Paths.
        $AppName = "$($Prefix)-AuditGraphEmail-$($env:USERDNSDOMAIN)-As-$(($user.UserPrincipalName).Split("@")[0])"
        $graphServicePrincipal = Get-MgServicePrincipal -Filter "DisplayName eq 'Microsoft Graph'"
        $graphResourceId = $graphServicePrincipal.AppId
        Write-AuditLog "Microsoft Graph Service Principal AppId is $graphResourceId"
        # Step 9:
        # Build resource requirements variable using Find-MgGraphCommand -Command New-MgApplication | Select -First 1 -ExpandProperty Permissions
        # Find-MgGraphPermission -PermissionType Application -All | ? {$_.name -eq "Mail.Send"}
        $resId = (Find-MgGraphPermission -PermissionType Application -All | Where-Object { $_.name -eq "Mail.Send" }).Id

        return @{
            "GraphDisplayname"      = $graphServicePrincipal.DisplayName
            "Context"               = $context
            "User"                  = $user
            "AppName"               = $AppName
            "GraphServicePrincipal" = $graphServicePrincipal
            "GraphResourceId"       = $graphResourceId
            "ResId"                 = $resId
        }
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\Initialize-GraphEmailApp.ps1' 54
#Region '.\Private\Initialize-ModuleEnv.ps1' 0
function Initialize-ModuleEnv {
    <#
        .SYNOPSIS
        Initializes the environment by installing required PowerShell modules.
        .DESCRIPTION
        This function installs PowerShell modules required by the script. It can install public or pre-release versions of the module, and it supports installation for all users or current user.
        .PARAMETER PublicModuleNames
        An array of module names to be installed. Required when using the Public parameter set.
        .PARAMETER PublicRequiredVersions
        An array of required module versions to be installed. Required when using the Public parameter set.
        .PARAMETER PrereleaseModuleNames
        An array of pre-release module names to be installed. Required when using the Prerelease parameter set.
        .PARAMETER PrereleaseRequiredVersions
        An array of required pre-release module versions to be installed. Required when using the Prerelease parameter set.
        .PARAMETER Scope
        The scope of the module installation. Possible values are "AllUsers" and "CurrentUser". This determines the installation scope of the module.
        .PARAMETER ImportModuleNames
        The specific modules you'd like to import from the installed package to streamline imports. This is used when you want to import only specific modules from a package, rather than all of them.
        .EXAMPLE
        Initialize-ModuleEnv -PublicModuleNames "PSnmap", "Microsoft.Graph" -PublicRequiredVersions "1.3.1","1.23.0" -Scope AllUsers
 
        This example installs the PSnmap and Microsoft.Graph modules in the AllUsers scope with the specified versions.
        .EXAMPLE
        $params1 = @{
            PublicModuleNames = "PSnmap","Microsoft.Graph"
            PublicRequiredVersions = "1.3.1","1.23.0"
            ImportModuleNames = "Microsoft.Graph.Authentication", "Microsoft.Graph.Identity.SignIns"
            Scope = "CurrentUser"
        }
        Initialize-ModuleEnv @params1
 
        This example installs Microsoft.Graph and Pester Modules in the CurrentUser scope with the specified versions.
        It will attempt to only import Microsoft.Graph Modules matching the names in the "ImportModulesNames" array.
        .EXAMPLE
        $params2 = @{
            PrereleaseModuleNames = "Sampler", "Pester"
            PrereleaseRequiredVersions = "2.1.5", "4.10.1"
            Scope = "CurrentUser"
        }
        Initialize-ModuleEnv @params2
        This example installs the PreRelease Sampler and Pester Modules in the CurrentUser scope with the specified versions.
        Double check https://www.powershellgallery.com/packages/<ModuleName>/<ModuleVersionNumber>
        to verify if the "-PreRelease" switch is needed.
        .INPUTS
        None
        .OUTPUTS
        None
        .NOTES
        Author: DrIOSx
        This function makes extensive use of the Write-AuditLog function for logging actions, warnings, and errors. It also uses a script-scope variable $script:VerbosePreference for controlling verbose output.
    #>

        [CmdletBinding(DefaultParameterSetName = "Public")]
        param (
            [Parameter(ParameterSetName = "Public", Mandatory)]
            [string[]]$PublicModuleNames,
            [Parameter(ParameterSetName = "Public", Mandatory)]
            [string[]]$PublicRequiredVersions,
            [Parameter(ParameterSetName = "Prerelease", Mandatory)]
            [string[]]$PrereleaseModuleNames,
            [Parameter(ParameterSetName = "Prerelease", Mandatory)]
            [string[]]$PrereleaseRequiredVersions,
            [ValidateSet(
                "AllUsers",
                "CurrentUser"
            )]
            [string]$Scope,
            [string[]]$ImportModuleNames = $null
        )
        # Start logging function execution
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        # Function limit needs to be set higher if installing graph module and if powershell is version 5.1.
        # The Microsoft.Graph module requires an increased function limit.
        # If we're installing this module, set the function limit to 8192.
        if ($PublicModuleNames -match 'Microsoft.Graph' -or $PrereleaseModuleNames -match "Microsoft.Graph") {
            if ($script:MaximumFunctionCount -lt 8192) {
                $script:MaximumFunctionCount = 8192
            }
        }
        # Check and install PowerShellGet.
        # PowerShellGet is required for module management in PowerShell.
        ### https://learn.microsoft.com/en-us/powershell/scripting/gallery/installing-psget?view=powershell-7.3
        # Get all available versions of PowerShellGet
        $PSGetVer = Get-Module -Name PowerShellGet -ListAvailable

        # Initialize flag to false
        $notOneFlag = $false

        # For each module version
        foreach ($module in $PSGetVer) {
            # Check if version is different from "1.0.0.1"
            if ($module.Version -ne "1.0.0.1") {
                $notOneFlag = $true
                break
            }
        }

        # If any version is different from "1.0.0.1", import the latest one
        if ($notOneFlag) {
            # Sort by version in descending order and select the first one (the latest)
            $latestModule = $PSGetVer | Sort-Object Version -Descending | Select-Object -First 1
            # Import the latest version
            Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version
        }
        else {
            switch (Test-IsAdmin) {
                $false {
                    Write-AuditLog "PowerShellGet is version 1.0.0.1. Please run this once as an administrator, to update PowershellGet." -Severity Error
                    throw "Elevation required to update PowerShellGet!"
                }
                Default {
                    Write-AuditLog "You have sufficient privileges to install to the PowershellGet"
                }
            }
            try {
                Write-AuditLog "Install the latest version of PowershellGet from the PSGallery?" -Severity Warning
                [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
                Install-Module PowerShellGet -AllowClobber -Force -ErrorAction Stop
                Write-AuditLog "PowerShellGet was installed successfully!"
                $PSGetVer = Get-Module -Name PowerShellGet -ListAvailable
                $latestModule = $PSGetVer | Sort-Object Version -Descending | Select-Object -First 1
                Import-Module -Name $latestModule.Name -RequiredVersion $latestModule.Version -ErrorAction Stop
            }
            catch {
                throw $_.Exception
            }
        }
        # End Region PowershellGet Install
        if ($Scope -eq "AllUsers") {
            switch (Test-IsAdmin) {
                $false {
                    Write-AuditLog "You must be an administrator to install in the `'AllUsers`' scope." -Severity Error
                    Write-AuditLog "If you intended to install the module only for this user, select the `'CurrentUser`' scope." -Severity Error
                    throw "Elevation required for `'AllUsers`' scope"
                }
                Default {
                    Write-AuditLog "You have sufficient privileges to install to the `'AllUsers`' scope."
                }
            }
        }
        if ($PSCmdlet.ParameterSetName -eq "Public") {
            $modules = $PublicModuleNames
            $versions = $PublicRequiredVersions
        }
        elseif ($PSCmdlet.ParameterSetName -eq "Prerelease") {
            $modules = $PrereleaseModuleNames
            $versions = $PrereleaseRequiredVersions
            $prerelease = $true
        }
        foreach ($module in $modules) {
            $name = $module
            $version = $versions[$modules.IndexOf($module)]
            $installedModule = Get-Module -Name $name -ListAvailable
            switch (($null -eq $ImportModuleNames)) {
                $false {
                    $SelectiveImports = $ImportModuleNames | Where-Object { $_ -match $name }
                    Write-AuditLog "Attempting to selecively install module/s:"
                }
                Default {
                    $SelectiveImports = $null
                    Write-AuditLog "Selective imports were not specified. All functions and commands will be imported."
                }
            }
            # Get Module Object
            switch ($prerelease) {
                $true {
                    $message = "The PreRelease module $name version $version is not installed. Would you like to install it?"
                    $throwmsg = "You must install the PreRelease module $name version $version to continue"
                }
                Default {
                    $message = "The $name module version $version is not installed. Would you like to install it?"
                    $throwmsg = "You must install the $name module version $version to continue."
                }
            }
            if (!$installedModule) {
                # Install Required Module
                Write-AuditLog $message -Severity Warning
                try {
                    Write-AuditLog "Installing $name module/s version $version -AllowPrerelease:$prerelease."
                    $SaveVerbosePreference = $script:VerbosePreference
                    Install-Module $name -Scope $Scope -RequiredVersion $version -AllowPrerelease:$prerelease -ErrorAction Stop -Verbose:$false
                    $script:VerbosePreference = $SaveVerbosePreference
                    Write-AuditLog "$name module successfully installed!"
                    if ($SelectiveImports) {
                        foreach ($Mod in $SelectiveImports) {
                            $name = $Mod
                            Write-AuditLog "Selectively importing the $name module."
                            $SaveVerbosePreference = $script:VerbosePreference
                            Import-Module $name -ErrorAction Stop -Verbose:$false
                            $script:VerbosePreference = $SaveVerbosePreference
                            Write-AuditLog "Successfully imported the $name module."
                        }
                    }
                    else {
                        Write-AuditLog "Importing the $name module."
                        $SaveVerbosePreference = $script:VerbosePreference
                        Import-Module $name -ErrorAction Stop -Verbose:$false
                        $script:VerbosePreference = $SaveVerbosePreference
                        Write-AuditLog "Successfully imported the $name module."
                    }
                }
                catch {
                    Write-AuditLog $throwmsg -Severity Error
                    throw $_.Exception
                }
            }
            else {
                try {
                    if ($SelectiveImports) {
                        foreach ($Mod in $SelectiveImports) {
                            $name = $Mod
                            Write-AuditLog "The $name module was found to be installed."
                            Write-AuditLog "Selectively importing the $name module."
                            $SaveVerbosePreference = $script:VerbosePreference
                            Import-Module $name -ErrorAction Stop -Verbose:$false
                            $script:VerbosePreference = $SaveVerbosePreference
                            Write-AuditLog "Successfully imported the $name module."
                            Write-AuditLog -EndFunction
                        }
                    }
                    else {
                        Write-AuditLog "The $name module was found to be installed."
                        Write-AuditLog "Importing the $name module."
                        $SaveVerbosePreference = $script:VerbosePreference
                        Import-Module $name -ErrorAction Stop -Verbose:$false
                        $script:VerbosePreference = $SaveVerbosePreference
                        Write-AuditLog "Successfully imported the $name module."
                        write-auditlog -EndFunction
                    }
                }
                catch {
                    Write-AuditLog $throwmsg -Severity Error
                    throw $_.Exception
                }
            }
        }
    }
#EndRegion '.\Private\Initialize-ModuleEnv.ps1' 242
#Region '.\Private\New-ExchangeEmailAppPolicy.ps1' 0
function New-ExchangeEmailAppPolicy {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "The application registration object.")]
        [PSObject]$AppRegistration,

        [Parameter(Mandatory = $true, HelpMessage = "The Mail Enabled Sending Group.")]
        [string]$MailEnabledSendingGroup
    )
        # Begin Logging
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
    try {
        Write-AuditLog -Message "Creating Exchange Application policy for $($MailEnabledSendingGroup) for AppId $($AppRegistration.AppId)."
        New-ApplicationAccessPolicy -AppId $AppRegistration.AppId `
            -PolicyScopeGroupId $MailEnabledSendingGroup -AccessRight RestrictAccess `
            -Description "Limit MSG application to only send emails as a group of users" -ErrorAction Stop
        Write-AuditLog -Message "Created Exchange Application policy for $($MailEnabledSendingGroup)."
    }
    catch {
        throw $_.Exception
    }
    Write-AuditLog -EndFunction
}
#EndRegion '.\Private\New-ExchangeEmailAppPolicy.ps1' 29
#Region '.\Private\Register-GraphApp.ps1' 0
function Register-GraphApp {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, HelpMessage = "The name of the application.")]
        [string]$AppName,

        [Parameter(Mandatory = $true, HelpMessage = "The Graph Resource Id.")]
        [string]$GraphResourceId,

        [Parameter(Mandatory = $true, HelpMessage = "The Resource Id.")]
        [string]$ResID,

        [Parameter(Mandatory = $true, HelpMessage = "The Certificate.")]
        [string]$CertThumbPrint
    )
    begin {
        # Begin Logging
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog "###############################################"
        # Install and import the Microsoft.Graph module. Tested: 1.22.0
    }
    process {
        try {
            Write-AuditLog "Creating app registration..."
            $RequiredResourceAccess = New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess
            $RequiredResourceAccess.ResourceAppId = $GraphResourceId
            $RequiredResourceAccess.ResourceAccess += @{ Id = $ResID; Type = "Role" }

            $AppPermissions = New-Object -TypeName System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess]
            $AppPermissions.Add($RequiredResourceAccess)

            Write-AuditLog "App permissions are: $AppPermissions"
            $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint }
            $AppRegistration = New-MgApplication -DisplayName $AppName -SignInAudience "AzureADMyOrg" `
                -Web @{ RedirectUris = "http://localhost"; } `
                -RequiredResourceAccess $RequiredResourceAccess `
                -AdditionalProperties @{} `
                -KeyCredentials @(@{ Type = "AsymmetricX509Cert"; Usage = "Verify"; Key = $Cert.RawData })

            if (!($AppRegistration)) {
                throw "The app creation failed for $($AppName)."
            }
            Write-AuditLog "App registration created with app ID $($AppRegistration.AppId)"
            Start-Sleep 1
        }
        catch {
            throw $_.Exception
        }
        return $AppRegistration
    }
    end {
        Write-AuditLog -EndFunction
    }
}
#EndRegion '.\Private\Register-GraphApp.ps1' 60
#Region '.\Private\Test-IsAdmin.ps1' 0
function Test-IsAdmin {
    <#
    .SYNOPSIS
    Checks if the current user is an administrator on the machine.
    .DESCRIPTION
    This private function returns a Boolean value indicating whether
    the current user has administrator privileges on the machine.
    It does this by creating a new WindowsPrincipal object, passing
    in a WindowsIdentity object representing the current user, and
    then checking if that principal is in the Administrator role.
    .INPUTS
    None.
    .OUTPUTS
    Boolean. Returns True if the current user is an administrator, and False otherwise.
    .EXAMPLE
    PS C:\> Test-IsAdmin
    True
    #>


    # Create a new WindowsPrincipal object for the current user and check if it is in the Administrator role
    (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
#EndRegion '.\Private\Test-IsAdmin.ps1' 23
#Region '.\Private\Write-AuditLog.ps1' 0
function Write-AuditLog {
    <#
    .SYNOPSIS
        Writes log messages to the console and updates the script-wide log variable.
    .DESCRIPTION
        The Write-AuditLog function writes log messages to the console based on the severity (Verbose, Warning, or Error) and updates
        the script-wide log variable ($script:LogString) with the log entry. You can use the Start, End, and EndFunction switches to
        manage the lifecycle of the logging.
    .INPUTS
        System.String
        You can pipe a string to the Write-AuditLog function as the Message parameter.
        You can also pipe an object with a Severity property as the Severity parameter.
    .OUTPUTS
        None
        The Write-AuditLog function doesn't output any objects to the pipeline. It writes messages to the console and updates the
        script-wide log variable ($script:LogString).
    .PARAMETER BeginFunction
        Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .PARAMETER Message
        The message string to log.
    .PARAMETER Severity
        The severity of the log message. Accepted values are 'Information', 'Warning', and 'Error'. Defaults to 'Information'.
    .PARAMETER Start
        Initializes the script-wide log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function.
    .PARAMETER End
        Sets the message to "End Log" and exports the log to a CSV file if the OutputPath parameter is provided.
    .PARAMETER EndFunction
        Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .PARAMETER OutputPath
        The file path for exporting the log to a CSV file when using the End switch.
    .EXAMPLE
        Write-AuditLog -Message "This is a test message."
 
        Writes a test message with the default severity (Information) to the console and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -Message "This is a warning message." -Severity "Warning"
 
        Writes a warning message to the console and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -Start
 
        Initializes the log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function.
    .EXAMPLE
        Write-AuditLog -BeginFunction
 
        Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -EndFunction
 
        Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable.
    .EXAMPLE
        Write-AuditLog -End -OutputPath "C:\Logs\auditlog.csv"
 
        Sets the message to "End Log", adds it to the log variable, and exports the log to a CSV file.
    .NOTES
    Author: DrIOSx
#>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        ###
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Input a Message string.',
            Position = 0,
            ParameterSetName = 'Default',
            ValueFromPipeline = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]$Message,
        ###
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Information, Warning or Error.',
            Position = 1,
            ParameterSetName = 'Default',
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('Information', 'Warning', 'Error')]
        [string]$Severity = 'Information',
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'End'
        )]
        [switch]$End,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'BeginFunction'
        )]
        [switch]$BeginFunction,
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'EndFunction'
        )]
        [switch]$EndFunction,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Start'
        )]
        [switch]$Start,
        ###
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'End'
        )]
        [string]$OutputPath
    )
    begin {
        $ErrorActionPreference = "SilentlyContinue"
        # Define variables to hold information about the command that was invoked.
        $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*'
        $FuncName = (Get-PSCallStack)[1].Command
        $ModuleVer = $MyInvocation.MyCommand.Version.ToString()
        # Set the error action preference to continue.
        $ErrorActionPreference = "Continue"
    }
    process {
        try {
            $Function = $($FuncName + '.v' + $ModuleVer)
            if ($Start) {
                $script:LogString = @()
                $Message = '+++ Begin Log | ' + $Function + ' |'
            }
            elseif ($BeginFunction) {
                $Message = '>>> Begin Function Log | ' + $Function + ' |'
            }
            $logEntry = [pscustomobject]@{
                Time      = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss'))
                Module    = $ModuleName
                PSVersion = ($PSVersionTable.PSVersion).ToString()
                PSEdition = ($PSVersionTable.PSEdition).ToString()
                IsAdmin   = $(Test-IsAdmin)
                User      = "$Env:USERDOMAIN\$Env:USERNAME"
                HostName  = $Env:COMPUTERNAME
                InvokedBy = $Function
                Severity  = $Severity
                Message   = $Message
                RunID     = -1
            }
            if ($BeginFunction) {
                $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Measure-Object -Property RunID -Maximum).Maximum
                if ($null -eq $maxRunID) { $maxRunID = -1 }
                $logEntry.RunID = $maxRunID + 1
            }
            else {
                $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Select-Object -Last 1).RunID
                if ($null -eq $lastRunID) { $lastRunID = 0 }
                $logEntry.RunID = $lastRunID
            }
            if ($EndFunction) {
                $FunctionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $Function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)"
                $startTime = ([DateTime]::ParseExact("$FunctionStart", 'yyyy-MM-dd hh:mmTss', $null))
                $endTime = Get-Date
                $timeTaken = $endTime - $startTime
                $Message = '<<< End Function Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec"
                $logEntry.Message = $Message
            }
            elseif ($End) {
                $startTime = ([DateTime]::ParseExact($($script:LogString[0].Time), 'yyyy-MM-dd hh:mmTss', $null))
                $endTime = Get-Date
                $timeTaken = $endTime - $startTime
                $Message = '--- End Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec"
                $logEntry.Message = $Message
            }
            $script:LogString += $logEntry
            switch ($Severity) {
                'Warning' {
                    Write-Warning ('[WARNING] ! ' + $Message)
                    $UserInput = Read-Host "Warning encountered! Do you want to continue? (Y/N)"
                    if ($UserInput -eq 'N') {
                        Write-Output "Script execution stopped by user!"
                        exit
                    }
                }
                'Error'       { Write-Error ('[ERROR] X - ' + $FuncName + ' ' + $Message) -ErrorAction Continue }
                'Verbose'     { Write-Verbose ('[VERBOSE] ~ ' + $Message) }
                Default { Write-Information ('[INFORMATION] * ' + $Message)  -InformationAction Continue}
            }
        }
        catch {
            throw "Write-AuditLog encountered an error (process block): $($_.Exception.Message)"
        }

    }
    end {
        try {
            if ($End) {
                if (-not [string]::IsNullOrEmpty($OutputPath)) {
                    $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8
                    Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)"
                }
                else {
                    throw "OutputPath is not specified for End action."
                }
            }
        }
        catch {
            throw "Error in Write-AuditLog (end block): $($_.Exception.Message)"
        }
    }
}
#EndRegion '.\Private\Write-AuditLog.ps1' 205
#Region '.\Public\Connect-toMgGraph.ps1' 0
<#
    .SYNOPSIS
        Connects to Microsoft Graph and Exchange Online using defined permission scopes.
    .DESCRIPTION
        The Connect-ToMGGraph function is designed to facilitate a connection to Microsoft Graph and Exchange Online.
        It uses modern authentication pop-up, requesting the user to grant permissions. It logs the process of
        connection, including any errors that might occur.
 
        The function operates on three permission scopes for Microsoft Graph:
        - Application.ReadWrite.All
        - DelegatedPermissionGrant.ReadWrite.All
        - Directory.ReadWrite.All
 
        Note: It is necessary to press Enter at each prompt to proceed with the connection or you can cancel by pressing ctrl+c.
    .PARAMETERS
        The function does not take any parameters.
    .EXAMPLE
        Connect-ToMGGraph
        Executes the function, initiating the connection process to Microsoft Graph and Exchange Online.
    .INPUTS
        None. You cannot pipe inputs to this function.
    .OUTPUTS
        None. This function does not return any output.
    .NOTES
        Logging details are handled by the Write-AuditLog function, which needs to be available in the scope.
        If any error occurs during the connection process, the function will throw the corresponding exception.
#>

function Connect-ToMGGraph {
    # Begin Logging
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog "###############################################"

    # Step 4:
    Read-Host "Press Enter to connect to Microsoft Graph scopes Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, and Directory.ReadWrite.All, or press ctrl+c to cancel " -ErrorAction Stop
    # Connect to MSGraph with the appropriate permission scopes and then Exchange.
    Write-AuditLog "Connecting to MgGraph and ExchangeOnline using modern authentication pop-up."
    try {
        Write-AuditLog "Connecting to MgGraph with scopes Application.ReadWrite.All, DelegatedPermissionGrant.ReadWrite.All, and Directory.ReadWrite.All."
        Connect-MgGraph -Scopes "Application.ReadWrite.All", "DelegatedPermissionGrant.ReadWrite.All", "Directory.ReadWrite.All"
        Write-AuditLog "Connected to MgGraph"
        Read-Host "Press Enter to connect to ExchangeOnline" -ErrorAction Stop
        Connect-ExchangeOnline -ErrorAction Stop
        Write-AuditLog "Connected to ExchangeOnline."
        Read-Host "Press Enter to continue" -ErrorAction Stop
    }
    catch {
        Write-AuditLog -Severity Error -Message "Error connecting to MgGraph or ExchangeOnline. Error: $($_.Exception.Message)"
        throw $_.Exception
    }
    Write-AuditLog -EndFunction
}
#EndRegion '.\Public\Connect-toMgGraph.ps1' 57
#Region '.\Public\Deploy-GraphEmailApp.ps1' 0
<#
    .SYNOPSIS
        Deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication.
    .DESCRIPTION
        This cmdlet deploys a new Microsoft Graph Email app and associates it with a certificate for app-only authentication.
        It requires an AppPrefix for the app, an optional CertThumbprint, an AuthorizedSenderUserName, and a MailEnabledSendingGroup.
    .PARAMETER AppPrefix
        A unique prefix for the Graph Email App to initialize. Ensure it is used consistently for grouping purposes.
    .PARAMETER CertThumbprint
        An optional parameter indicating the thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated.
    .PARAMETER AuthorizedSenderUserName
        The username of the authorized sender.
    .PARAMETER MailEnabledSendingGroup
        The mail-enabled group to which the sender belongs. This will be used to assign app policy restrictions.
    .EXAMPLE
        PS C:\> Deploy-GraphEmailApp -AppPrefix "ABC" -AuthorizedSenderUserName "jdoe@example.com" -MailEnabledSendingGroup "GraphAPIMailGroup@example.com" -CertThumbprint "AABBCCDDEEFF11223344556677889900"
    .INPUTS
        None
    .OUTPUTS
        Returns a pscustomobject containing the AppId, CertThumbprint, TenantID, and CertExpires.
    .NOTES
        This cmdlet requires that the user running the cmdlet have the necessary permissions
        to create the app and connect to Exchange Online. In addition, a mail-enabled security
        group must already exist in Exchange Online for the MailEnabledSendingGroup parameter.
#>

function Deploy-GraphEmailApp {
    [CmdletBinding()]
    param(

        [Parameter(Mandatory = $true, HelpMessage = "The prefix used to initialize the Graph Email App.")]
        [string]$AppPrefix,

        [Parameter(Mandatory = $false, HelpMessage = "The thumbprint of the certificate to be retrieved.")]
        [string]$CertThumbprint,

        [Parameter(Mandatory = $true, HelpMessage = "The username of the authorized sender.")]
        [string]$AuthorizedSenderUserName,

        [Parameter(Mandatory = $true, HelpMessage = "The Mail Enabled Sending Group.")]
        [string]$MailEnabledSendingGroup
    )

    $PublicMods = `
        "Microsoft.Graph", "ExchangeOnlineManagement", `
        "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan"
    $PublicVers = `
        "1.22.0", "3.1.0", `
        "1.1.2", "1.0.0"
    $ImportMods = `
        "Microsoft.Graph.Authentication", `
        "Microsoft.Graph.Applications", `
        "Microsoft.Graph.Identity.SignIns", `
        "Microsoft.Graph.Users"
    $params1 = @{
        PublicModuleNames      = $PublicMods
        PublicRequiredVersions = $PublicVers
        ImportModuleNames      = $ImportMods
        Scope                  = "CurrentUser"
    }
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog "###############################################"
    Initialize-ModuleEnv @params1
    Connect-ToMGGraph
    $AppSettings = Initialize-GraphEmailApp -Prefix "$AppPrefix" -UserId "$AuthorizedSenderUserName"

    $CertDetails = Get-GraphEmailAppCert -AppName $AppSettings.AppName -CertThumbprint $CertThumbprint

    $appRegistration = Register-GraphApp -AppName $AppSettings.AppName -GraphResourceId $AppSettings.graphResourceId -ResID $AppSettings.ResId -CertThumbprint $CertDetails.CertThumbprint


    Get-GraphEmailAppConfig -AppRegistration $appRegistration -GraphServicePrincipalId $AppSettings.GraphServicePrincipal.Id -Context $AppSettings.Context -CertThumbprint $CertDetails.CertThumbprint
    Read-Host "Provide admin consent now, or copy the url and provide admin consent later. Press Enter to continue."
    # Call to New-ExchangeEmailAppPolicy

    [void](New-ExchangeEmailAppPolicy -AppRegistration $appRegistration -MailEnabledSendingGroup $MailEnabledSendingGroup)
    $output = Get-AppSecret -AppName $AppSettings.AppName  -AppRegistration $appRegistration -CertThumbprint $CertDetails.CertThumbprint -Context $AppSettings.Context -User $AppSettings.User -MailEnabledSendingGroup $MailEnabledSendingGroup
    return $output
    #>
}
#EndRegion '.\Public\Deploy-GraphEmailApp.ps1' 85
#Region '.\Public\Get-GraphEmailAppCert.ps1' 0
<#
    .SYNOPSIS
        Retrieves or creates a new certificate for the Microsoft Graph Email app.
    .DESCRIPTION
        The Get-GraphEmailAppCert function retrieves a certificate for the specified app from the CurrentUser's certificate store based on the provided thumbprint.
        If a thumbprint is not provided, it will generate a new self-signed certificate.
    .PARAMETER CertThumbprint
        The thumbprint of the certificate to be retrieved. If not specified, a self-signed certificate will be generated.
    .PARAMETER AppName
        The name of the Graph Email App.
    .EXAMPLE
        PS C:\> Get-GraphEmailAppCert -AppName "MyApp" -CertThumbprint "9B8B40C5F148B710AD5C0E5CC8D0B71B5A30DB0C"
    .EXAMPLE
        PS C:\> Get-GraphEmailAppCert -AppName "MyApp"
    .INPUTS
        None
    .OUTPUTS
        A custom PowerShell object containing the certificate's thumbprint, expiration date, and the associated app's name.
    .NOTES
        The cmdlet requires that the user running the cmdlet have the necessary permissions to create or retrieve certificates from the certificate store.
        The certificate's expiration date is formatted as "yyyy-MM-dd HH:mm:ss".
#>

function Get-GraphEmailAppCert {
    param (
        [string]$CertThumbprint,
        [string]$AppName
    )
    if (!($script:LogString)) {
        Write-AuditLog -Start
    }
    else {
        Write-AuditLog -BeginFunction
    }
    Write-AuditLog "###############################################"
    # Step 10:
    # Create or retrieve certificate from the store.
    try {
        if (!$CertThumbprint) {
            # Create a self-signed certificate for the app.
            $Cert = New-SelfSignedCertificate -Subject "CN=$AppName" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 -KeyAlgorithm RSA -HashAlgorithm SHA256
            $CertThumbprint = $Cert.Thumbprint
            $CertExpirationDate = $Cert.NotAfter
            $output = [PSCustomObject] @{
                CertThumbprint = $CertThumbprint
                CertExpires    = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss")
                AppName        = $AppName
            }
        }
        else {
            # Retrieve the certificate from the CurrentUser's certificate store.
            $Cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint }
            if (!($Cert)) {
                throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store."
            }
            $CertThumbprint = $Cert.Thumbprint
            $CertExpirationDate = $Cert.NotAfter
            $output = [PSCustomObject] @{
                CertThumbprint = $CertThumbprint
                CertExpires    = $certExpirationDate.ToString("yyyy-MM-dd HH:mm:ss")
                AppName        = $AppName
            }
        }
        return $output
    }
    catch {
        # If there is an error, throw an exception with the error message.
        throw $_.Exception
    }
    write-auditlog "Certificate with thumbprint $CertThumbprint created or retrieved from the CurrentUser's certificate store."
    Write-AuditLog -EndFunction
}

#EndRegion '.\Public\Get-GraphEmailAppCert.ps1' 73
#Region '.\Public\Send-GraphAppEmail.ps1' 0
function Send-GraphAppEmail {
<#
    .SYNOPSIS
    Sends an email using the Microsoft Graph API.
    .DESCRIPTION
    The Send-GraphAppEmail function uses the Microsoft Graph API to send an email to a specified recipient.
    The function requires the Microsoft Graph API to be set up and requires a pre-created Microsoft Graph API
    app to send the email. The AppName can be passed in as a parameter and the function will retrieve the
    associated authentication details from the Credential Manager.
    .PARAMETER AppName
    The pre-created Microsoft Graph API app name used to send the email.
    .PARAMETER To
    The email address of the recipient.
    .PARAMETER FromAddress
    The email address of the sender who is a member of the Security Enabled Group allowed to send email
    that was configured using the Register-GraphEmailApp.
    .PARAMETER Subject
    The subject line of the email.
    .PARAMETER EmailBody
    The body text of the email.
    .PARAMETER AttachmentPath
    An array of file paths for any attachments to include in the email.
    .EXAMPLE
    Send-GraphAppEmail -AppName "GraphEmailApp" -To "recipient@example.com" -FromAddress "sender@example.com" -Subject "Test Email" -EmailBody "This is a test email."
    .NOTES
    The function requires the Microsoft.Graph and MSAL.PS modules to be installed and imported.
#>

    [CmdletBinding()]
    param (
        [Parameter(HelpMessage = "The Pre-created Register-GraphEmailApp Name for sending the email.")]
        [ValidateNotNullOrEmpty()]
        [string]$AppName,

        [Parameter(Mandatory = $true, HelpMessage = "The email address of the recipient.")]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")]
        [string]$To,

        [Parameter(Mandatory = $true, HelpMessage = "The email address of the sender.")]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$")]
        [string]$FromAddress,

        [Parameter(Mandatory = $true, HelpMessage = "The subject line of the email.")]
        [ValidateNotNullOrEmpty()]
        [string]$Subject,

        [Parameter(Mandatory = $true, HelpMessage = "The body text of the email.")]
        [ValidateNotNullOrEmpty()]
        [string]$EmailBody,

        [Parameter(Mandatory = $false, HelpMessage = "An array of file paths for any attachments to include in the email.")]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
        [string[]]$AttachmentPath
    )
    begin {
        if (!($script:LogString)) {
            Write-AuditLog -Start
        }
        else {
            Write-AuditLog -BeginFunction
        }
        Write-AuditLog "Begin Log"
        Write-AuditLog "###############################################"
        # Install and import the Microsoft.Graph module. Tested: 1.22.0
        $PublicMods = `
            "Microsoft.PowerShell.SecretManagement", "SecretManagement.JustinGrote.CredMan", "MSAL.PS"
        $PublicVers = `
            "1.1.2", "1.0.0", "4.37.0.0"
        $params1 = @{
            PublicModuleNames      = $PublicMods
            PublicRequiredVersions = $PublicVers
            Scope                  = "CurrentUser"
        }
        Initialize-ModuleEnv @params1
        # If a GraphEmailApp object was not passed in, attempt to retrieve it from the local machine
        if ($AppName) {
            try {
                # Step 7:
                # Define the application Name and Encrypted File Paths.
                $Auth = Get-Secret -Name "$AppName" -Vault GraphEmailAppLocalStore -AsPlainText -ErrorAction Stop
                $delimiter = "|"
                $values = $Auth.Split($delimiter)
                # Create a new PSCustomObject using the values
                $authobj = [PSCustomObject] @{
                    AppId                  = $values[0]
                    CertThumbprint         = $values[1]
                    TenantID               = $values[2]
                    CertExpires            = $values[3]
                    SendAsUser             = $values[4]
                    AppRestrictedSendGroup = $values[5]
                    AppName                = $values[6]
                }
                $GraphEmailApp = $authobj
            }
            catch {
                Write-Error $_.Exception.Message
            }
        } # End Region If
        if (!$GraphEmailApp) {
            throw "GraphEmailApp object not found. Please specify the GraphEmailApp object or provide the AppName and RedirectUri parameters."
        } # End Region If
        # Instatiate the required variables for retreiving the token.
        $AppId = $GraphEmailApp.AppId
        $CertThumbprint = $GraphEmailApp.CertThumbprint
        $Tenant = $GraphEmailApp.TenantID
        $Expiration = $GraphEmailApp.CertExpires
        Write-AuditLog "The Certificate $CertThumbprint will expire on $Expiration"
        # Retrieve the self-signed certificate from the CurrentUser's certificate store
        $cert = Get-ChildItem -Path Cert:\CurrentUser\My | Where-Object { $_.Thumbprint -eq $CertThumbprint }
        if (!($cert)) {
            throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser's certificate store"
        } # End Region If
        Write-AuditLog -Message "Retrieved Certificate with thumbprint $CertThumbprint."
    } # End Region Begin
    Process {
        # Authenticate with Azure AD and obtain an access token for the Microsoft Graph API using the certificate
        $MSToken = Get-MsalToken -ClientCertificate $Cert -ClientId $AppId -Authority "https://login.microsoftonline.com/$Tenant/oauth2/v2.0/token" -ErrorAction Stop
        # Set up the request headers
        $authheader = @{Authorization = "Bearer $($MSToken.AccessToken)" }
        # Set up the request URL
        $url = "https://graph.microsoft.com/v1.0/users/$($FromAddress)/sendMail"
        # Build the message body
        # Add a "from" field to the message object in $Message
        $FromField = @{
            emailAddress = @{
                address = "$($FromAddress)"
            }
        }
        $Message = @{
            message = @{
                subject      = "$Subject"
                body         = @{
                    contentType = "text"
                    content     = "$EmailBody"
                }
                toRecipients = @(
                    @{
                        emailAddress = @{
                            address = "$To"
                        }
                    }
                )
                from         = $FromField
            }
        }
        if ($AttachmentPath) {
            Write-AuditLog -Message "Attachments found. Processing..."
            $Message.message.attachments = @()
            foreach ($Path in $AttachmentPath) {
                $attachmentName = (Split-Path -Path $Path -Leaf)
                $attachmentBytes = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($Path))
                $attachment = @{
                    "@odata.type"  = "#microsoft.graph.fileAttachment"
                    "Name"         = $attachmentName
                    "ContentBytes" = $attachmentBytes
                }
                $Message.message.attachments += $attachment
            }
        }
        $jsonMessage = $message | ConvertTo-Json -Depth 4
        $body = $jsonMessage
        Write-AuditLog -Message "Processed message body. Ready to send email."
    }
    End {
        try {
            # Send the email message using the Invoke-RestMethod cmdlet
            Write-AuditLog "Sending email via Microsoft Graph."
            Invoke-RestMethod -Headers $authHeader -Uri $url -Body $body -Method POST -ContentType 'application/json'
            Write-AuditLog "Message sent to $To from $FromAddress with $(($Message.message.attachments).Count) attachments."
            Write-AuditLog -EndFunction
        }
        catch {
            throw $_.Exception
        }
    } # End Region End
}
#EndRegion '.\Public\Send-GraphAppEmail.ps1' 179