Services/MgGraph.ps1
Function ConvertTo-IMicrosoftGraphRecipient { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyCollection()] [AllowNull()] [pscustomobject]$EmailAddress, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Name ) # Return Null If Provided Recipient is Empty if (([string]::IsNullOrEmpty($EmailAddress)) -and ([string]::IsNullOrEmpty($EmailAddress.Address))) { return $null } # Loop through each of the recipient paramater array objects $IMicrosoftGraphRecipient = foreach ($address in $EmailAddress) { # Check if string (email address) or object/hashtable/etc. If not, separate out. if (-not ($address.GetType().Name -eq 'String')) { # Verify object contains 'Address' key or property. if ([string]::IsNullOrEmpty($address.AddressObj)) { throw "Improperly formatted sender, recipient, or reply to address." } # Set 'Name' & update 'Address' (do 'Name' 1st!) $Name = $address.Name $address = $address.AddressObj } if ([string]::IsNullOrEmpty($Name)) { @{ EmailAddress = @{Address = $address} } } else { @{ EmailAddress = @{ Name = $Name Address = $address} } } } return $IMicrosoftGraphRecipient } function ConvertTo-IMicrosoftGraphItemBody { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Content, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateSet('Text','HTML')] [string]$ContentType = 'Text' # The MIME type. See https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/message-format-and-transmission ) $IMicrosoftGraphItemBody = @{ ContentType = $ContentType Content = $Content } return $IMicrosoftGraphItemBody } Function ConvertTo-IMicrosoftGraphAttachment { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowNull()] [array]$Attachment ) if ([string]::IsNullOrEmpty($Attachment)) { return $null } [array]$IMicrosoftGraphAttachment = foreach ($currentAttachment in $Attachment) { switch ($currentAttachment.GetType().Name) { 'Hashtable' { # If direct file content is supplied. $AttachmentType = 'Content' if (($currentAttachment.ContainsKey('Name')) -and $currentAttachment.ContainsKey('Content')) { $Attachment_ByteEncoded = [System.Convert]::ToBase64String($currentAttachment.Content) } else { throw "The attachment hashtable object is improperly formatted. The hashtable requires the keys of `'Name`' and `'Contents`'" } [array]$IMicrosoftGraphAttachmentItem = @{ "@odata.type" = "#microsoft.graph.fileAttachment" Name = $currentAttachment.Name ContentBytes = $Attachment_ByteEncoded } } 'String' { # If a directory or file path is supplied. if (-not (Test-Path -Path $currentAttachment)) { throw 'Invalid path to attachment directory or file.' } switch ((Get-Item -Path $currentAttachment).GetType().Name) { 'FileInfo'{ $AttachmentType = 'FilePath' $FileInfo = Get-Item -Path $currentAttachment $Attachment_ByteEncoded = [convert]::ToBase64String([System.IO.File]::ReadAllBytes($FileInfo.FullName)) [array]$IMicrosoftGraphAttachmentItem = @{ "@odata.type" = "#microsoft.graph.fileAttachment" Name = $FileInfo.Name ContentBytes = $Attachment_ByteEncoded } } 'DirectoryInfo' { $AttachmentType = 'DirectoryPath' $DirectoryContent = Get-ChildItem $currentAttachment -File -Recurse [array]$IMicrosoftGraphAttachmentItem = foreach ($file in $DirectoryContent) { $Attachment_ByteEncoded = [convert]::ToBase64String([System.IO.File]::ReadAllBytes($file.FullName)) @{ "@odata.type" = "#microsoft.graph.fileAttachment" Name = $file.Name ContentBytes = $Attachment_ByteEncoded } } } Default {throw 'Unexpected attachment object type.'} } } Default {throw 'Unexpected attachment object type.'} } $IMicrosoftGraphAttachmentItem } return $IMicrosoftGraphAttachment } function Send-ScriptMessage_MgGraph { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$From, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$ReplyTo, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [pscustomobject]$To, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [pscustomobject]$CC, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [pscustomobject]$BCC, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [bool]$SaveToSentItems = $true, [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Subject, [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$Body, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [array]$Attachment, # Array of Content(bytes), File paths, and/or Directory paths [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string]$Sender ) # Set the Service ID. $ServiceId = 'MgGraph' # Set the necesasary configuration variables. $ScriptMessageConfig = Get-ScriptMessageConfig # Convert Parameters to IMicrosoft* $Message = @{} $Message['From'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $From [array]$Message['ReplyTo'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $ReplyTo [array]$Message['To'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $To [array]$Message['CC'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $CC [array]$Message['BCC'] = ConvertTo-IMicrosoftGraphRecipient -EmailAddress $BCC if (-not [string]::IsNullOrEmpty($Body.Content)) { if ([string]::IsNullOrEmpty($Body.ContentType)) # Don't send 'ContentType' if not provided. It will default to 'Text' { [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content } else { [hashtable]$Message['Body'] = ConvertTo-IMicrosoftGraphItemBody -Content $Body.Content -ContentType $Body.ContentType } } [array]$Message['Attachment'] = ConvertTo-IMicrosoftGraphAttachment -Attachment $Attachment # Build Email $EmailParams = [ordered]@{ SaveToSentItems = $SaveToSentItems Message = [ordered]@{ From = $Message.From ReplyTo = $Message.ReplyTo ToRecipients = $Message.To CcRecipients = $Message.CC BccRecipients = $Message.BCC Subject = $Subject Body = $Message.Body Attachments = $Message.Attachment } } # Check For Separate UserID Value if ([string]::IsNullOrEmpty($Sender)) { $Sender = $Message.From.emailAddress.Address } # Check if using beta Graph API & Send Email. if (-not ($ScriptMessageConfig.MgGraph.MgProfile -eq 'beta')) { $SendEmailMessageResult = Send-MgUserMail -UserId $Sender -BodyParameter $EmailParams -PassThru } else { $SendEmailMessageResult = Send-MgBetaUserMail -UserId $Sender -BodyParameter $EmailParams -PassThru } # Collect Return Info $SendScriptMessageResult = [ordered]@{} $SendScriptMessageResult.MessageService = $ServiceId $SendScriptMessageResult.Status = $SendEmailMessageResult # The SDK only returns $true and nothing else (and only that because of the 'PassThru') $SendScriptMessageResult.Error = $null $SendScriptMessageResult.SentFrom = @{} $SendScriptMessageResult.SentFrom.Name = $From.Name $SendScriptMessageResult.SentFrom.Address = $From.AddressObj $SendScriptMessageResult.Recipients = [ordered]@{} $SendScriptMessageResult.Recipients.To = ($Message.To).EmailAddress | Sort-Object $_.Value $SendScriptMessageResult.Recipients.CC = ($Message.CC).EmailAddress | Sort-Object $_.Value $SendScriptMessageResult.Recipients.BCC = ($Message.BCC).EmailAddress | Sort-Object $_.Value # If successfuul, return result info return $SendScriptMessageResult } function Connect-ScriptMessage_MgGraph { [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [pscustomobject]$ServiceConfig ) # Get Graph Profile From Config [string]$MgProfile = $ServiceConfig.MgProfile # 'beta' or 'v1.0'. # Check For Microsoft.Graph Module # Don't import the entire 'Microsoft.Graph' module. Only import the needed sub-modules. if (-not ($MgProfile -eq 'beta')) { Import-Module 'Microsoft.Graph.Authentication' -ErrorAction SilentlyContinue Import-Module 'Microsoft.Graph.Users.Actions' -ErrorAction SilentlyContinue if (!(Get-Module -Name "Microsoft.Graph.Users.Actions")) { # Module is not available. Write-Error "Please First Install the Microsoft.Graph Module from https://www.powershellgallery.com/packages/Microsoft.Graph/ " Return } } else { Import-Module 'Microsoft.Graph.Authentication' -ErrorAction SilentlyContinue # No beta version available for this required module. Import-Module 'Microsoft.Graph.Beta.Users.Actions' -ErrorAction SilentlyContinue if (!(Get-Module -Name "Microsoft.Graph.Beta.Users.Actions")) { # Module is not available. Write-Error @" Please First Install the Microsoft.Graph Module from https://www.powershellgallery.com/packages/Microsoft.Graph.Beta"/ . Installing the main modules of the SDK, Microsoft.Graph and Microsoft.Graph.Beta, will install all 38 sub modules for each module. Consider only installing the necessary modules, including Microsoft.Graph.Authentication which is installed by default when you opt to install the sub modules individually. For a list of available Microsoft Graph modules, use Find-Module Microsoft.Graph*. Only cmdlets for the installed modules will be available for use. "@ Return } } # Connect to the Microsoft Graph API. # E.g. Connect-MgGraph -Scopes "User.Read.All","Group.ReadWrite.All" # You can add additional permissions by repeating the Connect-MgGraph command with the new permission scopes. # View the current scopes under which the PowerShell SDK is (trying to) execute cmdlets: Get-MgContext | select -ExpandProperty Scopes # List all the scopes granted on the service principal object (you cn also do it via the Azure AD UI): Get-MgServicePrincipal -Filter "appId eq '14d82eec-204b-4c2f-b7e8-296a70dab67e'" | % { Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $_.Id } | fl # Find Graph permission needed. More info on permissions: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent) # E.g., Find-MgGraphPermission -SearchString "Teams" -PermissionType Delegated # E.g., Find-MgGraphPermission -SearchString "Teams" -PermissionType Application $MicrosoftGraphScopes = @( 'Mail.Send' #'Mail.Send.Shared' # Scope is not needed at the moment. ) $MgPermissionType = $ServiceConfig.MgPermissionType $MgTenantID = $ServiceConfig.MgTenantID $MgClientID = $ServiceConfig.MgClientID switch ($MgPermissionType) { Delegated { $null = Connect-MgGraph -Scopes $MicrosoftGraphScopes -TenantId $MgTenantID -ClientId $MgClientID } Application { [string]$MgApp_AuthenticationType = $ServiceConfig.MgApp_AuthenticationType if ($LoggingEnabled) {Write-PSFMessage -Message "Microsoft Graph App Authentication Type: $MgApp_AuthenticationType"} switch ($MgApp_AuthenticationType) { CertificateFile { $MgApp_CertificatePath = $ExecutionContext.InvokeCommand.ExpandString($ServiceConfig.MgApp_CertificatePath) # Try accessing private key certificate without password using current process credentials. [X509Certificate]$MgApp_Certificate = $null try { [X509Certificate]$MgApp_Certificate = Get-PfxCertificate -FilePath $MgApp_CertificatePath -NoPromptForPassword } catch # If that doesn't work try the included credentials. { $MgApp_EncryptedCertificatePassword = $ServiceConfig.MgApp_EncryptedCertificatePassword if ([string]::IsNullOrEmpty($MgApp_EncryptedCertificatePassword)) { if ($LoggingEnabled) {Write-PSFMessage -Level Error "Cannot access .pfx private key certificate file and no password has been provided."} throw $_ } else { [SecureString]$MgApp_EncryptedCertificateSecureString = $MgApp_EncryptedCertificatePassword | ConvertTo-SecureString # Can only be decrypted by the same AD account on the same computer. [X509Certificate]$MgApp_Certificate = Get-PfxCertificate -FilePath $MgApp_CertificatePath -NoPromptForPassword -Password $MgApp_EncryptedCertificateSecureString } } $null = Connect-MgGraph -TenantId $MgTenantID -ClientId $MgClientID -Certificate $MgApp_Certificate } CertificateName { $MgApp_CertificateName = $ServiceConfig.MgApp_CertificateName $null = Connect-MgGraph -TenantId $MgTenantID -ClientId $MgClientID -CertificateName $MgApp_CertificateName } CertificateThumbprint { $MgApp_CertificateThumbprint = $ServiceConfig.MgApp_CertificateThumbprint $null = Connect-MgGraph -TenantId $MgTenantID -ClientId $MgClientID -CertificateThumbprint $MgApp_CertificateThumbprint } ClientSecret { $MgApp_EncryptedSecret = $ServiceConfig.MgApp_EncryptedSecret $MgApp_Secret = [System.Net.NetworkCredential]::new("", $($MgApp_EncryptedSecret | ConvertTo-SecureString)).Password # Can only be decrypted by the same AD account on the same computer. $Body = @{ Grant_Type = "client_credentials" Scope = "https://graph.microsoft.com/.default" Client_Id = $MgClientID Client_Secret = $MgApp_Secret } $Connection = Invoke-RestMethod ` -Uri https://login.microsoftonline.com/$MgTenantID/oauth2/v2.0/token ` -Method POST ` -Body $Body $AccessToken = $Connection.access_token $null = Connect-MgGraph -AccessToken $AccessToken } Default {throw "Invalid `'MgApp_AuthenticationType`' value."} } } Default {throw "Invalid `'MgPermissionType`' value."} } } |