Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <#
- .SYNOPSIS
- Grabs files from a specified folder, delivers them to a specified email address, and archives them in a different specified folder
- .PARAMETER dropOffDir
- provide a Directory to look for files to ship and zip
- .PARAMETER destinationEmail
- email address to send files to as attachments
- .PARAMETER archiveRootDir
- Directory to move sent files to for archival purposes
- .PARAMETER sourceEmail
- This email will show in the "from" field of the sent email
- .PARAMETER accessTokenURL
- URL to get an access token from the MSGraph API
- .PARAMETER appClientID
- Unique GUID tied to AzureAD app that delegates access to this script to run against our O365 Tenant/ MSGraph API
- .PARAMETER clientSecret
- Secret key used in conjunction with appClientID to request access token from MSGraph API
- .PARAMETER graphScope
- "User scope" of application in GraphAPI. Defaulted to .Default as there is no special scoping on our AzureAD App
- .PARAMETER graphAPIURL
- Base URL of the Microsoft GraphAPI upon which we will build URIs
- .NOTES
- Author: Smartguy5000
- Version:
- 1.0 Initial Write - 20200707
- 2.0 revise to use MSGraph API as mail delivery mechanism - 20200901
- #>
- [CmdletBinding(SupportsShouldProcess = $true)]
- Param(
- $dropOffDir,
- $destinationEmail,
- $archiveRootDir,
- $AccessTokenURL,
- $sourceEmail,
- $appClientID,
- $clientSecret,
- $graphScope,
- $graphAPIUrl
- )
- $items = Get-ChildItem -Path $dropOffDir
- #get all files in dropoff directory at script runtime
- $body = "This is an automated file delivery message."
- Add-Type -AssemblyName System.Net.Http
- #this is required for the httpcontent and httpclient .NET classes
- $archiveDirDateCode = get-date -Format "yyyy-MM"
- $currentArchiveDir = "$archiveRootDir\$archiveDirDateCode"
- If (test-path $currentArchiveDir) {
- Write-Debug "$currentArchiveDir :: Archive folder exists... Continuing"
- }#do nothing if current archive dir doesn't exist
- Else {
- Write-Debug "Archive folder doesn't exist at $currentArchiveDir. Creating..."
- New-Item -ItemType Directory -Path $archiveRootDir -Name $archiveDirDateCode
- }#per requirement, create new subfolder under archive dir with Month and Year for organization purposes
- Write-Debug "Fetching MS Graph API Access Token"
- $postParams = @{
- scope = $($graphAPIURL + $graphScope)
- client_id = $appClientID
- grant_type = "client_credentials"
- client_secret = $clientSecret
- }
- $tokenResponse = Invoke-RestMethod -UseBasicParsing -Uri $accessTokenURL -Method POST -UseDefaultCredentials -Body $postParams
- #POST request to MSGraphAPI to get Access Token we will continue to use for the rest of this session
- $headers = @{
- "Content-Type" = "application/json"
- Authorization = "Bearer $($tokenResponse.Access_Token)"
- }#Build access token header
- $createMessageUri = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/messages"
- #used to send large files, need to create draft message then add attachment via upload session
- $sendMailUri = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/sendMail"
- #used to send mail on behalf of another account
- If ($items.count -gt 0) {
- Write-Output "Attempting to send $($items.count) files"
- ForEach ($item in $items) {
- Write-Debug "Sending... $($item.name)"
- If ($($item.Length) -lt $([double]149mb)) { #if file is greater than 149MB, it cannot be sent with MSGraph API. Don't attempt to send
- If ($($item.Length) -lt $([double]3mb)) { #if file is less than 3MB, it can be send with a single POST request with the data base64 encoded in the POST Request.
- Write-Debug "smaller than 3MB, sending as single POST"
- $base64Attachment = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($item.FullName)) #convert the file to base64
- $mailBody = [ordered]@{
- message = @{
- subject = $item.name
- body = @{
- contentType = "Text"
- content = $body
- }
- toRecipients = @(@{
- emailAddress = @{
- address = $destinationEmail
- }
- })
- attachments = @(@{
- "@odata.type" = "#microsoft.graph.fileAttachment"
- name = $item.Name
- contentType = "application/pdf"
- contentBytes = $base64Attachment
- })
- }
- saveToSentItems = $false
- }#build a hash table of the email message to be sent per the MSGraphAPI Spec https://docs.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http
- $jsonBody = $mailBody | ConvertTo-Json -Depth 5 -Compress #depth allows us to have the full nested json
- $postResponse = Invoke-WebRequest -Headers $headers -UseBasicParsing -Method POST -Body $jsonBody -Uri $sendMailuri
- If ($postResponse.statuscode -eq 429) { #check if we have hit a rate limit. Sleep until API resposne tells us we don't have to and try again.
- $retryTime = $postResposne.Headers.RetryAfter
- Write-Debug "Sleeping for $($retryTime.delta.TotalSeconds) seconds due to API Rate Limit"
- Start-Sleep -Seconds $retryTime.delta.TotalSeconds
- $postResponse = Invoke-WebRequest -Headers $headers -UseBasicParsing -Method POST -Body $jsonBody -Uri $sendMailuri
- }
- }
- Else { #file is larger than 3MB. Must now do chunk uploading
- Write-Debug "Sending via multiple PUT requests as file exceeds 3MB size. "
- $byteArray = get-content -LiteralPath $item.fullname -Encoding Byte -Raw #pull in the file as an array of binary bytes
- $messageBody = [ordered]@{
- subject = $item.name
- body = @{
- contentType = "Text"
- content = $body
- }
- toRecipients = @(@{
- emailAddress = @{
- address = $destinationEmail
- }
- })
- }#build hash table to scaffold out draft message body
- $jsonCreateMessageBody = $messageBody | ConvertTo-Json -Compress -depth 5
- $messageCreationResponse = Invoke-RestMethod -UseBasicParsing -Headers $headers -Method POST -Uri $createMessageUri -Body $jsonCreateMessageBody
- $sendUri = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/Messages/$($messagecreationresponse.id)/send"
- #using message id from our draft creation, we here build a URI that will allow us to send the message once the attachment is sent.
- $createUploadSessionURI = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/messages/$($messageCreationResponse.ID)/attachments/createUploadSession"
- #using message id from our draft creation, we here build the URI to create an upload session for our chunked large file
- $attachmentItem = @{
- attachmentItem = @{
- attachmentType = "file"
- name = $item.Name
- size = $item.length
- }
- }#build hash table with attachment metadata
- $attachmentItemJson = $attachmentItem | ConvertTo-Json -Compress
- $uploadSession = Invoke-RestMethod -Uri $createUploadSessionURI -UseBasicParsing -Body $attachmentItemJson -Headers $headers -Method POST
- #send attachment metadata json to graph API, and get our upload session back.
- $i = 0
- #set to 0 so we only do an initial upload once
- do {#wrap in do loop to ensure that the entire file gets uploaded
- if ($i -eq 0) { #upload initial chunk of data
- Write-Debug "initial upload"
- $httpclient = [System.Net.Http.HttpClient]::new()
- #Instantiate HTTP Client object https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=netframework-4.8
- #This object allows us to use the PUT method on our HTTP content below
- $httpContent = [System.Net.Http.ByteArrayContent]::new($byteArray, [int32]0, [int32]3145728) #Get the first 3MBs of the file to be uploaded
- #Instantiate ByteArrayContent object which is derifed from an httpcontent class. https://docs.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent?view=netframework-4.8
- #this object is where we load the data about the HTTP request as well as the headers for the request.
- $httpcontent.Headers.ContentType = 'application/octet-stream'
- $byteTask = $httpContent.ReadAsByteArrayAsync()
- $byteTask.Wait()
- $httpcontent.Headers.ContentRange = "bytes 0-$($httpContent.Headers.ContentLength - 1)/$($byteArray.longLength)"
- #build our byteArray-HTTPContent object
- $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
- #upload the first 3MB chunk
- $uploadResponse.Wait()
- If ($uploadResponse.result.statuscode -eq 429) { #if we are at a rate limit, wait and retry again based on what the server tells us
- $rateLimitTask = $uploadResponse
- $rateLimitTask.Wait()
- $rateLimitResponse = $rateLimitTask
- $rateLimitObj = $rateLimitResponse.Result.Headers
- $retryTime = $rateLimitObj.RetryAfter
- Write-Debug "Sleeping for $($retryTime.delta.TotalSeconds) seconds due to API Rate Limit"
- Start-Sleep -Seconds $retryTime.delta.TotalSeconds
- $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
- $uploadResponse.Wait()
- continue
- }
- $i = $i + 1
- }
- Else { #upload any remaining chunks
- Write-Debug "subsequent uploads"
- $responseJson = $uploadResponse.Result.Content.ReadAsStringAsync() #tell powershell to start a read task
- $responseJson.Wait() #wait for read task to complete
- $responseObj = $responseJson.Result | convertfrom-json #read the repsonse from the initial or previous subsequent upload, and make it useful
- [int64]$byteStart = $responseObj.nextexpectedranges[0] #get the next starting position in the byte array that the server is expecting
- If (($byteArray.LongLength - $byteStart) -le 3145728) { #if this is our last chunk, do the math to find the byte to end on for the array
- $byteStop = (($byteArray.LongLength - $byteStart) + $byteStart) - 1
- $byteDiff = ($byteStop - $byteStart) + 1
- }
- Else { #get the next 3MB chunk of the file
- $byteStop = $byteStart + 3145728
- $byteDiff = ($byteStop - $byteStart) + 1
- }
- $httpclient = [System.Net.Http.HttpClient]::new()
- $httpContent = [System.Net.Http.ByteArrayContent]::new($byteArray, $byteStart, $byteDiff)
- #instantiate the next HTTP Client and HTTP ByteArray content objects
- $httpcontent.Headers.ContentType = 'application/octet-stream'
- $httpcontent.Headers.ContentRange = "bytes $byteStart-$byteStop/$($byteArray.longLength)"
- #build httpcontent headers
- $byteTask = $httpContent.ReadAsByteArrayAsync()
- #read the next chunk into memory as the body bytearray http content object
- $byteTask.Wait() #force it to wait until the bytearay is read
- $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
- #use put method to asynchronously send the current chunk
- $uploadResponse.Wait()
- #wait until upload is complete or a response comes back from the API.
- If ($uploadResponse.result.statuscode -eq 429) { #if we hit a rate limit, wait until the server tells us, try again and then pop back to the top of the loop
- $rateLimitTask = $uploadResponse
- $rateLimitTask.Wait()
- $rateLimitResponse = $rateLimitTask
- $rateLimitObj = $rateLimitResponse.Result.Headers
- $retryTime = $rateLimitObj.RetryAfter
- Write-Debug "Sleeping for $($retryTime.delta.TotalSeconds) seconds due to API Rate Limit"
- Start-Sleep -Seconds $retryTime.delta.TotalSeconds
- $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
- $uploadResponse.Wait()
- }
- }
- } until ($uploadResponse.Result.StatusCode -eq 201) #continue until we get a message created response from the api endpoint
- $responseObj = $null
- $uploadResponse = $null
- #null out loop scope variables just in case bc vscode debugger is weird sometimes
- Invoke-RestMethod -Method POST -UseBasicParsing -Uri $sendUri -Headers $headers
- #trigger final send of the message with completed attachment upload to the draft message
- }
- }
- Else {
- Write-Error " $($item.name) is too large to be sent via O365."
- }
- Move-Item -Path $($item.fullname) -Destination "$currentArchiveDir\$($item.Name)" -force
- #move file to specified archive subfolder
- }
- }
- Else {
- Write-Output "No files found in $dropOffDir."
- }
Add Comment
Please, Sign In to add comment