Send Mail with attachment via Powershell Graph API - powershell

I'm currently trying to send a mail using an automated Powershell script using the GraphAPI from Microsoft.
Here the full story : I need to check every 15 min the mail received on an Office 365 mailbox, get a specific file attachment (filter by is name), download the file, and re-send it with a new mail.
Everything working but I canno't send the new mail with the attachment ( Works without the attachment).
Here my code :
$date = (get-date).ToUniversalTime().AddMinutes(-15).ToString("yyyy-MM-ddTHH:mm:ssZ")
$Uri = "https://graph.microsoft.com/v1.0/me/messages"
$UriSend = "https://graph.microsoft.com/v1.0/me/sendMail"
$filter = "?`$select=Id,ReceivedDateTime&`$filter=HasAttachments eq true and ReceivedDateTime ge " + $date
$url = $Uri + $filter
$Result = (Invoke-RestMethod -Method Get -Headers $requestheader -Uri $url)
if($Result.value){
## Loop through each results
foreach ($message in $Result.value)
{
# get attachments and save to file system
$query = $Uri + "/" + $message.Id + "/attachments"
$attachments = (Invoke-RestMethod -Method Get -Headers $requestheader -Uri $query)
# in case of multiple attachments in email
foreach ($attachment in $attachments.value)
{
$patternPDF = "FAX AR document\.pdf$"
if($attachment.Name -match $patternPDF)
{
$name = $message.ReceivedDateTime -replace "T","-"
$name = $name -replace "Z",""
$name = $name -replace ":","-"
$path = "c:\Temp\" + $name + ".pdf"
# Creation of the PDF file
$Content = [System.Convert]::FromBase64String($attachment.ContentBytes)
Set-Content -Path $path -Value $Content -Encoding Byte
$file = Get-Content $path -Encoding Byte -ReadCount 0
#Send File by Mail
$body =
#"
{
"message" : {
"subject": "AR Fax",
"body" : {
"contentType": "Text",
"content": "Accusé Fax"
},
"toRecipients": [
{
"emailAddress" : {
"address" : "mail#domain.com"
}
}
],
"attachments":[
{
"##odata.type":"#microsoft.graph.fileAttachment",
"name":"Fax_AR.pdf",
"contentType":"application/pdf",
"contentBytes":"$file"
}
]
},
"saveToSentItems": "true"
}
"#
# Invokes the request
Invoke-RestMethod -Headers $requestheader -Uri $uriSend -Method Post -Body $body
I'm not very good in powershell (or in code generally) so please be indulgent^^
EDIT : I've find another way to do what i want (I print the file now)

I've find another way to do what i want (I print the file now)
So I'm closing the topic.

Related

Getting error : The underlying connection was closed: An unexpected error occurred on a send while running powershell script

$API_KEY = "xxxxxxxxxx"
# Source image files
$ImageFiles = (Get-ChildItem -Path C:\Users\sam\Desktop\jpeg\* -filter *).Name
$uploadedFiles = #()
try {
foreach ($imageFile in $ImageFiles ) {
# 1a. RETRIEVE THE PRESIGNED URL TO UPLOAD THE FILE.
# Prepare URL for `Get Presigned URL` API call
$query = "https://api.pdf.co/v1/file/upload/get-presigned-url?
contenttype=application/octet-stream&name=" + `
[IO.Path]::GetFileName($imageFile)
$query = [System.Uri]::EscapeUriString($query)
# Execute request
$jsonResponse = Invoke-RestMethod -Method Get -Headers #{ "x-api-key" = $API_KEY } -Uri
$query
if ($jsonResponse.error -eq $false) {
# Get URL to use for the file upload
$uploadUrl = $jsonResponse.presignedUrl
# Get URL of uploaded file to use with later API calls
$uploadedFileUrl = $jsonResponse.url
# 1b. UPLOAD THE FILE TO CLOUD.
$r = Invoke-WebRequest -Method Put -Headers #{ "x-api-key" = $API_KEY; "content-type"
= "application/octet-stream" } -InFile $imageFile -Uri $uploadUrl
if ($r.StatusCode -eq 200) {
# Keep uploaded file URL
$uploadedFiles += $uploadedFileUrl
}
else {
# Display request error status
Write-Host $r.StatusCode + " " + $r.StatusDescription
}
}
else {
# Display service reported error
Write-Host $jsonResponse.message
}
}
if ($uploadedFiles.length -gt 0) {
# 2. CREATE PDF DOCUMENT FROM UPLOADED IMAGE FILES
# Prepare URL for `DOC To PDF` API call
$query = "https://api.pdf.co/v1/pdf/convert/from/image"
# Prepare request body (will be auto-converted to JSON by Invoke-RestMethod)
# See documentation: https://apidocs.pdf.co
$body = #{
"name" = $(Split-Path $DestinationFile -Leaf)
"url" = $uploadedFiles -join ","
} | ConvertTo-Json
# Execute request
$response = Invoke-WebRequest -Method Post -Headers #{ "x-api-key" = $API_KEY; "Content-
Type" = "application/json" } -Body $body -Uri $query
$jsonResponse = $response.Content | ConvertFrom-Json
if ($jsonResponse.error -eq $false) {
# Get URL of generated PDF file
$resultFileUrl = $jsonResponse.url;
$DestinationFile = "C:\Users\sam\Desktop\pdf\$imagefile.split('.')[0]"
# Download PDF file
Invoke-WebRequest -Headers #{ "x-api-key" = $API_KEY } -OutFile $DestinationFile -Uri
$resultFileUrl
Write-Host "Generated PDF file saved as `"$($DestinationFile)`" file."
}
else {
# Display service reported error
Write-Host $jsonResponse.message
}
}
}
catch {
# Display request error
Write-Host $_.Exception
}
Basically this script converts bulk number of JPEG images to PDF format .Its working initially but later when i execute the script it is getting failed by this error "The underlying connection was closed: An unexpected error occurred on a send." . I googled this issue and added these two lines at the beginning of the script –
#[Net.ServicePointManager]::SecurityProtocolNet.SecurityProtocolType]::Tls12 [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 .
Even after adding these two am getting the same old error .Can anyone please help me with this issue

synchronize an onprem fileshare to a sharepoint online site collection using powershell and Microsof RestAPI

I am trying to work out a powershell script that:
retrieves an accesstoken (MSAL) to access (read/write) a sharepoint online site with subsites and documents. Preferably the Azure APP-registration ServicePrincipal can be granted access to just that site and access the sharepoint site/files without giving consent to the whole sharepoint environment. I don't know if that is possible currently as I can only grant application permission to files.readwrite.all and sites.readwrite.all. I do not see anything like files.readwrite.shared to grant access only to sites/collections that the serviceprincipal has access to. Anyone done this? I currently use the MSAL.PS powershell module to get a token using an AppRegistration with the admin-consented readwrite.all access but would like to limit that. The code for this is now:
Import-Module MSAL.PS;
$clientid = "my-appreg-client-id";
$tenantID = 'my-tenant-id';
$thumbPrint = 'certificate-thumbprint';
$ClientCertificate = Get-Item "Cert:\CurrentUser\My\$thumbPrint";
$myAccessToken = Get-MsalToken -ClientId $clientID -TenantId $tenantID -ClientCertificate
$ClientCertificate;
The script will read all files and folders from an UNC-share and build a file-collection of the onprem files. That part of the code is in place using a Get-ChildItem call to the UNC filetree.
Then, after getting the token, I need to get the current available files in the sharepoint online site document library structure and store that in a variable/hashtable which I can use to perform lookups between the onprem filecollection and the presence of those files and (sub)folders in the sharepoint site. If a folder does not yet exist I need to create that sharepoint folder and if a file is not yet present or the onprem version is newer I need to upload that file into sharepoint.
I have a script that does this using the old sharepoint.client.dll libraries but those support only basic authentication which will be unavailable any time soon for accessing the MS Online environment. So now I am searching for code to do this using the Microsoft Graph Api or other Rest API call. I am already struggling to get the contents of a site file collection so I hope that this generic problem description is enough to get some hints and tips/resources to get going.
Many thanks,
Eric
This is what I use. I'm using powershell in Linux.
## Get the Token
$clientId = "Application (Client) ID"
$clientSecret = "Client secret"
$tenantName = "TenantName.onmicrosoft.com"
$tokenBody = #{
Grant_Type = 'client_credentials'
Scope = 'https://graph.microsoft.com/.default'
Client_Id = $clientId
Client_Secret = $clientSecret
}
$tokenResponse = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" -Method POST -Body $tokenBody -ErrorAction Stop
$headers = #{
"Authorization" = "Bearer $($tokenResponse.access_token)"
"Content-Type" = "application/json"
}
## Use the SharePoint groups ObjectID. From this we'll get the drive ID.
$site_objectid = "Groups ObjectID"
## Create all the folders on the SharePoint site first. I've set microsoft.graph.conflictBehavior below to fail because I never want to rename or replace folders.
# Set the base directory.
$baseDirectory = "/test"
$directories = get-childItem -path $baseDirectory -recurse -directory
foreach ($directory in $directories) {
$URL = "https://graph.microsoft.com/v1.0/groups/$site_objectid/sites/root"
$subsite_ID = (Invoke-RestMethod -Headers $headers -Uri $URL -Method Get).ID
$URL = "https://graph.microsoft.com/v1.0/sites/$subsite_ID/drives"
$Drives = Invoke-RestMethod -Headers $headers -Uri $URL -Method Get
$Document_drive_ID = ($Drives.value | Where-Object { $_.name -eq 'Documents' }).id
$createFolderURL = "https://graph.microsoft.com/v1.0/drives/$Document_drive_ID/items/root:{0}:/children" -f $directory.parent.FullName
$file = $directory.Name
$uploadFolderRequestBody = #{
name= "$file"
folder = #{}
"#microsoft.graph.conflictBehavior"= "fail"
} | ConvertTo-Json
invoke-restMethod -headers $headers -method Post -body $uploadFolderRequestBody -contentType "application/json" -uri $createFolderURL
}
## Upload the files. I'm only adding files that are 4 days old or less because I run the script every 3 days for backup.
## These are set in the $sharefiles variable. To upload all files just remove everything after the pipe.
$sharefiles = get-childItem $baseDirectory -recurse | Where-Object {$_.LastWriteTime -gt (Get-Date).AddDays(-4)}
foreach ($sharefile in $sharefiles) {
$Filepath = $sharefile.FullName
$URL = "https://graph.microsoft.com/v1.0/groups/$site_objectid/sites/root"
$subsite_ID = (Invoke-RestMethod -Headers $headers -Uri $URL -Method Get).ID
$URL = "https://graph.microsoft.com/v1.0/sites/$subsite_ID/drives"
$Drives = Invoke-RestMethod -Headers $headers -Uri $URL -Method Get
$Document_drive_ID = ($Drives.value | Where-Object { $_.name -eq 'Documents' }).id
$Filename = $sharefile.Name
$upload_session = "https://graph.microsoft.com/v1.0/drives/$Document_drive_ID/root:{0}/$($Filename):/createUploadSession" -f $sharefile.directory.FullName
$upload_session_url = (Invoke-RestMethod -Uri $upload_session -Headers $headers -Method Post).uploadUrl
## We'll upload files in chunks.
$ChunkSize = 62259200
$file = New-Object System.IO.FileInfo($Filepath)
$reader = [System.IO.File]::OpenRead($Filepath)
$buffer = New-Object -TypeName Byte[] -ArgumentList $ChunkSize
$position = 0
$counter = 0
Write-Host "ChunkSize: $ChunkSize" -ForegroundColor Cyan
Write-Host "BufferSize: $($buffer.Length)" -ForegroundColor Cyan
$moreData = $true
While ($moreData) {
#Read a chunk
$bytesRead = $reader.Read($buffer, 0, $buffer.Length)
$output = $buffer
If ($bytesRead -ne $buffer.Length) {
#no more data to be read
$moreData = $false
#shrink the output array to the number of bytes
$output = New-Object -TypeName Byte[] -ArgumentList $bytesRead
[Array]::Copy($buffer, $output, $bytesRead)
Write-Host "no more data" -ForegroundColor Yellow
}
#Upload the chunk
$Header = #{
'Content-Range' = "bytes $position-$($position + $output.Length - 1)/$($file.Length)"
}
Write-Host "Content-Range = bytes $position-$($position + $output.Length - 1)/$($file.Length)" -ForegroundColor Cyan
#$position = $position + $output.Length - 1
$position = $position + $output.Length
Invoke-RestMethod -Method Put -Uri $upload_session_url -Body $output -Headers $Header -SkipHeaderValidation
#Increment counter
$counter++
}
$reader.Close()
}

Email Attachment and Body Naming

I am working on downloading email bodies and their attachments. I want to make sure that each sender has numbers attached to the filename to differentiate from other documents. The counter should start per each sender. For example
Sender1
Sender1-Emailbody-0
Sender1-pdf attachment-1
Sender1-jpg attachment -2
Sender2
Sender2-Emailbody-0
Sender2-pdf attachment -1
Sender2-jpg attachment -2
I am not sure of how to about it. Any help, please. Thanks
$emailQuery1 = $url + "?`$select=Id,Sender,DateTimeReceived&`$filter=HasAttachments eq true and DateTimeReceived ge " + $datetime
$emails = Invoke-RestMethod -Uri $emailQuery1 -Headers $head
## Loop through each results
Foreach ($email in $emails.value)
{
#get attachments and save to file system
$query = $url + "/" + $email.Id + "/attachments"
$body = $url + "/" + $email.Id + "/body"
$receivedtime = "/" + $email.id + "/DateTimeReceived"
$attachments = Invoke-RestMethod -Uri $query -Headers $head
$emailbody = Invoke-RestMethod -Uri $body -Headers $head
Foreach($em in $emailbody)
{
$bodyitems = $em.content
Foreach ($attachment in $attachments.value)
{
$attachment.Name
$path = $DFSPath + $recvtime +"_" + $email.Sender.EmailAddress.Address + "_" + $attachment.Name
$Content = [System.Convert]::FromBase64String($attachment.ContentBytes)
$recvtime = $email.DateTimeReceived | get-date -Format yyyyMMddHHmmss
$bodyitems | out-file -FilePath ($DFSPath +$recvtime +"_" + $email.Sender.EmailAddress.Address`
+"_emailbody-" +$Counter+".html")
Set-Content -Path $path -Value $Content -Encoding Byte
}
}
}
I recommend using a counting variable that gets incremented right before you save the file. Set it to 0 at the beginning of the foreach email loop, $x=0, and then put $x++ right before the Out-File command. You can then append it to the file name when saving the file.

Nessus IO Powershell API HTML/PDF report

When exporting PDF & HTML format reports the reports are empty, best I can tell there needs to be a report attribute but after 5 hours of running through the API and searching every which way I can think of I am not finding anything referencing that.
For those interested, this is the starting script before I started optimizing it.
https://github.com/Pwd9000-ML/NessusV7-Report-Export-PowerShell/blob/master/NessusPro_v7_Report_Exporter_Tool.ps1
add-type #"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint srvPoint, X509Certificate certificate,
WebRequest request, int certificateProblem) {
return true;
}
}
"#
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
[System.Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$GNR = #{
OutputDir = "$Env:SystemDrive\Nessus\$(([DateTime]::Now).ToString("yyyy-MM-dd"))"
StatusUri = [System.Collections.ArrayList]::new()
}
#------------------Input Variables-----------------------------------------------------------------
$Baseurl = "https://$($env:COMPUTERNAME):8834"
$Username = <Removed>
$Password = <Removed>
$ContentType = "application/json"
$POSTMethod = 'POST'
$GETMethod = 'GET'
#------------------Stage props to obtain session token (Parameters)--------------------------------
$session = #{
Uri = $Baseurl + "/session"
ContentType = $ContentType
Method = $POSTMethod
Body = convertto-json (New-Object PSObject -Property #{username = $Username; password = $Password})
}
#------------------Commit session props for token header X-cookie----------------------------------
$TokenResponse = Invoke-RestMethod #session
if ($TokenResponse) {
$Header = #{"X-Cookie" = "token=" + $TokenResponse.token}
} else {
Write-nLog -Message "Error occured obtaining session token. Script Terminating... Please ensure Username and Password Correct." -Type Error -TerminatingError
}
IF (![System.IO.Directory]::Exists($GNR.OutputDir)) {
New-Item -Path $GNR.OutputDir -ItemType directory -Force |Out-Null
}
#------------------Output completed scans----------------------------------------------------------
$Scans = (Invoke-RestMethod -Uri "$baseurl/scans" -Headers $Header -Method $GETMethod -ContentType "application/json").scans
ForEach ($Format in #("nessus","pdf")) {
$StatusURI = [System.Collections.ArrayList]::new()
$StatusArray = [System.Collections.ArrayList]::new()
ForEach ($Scan in $Scans) {
Add-Content -Path "$($GNR.OutputDir)\ScanReport.txt" -Value "$($Scan.Name) ($($Scan.status))"
IF ($Scan.status -eq "Completed") {
$File = (Invoke-RestMethod -URI "$baseurl/scans/$($Scan.ID)/export" -ContentType $ContentType -Headers $Header -Method $POSTMethod -Body $(convertto-json (New-Object PSObject -Property #{format = "$Format"}))).file
[Void]$StatusArray.Add(
[pscustomobject]#{
ScanName = $scan.name
StatusUri = $baseurl + "/scans" + "/" + $Scan.id + "/export/" + "$file" + "/status"
DownloadUri = $baseurl + "/scans" + "/" + $Scan.id + "/export/" + "$file" + "/download"
}
)
}
}
#------------------Check Status of Export requests-------------------------------------------------
While ($StatusArray.StatusUri.count -GT $StatusURI.Count) {
ForEach ($ScanStatus in $StatusArray.StatusURI) {
IF ((Invoke-RestMethod -Uri $ScanStatus -ContentType $ContentType -Headers $Header -Method $GETMethod).status -EQ "Ready") {
if ($StatusURI -notcontains $ScanStatus) {
Write-Host "Adding $ScanStatus"
[void]$StatusURI.Add($ScanStatus)
}
} Else {
Write-nLog -Type "Info" -Message "Not all scans complete. ($($GNR.StatusURI.Count)/$($StatusArray.StatusUri.count)"
Start-Sleep -s 5
}
}
}
#------------------Download the Reports------------------------------------------------------------
$ExportUri = $StatusArray.DownloadUri
$outputs = $StatusArray.ScanName
foreach ($i in 0..($ExportUri.Count - 1)) {
Write-nLog -Type Info -Message "Exporting Report: $($outputs[$i])"
Invoke-WebRequest -Uri $ExportUri[$i] -ContentType $ContentType -Headers $Header -Method $GETMethod -OutFile "$($GNR.OutputDir)\$($outputs[$i]).$Format"
}
}
#------------------Script END----------------------------------------------------------------------
There are several additional parameters you can set on the POST /scans/{id}/export endpoint. The important one missed here is chapters which accepts a semi-colon delimted list of the desired content sections. This must be set for exports of pdf or html types, otherwise you get an empty result.
For example, to get the executive summary, in addition to format of html/pdf/csv etc, set chapters to vuln_hosts_summary. The other available options are:
vuln_by_host
compliance_exec
remediations
vuln_by_plugin
compliance
Hopefully this helps the next person trying to debug empty Nessus API exports too!
For full API docs for your version check out https://{YOUR_NESSUS_INSTALL}/api

setting the field in the JSON body to contents in a file while making a Http post call in PowerShell

I am trying to install a certificate issued by a digicert CA. First i am generating the CSR (Certificate Signing request) using the Powershell Script. I am putting the generated CSR in a txt file and removing all the line breaks, spaces and carriage returns. I am using the following script to do that:
$CSR > C:\Scripts\CSR.txt
$path = "C:\Scripts\CSR.txt"
(Get-Content $path -Raw).Replace("`r`n","") | Set-Content $path -Force
(Get-Content $path -Raw).Replace(" ","") | Set-Content $path -Force
(Get-Content $path -Raw).Replace("`r`n","") | Set-Content $path -Force -NoNewline
After that i want to make a http post call to digiCert to get the Certificate Id. For that I am Using the following script:
[Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
$body = #"
{
"profile_name": "XXX",
"common_name": "XX",
"additional_dns_names": [
"XX",
"XX"
],
"csr":$CSRWithNoCRLF,
"signature_hash": "sha256",
"validity": {
"months": 36
},
"organization": {
"name": "XXX",
"units": [
]
}
}
"#
$header = #{
"Accept"="application/json"
"KEY"="XXXX"
"Content-Type"="application/json"
}
$response = Invoke-WebRequest -Uri "https://www.digicert.comXXXX" -Method Post -Body $body -Headers $header
$Content = $response.Content
For the csr field in the body of the JSON i want to directly copy the contents of C:\Scripts\CSR.txt file. I tried following approach but it is not working.
$CSRWithNoCRLF = Get-Content $path
And use $CSRWithNoCRLF as the value for the csr. But the Get-Content is adding line breaks and carriage returns and malforming the CSR value. So this is not Working. Any Idea on how to resolve this?
Get-Content is adding line breaks
Several ways, each is a complete solution:
$CSRWithNoCRLF = Get-Content $path -Raw # requires PSv3+
Get-Content $path | Out-String
(Get-Content $path) -join "`n" # use "`r`n" for Windows-style line-endings
Edit
Examine what you are sending. Try this:
[regex]::Escape($Body)
If you used (Get-Content $path) -join "n"` then you will not have \r\n line endings in the CSR part. However, why are you handcrafting JSON? If you do that on a windows box, you'll have Windows line endings in your JSON string.
Try this:
$Body = [pscustomobject]#{
profile_name = "XXX"
common_name = "XX"
additional_dns_names = #(
"XX",
"XX"
)
csr = (Get-Content $Path) -join "`n"
signature_hash = "sha256"
validity = [pscustomobject]#{
months = 36
}
organization = [pscustomobject]#{
name = "XXX"
units = #()
}
} | ConvertTo-Json -Compress
Note that I'm using Compress.
> [regex]::Escape($Body) -match '\\r'
False
Give that a go.
Try this :
$CSRWithNoCRLF = (Get-Content $path -Raw).Replace("`r`n","").Replace(" ","")
$body = #"
{
"profile_name": "XXX",
"common_name": "XX",
"additional_dns_names": [
"XX",
"XX"
],
"csr":"$CSRWithNoCRLF",
"signature_hash": "sha256",
"validity": {
"months": 36
},
"organization": {
"name": "XXX",
"units": [
]
}
}
"#
$header = #{
"Accept"="application/json"
"KEY"="XXXX"
"Content-Type"="application/json"
}
$response = Invoke-WebRequest -Uri "https://www.digicert.comXXXX" -Method Post -Body $body -Headers $header
$Content = $response.Content
I don't see the need to save back the certificate to the file after modifying it.