· 5 years ago · Sep 15, 2020, 07:04 PM
1<#
2.SYNOPSIS
3Grabs files from a specified folder, delivers them to a specified email address, and archives them in a different specified folder
4
5.PARAMETER dropOffDir
6provide a Directory to look for files to ship and zip
7
8.PARAMETER destinationEmail
9email address to send files to as attachments
10
11.PARAMETER archiveRootDir
12Directory to move sent files to for archival purposes
13
14.PARAMETER sourceEmail
15This email will show in the "from" field of the sent email
16
17.PARAMETER accessTokenURL
18URL to get an access token from the MSGraph API
19
20.PARAMETER appClientID
21Unique GUID tied to AzureAD app that delegates access to this script to run against our O365 Tenant/ MSGraph API
22
23.PARAMETER clientSecret
24Secret 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
30Base 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)]
40Param(
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
57Add-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
64If (test-path $currentArchiveDir) {
65 Write-Debug "$currentArchiveDir :: Archive folder exists... Continuing"
66
67}#do nothing if current archive dir doesn't exist
68Else {
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
73Write-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
96If ($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}
285Else {
286 Write-Output "No files found in $dropOffDir."
287}
288
289