class AuthorizationRequestException: System.Exception {
AuthorizationRequestException() {
}
AuthorizationRequestException([string]$Message): base($Message) {
}
AuthorizationRequestException([string]$Message, [System.Exception]$InnerException): base($Message, $InnerException) {
}
}
class DataStoreFolderNotFoundException: System.Exception {
DataStoreFolderNotFoundException() {
}
DataStoreFolderNotFoundException([string]$Message): base($Message) {
}
DataStoreFolderNotFoundException([string]$Message, [System.Exception]$InnerException): base($Message, $InnerException) {
}
}
function Read-AccessToken {
param(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$JsonFile,
[bool]$KeepMeSignedIn,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ApplicationName,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ClientId,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ClientSecret,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]$Scope
)
#Nếu tập tin credential.json tồn tại
if ([System.IO.File]::Exists($JsonFile)) {
#Đọc nội dung tập tin chứa thông tin access token và refresh token (nếu có), credential.json
[PSCustomObject]$CredentialJson = Get-Content -Path $JsonFile | ConvertFrom-Json
#Xác định ngày hết hạn của access token
[datetime]$AccessTokenExpirationDateTime = [System.DateTime]::Parse($CredentialJson.expires_in)
#Nếu access token đã hết hạn
if ([System.DateTime]::Compare([System.DateTime]::Now, $AccessTokenExpirationDateTime) -ge 0) {
#Trường hợp có refresh token
if ($KeepMeSignedIn) {
#Gửi refresh token để lấy access token mới
[string]$RefreshToken = $CredentialJson.refresh_token
[System.Func[string, string]]$RefreshAccessToken = {
param([string]$RefreshToken)
[string]$Uri = "https://oauth2.googleapis.com/token"
[string]$FormData = "code=$($AuthorizationCode)&client_id=$($ClientId)&client_secret=$($ClientSecret)&grant_type=refresh_token&refresh_token=$($RefreshToken)"
[PsCustomObject]$Response = Invoke-RestMethod -Uri $Uri -Method Post -ContentType "application/x-www-form-urlencoded" -Body $FormData
[datetime]$NewExpiration = [System.DateTime]::Now.AddSeconds($Response.expires_in)
[PSCustomObject]$OldResponse = Get-Content $JsonFile | ConvertFrom-Json
$OldResponse.access_token = $Response.access_token
$OldResponse.expires_in = $NewExpiration
$OldResponse | ConvertTo-Json | Out-File -FilePath $JsonFile -Force
return $Response.access_token
}
return $RefreshAccessToken.Invoke($RefreshToken)
}
#Nếu không có refresh token thì gửi yêu cầu ủy quyền để lấy access token mới
else {
Remove-Item -Path $JsonFile -Force
New-AuthorizationRequest -ClientId $ClientId -ClientSecret $ClientSecret -KeepMeSignedIn $KeepMeSignedIn -ApplicationName $ApplicationName -Scope $Scope
return Read-AccessToken -JsonFile $JsonFile -KeepMeSignedIn $KeepMeSignedIn -ClientId $ClientId -ClientSecret $ClientSecret -ApplicationName $ApplicationName -Scope $Scope
}
}
else {
#Nếu access token chưa hết hạn thì lấy thông tin access token từ tập tin credential.json
return $CredentialJson.access_token
}
}
#Nếu tập tin credential.json không tồn tại thì gửi yêu cầu ủy quyền để lấy access token
else {
New-AuthorizationRequest -ClientId $ClientId -ClientSecret $ClientSecret -KeepMeSignedIn $KeepMeSignedIn -ApplicationName $ApplicationName -Scope $Scope
return Read-AccessToken -JsonFile $JsonFile -KeepMeSignedIn $KeepMeSignedIn -ClientId $ClientId -ClientSecret $ClientSecret -ApplicationName $ApplicationName -Scope $Scope
}
}
function Start-AuthorizationProcess {
param(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ClientId,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ClientSecret,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ApplicationName,
[bool]$KeepMeSignedIn,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]$Scope
)
if ($null -eq $ApplicationName) {
throw [System.ArgumentNullException]::new("Please specify a name for your application. ApplicationName is null.")
}
[string]$CredentialJsonFile = [System.IO.Path]::Combine($env:APPDATA, $ApplicationName, "credential.json")
[string]$AccessToken = Read-AccessToken -JsonFile $CredentialJsonFile -KeepMeSignedIn $KeepMeSignedIn -ClientId $ClientId -ClientSecret $ClientSecret -ApplicationName $ApplicationName -Scope $Scope
return $AccessToken
}
function New-AuthorizationRequest {
param(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ClientId,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ClientSecret,
[bool]$KeepMeSignedIn,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ApplicationName,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string[]]$Scope
)
[System.Func[string]]$GenerateCodeVerifier = {
return ConvertTo-Base64EncodedNoPaddingString -Data (Get-RandomNumber -Length 128)
}
[string]$CodeVerifier = $GenerateCodeVerifier.Invoke()
[string]$State = $GenerateCodeVerifier.Invoke()
[System.Func[string, string]]$GenerateCodeChallenge = {
param([string]$CodeVerifier)
[System.Func[string, byte[]]]$ConvertToSha256 = {
param([string]$Data)
[byte[]]$InputAsBinary = [System.Text.ASCIIEncoding]::UTF8.GetBytes($Data)
[System.Security.Cryptography.SHA256]$Sha256 = [System.Security.Cryptography.SHA256]::Create()
[byte[]]$InputSha256Hash = $Sha256.ComputeHash($InputAsBinary)
return $InputSha256Hash
}
return ConvertTo-Base64EncodedNoPaddingString -Data ($ConvertToSha256.Invoke($CodeVerifier))
}
[string]$CodeChallenge = $GenerateCodeChallenge.Invoke($CodeVerifier)
[string]$AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth"
[System.Func[int]]$GetRandomUnusedPort = {
[System.Net.Sockets.TcpListener]$Server = New-Object System.Net.Sockets.TcpListener -ArgumentList ([System.Net.IPAddress]::Loopback, 0)
$Server.Start()
[System.Net.IPEndPoint]$IP = $Server.LocalEndpoint
$Server.Stop()
return $IP.Port
}
[int]$UnusedPort = $GetRandomUnusedPort.Invoke()
[string]$RedirectUri = "http://$([System.Net.IPAddress]::Loopback):$($UnusedPort)/"
[System.Net.HttpListener]$HttpServer = New-Object System.Net.HttpListener
$HttpServer.Prefixes.Add($RedirectUri)
try {
$HttpServer.Start()
[string]$AuthorizationUri = "$($AuthorizationEndpoint)?response_type=code&scope=$([System.Web.HttpUtility]::UrlEncode($Scope -join " "))&redirect_uri=$([System.Uri]::EscapeDataString($RedirectUri))&client_id=$($ClientId)&code_challenge=$($CodeChallenge)&code_challenge_method=S256&state=$($State)"
if ($KeepMeSignedIn) {
$AuthorizationUri += "&access_type=offline"
}
try {
Start-Process $AuthorizationUri
}
catch {
if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) {
Start-Proces -ArgumentList "xdg-open", $AuthorizationUri
}
if ([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::OSX)) {
Start-Proces -ArgumentList "open", $AuthorizationUri
}
else {
throw [System.NotSupportedException]::new("Operating system not supported.")
}
}
[System.Net.HttpListenerContext]$Context = $HttpServer.GetContext()
[System.Action[string]]$DisplayInfoOnBrowser = {
param([string]$Text)
if ($null -eq $Text) {
throw [System.ArgumentNullException]::new("Parameter $("$Text") is required.")
}
[byte[]]$Buffer = [System.Text.Encoding]::UTF8.GetBytes("<html><body><p>$($Text)</p><body></html>")
$Context.Response.ContentType = "text/html"
$Context.Response.ContentLength64 = $Buffer.Length
$Context.Response.OutputStream.Write($Buffer)
}
[string]$IncomingState = $Context.Request.QueryString.Get("state")
if ([string]::IsNullOrEmpty($State)) {
throw [AuthorizationRequestException]::new("Received request with invalid state: $($IncomingState)")
}
if ([string]::IsNullOrEmpty($Context.Request.QueryString.Get("error")) -eq $false) {
[string]$Error = $Context.Request.QueryString.Get("error")
switch ($Error) {
"access_denied"
{
$DisplayInfoOnBrowser.Invoke("Authorization request failed.")
throw [AuthorizationRequestException]::new("The user denied granting access to your application. Please try again!")
}
Default {}
}
}
[string]$Code = $Context.Request.QueryString.Get("code")
if ([string]::IsNullOrEmpty($Code)) {
throw [AuthorizationRequestException]::new("Malformed authorization response.")
}
[System.Action[string, string, string]]$ExchangeAuthorizationCodeForAccessToken = {
param([string]$AuthorizationCode, [string]$CodeVerifier, [string]$OutputFile)
[string]$Uri = "https://oauth2.googleapis.com/token"
[string]$FormData = "code=$($AuthorizationCode)&client_id=$($ClientId)&client_secret=$($ClientSecret)&redirect_uri=$([System.Uri]::EscapeDataString($RedirectUri))&grant_type=authorization_code&code_verifier=$($CodeVerifier)"
[PSCustomObject]$Response = Invoke-RestMethod -Uri $Uri -Method Post -ContentType "application/x-www-form-urlencoded" -Body $FormData
[datetime]$NewExpiration = [System.DateTime]::Now.AddSeconds($Response.expires_in)
$Response = $Response | Select-Object -ExcludeProperty expires_in
Add-Member -InputObject $Response -MemberType NoteProperty -Name expires_in -Value $NewExpiration
[string]$DataStoreFolder = [System.IO.Path]::GetDirectoryName($OutputFile)
if ((Test-Path $DataStoreFolder) -eq $false) {
New-Item -Path $DataStoreFolder -ItemType Directory
}
$Response | ConvertTo-Json | Out-File -FilePath $OutputFile -Force
}
[string]$CredentialJsonFile = [System.IO.Path]::Combine($env:APPDATA, $ApplicationName, "credential.json")
$DisplayInfoOnBrowser.Invoke("Authorization complete. You can close this browser now.")
$ExchangeAuthorizationCodeForAccessToken.Invoke($Code, $CodeVerifier, $CredentialJsonFile)
}
finally {
if ($HttpServer.IsListening) {
$HttpServer.Close()
}
}
}
function Get-RandomNumber {
param(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[int]$Length
)
[System.Security.Cryptography.RandomNumberGenerator] $RandomNums = [System.Security.Cryptography.RandomNumberGenerator]::Create()
[byte[]]$NumsAsBinary = [byte[]] @(,0) * $Length
$RandomNums.GetBytes($NumsAsBinary)
return $NumsAsBinary
}
function ConvertTo-Base64EncodedNoPaddingString {
param(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[byte[]]$Data
)
[string]$Base64EncodedString = [System.Convert]::ToBase64String($Data)
return $Base64EncodedString.Replace("+", "-").Replace("=", "").Replace("/", "_")
}
function Revoke-AccessToken {
param(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ApplicationName
)
if ($null -eq $ApplicationName) {
throw [System.ArgumentNullException]::new("Please specify a name for your application. ApplicationName is null.")
}
[string]$DataStoreFolder = [System.IO.Path]::Combine($env:APPDATA, $ApplicationName)
if ((Test-Path $DataStoreFolder) -eq $false) {
throw [DataStoreFolderNotFoundException]::new("The data store folder for $($ApplicationName) was not found.")
}
[string]$JsonFile = [System.IO.Path]::Combine($DataStoreFolder, "credential.json")
[System.Func[string, string]]$RetriveRefreshToken = {
param([string]$JsonFile)
if ([System.IO.File]::Exists($JsonFile) -eq $false) {
throw [System.IO.FileNotFoundException]::new("The 'credential.json' was not found.")
}
[PSCustomObject]$Json = Get-Content -Path $JsonFile | ConvertFrom-Json | Select-Object -Property refresh_token
if ($null -eq $JsonFile.refresh_token) {
throw [System.Exception]::new("The data store folder $($ApplicationName) does not contain a refresh token to be revoked.")
}
return $Json.refresh_token
}
[string]$RefreshToken = $RetriveRefreshToken.Invoke($JsonFile)
[string]$Uri = "https://oauth2.googleapis.com/revoke?token=$($RefreshToken)"
[Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]$Response = Invoke-WebRequest -Uri $Uri -ContentType "application/x-www-form-urlencoded" -Method Post
if ($Response.StatusCode -eq 200) {
Remove-Item -Path $JsonFile
}
else {
throw [System.Exception]::new("Failed to revoke the access token.")
}
}
function Send-GmailMessage {
param(
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$From,
[string]$To,
[string]$Subject,
[string]$Body,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$AccessToken,
[parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[bool]$HasAttach,
[string[]]$AttachmentFiles,
[parameter(Mandatory = $true)]
[ValidateSet("Plain", "Html")]
[ValidateNotNullOrEmpty()]
[string]$BodyFormat
)
[System.Func[hashtable, PSCustomObject]] $CreateMessage = {
param([hashtable]$Headers)
if ($HasAttach -and ($null -eq $AttachmentFiles)) {
throw [System.ArgumentNullException]::new("Missing attachment file(s) while 'HasAttach' is set to true.")
}
[System.Text.StringBuilder]$StringBuilder = New-Object System.Text.StringBuilder
$StringBuilder.Append("Content-Type: multipart/mixed; boundary=EmailWithAttachments")
$StringBuilder.AppendLine()
$StringBuilder.Append("MIME-Version: 1.0")
$StringBuilder.AppendLine()
$StringBuilder.Append("to: $($To)")
$StringBuilder.AppendLine()
$StringBuilder.Append("from: $($From)")
$StringBuilder.AppendLine()
$StringBuilder.Append("subject: $($Subject)")
$StringBuilder.AppendLine()
$StringBuilder.AppendLine()
$StringBuilder.Append("--EmailWithAttachments")
$StringBuilder.AppendLine()
switch ($BodyFormat) {
"Plain" { $StringBuilder.Append("Content-Type: text/plain; charset=UTF-8"); break }
"Html" { $StringBuilder.Append("Content-Type: text/html; charset=UTF-8"); break }
Default {}
}
$StringBuilder.AppendLine()
$StringBuilder.Append($Body)
$StringBuilder.AppendLine()
$StringBuilder.AppendLine()
$StringBuilder.Append("--EmailWithAttachments--")
[string]$Message = $StringBuilder.ToString()
$Headers.Add("Content-Length", [System.Text.Encoding]::UTF8.GetByteCount($Message)) ([System.Text.Encoding]::UTF8.GetBytes($StringBuilder.ToString()))
if ($HasAttach -eq $false) {
return [PSCustomObject]@{
Message = $Message
Uri = "https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media"
Headers = $Headers
ContentType = "message/rfc822"
}
}
else {
[System.Func[string, string]]$GetMimeType = {
param([string]$FileExtension)
New-PSDrive -Name "HKCR" -PSProvider Registry -Root "HKEY_CLASSES_ROOT" | Out-Null
[string]$Path = "HKCR:\$($FileExtension)"
if (Test-Path $Path) {
[string]$MimeType = Get-ItemPropertyValue -Path "HKCR:\$($FileExtension)" -Name "Content Type"
}
else {
return "application/unknown"
}
Remove-PSDrive -Name "HKCR"
return $MimeType
}
[System.Text.StringBuilder]$MessageWithAttachmentsBuilder = New-Object System.Text.StringBuilder
$MessageWithAttachmentsBuilder.Append("Content-Type: multipart/mixed; boundary=EmailWithAttachments")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("MIME-Version: 1.0")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("to: $($To)")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("from: $($From)")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("subject: $($Subject)")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("--EmailWithAttachments")
$MessageWithAttachmentsBuilder.AppendLine()
switch ($BodyFormat) {
"Plain" { $MessageWithAttachmentsBuilder.Append("Content-Type: text/plain; charset=UTF-8"); break }
"Html" { $MessageWithAttachmentsBuilder.Append("Content-Type: text/html; charset=UTF-8"); break }
Default {}
}
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append($Body)
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.AppendLine()
ForEach-Object -InputObject $AttachmentFiles {
if ([System.IO.File]::Exists($_) -eq $false) {
throw [System.IO.FileNotFoundException]::new("The attachment file $($_) was not found.")
}
$File = Get-Item -Path $_
[string]$MimeType = $GetMimeType.Invoke($File.Extension)
$MessageWithAttachmentsBuilder.Append("--EmailWithAttachments")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("Content-Type: $($MimeType)")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("MIME-Version: 1.0")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("Content-Transfer-Encoding: base64")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("Content-Disposition: attachment; filename=`u{0022}$($File.Name)`u{0022}")
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.AppendLine()
[byte[]]$Buffer = [System.IO.File]::ReadAllBytes($_)
[string]$Base64EncodedString = [System.Convert]::ToBase64String($Buffer)
$MessageWithAttachmentsBuilder.Append($Base64EncodedString)
$MessageWithAttachmentsBuilder.AppendLine()
}
$MessageWithAttachmentsBuilder.AppendLine()
$MessageWithAttachmentsBuilder.Append("--EmailWithAttachments--")
[string]$MessageAsString = $MessageWithAttachmentsBuilder.ToString()
$Headers.Add("Content-Length", [System.Text.Encoding]::UTF8.GetByteCount($MessageAsString))
return [PSCustomObject]@{
Message = $MessageAsString
Uri = "https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media"
Headers = $Headers
ContentType = "message/rfc822"
}
}
}
[hashtable]$Headers = @{
Authorization = "Bearer $($AccessToken)"
}
[PSCustomObject]$Payload = $CreateMessage.Invoke($Headers)
[Microsoft.PowerShell.Commands.BasicHtmlWebResponseObject]$Response = Invoke-WebRequest -Uri $Payload.Uri -Method Post -ContentType $Payload.ContentType -Headers $Payload.Headers -Body $Payload.Message -SkipHttpErrorCheck -SkipHeaderValidation
if ($Response.StatusCode -eq 200) {
[PSCustomObject]$Json = $Response.Content | ConvertFrom-Json
Write-Host $Json
}
else {
Write-Error $Response.Content
}
}
$ClientId = "" #ClientId
$ClientSecret = "" #ClientSecret
[string]$AccessToken = Start-AuthorizationProcess -ClientId $ClientId -ClientSecret $ClientSecret -KeepMeSignedIn $true -ApplicationName "Gmail" -Scope @("https://www.googleapis.com/auth/gmail.send")
[string]$HtmlBody = @"
<h1>Test Mail</h1>
<p>This is a <b>test mail</b>.</p>
"@
Send-GmailMessage -From "abc@gmail.com" -To "bcd@yahoo.com.vn" -Subject "Test Mail" -Body $HtmlBody -BodyFormat "Html" -AccessToken $AccessToken -HasAttach $false