├── .github └── workflows │ └── PublishModule.yml ├── MSGraphMail.code-workspace ├── MSGraphMail.psd1 ├── MSGraphMail.psm1 ├── MSGraphMailSummary.Format.ps1xml ├── Private ├── Get-TokenExpiry.ps1 ├── Invoke-EmailObjectParser.ps1 ├── Invoke-EmailStringParser.ps1 ├── Invoke-MSGraphHTTPClientRequest.ps1 ├── Invoke-MSGraphWebRequest.ps1 ├── New-MSGraphError.ps1 ├── New-MSGraphErrorRecord.ps1 ├── New-MSGraphMailAttachment.ps1 ├── New-MSGraphMailBody.ps1 ├── New-MSGraphMailDELETERequest.ps1 ├── New-MSGraphMailGETRequest.ps1 ├── New-MSGraphMailPOSTRequest.ps1 ├── New-MSGraphMailPUTRequest.ps1 └── Write-CustomMessage.ps1 ├── Public ├── Connect-MSGraphMail.ps1 ├── Get-MSGraphMail.ps1 ├── Move-MSGraphMail.ps1 ├── New-MSGraphMail.ps1 ├── Remove-MSGraphMail.ps1 └── Send-MSGraphMail.ps1 └── README.md /.github/workflows/PublishModule.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | name: Build and Publish Module 6 | jobs: 7 | build: 8 | name: Build PowerShell Module 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set required Powershell modules 13 | id: psmodulecache 14 | uses: potatoqualitee/psmodulecache@v1 15 | with: 16 | modules-to-cache: Pester, PSSCriptAnalyzer, InvokeBuild, platyPS 17 | publish-module: 18 | name: Publish PowerShell Module 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: pcgeek86/publish-powershell-module-action@v20 22 | with: 23 | # The NuGet API Key for PowerShell Gallery, with permission to push this module. 24 | NuGetApiKey: ${{ secrets.PS_GALLERY_KEY }} 25 | -------------------------------------------------------------------------------- /MSGraphMail.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /MSGraphMail.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/homotechsual/MSGraphMail/74e5125ca15975577b1bf8fa80bcc16a5aa37be6/MSGraphMail.psd1 -------------------------------------------------------------------------------- /MSGraphMail.psm1: -------------------------------------------------------------------------------- 1 | # MsGraphMail.psm1 2 | $FilesToImport = @(Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue) + @(Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue) 3 | 4 | foreach ($ImportedFile in @($FilesToImport)){ 5 | try 6 | { 7 | . $ImportedFile.FullName 8 | } 9 | catch 10 | { 11 | Write-Error -Message "Failed to import function $($ImportedFile.FullName): $_" 12 | } 13 | } 14 | Export-ModuleMember -Function $FilesToImport.BaseName -Alias * -------------------------------------------------------------------------------- /MSGraphMailSummary.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MSGraphMailSummary 6 | 7 | MSGraphMailSummary 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | subject 16 | 17 | 18 | 19 | id 20 | 21 | 22 | 23 | fromString 24 | 25 | 26 | 27 | toString 28 | 29 | 30 | 31 | ccString 32 | 33 | 34 | 35 | receivedDateTime 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Private/Get-TokenExpiry.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7 2 | function Get-TokenExpiry { 3 | <# 4 | .SYNOPSIS 5 | Calculates and returns the expiry date/time of an auth token. 6 | .DESCRIPTION 7 | Takes the expires in time for an auth token and returns a PowerShell date/time object containing the expiry date/time of the token. 8 | .OUTPUTS 9 | A powershell date/time object representing the token expiry. 10 | #> 11 | [CmdletBinding()] 12 | [OutputType([DateTime])] 13 | param ( 14 | # Timestamp value for token expiry. e.g 3600 15 | [Parameter( 16 | Mandatory = $True 17 | )] 18 | [int64]$ExpiresIn 19 | ) 20 | $Now = Get-Date 21 | $TimeZone = Get-TimeZone 22 | $UTCTime = $Now.AddMilliseconds($ExpiresIn) 23 | $UTCOffset = $TimeZone.GetUtcOffset($(Get-Date)).TotalMinutes 24 | $ExpiryDateTime = $UTCTime.AddMinutes($UTCOffset) 25 | Write-Verbose "Calcuated token expiry as $($ExpiryDateTime.ToString())" 26 | Return $ExpiryDateTime 27 | } -------------------------------------------------------------------------------- /Private/Invoke-EmailObjectParser.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-EmailObjectParser { 2 | [CmdletBinding()] 3 | Param ( 4 | [Parameter(Mandatory = $True)] 5 | [Object[]]$Objects 6 | ) 7 | # Loop over each email object and add it to a string. 8 | Write-Debug "Email object parser received $($Objects | ConvertTo-Json)" 9 | $EmailAddresses = foreach ($EmailObject in $Objects) { 10 | $Name = $EmailObject.emailAddress.Name 11 | $Address = $EmailObject.emailAddress.Address 12 | Write-Debug "Got name $($Name) and email $($Address) from object $($EmailObject.emailAddress | Out-String)" 13 | # Turn the email into an output string. 14 | $EmailAddress = "$($Name) <$($Address)>" 15 | $EmailAddress 16 | } 17 | return $EmailAddresses -Join ';' 18 | } -------------------------------------------------------------------------------- /Private/Invoke-EmailStringParser.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-EmailStringParser { 2 | [CmdletBinding()] 3 | Param ( 4 | [Parameter(Mandatory = $True)] 5 | [String[]]$Strings 6 | ) 7 | # Split input string on ";" character. 8 | if ($Strings.Length -ge 2) { 9 | $EmailStrings = $Strings 10 | } else { 11 | $EmailStrings = $Strings.Split(';') 12 | } 13 | # Loop over each email string and add it to a hashtable in the expected format for an IMicrosoftGraphRecipient[] object. 14 | $EmailAddresses = foreach ($EmailString in $EmailStrings) { 15 | $ParsedEmailString = [regex]::Matches($EmailString, '\s?"?((?.*?)"?\s*<)?(?.*?[^>]*)') 16 | $Name = $ParsedEmailString[0].Groups['name'].value 17 | $Address = $ParsedEmailString[0].Groups['email'].value 18 | Write-Debug "Got name $($Name) and email $($Address) from string $($EmailString)" 19 | # Add the email address in the expected format for an IMicrosoftGraphEmailAddress object. 20 | $EmailAddress = @{ 21 | 'emailAddress' = @{ 22 | name = $Name 23 | address = $Address 24 | } 25 | } 26 | $EmailAddress 27 | } 28 | Write-Verbose "Got $($EmailAddresses.Length) email addresses" 29 | Write-Verbose ($EmailAddresses | ConvertTo-Json) 30 | return $EmailAddresses 31 | } -------------------------------------------------------------------------------- /Private/Invoke-MSGraphHTTPClientRequest.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Net.Http 2 | using namespace System.Net.Http.Headers 3 | #Requires -Version 7 4 | function Invoke-MSGraphHTTPClientRequest { 5 | <# 6 | .SYNOPSIS 7 | Sends a request to the Microsoft Graph API using HTTPClient. 8 | .DESCRIPTION 9 | Wrapper function to send web requests to the Microsoft Graph API using the .NET HTTP client implementation. 10 | .OUTPUTS 11 | Outputs an object containing the response from the web request. 12 | #> 13 | [Cmdletbinding()] 14 | [OutputType([Object])] 15 | param ( 16 | # The request URI. 17 | [Parameter(Mandatory = $True)] 18 | [uri]$URI, 19 | [Parameter(Mandatory = $True)] 20 | [string]$Method, 21 | [object]$Body, 22 | # The content type for the request. 23 | [string]$ContentType 24 | ) 25 | $ProgressPreference = 'SilentlyContinue' 26 | if ([DateTime]::Now -ge $Script:MSGMAuthenticationInformation.Expires) { 27 | Write-Verbose 'The auth token has expired, renewing.' 28 | $ReconnectParameters = @{ 29 | Reconnect = $True 30 | } 31 | Connect-MSGraphMail @ReconnectParameters 32 | } 33 | if (($null -ne $Script:MSGMAuthenticationInformation) -and ($Method -ne 'PUT')) { 34 | $AuthHeader = [AuthenticationHeaderValue]::New($Script:MSGMAuthenticationInformation.Type, $Script:MSGMAuthenticationInformation.Token) 35 | } 36 | try { 37 | Write-Verbose "Making a $($Method) request to $($URI)" 38 | Write-Debug "Authentication headers: $($AuthHeader.ToString())" 39 | $HTTPClient = [HttpClient]::new() 40 | $HTTPClient.DefaultRequestHeaders.Authorization = $AuthHeader 41 | $HTTPClient.DefaultRequestHeaders.Add('Prefer', 'IdType%3D%22ImmutableId%22') 42 | if ($Method = 'GET') { 43 | $Request = $HTTPClient.GetAsync($URI) 44 | } elseif ($Method = 'PUT') { 45 | if (-Not $Body) { 46 | Throw 'Body is missing on PUT request.' 47 | } 48 | $Request = $HTTPClient.PutAsync($URI, $Body) 49 | } 50 | $Request.Wait() 51 | $Result = $Request.Result 52 | if ($Result.isFaulted) { 53 | Throw $Result.Exception 54 | } 55 | $Response = $Result.Content.ReadAsStringAsync().Result 56 | Write-Debug "Response headers: $($Result.Headers | Out-String)" 57 | Write-Debug "Raw response: $($Result | Out-String)" 58 | return $Response 59 | } catch { 60 | $ErrorRecord = @{ 61 | ExceptionType = 'System.Net.Http.HttpRequestException' 62 | ErrorMessage = "Microsoft Graph API request $($_.TargetObject.Method) $($_.TargetObject.RequestUri) failed." 63 | InnerException = $_.Exception 64 | ErrorID = 'MicrosoftGraphRequestFailed' 65 | ErrorCategory = 'ProtocolError' 66 | TargetObject = $_.TargetObject 67 | ErrorDetails = $_.ErrorDetails 68 | BubbleUpDetails = $True 69 | } 70 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 71 | $PSCmdlet.ThrowTerminatingError($RequestError) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Private/Invoke-MSGraphWebRequest.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7 2 | function Invoke-MSGraphWebRequest { 3 | <# 4 | .SYNOPSIS 5 | Sends a request to the Microsoft Graph API using Invoke-WebRequest. 6 | .DESCRIPTION 7 | Wrapper function to send web requests to the Microsoft Graph API using the Invoke-WebRequest commandlet. 8 | .OUTPUTS 9 | Outputs an object containing the response from the web request. 10 | #> 11 | [Cmdletbinding()] 12 | [OutputType([Object])] 13 | param ( 14 | # The request URI. 15 | [Parameter(Mandatory = $True)] 16 | [uri]$URI, 17 | # The request method. 18 | [Parameter(Mandatory = $True)] 19 | [string]$Method, 20 | # Don't authenticate. 21 | [switch]$Anonymous, 22 | # The content type for the request. 23 | [string]$ContentType, 24 | # The body content of the request. 25 | [object]$Body, 26 | # Additional headers. 27 | [hashtable]$AdditionalHeaders 28 | ) 29 | $ProgressPreference = 'SilentlyContinue' 30 | if ([DateTime]::Now -ge $Script:MSGMAuthenticationInformation.Expires) { 31 | Write-Verbose 'The auth token has expired, renewing.' 32 | $ReconnectParameters = @{ 33 | Reconnect = $True 34 | } 35 | Connect-MSGraphMail @ReconnectParameters 36 | } 37 | if ($null -ne $Script:MSGMAuthenticationInformation -and (-not $Anonymous)) { 38 | $AuthHeader = @{ 39 | Authorization = "$($Script:MSGMAuthenticationInformation.Type) $($Script:MSGMAuthenticationInformation.Token)" 40 | } 41 | } 42 | if ($null -ne $AdditionalHeaders) { 43 | $RequestHeaders = $AuthHeader + $AdditionalHeaders 44 | } else { 45 | $RequestHeaders = $AuthHeader 46 | } 47 | if ($Method -eq 'PUT') { 48 | $SkipHeaderValidation = $True 49 | } else { 50 | $SkipHeaderValidation = $False 51 | } 52 | try { 53 | Write-Verbose "Making a $($Method) request to $($URI)" 54 | Write-Debug "Request headers: $($RequestHeaders | Out-String -Width 5000)" 55 | $WebRequestParams = @{ 56 | URI = $URI 57 | Method = $Method 58 | ContentType = $ContentType 59 | Headers = $RequestHeaders 60 | SkipHeaderValidation = $SkipHeaderValidation 61 | } 62 | Write-Debug "Request parameters: $($WebRequestParams | Out-String -Width 5000)" 63 | if ($Body -and (($Method -eq 'POST') -or ($Method -eq 'PUT'))) { 64 | $WebRequestParams.Body = $Body 65 | } 66 | $Response = Invoke-WebRequest @WebRequestParams 67 | Write-Debug "Response headers: $($Response.Headers | Out-String)" 68 | Write-Debug "Raw response: $($Response | Out-String -Width 5000)" 69 | return $Response 70 | } catch { 71 | New-MSGraphError $_ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Private/New-MSGraphError.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | using namespace System.Management.Automation 3 | function New-MSGraphError { 4 | [CmdletBinding()] 5 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private function - no need to support.')] 6 | param ( 7 | [Parameter(Mandatory = $true)] 8 | [errorrecord]$ErrorRecord, 9 | [Parameter()] 10 | [switch]$HasResponse 11 | 12 | ) 13 | Write-Verbose 'Generating Microsoft Graph error output.' 14 | $ExceptionMessage = [Hashset[String]]::New() 15 | $APIResultMatchString = '*The Microsoft Graph API said*' 16 | $HTTPResponseMatchString = '*The API returned the following HTTP*' 17 | if ($ErrorRecord.ErrorDetails) { 18 | Write-Verbose 'ErrorDetails contained in error record.' 19 | $ErrorDetailsIsJson = Test-Json -Json $ErrorRecord.ErrorDetails -ErrorAction SilentlyContinue 20 | if ($ErrorDetailsIsJson) { 21 | Write-Verbose 'ErrorDetails is JSON.' 22 | $ErrorDetails = $ErrorRecord.ErrorDetails | ConvertFrom-Json 23 | Write-Debug "Raw error details: $($ErrorDetails | Out-String)" 24 | if ($null -ne $ErrorDetails) { 25 | if (($null -ne $ErrorDetails.error.code) -and ($null -ne $ErrorDetails.error.message)) { 26 | Write-Verbose 'ErrorDetails contains code and message.' 27 | $ExceptionMessage.Add("The Microsoft Graph API said $($ErrorDetails.error.code): $($ErrorDetails.error.message).") | Out-Null 28 | } elseif ($null -ne $ErrorDetails.error.code) { 29 | Write-Verbose 'ErrorDetails contains code.' 30 | $ExceptionMessage.Add("The Microsoft Graph API said $($ErrorDetails.code).") | Out-Null 31 | } elseif ($null -ne $ErrorDetails.error) { 32 | Write-Verbose 'ErrorDetails contains error.' 33 | $ExceptionMessage.Add("The Microsoft Graph API said $($ErrorDetails.error).") | Out-Null 34 | } elseif ($null -ne $ErrorDetails) { 35 | Write-Verbose 'ErrorDetails is not null.' 36 | $ExceptionMessage.Add("The Microsoft Graph API said $($ErrorRecord.ErrorDetails).") | Out-Null 37 | } else { 38 | Write-Verbose 'ErrorDetails is null.' 39 | $ExceptionMessage.Add('The Microsoft Graph API returned an error.') | Out-Null 40 | } 41 | } 42 | } elseif ($ErrorRecord.ErrorDetails -like $APIResultMatchString -and $ErrorRecord.ErrorDetails -like $HTTPResponseMatchString) { 43 | $Errors = $ErrorRecord.ErrorDetails -Split "`r`n" 44 | if ($Errors -is [array]) { 45 | ForEach-Object -InputObject $Errors { 46 | $ExceptionMessage.Add($_) | Out-Null 47 | } 48 | } elseif ($Errors -is [string]) { 49 | $ExceptionMessage.Add($_) 50 | } 51 | } 52 | } else { 53 | $ExceptionMessage.Add('The Microsoft Graph API returned an error but did not provide a result code or error message.') | Out-Null 54 | } 55 | if (($ErrorRecord.Exception.Response -and $HasResponse) -or $ExceptionMessage -notlike $HTTPResponseMatchString) { 56 | $Response = $ErrorRecord.Exception.Response 57 | Write-Debug "Raw HTTP response: $($Response | Out-String)" 58 | if ($Response.StatusCode.value__ -and $Response.ReasonPhrase) { 59 | $ExceptionMessage.Add("The API returned the following HTTP error response: $($Response.StatusCode.value__) $($Response.ReasonPhrase)") | Out-Null 60 | } else { 61 | $ExceptionMessage.Add('The API returned an HTTP error response but did not provide a status code or reason phrase.') 62 | } 63 | } else { 64 | $ExceptionMessage.Add('The API did not provide a response code or status.') | Out-Null 65 | } 66 | $Exception = [System.Exception]::New( 67 | $ExceptionMessage, 68 | $ErrorRecord.Exception 69 | ) 70 | $MSGraphError = [ErrorRecord]::New( 71 | $ErrorRecord, 72 | $Exception 73 | ) 74 | $UniqueExceptions = $ExceptionMessage | Get-Unique 75 | $MSGraphError.ErrorDetails = [String]::Join("`r`n", $UniqueExceptions) 76 | $PSCmdlet.ThrowTerminatingError($MSGraphError) 77 | } -------------------------------------------------------------------------------- /Private/New-MSGraphErrorRecord.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Collections.Generic 2 | function New-MSGraphErrorRecord { 3 | [CmdletBinding()] 4 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private function - no need to support.')] 5 | param ( 6 | [Parameter(Mandatory = $true)] 7 | [type]$ExceptionType, 8 | [Parameter(Mandatory = $true)] 9 | [string]$ErrorMessage, 10 | [exception]$InnerException = $null, 11 | [Parameter(Mandatory = $true)] 12 | [string]$ErrorID, 13 | [Parameter(Mandatory = $true)] 14 | [errorcategory]$ErrorCategory, 15 | [object]$TargetObject = $null, 16 | [object]$ErrorDetails = $null, 17 | [switch]$BubbleUpDetails 18 | ) 19 | $ExceptionMessage = [list[string]]::New() 20 | $ExceptionMessage.Add($ErrorMessage) 21 | if ($ErrorDetails.Message) { 22 | $MSGraphError = $_.ErrorDetails.Message | ConvertFrom-Json 23 | if ($MSGraphError.Message) { 24 | $ExceptionMessage.Add("The Microsoft Graph API said $($MSGraphError.ClassName): $($MSGraphError.Message).") 25 | } 26 | } 27 | if ($InnerException.Response) { 28 | $Response = $InnerException.Response 29 | } 30 | if ($InnerException.InnerException.Response) { 31 | $Response = $InnerException.InnerException.Response 32 | } 33 | if ($InnerException.InnerException.InnerException.Response) { 34 | $Response = $InnerException.InnerException.InnerException.Response 35 | } 36 | if ($Response) { 37 | $ExceptionMessage.Add("The Microsoft Graph API provided the status code $($Response.StatusCode.Value__): $($Response.ReasonPhrase).") 38 | } 39 | $Exception = $ExceptionType::New( 40 | $ExceptionMessage, 41 | $InnerException 42 | ) 43 | #if (($Exception -and ($ExceptionType -is 'Microsoft.PowerShell.Commands.HttpResponseException')) -and $Response) { 44 | # $Exception.Response = $Response 45 | #} 46 | $ExceptionMessage.Add('You can use "Get-Error" for detailed error information.') 47 | $MSGraphError = [ErrorRecord]::New( 48 | $Exception, 49 | $ErrorID, 50 | $ErrorCategory, 51 | $TargetObject 52 | ) 53 | if ($BubbleUpDetails) { 54 | $MSGraphError.ErrorDetails = $ErrorDetails 55 | } 56 | Return $MSGraphError 57 | } -------------------------------------------------------------------------------- /Private/New-MSGraphMailAttachment.ps1: -------------------------------------------------------------------------------- 1 | function New-MSGraphMailAttachment { 2 | [CmdletBinding()] 3 | param ( 4 | [Parameter(Mandatory = $True)] 5 | [string]$Mailbox, 6 | [Parameter(Mandatory = $True)] 7 | [string]$MessageID, 8 | [string]$Folder, 9 | [Parameter(Mandatory = $True)] 10 | [string[]]$Attachments, 11 | [switch]$InlineAttachments 12 | ) 13 | Write-Debug "Got attachments $($Attachments -join ', ')" 14 | foreach ($AttachmentItem in $Attachments) { 15 | if ($InlineAttachments) { 16 | $IAParts = $AttachmentItem.Split(';') 17 | $CID = $IAParts[0] 18 | Write-Verbose ("Content ID: $CID") 19 | $Attachment = $IAParts[1] 20 | Write-Verbose ("Attachment: $Attachment") 21 | } else { 22 | $Attachment = $AttachmentItem 23 | } 24 | Test-Path -Path $Attachment -ErrorAction Stop | Out-Null 25 | $AttachmentFile = Get-Item -Path $Attachment -ErrorAction Stop 26 | $Bytes = Get-Content -Path $AttachmentFile.FullName -AsByteStream -Raw 27 | if ($Bytes.Length -le 2999999) { 28 | Write-Debug "Attachment $($AttachmentFile.Fullname) size is $($Bytes.Length) which is less than 3MB - using direct upload" 29 | $UploadSession = $False 30 | } else { 31 | Write-Debug "Attachment $($AttachmentFile.Fullname) size is $($Bytes.Length) which is greater than 3MB - using streaming upload" 32 | $UploadSession = $True 33 | } 34 | $AttachmentItem = @{ 35 | "@odata.type" = "#microsoft.graph.fileAttachment" 36 | attachmentType = 'file' 37 | name = $AttachmentFile.Name 38 | } 39 | if ($CID) { 40 | $AttachmentItem.contentId = $CID 41 | $AttachmentItem.isInline = $True 42 | if ($AttachmentFile.Extension -eq '.png') { 43 | $AttachmentItem.contentType = 'image/png' 44 | } elseif (($AttachmentFile.Extension -eq '.jpg') -or ($AttachmentFile.Extension -eq '.jpeg')) { 45 | $AttachmentItem.contentType = 'image/jpeg' 46 | } elseif ($AttachmentFile.Extension -eq '.gif') { 47 | $AttachmentItem.contentType = 'image/gif' 48 | } 49 | } else { 50 | $AttachmentItem.size = $($Bytes.Length) 51 | } 52 | Write-Debug "Generated attachment item $($AttachmentItem | ConvertTo-JSON)" 53 | $RequestURI = [System.UriBuilder]::New('https', 'graph.microsoft.com') 54 | if ($UploadSession) { 55 | $UploadTry = 0 56 | do { 57 | if ($Folder) { 58 | $RequestURI.Path = "v1.0/users/$($Mailbox)/mailFolders/$($Folder)/messages/$($MessageID)/attachments/createUploadSession" 59 | } else { 60 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages/$($MessageID)/attachments/createUploadSession" 61 | } 62 | $AttachmentItem.Remove('@odata.type') 63 | $SessionAttachmentItem = @{ 64 | AttachmentItem = $AttachmentItem 65 | } 66 | $UploadSessionParams = @{ 67 | URI = $RequestURI.ToString() 68 | Body = $SessionAttachmentItem 69 | ContentType = 'application/json' 70 | Raw = $False 71 | } 72 | try { 73 | $UploadTry++ 74 | $InternalServerError = $False 75 | Write-CustomMessage "Attempting to upload $($AttachmentFile.FullName) attempt number $($UploadTry)" -Type 'Information' 76 | $AttachmentSession = New-MSGraphMailPOSTRequest @UploadSessionParams 77 | Write-Debug "Got upload session details $($AttachmentSession)" 78 | $AttachmentSessionURI = $AttachmentSession.uploadurl 79 | } catch { 80 | $ErrorRecord = @{ 81 | ExceptionType = 'System.Net.Http.HttpRequestException' 82 | ErrorMessage = 'Creating session for attachment upload to the Microsoft Graph API failed.' 83 | InnerException = $_.Exception 84 | ErrorID = 'MSGraphMailFailedToGetAttachmentUploadSession' 85 | ErrorCategory = 'ProtocolError' 86 | TargetObject = $_.TargetObject 87 | ErrorDetails = $_.ErrorDetails 88 | BubbleUpDetails = $True 89 | } 90 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 91 | $PSCmdlet.ThrowTerminatingError($RequestError) 92 | } 93 | if ($AttachmentSession) { 94 | $AdditionalHeaders = @{ 95 | "Content-Range" = "bytes 0-$($Bytes.Length -1)/$($Bytes.Length)" 96 | } 97 | $AttachmentUploadParams =@{ 98 | URI = $AttachmentSessionURI 99 | Body = $Bytes 100 | Anonymous = $True 101 | AdditionalHeaders = $AdditionalHeaders 102 | Raw = $False 103 | } 104 | try { 105 | $AttachmentUpload = New-MSGraphMailPUTRequest @AttachmentUploadParams 106 | if ($AttachmentUpload) { 107 | $InternalServerError = $False 108 | Write-CustomMessage -Message "Attached file '$($AttachmentFile.FullName)' to message $($MessageID)" -Type 'Success' 109 | } 110 | } catch { 111 | if ($_.Exception.InnerException.InnerException.Response.StatusCode.value__ -eq 500) { 112 | Write-Warning "Attempt to upload '$($AttachmentFile.FullName)' failed. Retrying." 113 | $InternalServerError = $True 114 | } else { 115 | $ErrorRecord = @{ 116 | ExceptionType = 'System.Net.Http.HttpRequestException' 117 | ErrorMessage = "Sending attachment '$($AttachmentFile.Name)' to the Microsoft Graph API failed." 118 | InnerException = $_.Exception 119 | ErrorID = 'MSGraphMailAttachmentUploadFailed' 120 | ErrorCategory = 'ProtocolError' 121 | TargetObject = $_.TargetObject 122 | ErrorDetails = $_.ErrorDetails 123 | BubbleUpDetails = $True 124 | } 125 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 126 | $PSCmdlet.ThrowTerminatingError($RequestError) 127 | } 128 | } 129 | } 130 | } while (($InternalServerError) -and ($UploadTry -le 5)) 131 | } else { 132 | if ($Folder) { 133 | $RequestURI.Path = "v1.0/users/$($Mailbox)/mailFolders/$($Folder)/messages/$($MessageID)/attachments" 134 | } else { 135 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages/$($MessageID)/attachments" 136 | } 137 | $AttachmentItem.contentBytes = [convert]::ToBase64String($Bytes) 138 | $SimpleAttachmentParams = @{ 139 | URI = $RequestURI.ToString() 140 | Body = $($AttachmentItem) 141 | ContentType = 'application/json' 142 | Raw = $False 143 | } 144 | try { 145 | $AttachmentUpload = New-MSGraphMailPOSTRequest @SimpleAttachmentParams 146 | if ($AttachmentUpload) { 147 | Write-CustomMessage -Message "Attached file '$($AttachmentFile.Name)' to message $($MessageID)" -Type 'Success' 148 | } 149 | } catch { 150 | $ErrorRecord = @{ 151 | ExceptionType = 'System.Net.Http.HttpRequestException' 152 | ErrorMessage = "Sending attachment '$($AttachmentFile.Name)' to the Microsoft Graph API failed." 153 | InnerException = $_.Exception 154 | ErrorID = 'MSGraphMailAttachmentUploadFailed' 155 | ErrorCategory = 'ProtocolError' 156 | TargetObject = $_.TargetObject 157 | ErrorDetails = $_.ErrorDetails 158 | BubbleUpDetails = $True 159 | } 160 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 161 | $PSCmdlet.ThrowTerminatingError($RequestError) 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /Private/New-MSGraphMailBody.ps1: -------------------------------------------------------------------------------- 1 | function New-MSGraphMailBody { 2 | [CmdletBinding()] 3 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private function - no need to support.')] 4 | Param ( 5 | [Parameter(Mandatory = $True)] 6 | [ValidateSet('HTML', 'text')] 7 | [string]$BodyFormat, 8 | [Parameter(Mandatory = $True)] 9 | [string]$BodyContent, 10 | [string]$FooterContent 11 | ) 12 | if (Test-Path $BodyContent) { 13 | $MailContent = (Get-Content $BodyContent -Raw) 14 | Write-Verbose "Using file $BodyContent as body content." 15 | Write-Debug "Body content: `r`n$MailContent" 16 | } else { 17 | $MailContent = $BodyContent 18 | Write-Verbose "Using string as body content." 19 | Write-Debug "Body content: `r`n$MailContent" 20 | } 21 | if (Test-Path $FooterContent) { 22 | $MailFooter = (Get-Content $FooterContent -Raw) 23 | Write-Verbose "Using file $FooterContent as footer content." 24 | Write-Debug "Footer content: `r`n$MailFooter" 25 | } else { 26 | $MailFooter = $FooterContent 27 | Write-Verbose "Using string as footer content." 28 | Write-Debug "Footer content: `r`n$MailFooter" 29 | } 30 | $MailBody = @{ 31 | content = "$($MailContent)$([System.Environment]::NewLine)$($MailFooter)" 32 | contentType = $BodyFormat 33 | } 34 | Return $MailBody 35 | } 36 | -------------------------------------------------------------------------------- /Private/New-MSGraphMailDELETERequest.ps1: -------------------------------------------------------------------------------- 1 | function New-MSGraphMailDELETERequest { 2 | <# 3 | .SYNOPSIS 4 | Builds a DELETE request for the Microsoft Graph API. 5 | .DESCRIPTION 6 | Wrapper function to build web requests for the Microsoft Graph API. 7 | .OUTPUTS 8 | Outputs an object containing the response from the web request. 9 | #> 10 | [CmdletBinding()] 11 | [OutputType([Object])] 12 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private function - no need to support.')] 13 | param ( 14 | # The request URI. 15 | [uri]$URI, 16 | # The content type for the request. 17 | [string]$ContentType 18 | ) 19 | if ($null -eq $Script:MSGMConnectionInformation) { 20 | Throw "Missing Microsoft Graph connection information, please run 'Connect-MSGraphMail' first." 21 | } 22 | if ($null -eq $Script:MSGMAuthenticationInformation) { 23 | Throw "Missing Microsoft Graph authentication tokens, please run 'Connect-MSGraphMail' first." 24 | } 25 | try { 26 | $WebRequestParams = @{ 27 | Method = 'DELETE' 28 | URI = $URI 29 | ContentType = $ContentType 30 | } 31 | Write-Debug "Building new Microsoft Graph DELETE request with params: $($WebRequestParams | Out-String)" 32 | $Result = Invoke-MSGraphWebRequest @WebRequestParams 33 | if ($Result) { 34 | Write-Debug "Microsoft Graph request returned $($Result | Out-String)" 35 | Return $Result 36 | } else { 37 | Throw 'Failed to process DELETE request.' 38 | } 39 | } catch { 40 | $ErrorRecord = @{ 41 | ExceptionType = 'System.Net.Http.HttpRequestException' 42 | ErrorMessage = 'DELETE request sent to the Microsoft Graph API failed.' 43 | InnerException = $_.Exception 44 | ErrorID = 'MSGraphMailDeleteRequestFailed' 45 | ErrorCategory = 'ProtocolError' 46 | TargetObject = $_.TargetObject 47 | ErrorDetails = $_.ErrorDetails 48 | BubbleUpDetails = $True 49 | } 50 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 51 | $PSCmdlet.ThrowTerminatingError($RequestError) 52 | } 53 | } -------------------------------------------------------------------------------- /Private/New-MSGraphMailGETRequest.ps1: -------------------------------------------------------------------------------- 1 | function New-MSGraphMailGETRequest { 2 | <# 3 | .SYNOPSIS 4 | Builds a GET request for the Microsoft Graph API. 5 | .DESCRIPTION 6 | Wrapper function to build web requests for the Microsoft Graph API. 7 | .OUTPUTS 8 | Outputs an object containing the response from the web request. 9 | #> 10 | [CmdletBinding()] 11 | [OutputType([Object])] 12 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private function - no need to support.')] 13 | param ( 14 | # The request URI. 15 | [uri]$URI, 16 | # The content type for the request. 17 | [string]$ContentType, 18 | # Use HTTPClient instead of Invoke-WebRequest. 19 | [switch]$UseHTTPClient 20 | ) 21 | if ($null -eq $Script:MSGMConnectionInformation) { 22 | Throw "Missing Microsoft Graph connection information, please run 'Connect-MSGraphMail' first." 23 | } 24 | if ($null -eq $Script:MSGMAuthenticationInformation) { 25 | Throw "Missing Microsoft Graph authentication tokens, please run 'Connect-MSGraphMail' first." 26 | } 27 | try { 28 | if ($UseHTTPClient) { 29 | $WebRequestParams = @{ 30 | Method = 'GET' 31 | URI = $URI 32 | ContentType = $ContentType 33 | } 34 | Write-Debug "Building new Microsoft Graph GET request with params: $($WebRequestParams | Out-String)" 35 | $Result = Invoke-MSGraphHTTPClientRequest @WebRequestParams 36 | } else { 37 | $WebRequestParams = @{ 38 | Method = 'GET' 39 | URI = $URI 40 | ContentType = $ContentType 41 | } 42 | Write-Debug "Building new Microsoft Graph GET request with params: $($WebRequestParams | Out-String)" 43 | $Result = Invoke-MSGraphWebRequest @WebRequestParams 44 | } 45 | if ($Result) { 46 | Write-Debug "Microsoft Graph request returned $($Result | Out-String)" 47 | Return $Result 48 | } else { 49 | Throw 'Failed to process GET request.' 50 | } 51 | } catch { 52 | $ErrorRecord = @{ 53 | ExceptionType = 'System.Net.Http.HttpRequestException' 54 | ErrorMessage = 'GET request sent to the Microsoft Graph API failed.' 55 | InnerException = $_.Exception 56 | ErrorID = 'MSGraphMailGetRequestFailed' 57 | ErrorCategory = 'ProtocolError' 58 | TargetObject = $_.TargetObject 59 | ErrorDetails = $_.ErrorDetails 60 | BubbleUpDetails = $True 61 | } 62 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 63 | $PSCmdlet.ThrowTerminatingError($RequestError) 64 | } 65 | } -------------------------------------------------------------------------------- /Private/New-MSGraphMailPOSTRequest.ps1: -------------------------------------------------------------------------------- 1 | function New-MSGraphMailPOSTRequest { 2 | <# 3 | .SYNOPSIS 4 | Builds a POST request for the Microsoft Graph API. 5 | .DESCRIPTION 6 | Wrapper function to build web requests for the Microsoft Graph API. 7 | .OUTPUTS 8 | Outputs an object containing the response from the web request. 9 | #> 10 | [CmdletBinding()] 11 | [OutputType([Object])] 12 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private function - no need to support.')] 13 | param ( 14 | # The request URI. 15 | [uri]$URI, 16 | # The content type for the request. 17 | [string]$ContentType, 18 | # The request body. 19 | [object]$Body, 20 | # Don't authenticate. 21 | [switch]$Anonymous, 22 | # Additional headers. 23 | [hashtable]$AdditionalHeaders = $null, 24 | # Return raw result? 25 | [switch]$Raw 26 | ) 27 | if ($null -eq $Script:MSGMConnectionInformation) { 28 | Throw "Missing Microsoft Graph connection information, please run 'Connect-MSGraphMail' first." 29 | } 30 | if ($null -eq $Script:MSGMAuthenticationInformation) { 31 | Throw "Missing Microsoft Graph authentication tokens, please run 'Connect-MSGraphMail' first." 32 | } 33 | try { 34 | $WebRequestParams = @{ 35 | Method = 'POST' 36 | Uri = $URI 37 | ContentType = $ContentType 38 | Anonymous = $Anonymous 39 | AdditionalHeaders = $AdditionalHeaders 40 | } 41 | if ($ContentType -like 'application/json*' -and $Body) { 42 | $WebRequestParams.Body = ConvertTo-Json -InputObject $Body -Depth 5 43 | } 44 | if ($ContentType -eq 'text/plain' -and $Body) { 45 | $WebRequestParams.Body = $Body 46 | } 47 | Write-Debug "Building new Microsoft Graph POST request with body: $($WebRequestParams | Out-String -Width 5000)" 48 | Write-Verbose "Using Content-Type: $($WebRequestParams.ContentType)" 49 | $Result = Invoke-MSGraphWebRequest @WebRequestParams 50 | if ($Result) { 51 | if ($Raw) { 52 | Return $Result 53 | } else { 54 | Return $Result.content | ConvertFrom-Json -Depth 5 55 | } 56 | } else { 57 | Throw 'No response to POST request' 58 | } 59 | } catch { 60 | New-MSGraphError $_ 61 | } 62 | } -------------------------------------------------------------------------------- /Private/New-MSGraphMailPUTRequest.ps1: -------------------------------------------------------------------------------- 1 | function New-MSGraphMailPUTRequest { 2 | <# 3 | .SYNOPSIS 4 | Builds a PUT request for the Microsoft Graph API. 5 | .DESCRIPTION 6 | Wrapper function to build web requests for the Microsoft Graph API. 7 | .OUTPUTS 8 | Outputs an object containing the response from the web request. 9 | #> 10 | [CmdletBinding()] 11 | [OutputType([Object])] 12 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Private function - no need to support.')] 13 | param ( 14 | # The request URI. 15 | [uri]$URI, 16 | # The content type for the request. 17 | [string]$ContentType, 18 | # The request body. 19 | [object]$Body, 20 | # Don't authenticate. 21 | [switch]$Anonymous, 22 | # Additional headers. 23 | [hashtable]$AdditionalHeaders = $null, 24 | # Return raw result? 25 | [switch]$Raw 26 | ) 27 | if ($null -eq $Script:MSGMConnectionInformation) { 28 | Throw "Missing Microsoft Graph connection information, please run 'Connect-MSGraphMail' first." 29 | } 30 | if ($null -eq $Script:MSGMAuthenticationInformation) { 31 | Throw "Missing Microsoft Graph authentication tokens, please run 'Connect-MSGraphMail' first." 32 | } 33 | try { 34 | $WebRequestParams = @{ 35 | Method = 'PUT' 36 | Uri = $URI 37 | ContentType = $ContentType 38 | Anonymous = $Anonymous 39 | Body = ($Body) 40 | AdditionalHeaders = $AdditionalHeaders 41 | } 42 | #Write-Debug "Building new Microsoft Graph PUT request with body: $($WebRequestParams.Body | ConvertTo-Json | Out-String)" 43 | $Result = Invoke-MSGraphWebRequest @WebRequestParams 44 | if ($Result) { 45 | Write-Debug "Microsoft Graph request returned $($Result | Out-String)" 46 | Return $Result 47 | } else { 48 | Throw 'Failed to process PUT request.' 49 | } 50 | } catch { 51 | $ErrorRecord = @{ 52 | ExceptionType = 'System.Net.Http.HttpRequestException' 53 | ErrorMessage = 'PUT request sent to the Microsoft Graph API failed.' 54 | InnerException = $_.Exception 55 | ErrorID = 'MSGraphMailPutRequestFailed' 56 | ErrorCategory = 'ProtocolError' 57 | TargetObject = $_.TargetObject 58 | ErrorDetails = $_.ErrorDetails 59 | BubbleUpDetails = $True 60 | } 61 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 62 | $PSCmdlet.ThrowTerminatingError($RequestError) 63 | } 64 | } -------------------------------------------------------------------------------- /Private/Write-CustomMessage.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Management.Automation 2 | #Requires -Version 7 3 | function Write-CustomMessage { 4 | [CmdletBinding()] 5 | param ( 6 | [Parameter(Mandatory = $True)] 7 | [string]$Message, 8 | [Parameter(Mandatory = $True)] 9 | [string]$Type 10 | ) 11 | Switch ($Type) { 12 | 'Success' { 13 | $ForegroundColour = 'Green' 14 | $Prefix = 'SUCCESS: ' 15 | } 16 | 'Information' { 17 | $ForegroundColour = 'Blue' 18 | $Prefix = 'INFO: ' 19 | } 20 | } 21 | $MessageData = [HostInformationMessage]@{ 22 | Message = "$($Prefix)$($Message)" 23 | ForegroundColor = $ForegroundColour 24 | } 25 | Write-Information -MessageData $MessageData -InformationAction Continue 26 | } -------------------------------------------------------------------------------- /Public/Connect-MSGraphMail.ps1: -------------------------------------------------------------------------------- 1 | function Connect-MSGraphMail { 2 | [CmdletBinding()] 3 | Param( 4 | # Azure AD application id. 5 | [Parameter(Mandatory = $True, ParameterSetName = 'Connect')] 6 | [string]$ApplicationID, 7 | # Azure AD application secret. 8 | [Parameter(Mandatory = $True, ParameterSetName = 'Connect')] 9 | [string]$ApplicationSecret, 10 | # Graph permission scope. 11 | [Parameter(ParameterSetName = 'Connect')] 12 | [uri]$Scope = [uri]'https://graph.microsoft.com/.default', 13 | # Tenant ID. 14 | [Parameter(ParameterSetName = 'Connect')] 15 | [string]$TenantID, 16 | # Reconnect mode 17 | [Parameter(Mandatory = $True, ParameterSetName = 'Reconnect')] 18 | [switch]$Reconnect 19 | ) 20 | if ((-not $Script:MSGMAuthenticationInformation.Token) -or ([DateTime]::Now -ge $Script:MSGMAuthenticationInformation.Expires)) { 21 | if (([DateTime]::Now -ge $Script:MSGMAuthenticationInformation.Expiry)) { 22 | try { 23 | if ((-not $Script:MSGMConnectionInformation) -and (-not $Reconnect)) { 24 | $ConnectionInformation = @{ 25 | ClientID = $ApplicationID 26 | ClientSecret = $ApplicationSecret 27 | Scope = $Scope 28 | URI = "https://login.microsoftonline.com/$($TenantID)/oauth2/v2.0/token" 29 | TenantID = $TenantID 30 | } 31 | New-Variable -Name 'MSGMConnectionInformation' -Value $ConnectionInformation -Scope 'Script' 32 | } 33 | $AuthenticationBody = @{ 34 | client_id = $Script:MSGMConnectionInformation.ClientID 35 | client_secret = $Script:MSGMConnectionInformation.ClientSecret 36 | scope = $Script:MSGMConnectionInformation.Scope 37 | grant_type = 'client_credentials' 38 | } 39 | $AuthenticationParameters = @{ 40 | URI = $Script:MSGMConnectionInformation.URI 41 | Method = 'POST' 42 | ContentType = 'application/x-www-form-urlencoded' 43 | Body = $AuthenticationBody 44 | } 45 | $TokenResponse = Invoke-WebRequest @AuthenticationParameters 46 | $TokenPayload = ($TokenResponse.Content | ConvertFrom-Json) 47 | $AuthenticationInformation = @{ 48 | Token = $TokenPayload.access_token 49 | Expires = Get-TokenExpiry -ExpiresIn $TokenPayload.expires_in 50 | Type = $TokenPayload.token_type 51 | } 52 | if (-Not $Script:MSGMAuthenticationInformation) { 53 | New-Variable -Name 'MSGMAuthenticationInformation' -Value $AuthenticationInformation -Scope 'Script' 54 | } else { 55 | Set-Variable -Name 'MSGMAuthenticationInformation' -Value $AuthenticationInformation -Scope 'Script' 56 | } 57 | Write-CustomMessage -Message 'Connected to the Microsoft Graph API' -Type 'Success' 58 | } catch { 59 | $ErrorRecord = @{ 60 | ExceptionType = 'System.Net.Http.HttpRequestException' 61 | ErrorMessage = "Graph API request $($_.TargetObject.Method) $($_.TargetObject.RequestUri) failed." 62 | InnerException = $_.Exception 63 | ErrorID = 'GraphAuthenticationFailed' 64 | ErrorCategory = 'ProtocolError' 65 | TargetObject = $_.TargetObject 66 | ErrorDetails = $_.ErrorDetails 67 | BubbleUpDetails = $True 68 | } 69 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 70 | $PSCmdlet.ThrowTerminatingError($RequestError) 71 | } 72 | } 73 | } else { 74 | Write-CustomMessage -Message "Already connected to Microsoft Graph API." -Type 'Information' 75 | } 76 | } -------------------------------------------------------------------------------- /Public/Get-MSGraphMail.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.Management.Automation 2 | function Get-MSGraphMail { 3 | [CmdletBinding()] 4 | param ( 5 | # Specify the mailbox (or UPN) to retrieve emails for. 6 | [Parameter(Mandatory = $true, ParameterSetName = 'Multi')] 7 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 8 | [string]$Mailbox, 9 | # Retrieve a single message using a message ID. 10 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 11 | [Alias('id')] 12 | [string[]]$MessageID, 13 | # Retrieve from folder. 14 | [Parameter(ParameterSetName = 'Multi')] 15 | [Parameter(ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 16 | [Alias('parentFolderId')] 17 | [string]$Folder, 18 | # Retrieve headers only. 19 | [Parameter(ParameterSetName = 'Multi')] 20 | [switch]$HeadersOnly, 21 | # Retrieve the message in MIME format. 22 | [Parameter(ParameterSetName = 'Single')] 23 | [switch]$MIME, 24 | # Search for emails based on a string. 25 | [Parameter(ParameterSetName = 'Multi')] 26 | [string]$Search, 27 | # Selects the specified properties. 28 | [Parameter(ParameterSetName = 'Multi')] 29 | [Parameter(ParameterSetName = 'Single')] 30 | [string[]]$Select, 31 | # Return this number of results. 32 | [Parameter(ParameterSetName = 'Multi')] 33 | [int]$PageSize = 500, 34 | # Transform the output into an object suitable for piping to other commands. 35 | [Parameter(ParameterSetName = 'Multi')] 36 | [Parameter(ParameterSetName = 'Single')] 37 | [switch]$Pipeline, 38 | # Transform the output into a summary format. 39 | [Parameter(ParameterSetName = 'Multi')] 40 | [switch]$Summary 41 | ) 42 | try { 43 | $QueryStringCollection = [system.web.httputility]::ParseQueryString([string]::Empty) 44 | if ($HeadersOnly) { 45 | $QueryStringCollection.Add('$select', 'internetMessageHeaders') 46 | } 47 | if ($Search) { 48 | $QueryStringCollection.Add('$search', $Search) 49 | } 50 | if (($PageSize) -and ($PSCmdlet.ParameterSetName -ne 'Single')) { 51 | $QueryStringCollection.Add('$top', $PageSize) 52 | } 53 | if ($Select) { 54 | if ($Select.Length -gt 1) { 55 | $Select = $Select -join ',' 56 | } 57 | $QueryStringCollection.Add('$select', $Select) 58 | } 59 | $RequestURI = [System.UriBuilder]::New('https', 'graph.microsoft.com') 60 | if ($MessageID) { 61 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages/$($MessageID)" 62 | $ContentType = 'application/json' 63 | if ($MIME) { 64 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages/$($MessageID)/`$value" 65 | $ContentType = 'text/plain' 66 | } 67 | } elseif ($Folder) { 68 | $RequestURI.Path = "v1.0/users/$($Mailbox)/mailfolders/$($Folder)/messages" 69 | } else { 70 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages" 71 | $ContentType = 'application/json' 72 | } 73 | if ($QueryStringCollection.Count -gt 0) { 74 | $RequestURI.Query = $QueryStringCollection.toString() 75 | } 76 | $GETRequestParameters = @{ 77 | URI = $RequestURI.ToString() 78 | ContentType = $ContentType 79 | UseHTTPClient = $True 80 | } 81 | $Content = New-MSGraphMailGETRequest @GETRequestParameters 82 | if ($Content) { 83 | if (-not $MIME) { 84 | $Content = $Content | ConvertFrom-Json 85 | } else { 86 | $Result = $Content 87 | Return $Result 88 | } 89 | } 90 | if ($Content.value) { 91 | if ($Pipeline) { 92 | $Result = [PSCustomObject]@{ 93 | id = $($Content).value.id 94 | mailbox = $($Content).value.toRecipients.emailAddress.address 95 | folder = $($Content).value.parentFolderId 96 | } 97 | Return $Result 98 | } elseif ($Summary) { 99 | $Content.value | ForEach-Object { 100 | $_.PSTypeNames.Insert(0, 'MSGraphMailSummary') 101 | if ($_.from) { 102 | $fromValue = Invoke-EmailObjectParser $_.from 103 | $_.PSObject.Properties.Add( 104 | [PSNoteProperty]::New('fromString', $fromValue) 105 | ) 106 | } 107 | if ($_.toRecipients) { 108 | $toValue = Invoke-EmailObjectParser $_.toRecipients 109 | $_.PSObject.Properties.Add( 110 | [PSNoteProperty]::New('toString', $toValue) 111 | ) 112 | } 113 | if ($_.ccRecipients) { 114 | $ccValue = Invoke-EmailObjectParser $_.ccRecipients 115 | $_.PSObject.Properties.Add( 116 | [PSNoteProperty]::New('ccString', $ccValue) 117 | ) 118 | } 119 | } 120 | Return $Content.value 121 | } elseif ($Content.value) { 122 | Return $Content.value 123 | } 124 | } elseif ($Content) { 125 | if ($Pipeline) { 126 | $Result = [PSCustomObject]@{ 127 | id = $($Content).id 128 | mailbox = $($Content).toRecipients.emailAddress.address 129 | } 130 | Return $Result 131 | } elseif ($Summary) { 132 | $Content | ForEach-Object { 133 | $_.PSTypeNames.Insert(0, 'MSGraphMailSummary') 134 | if ($_.from) { 135 | $fromValue = Invoke-EmailObjectParser $_.from 136 | $_.PSObject.Properties.Add( 137 | [PSNoteProperty]::New('fromString', $fromValue) 138 | ) 139 | } 140 | if ($_.toRecipients) { 141 | $toValue = Invoke-EmailObjectParser $_.toRecipients 142 | $_.PSObject.Properties.Add( 143 | [PSNoteProperty]::New('toString', $toValue) 144 | ) 145 | } 146 | if ($_.ccRecipients) { 147 | $ccValue = Invoke-EmailObjectParser $_.ccRecipients 148 | $_.PSObject.Properties.Add( 149 | [PSNoteProperty]::New('ccString', $ccValue) 150 | ) 151 | } 152 | } 153 | Return $Content 154 | } elseif ($Content) { 155 | Return $Content 156 | } 157 | } 158 | } catch { 159 | $ErrorRecord = @{ 160 | ExceptionType = 'System.Exception' 161 | ErrorMessage = "Microsoft Graph API request $($_.TargetObject.Method) $($_.TargetObject.RequestUri) failed." 162 | InnerException = $_.Exception 163 | ErrorID = 'MicrosoftGraphRequestFailed' 164 | ErrorCategory = 'ProtocolError' 165 | TargetObject = $_.TargetObject 166 | ErrorDetails = $_.ErrorDetails 167 | BubbleUpDetails = $True 168 | } 169 | $RequestError = New-MSGraphErrorRecord @ErrorRecord 170 | $PSCmdlet.ThrowTerminatingError($RequestError) 171 | } 172 | } -------------------------------------------------------------------------------- /Public/Move-MSGraphMail.ps1: -------------------------------------------------------------------------------- 1 | function Move-MSGraphMail { 2 | [CmdletBinding()] 3 | param ( 4 | # Specify the mailbox (or UPN) to move emails for. 5 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 6 | [string]$Mailbox, 7 | # Retrieve a single message using a message ID. 8 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 9 | [Alias('id')] 10 | [string[]]$MessageID, 11 | # Retrieve from folder. 12 | [Parameter(ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 13 | [Alias('parentFolderId')] 14 | [string]$Folder, 15 | # Destination. 16 | [string]$Destination = 'deleteditems' 17 | ) 18 | try { 19 | $CommandName = $MyInvocation.InvocationName 20 | $MoveParams = @{ 21 | destinationId = $Destination 22 | } 23 | $RequestURI = [System.UriBuilder]::New('https', 'graph.microsoft.com') 24 | if ($Folder) { 25 | $RequestURI.Path = "v1.0/users/$($Mailbox)/mailfolders/$($Folder)/messages/$($MessageID)/move" 26 | } else { 27 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages/$($MessageID)/move" 28 | } 29 | $POSTRequestParams = @{ 30 | URI = $RequestURI.ToString() 31 | ContentType = 'application/json' 32 | Body = $MoveParams 33 | } 34 | $Message = New-MSGraphMailPOSTRequest @POSTRequestParams 35 | Write-Debug "Microsoft Graph returned $($Message)" 36 | if ($Message) { 37 | Write-CustomMessage -Message "Moved message '$($Message.subject)' with ID $($Message.id) to folder $($Message.parentFolderId)" -Type 'Success' 38 | } 39 | } catch { 40 | $Command = $CommandName -Replace '-', '' 41 | $ErrorRecord = @{ 42 | ExceptionType = 'System.Exception' 43 | ErrorMessage = "$($CommandName) failed." 44 | InnerException = $_.Exception 45 | ErrorID = "MicrosoftGraph$($Command)CommandFailed" 46 | ErrorCategory = 'ReadError' 47 | TargetObject = $_.TargetObject 48 | ErrorDetails = $_.ErrorDetails 49 | BubbleUpDetails = $True 50 | } 51 | $CommandError = New-MSGraphErrorRecord @ErrorRecord 52 | $PSCmdlet.ThrowTerminatingError($CommandError) 53 | } 54 | } -------------------------------------------------------------------------------- /Public/New-MSGraphMail.ps1: -------------------------------------------------------------------------------- 1 | function New-MSGraphMail { 2 | [CmdletBinding()] 3 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not change system state.')] 4 | param ( 5 | [Parameter(Mandatory = $true, ParameterSetName = 'MIME')] 6 | [String]$Mailbox, 7 | [Parameter(Mandatory = $true, ParameterSetName = 'Standard')] 8 | [String[]]$From, 9 | [Parameter(Mandatory = $true, ParameterSetName = 'Standard')] 10 | [String[]]$To, 11 | [Parameter(ParameterSetName = 'Standard')] 12 | [String[]]$CC, 13 | [Parameter(ParameterSetName = 'Standard')] 14 | [String[]]$BCC, 15 | [Parameter(ParameterSetName = 'Standard')] 16 | [String]$Subject, 17 | [Parameter(Mandatory = $true, ParameterSetName = 'Standard')] 18 | [String]$BodyContent, 19 | [Parameter(ParameterSetName = 'Standard')] 20 | [String]$FooterContent, 21 | [Parameter(Mandatory = $true, ParameterSetName = 'Standard')] 22 | [ValidateSet('HTML', 'text')] 23 | [string]$BodyFormat, 24 | [Parameter(Mandatory = $true, ParameterSetName = 'MIME')] 25 | [String]$MIMEMessage, 26 | [Parameter(ParameterSetName = 'Standard')] 27 | [Parameter(ParameterSetName = 'MIME')] 28 | [String]$Folder, 29 | [Parameter(ParameterSetName = 'Standard')] 30 | [String[]]$Attachments, 31 | [Parameter(ParameterSetName = 'Standard')] 32 | [String[]]$InlineAttachments, 33 | [Parameter(ParameterSetName = 'Standard')] 34 | [Switch]$Draft, 35 | [Parameter(ParameterSetName = 'Standard')] 36 | [Switch]$RequestDeliveryReceipt, 37 | [Parameter(ParameterSetName = 'Standard')] 38 | [Switch]$RequestReadReceipt, 39 | [Parameter(ParameterSetName = 'Standard')] 40 | [Parameter(ParameterSetName = 'MIME')] 41 | [Switch]$Pipeline, 42 | [Parameter(ParameterSetName = 'Standard')] 43 | [Parameter(ParameterSetName = 'MIME')] 44 | [Switch]$Send, 45 | [Parameter(ParameterSetName = 'Standard')] 46 | [Parameter(ParameterSetName = 'MIME')] 47 | [Switch]$SaveandSend 48 | ) 49 | try { 50 | Write-Verbose "Using parameter set $($PSCmdlet.ParameterSetName)." 51 | if ($PSCmdlet.ParameterSetName -eq 'Standard') { 52 | $MailFrom = Invoke-EmailStringParser -Strings $From 53 | $MailTo = Invoke-EmailStringParser -Strings @($To) 54 | if ($CC) { 55 | $MailCC = Invoke-EmailStringParser -Strings @($CC) 56 | } else { 57 | $MailCC = @() 58 | } 59 | if ($BCC) { 60 | $MailBCC = Invoke-EmailStringParser -Strings @($BCC) 61 | } else { 62 | $MailBCC = @() 63 | } 64 | if ($Draft) { 65 | $MailParams.isDraft = $true 66 | } 67 | if ($RequestDeliveryReceipt) { 68 | $MailParams.isDeliveryReceiptRequested = $true 69 | } 70 | if ($RequestReadReceipt) { 71 | $MailParams.isReadReceiptRequested = $true 72 | } 73 | $MailBody = New-MSGraphMailBody -BodyFormat $BodyFormat -BodyContent $BodyContent -FooterContent $FooterContent 74 | $MailParams = @{ 75 | toRecipients = @($MailTo) 76 | from = $MailFrom 77 | subject = $Subject 78 | body = $MailBody 79 | ccRecipients = @($MailCC) 80 | bccRecipients = @($MailBCC) 81 | } 82 | $ContentType = 'application/json; charset=utf-8' 83 | } elseif ($PSCmdlet.ParameterSetName -eq 'MIME') { 84 | $MailParams = $MIMEMessage 85 | $ContentType = 'text/plain' 86 | } 87 | $RequestURI = [System.UriBuilder]::New('https', 'graph.microsoft.com') 88 | if ($Folder) { 89 | $MessageBody = $MailParams 90 | if ($PSCmdlet.ParameterSetName -eq 'Standard') { 91 | $RequestURI.Path = "v1.0/users/$($MailFrom.EmailAddress.Address)/mailfolders/$($Folder)/messages" 92 | } elseif ($PSCmdlet.ParameterSetName -eq 'MIME') { 93 | $RequestURI.Path = "v1.0/users/$($Mailbox)/mailfolders/$($Folder)/messages" 94 | } 95 | } elseif ($Send) { 96 | if ($PSCmdlet.ParameterSetName -eq 'Standard') { 97 | $MessageBody = @{ 98 | message = $MailParams 99 | saveToSentItems = $true 100 | } 101 | $RequestURI.Path = "v1.0/users/$($MailFrom.EmailAddress.Address)/sendmail" 102 | } elseif ($PSCmdlet.ParameterSetName -eq 'MIME') { 103 | $MessageBody = $MailParams 104 | $RequestURI.Path = "v1.0/users/$($Mailbox)/sendmail" 105 | } 106 | 107 | } else { 108 | $MessageBody = $MailParams 109 | if ($PSCmdlet.ParameterSetName -eq 'Standard') { 110 | $RequestURI.Path = "v1.0/users/$($MailFrom.EmailAddress.Address)/messages" 111 | } elseif ($PSCmdlet.ParameterSetName -eq 'MIME') { 112 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages" 113 | } 114 | } 115 | $POSTRequestParams = @{ 116 | URI = $RequestURI.ToString() 117 | ContentType = $ContentType 118 | Body = $MessageBody 119 | } 120 | $Message = New-MSGraphMailPOSTRequest @POSTRequestParams 121 | Write-Debug "Microsoft Graph returned $($Message)" 122 | if ($Message) { 123 | Write-CustomMessage -Message "Created message '$($Message.subject)' with ID $($Message.id)" -Type 'Success' 124 | } 125 | if ($Attachments) { 126 | $AttachmentParams = @{ 127 | Mailbox = $MailFrom.EmailAddress.Address 128 | MessageID = $Message.id 129 | Attachments = $Attachments 130 | } 131 | New-MSGraphMailAttachment @AttachmentParams | Out-Null 132 | } 133 | if ($InlineAttachments) { 134 | $InlineAttachmentParams = @{ 135 | Mailbox = $MailFrom.EmailAddress.Address 136 | MessageID = $Message.id 137 | Attachments = $InlineAttachments 138 | InlineAttachments = $True 139 | } 140 | New-MSGraphMailAttachment @InlineAttachmentParams | Out-Null 141 | } 142 | if ($Pipeline -and $Message) { 143 | $Result = [PSCustomObject]@{ 144 | id = $($Message).id 145 | mailbox = $MailFrom.EmailAddress.Address 146 | folder = $($Message).parentFolderId 147 | } 148 | Return $Result 149 | } elseif ($SaveandSend) { 150 | $SendParams = @{ 151 | MessageID = $($Message).id 152 | Mailbox = $MailFrom.EmailAddress.Address 153 | Folder = $($Message).parentFolderId 154 | } 155 | Send-MSGraphMail @SendParams 156 | } elseif ($Message) { 157 | Return $Message 158 | } 159 | } catch { 160 | New-MSGraphError $_ 161 | } 162 | } -------------------------------------------------------------------------------- /Public/Remove-MSGraphMail.ps1: -------------------------------------------------------------------------------- 1 | function Remove-MSGraphMail { 2 | [CmdletBinding( SupportsShouldProcess = $True, ConfirmImpact = 'High' )] 3 | param ( 4 | # Specify the mailbox (or UPN) to remove an email for. 5 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 6 | [string]$Mailbox, 7 | # The ID of the message to remove. 8 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 9 | [Alias('id')] 10 | [string[]]$MessageID, 11 | # Retrieve from folder. 12 | [Parameter(ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 13 | [Alias('parentFolderId')] 14 | [string]$Folder 15 | ) 16 | try { 17 | $CommandName = $MyInvocation.InvocationName 18 | $RequestURI = [System.UriBuilder]::New('https', 'graph.microsoft.com') 19 | if ($Folder) { 20 | $RequestURI.Path = "v1.0/users/$($Mailbox)/mailfolders/$($Folder)/messages$($MessageID)" 21 | } else { 22 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages/$($MessageID)" 23 | } 24 | $DELETERequestParams = @{ 25 | URI = $RequestURI.ToString() 26 | ContentType = 'application/json' 27 | } 28 | if ($PSCmdlet.ShouldProcess("Message $($MessageID)", 'Delete')) { 29 | $Result = New-MSGraphMailDELETERequest @DELETERequestParams 30 | if ($Result.StatusCode -eq 204) { 31 | Write-CustomMessage -Message "Removed message with ID $($MessageID)" -Type 'Success' 32 | } 33 | } 34 | } catch { 35 | $Command = $CommandName -Replace '-', '' 36 | $ErrorRecord = @{ 37 | ExceptionType = 'System.Exception' 38 | ErrorMessage = "$($CommandName) failed." 39 | InnerException = $_.Exception 40 | ErrorID = "MicrosoftGraph$($Command)CommandFailed" 41 | ErrorCategory = 'ReadError' 42 | TargetObject = $_.TargetObject 43 | ErrorDetails = $_.ErrorDetails 44 | BubbleUpDetails = $True 45 | } 46 | $CommandError = New-MSGraphErrorRecord @ErrorRecord 47 | $PSCmdlet.ThrowTerminatingError($CommandError) 48 | } 49 | } -------------------------------------------------------------------------------- /Public/Send-MSGraphMail.ps1: -------------------------------------------------------------------------------- 1 | function Send-MSGraphMail { 2 | [CmdletBinding()] 3 | param ( 4 | # Specify the mailbox (or UPN) to move emails for. 5 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 6 | [string]$Mailbox, 7 | # Retrieve a single message using a message ID. 8 | [Parameter(Mandatory = $true, ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 9 | [Alias('id')] 10 | [string[]]$MessageID, 11 | # Retrieve from folder. 12 | [Parameter(ParameterSetName = 'Single', ValueFromPipelineByPropertyName)] 13 | [Alias('parentFolderId')] 14 | [string]$Folder 15 | ) 16 | try { 17 | $RequestURI = [System.UriBuilder]::New('https', 'graph.microsoft.com') 18 | if ($Folder) { 19 | $RequestURI.Path = "v1.0/users/$($Mailbox)/mailfolders/$($Folder)/messages/$($MessageID)/send" 20 | } else { 21 | $RequestURI.Path = "v1.0/users/$($Mailbox)/messages/$($MessageID)/send" 22 | } 23 | $POSTRequestParams = @{ 24 | URI = $RequestURI.ToString() 25 | ContentType = 'application/json; charset=utf-8' 26 | } 27 | $Message = New-MSGraphMailPOSTRequest @POSTRequestParams 28 | Write-Debug "Microsoft Graph returned $($Message)" 29 | if ($Message) { 30 | Write-CustomMessage -Message "Sent message '$($Message.subject)' with ID $($Message.id)" -Type 'Success' 31 | } 32 | } catch { 33 | New-MSGraphError $_ 34 | } 35 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MS Graph Mail - A pure-PowerShell Graph API mail client 2 | 3 | ![PowerShell Gallery Version](https://img.shields.io/powershellgallery/v/MSGraphMail?style=for-the-badge) ![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/MSGraphMail?style=for-the-badge) 4 | 5 | ## Preparations 6 | 7 | You will need the following: 8 | 9 | 1. An [**Azure AD Application ID**](https://aad.portal.azure.com) 10 | 2. An **Azure AD Application Secret** 11 | 3. An **Azure AD Tenant ID** 12 | 4. [**PowerShell 7**](https://aka.ms/powershell-release?tag=stable) installed on your Windows, Linux or MacOS device. 13 | 5. The code from this module in your PowerShell modules folder find this by running `$env:PSModulePath` in your PowerShell 7 session. Install from PSGallery with `Install-Module MSGraphMail` 14 | 15 | ## Import the Module 16 | 17 | Run `Import-Module 'MSGraphMail'` to load the module into your current session. 18 | 19 | ## Connecting to the Microsoft Graph API 20 | 21 | Connecting to the Microsoft Graph API uses the Azure AD Application information and the Connect-MSGraphMail client application. 22 | 23 | Using the **Splatting** technique: 24 | 25 | Splatting is a system in PowerShell that lets us put our parameters in a nicely formatted easy to read object (a HashTable to be specific!) and then "splat" them at the command. To do this, first things first setup a PowerShell object to hold your credentials. For example: 26 | 27 | ```powershell 28 | $MSGraphMailConnectionParameters = @{ 29 | ApplicationID = '' 30 | ApplicationSecret = '' 31 | TenantID = '' 32 | } 33 | Connect-MSGraphMail @MSGraphMailConnectionParameters 34 | ``` 35 | 36 | Using the **Traditional** technique: 37 | 38 | If you don't want to - or can't "splat" - we can fall back on a more traditional route: 39 | 40 | ```powershell 41 | Connect-MSGraphMail -ApplicationID '' -ApplicationSecret '' -TenantID '' 42 | ``` 43 | 44 | ## Getting Emails 45 | 46 | Getting emails hinges around a single command `Get-MSGraphMail` at it's most basic this looks like this. 47 | 48 | Using the **Splatting** technique: 49 | 50 | ```powershell 51 | $MailParameters = @{ 52 | Mailbox = 'you@example.uk' 53 | } 54 | Get-MSGraphMail @MailParameters 55 | ``` 56 | 57 | Using the **Traditional** technique: 58 | 59 | ```powershell 60 | Get-MSGraphMail -Mailbox 'you@example.uk' 61 | ``` 62 | 63 | You can get more specific with the following parameters: 64 | 65 | * **MessageID** - Retrieves a single message by ID. 66 | * **Folder** - Retrieves messages (or a single message) from a specific folder. 67 | * **HeadersOnly** - Retrieves only the message headers. 68 | * **MIME** - Retrieves a single message in MIME format (Requires **MessageID**). 69 | * **Search** - Searches emails based on a string. 70 | * **PageSize** - Retrieves only the given number of results. 71 | * **Pipeline** - Formats the output for Pipelining to other commands - like `Move-MSGraphMail` or `Delete-MSGraphMail`. 72 | * **Select** - Retrieves only the specified fields from the Graph API. 73 | * **Summary** - Displays a summary of the message(s) retrieved. See #1 for details. 74 | 75 | ## Creating an E-Mail 76 | 77 | Creating an email requires passing parameters to the `New-MSGraphMail` commandlet like so: 78 | 79 | Using the **Splatting** technique: 80 | 81 | ```powershell 82 | $MailParameters = @{ 83 | From = 'You ' 84 | To = 'Them ', 'Someone ' 85 | Subject = 'Your invoice #1234 is ready.' 86 | BodyContent = 'X:\Emails\BodyContent.txt' 87 | FooterContent = 'X:\Emails\FooterContent.txt' 88 | Attachments = 'X:\Files\SendtoExample.docx','X:\Files\SendToExample.zip' 89 | BodyFormat = 'text' 90 | } 91 | New-MSGraphMail @MailParameters 92 | ``` 93 | 94 | Using the **Traditional** technique: 95 | 96 | ```powershell 97 | New-MSGraphMail -From 'You ' -To 'Them ', 'Someone ' -Subject 'Your invoice #1234 is ready.' -BodyContent 'X:\Emails\BodyContent.txt' -FooterContent 'X:\Emails\FooterContent.txt' -Attachments 'X:\Files\SendtoExample.docx','X:\Files\SendToExample.zip' -BodyFormat 'text' 98 | ``` 99 | 100 | If this works we'll see: 101 | 102 | > SUCCESS: Created message 'Your invoice #1234 is ready.' with ID AAMkADg0MTI1YTY5LTZhNTAtNGY2Ni1iYmFmLTYyNTIxNmQ3ZTAyMQBGAAAAAADcjV4oGXn1Sb6mQOgHYL6tBwAynr9oS8bwR42_Ec20-qUkAAAAAAEQAAAynr9oS8bwR42_Ec20-qUkAAcuZgfeAAA= 103 | 104 | A draft email will have appeared in the account provided to `From`. Unless you specify the `-Send` parameter which immediately sends the email bypassing the draft creation. 105 | 106 | You can use inline attachments by using `-InlineAttachments` and specifying attachments in the format `'cid;filepath'` e.g: 107 | 108 | ```powershell 109 | New-MSGraphMail -From 'You ' -To 'Them ', 'Someone ' -Subject 'Your invoice #1234 is ready.' -BodyContent 'X:\Emails\BodyContent.html' -FooterContent 'X:\Emails\FooterContent.html' -Attachments 'X:\Files\SendtoExample.docx','X:\Files\SendToExample.zip' -BodyFormat 'html' -InlineAttachments 'signaturelogo;X\Common\EmailSignatureLogo.png', 'productlogo;X:\Products\Widgetiser\WidgetiserLogoEmail.png' 110 | ``` 111 | 112 | The two inline attachments would map to: 113 | 114 | ```html 115 | Our Logo 116 | ``` 117 | 118 | and 119 | 120 | ```html 121 | Widgetiser Logo 122 | ``` 123 | 124 | respectively. 125 | 126 | ## Sending an E-Mail 127 | 128 | Sending an email requires one small alteration of the above command - adding: 129 | 130 | ```powershell 131 | Pipeline = $True 132 | ``` 133 | 134 | if splatting or 135 | 136 | ```powershell 137 | -Pipeline 138 | ``` 139 | 140 | if using traditional parameter passing. 141 | 142 | This tells the command that we're going to pipeline the output - specifically that we're going to send it to another command. In our case we'd end up doing: 143 | 144 | ```powershell 145 | New-MSGraphMail @MailParameters | Send-MSGraphMail 146 | ``` 147 | 148 | The important part here is `| Send-MSGraphMail` quite literally `|` or **Pipe** and then the next command. 149 | 150 | ## Moving an E-Mail 151 | 152 | Moving an email requires one small alteration of the Get or New command - adding: 153 | 154 | ```powershell 155 | Pipeline = $True 156 | ``` 157 | 158 | if splatting or 159 | 160 | ```powershell 161 | -Pipeline 162 | ``` 163 | 164 | if using traditional parameter passing. 165 | 166 | This tells the command that we're going to pipeline the output - specifically that we're going to send it to another command. In our case we'd end up doing: 167 | 168 | ```powershell 169 | New-MSGraphMail @MailParameters | Move-MSGraphMail -Destination 'deleteditems' 170 | ``` 171 | 172 | The `-Destination` parameter for `Move-MSGraphMail` accepts a "well known folder name" e.g: "deleteditems" or "drafts" or "inbox" or a Folder ID. 173 | 174 | The important part here is `| Move-MSGraphMail` quite literally **Pipe (|)** and then the next command. 175 | 176 | ## Deleting an E-Mail 177 | 178 | If you want to "permanently" delete an email you can pipe the email to the `Remove-MSGraphMail` command. Similar to moving an email this works as so: 179 | 180 | ```powershell 181 | Get-MSGraphMail @MailParameters | Remove-MSGraphMail -Confirm:$False 182 | ``` 183 | 184 | Disecting this - we're getting the mail and then passing it down the pipeline an telling `Remove-MSGraphMail` not to prompt us for permission by setting `-Confirm:$False` 185 | --------------------------------------------------------------------------------