smartguy5000

Invoke-ShipAndZip.ps1

Sep 15th, 2020
125
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <#
  2. .SYNOPSIS
  3. Grabs files from a specified folder, delivers them to a specified email address, and archives them in a different specified folder
  4.  
  5. .PARAMETER dropOffDir
  6. provide a Directory to look for files to ship and zip
  7.  
  8. .PARAMETER destinationEmail
  9. email address to send files to as attachments
  10.  
  11. .PARAMETER archiveRootDir
  12. Directory to move sent files to for archival purposes
  13.  
  14. .PARAMETER sourceEmail
  15. This email will show in the "from" field of the sent email
  16.  
  17. .PARAMETER accessTokenURL
  18. URL to get an access token from the MSGraph API
  19.  
  20. .PARAMETER appClientID
  21. Unique GUID tied to AzureAD app that delegates access to this script to run against our O365 Tenant/ MSGraph API
  22.  
  23. .PARAMETER clientSecret
  24. Secret key used in conjunction with appClientID to request access token from MSGraph API
  25.  
  26. .PARAMETER graphScope
  27. "User scope" of application in GraphAPI. Defaulted to .Default as there is no special scoping on our AzureAD App
  28.  
  29. .PARAMETER graphAPIURL
  30. Base URL of the Microsoft GraphAPI upon which we will build URIs
  31.  
  32. .NOTES
  33.     Author: Smartguy5000
  34.     Version:
  35.         1.0 Initial Write - 20200707
  36.         2.0 revise to use MSGraph API as mail delivery mechanism - 20200901
  37.  
  38. #>
  39. [CmdletBinding(SupportsShouldProcess = $true)]
  40. Param(
  41.     $dropOffDir,
  42.     $destinationEmail,
  43.     $archiveRootDir,
  44.     $AccessTokenURL,
  45.     $sourceEmail,
  46.     $appClientID,
  47.     $clientSecret,
  48.     $graphScope,
  49.     $graphAPIUrl
  50. )
  51.  
  52. $items = Get-ChildItem -Path $dropOffDir
  53. #get all files in dropoff directory at script runtime
  54.  
  55. $body = "This is an automated file delivery message."
  56.  
  57. Add-Type -AssemblyName System.Net.Http
  58. #this is required for the httpcontent and httpclient .NET classes
  59.  
  60. $archiveDirDateCode = get-date -Format "yyyy-MM"
  61.  
  62. $currentArchiveDir = "$archiveRootDir\$archiveDirDateCode"
  63.  
  64. If (test-path $currentArchiveDir) {
  65.     Write-Debug "$currentArchiveDir :: Archive folder exists... Continuing"
  66.    
  67. }#do nothing if current archive dir doesn't exist
  68. Else {
  69.     Write-Debug "Archive folder doesn't exist at $currentArchiveDir. Creating..."
  70.     New-Item -ItemType Directory -Path $archiveRootDir -Name $archiveDirDateCode
  71. }#per requirement, create new subfolder under archive dir with Month and Year for organization purposes
  72.  
  73. Write-Debug "Fetching MS Graph API Access Token"
  74.  
  75. $postParams = @{
  76.     scope         = $($graphAPIURL + $graphScope)
  77.     client_id     = $appClientID
  78.     grant_type    = "client_credentials"
  79.     client_secret = $clientSecret
  80. }
  81. $tokenResponse = Invoke-RestMethod -UseBasicParsing -Uri $accessTokenURL -Method POST -UseDefaultCredentials -Body $postParams
  82. #POST request to MSGraphAPI to get Access Token we will continue to use for the rest of this session
  83.  
  84. $headers = @{
  85.     "Content-Type" = "application/json"
  86.     Authorization  = "Bearer $($tokenResponse.Access_Token)"
  87. }#Build access token header
  88.  
  89. $createMessageUri = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/messages"
  90. #used to send large files, need to create draft message then add attachment via upload session
  91.  
  92. $sendMailUri = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/sendMail"
  93. #used to send mail on behalf of another account
  94.  
  95.  
  96. If ($items.count -gt 0) {
  97.     Write-Output "Attempting to send $($items.count) files"
  98.     ForEach ($item in $items) {
  99.         Write-Debug "Sending... $($item.name)"
  100.        
  101.         If ($($item.Length) -lt $([double]149mb)) { #if file is greater than 149MB, it cannot be sent with MSGraph API. Don't attempt to send
  102.             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.
  103.                 Write-Debug "smaller than 3MB, sending as single POST"
  104.                 $base64Attachment = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($item.FullName))  #convert the file to base64    
  105.                 $mailBody = [ordered]@{
  106.                     message         = @{
  107.                         subject      = $item.name
  108.                         body         = @{
  109.                             contentType = "Text"
  110.                             content     = $body
  111.                         }        
  112.                         toRecipients = @(@{
  113.                                 emailAddress = @{
  114.                                     address = $destinationEmail
  115.                                 }
  116.                             })
  117.                         attachments  = @(@{
  118.                                 "@odata.type" = "#microsoft.graph.fileAttachment"
  119.                                 name          = $item.Name
  120.                                 contentType   = "application/pdf"
  121.                                 contentBytes  = $base64Attachment
  122.                             })
  123.                
  124.                     }
  125.                     saveToSentItems = $false
  126.                 }#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
  127.  
  128.                 $jsonBody = $mailBody | ConvertTo-Json -Depth 5 -Compress #depth allows us to have the full nested json
  129.  
  130.                 $postResponse = Invoke-WebRequest -Headers $headers -UseBasicParsing -Method POST -Body $jsonBody -Uri $sendMailuri
  131.                        
  132.                 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.                
  133.                     $retryTime = $postResposne.Headers.RetryAfter
  134.                     Write-Debug "Sleeping for $($retryTime.delta.TotalSeconds) seconds due to API Rate Limit"
  135.                     Start-Sleep -Seconds $retryTime.delta.TotalSeconds
  136.                     $postResponse = Invoke-WebRequest -Headers $headers -UseBasicParsing -Method POST -Body $jsonBody -Uri $sendMailuri  
  137.                    
  138.                 }  
  139.             }
  140.             Else { #file is larger than 3MB. Must now do chunk uploading
  141.                 Write-Debug "Sending via multiple PUT requests as file exceeds 3MB size. "
  142.                 $byteArray = get-content -LiteralPath $item.fullname -Encoding Byte -Raw #pull in the file as an array of binary bytes
  143.  
  144.        
  145.                 $messageBody = [ordered]@{            
  146.                     subject      = $item.name
  147.                     body         = @{
  148.                         contentType = "Text"
  149.                         content     = $body
  150.                     }        
  151.                     toRecipients = @(@{
  152.                             emailAddress = @{
  153.                                 address = $destinationEmail
  154.                             }
  155.                         })
  156.                 }#build hash table to scaffold out draft message body
  157.  
  158.                 $jsonCreateMessageBody = $messageBody | ConvertTo-Json -Compress -depth 5
  159.                 $messageCreationResponse = Invoke-RestMethod -UseBasicParsing -Headers $headers -Method POST -Uri $createMessageUri -Body $jsonCreateMessageBody
  160.  
  161.                 $sendUri = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/Messages/$($messagecreationresponse.id)/send"
  162.                 #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.
  163.                
  164.                 $createUploadSessionURI = "https://graph.microsoft.com/v1.0/users/" + "$($sourceEmail.Replace('@','%40'))" + "/messages/$($messageCreationResponse.ID)/attachments/createUploadSession"
  165.                 #using message id from our draft creation, we here build the URI to create an upload session for our chunked large file
  166.  
  167.                 $attachmentItem = @{
  168.                     attachmentItem = @{
  169.                         attachmentType = "file"
  170.                         name           = $item.Name
  171.                         size           = $item.length
  172.                     }
  173.                 }#build hash table with attachment metadata
  174.            
  175.                 $attachmentItemJson = $attachmentItem | ConvertTo-Json -Compress
  176.        
  177.                 $uploadSession = Invoke-RestMethod -Uri $createUploadSessionURI -UseBasicParsing -Body $attachmentItemJson -Headers $headers -Method POST
  178.                 #send attachment metadata json to graph API, and get our upload session back.
  179.                
  180.                 $i = 0
  181.                 #set to 0 so we only do an initial upload once
  182.                 do {#wrap in do loop to ensure that the entire file gets uploaded
  183.                    
  184.                     if ($i -eq 0) { #upload initial chunk of data
  185.                         Write-Debug "initial upload"
  186.                         $httpclient = [System.Net.Http.HttpClient]::new()
  187.                         #Instantiate HTTP Client object https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=netframework-4.8
  188.                         #This object allows us to use the PUT method on our HTTP content below
  189.  
  190.                         $httpContent = [System.Net.Http.ByteArrayContent]::new($byteArray, [int32]0, [int32]3145728) #Get the first 3MBs of the file to be uploaded
  191.                         #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
  192.                         #this object is where we load the data about the HTTP request as well as the headers for the request.
  193.  
  194.                         $httpcontent.Headers.ContentType = 'application/octet-stream'                    
  195.                         $byteTask = $httpContent.ReadAsByteArrayAsync()
  196.                         $byteTask.Wait()    
  197.                         $httpcontent.Headers.ContentRange = "bytes 0-$($httpContent.Headers.ContentLength - 1)/$($byteArray.longLength)"
  198.                         #build our byteArray-HTTPContent object
  199.                                                    
  200.                         $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
  201.                         #upload the first 3MB chunk
  202.                         $uploadResponse.Wait()
  203.                      
  204.                        
  205.                         If ($uploadResponse.result.statuscode -eq 429) { #if we are at a rate limit, wait and retry again based on what the server tells us
  206.                             $rateLimitTask = $uploadResponse
  207.                             $rateLimitTask.Wait()
  208.                             $rateLimitResponse = $rateLimitTask
  209.                             $rateLimitObj = $rateLimitResponse.Result.Headers
  210.                             $retryTime = $rateLimitObj.RetryAfter
  211.                             Write-Debug "Sleeping for $($retryTime.delta.TotalSeconds) seconds due to API Rate Limit"
  212.                             Start-Sleep -Seconds $retryTime.delta.TotalSeconds
  213.                             $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
  214.                             $uploadResponse.Wait()
  215.                        
  216.                             continue
  217.                         }                
  218.                        
  219.                         $i = $i + 1
  220.                    
  221.                     }
  222.                     Else { #upload any remaining chunks
  223.                         Write-Debug "subsequent uploads"
  224.                         $responseJson = $uploadResponse.Result.Content.ReadAsStringAsync() #tell powershell to start a read task
  225.                         $responseJson.Wait() #wait for read task to complete
  226.                         $responseObj = $responseJson.Result | convertfrom-json #read the repsonse from the initial or previous subsequent upload, and make it useful
  227.                         [int64]$byteStart = $responseObj.nextexpectedranges[0] #get the next starting position in the byte array that the server is expecting
  228.                         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
  229.                             $byteStop = (($byteArray.LongLength - $byteStart) + $byteStart) - 1
  230.                             $byteDiff = ($byteStop - $byteStart) + 1
  231.                         }
  232.                         Else { #get the next 3MB chunk of the file
  233.                             $byteStop = $byteStart + 3145728
  234.                             $byteDiff = ($byteStop - $byteStart) + 1
  235.                         }
  236.                        
  237.                         $httpclient = [System.Net.Http.HttpClient]::new()
  238.                         $httpContent = [System.Net.Http.ByteArrayContent]::new($byteArray, $byteStart, $byteDiff)
  239.                         #instantiate the next HTTP Client and HTTP ByteArray content objects
  240.                        
  241.                        
  242.                         $httpcontent.Headers.ContentType = 'application/octet-stream'                    
  243.                         $httpcontent.Headers.ContentRange = "bytes $byteStart-$byteStop/$($byteArray.longLength)"
  244.                         #build httpcontent headers
  245.                         $byteTask = $httpContent.ReadAsByteArrayAsync()
  246.                         #read the next chunk into memory as the body bytearray http content object
  247.                         $byteTask.Wait() #force it to wait until the bytearay is read
  248.                         $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
  249.                         #use put method to asynchronously send the current chunk
  250.                         $uploadResponse.Wait()
  251.                         #wait until upload is complete or a response comes back from the API.
  252.                         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
  253.                             $rateLimitTask = $uploadResponse
  254.                             $rateLimitTask.Wait()
  255.                             $rateLimitResponse = $rateLimitTask
  256.                             $rateLimitObj = $rateLimitResponse.Result.Headers
  257.                             $retryTime = $rateLimitObj.RetryAfter
  258.                             Write-Debug "Sleeping for $($retryTime.delta.TotalSeconds) seconds due to API Rate Limit"
  259.                             Start-Sleep -Seconds $retryTime.delta.TotalSeconds
  260.                             $uploadResponse = $httpclient.putAsync($uploadSession.uploadUrl, $httpcontent)
  261.                             $uploadResponse.Wait()
  262.                            
  263.                         }    
  264.  
  265.                     }
  266.                 } until ($uploadResponse.Result.StatusCode -eq 201) #continue until we get a message created response from the api endpoint
  267.  
  268.                 $responseObj = $null
  269.                 $uploadResponse = $null
  270.                 #null out loop scope variables just in case bc vscode debugger is weird sometimes
  271.  
  272.                 Invoke-RestMethod -Method POST -UseBasicParsing -Uri $sendUri -Headers $headers
  273.                 #trigger final send of the message with completed attachment upload to the draft message
  274.             }
  275.  
  276.         }
  277.         Else {
  278.             Write-Error " $($item.name) is too large to be sent via O365."
  279.         }
  280.        
  281.         Move-Item -Path $($item.fullname) -Destination "$currentArchiveDir\$($item.Name)" -force
  282.         #move file to specified archive subfolder
  283.     }
  284. }
  285. Else {
  286.     Write-Output "No files found in $dropOffDir."
  287. }
  288.  
  289.  
Add Comment
Please, Sign In to add comment