Lấy dữ liệu cổ phiếu từ Website Cafef.vn về Excel

Liên hệ QC
Status
Không mở trả lời sau này.
Nếu dùng API của Google thì có hết từ lâu lắm rồi mà.

Còn dùng OAuth thì phải hết sức cẩn thận với đám mây, cái này phải có ràng buộc trách nhiệm nghiêm ngặt, và không ai làm như ai đó (có một nhược điểm chí mạng và buồn cười). Lần này thì mặc kệ nó. =]]]

Mà hài nhất là khúc đẩy dữ liệu phải chọn Range trước. :D

Ah nay mới nhìn code Py trong bài này. Chọn vùng để upload kìa. ôi thế mà lại nói gì là chọn vùng cập nhật là buồn cười? Video thì lấp liếm , kiến thức vay mượl lại rất chi là mờ nhạt. Ôi be phèn ơi là be phèn.
 
Ah nay mới nhìn code Py trong bài này. Chọn vùng để upload kìa. ôi thế mà lại nói gì là chọn vùng cập nhật là buồn cười? Video thì lấp liếm , kiến thức vay mượl lại rất chi là mờ nhạt. Ôi be phèn ơi là be phèn.
đề nghị qua đó có gì chưa rõ thì yêu cầu làm rõ đi .... xem họ trả lời sao thì biết thôi
 
Change event, Target.address.
Chán con cò be bé, đọc 1.0 thì hiểu 1.0001 chứ.
 
đề nghị qua đó có gì chưa rõ thì yêu cầu làm rõ đi .... xem họ trả lời sao thì biết thôi

nhìn vào cái nội dung đăng và cờ líp kiểu lấp lửng lấp liếm vậy là biết chả có kiến thức và giá trị chia sẻ gì đâu. Chả thế mà từ năm 2019 chả ai thèm hỏi. Vì họ share vào đây rồi phán như chiên rau nên mình mới ngó xem thế nào. họ chia sẻ, phán phiếc kiểu “phông bạt” thôi.
 
Thôi hai bên bớt lại chút cho lành đi ... tôi thù dai một gốc trả 10 lời luôn ... nhưng cũng có chừng mực tới lằm danh đỏ là dừng

quay lại chủ đề này ghi dữ liệu xong rồi ... đơn giản thôi ... thong thả tôi xuất hàm API cho dùng ... @

befaint theo bài đó ok​


1726795177907.png
 
Lần chỉnh sửa cuối:
đang dò nó sẻ có giao diện này .. qua TWebBrowser nhưng tôi không thích .. dò tiếp

thong thả và thư giản vi hành Google xem tình hình sao xong tính

View attachment 304058
Mấy năm trước, Google bắt đầu chặn yêu cầu ủy quyền thông qua webview hoặc webview được nhúng vào WinForm, đại loại là thường trả về lỗi "This browser or app may not be secure". Mình có tra cứu trên GitHub và tìm thấy đoạn code mẫu C# .NET này của Google, nhìn chung logic như sau:
  1. Tạo uri yêu cầu ủy quyền.
  2. Tạo redirect uri (tức là uri chuyển hướng sau khi thực hiện xong ủy quyền) để theo dõi.
  3. Dùng lớp (class) HttpListener để theo dõi lưu lượng HTTP từ redirect uri trên.
  4. Tạo tiến trình (process) với uri yêu cầu ủy quyền để Windows mở bằng trình duyệt mặc định.
  5. Khi người dùng thực hiện ủy quyền xong (thành công hay thất bại) sẽ trả về redirect uri, HttpListener bắt được thông tin từ redirect uri và gửi thông báo đến trình duyệt rằng người dùng có thể đóng trình duyệt lại.
  6. Tiến hành phân tích thông tin nhận được, trao đổi access token nếu có authorization code, thông báo lỗi nếu có.
1726795066055.png
OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs
Cái này mình không rõ Delphi có thư viện nào tương tự như vậy không, gửi bạn tham khảo thử.
 
có lẻ keo xác thực chỉ lần đầu đúng nghĩa hơn vì Tôi sử dụng File *.JSON nên nó quá đơn giản luôn

1/ Người dùng tạo APIKeys

2/ Tải File *.JSON

3/ chạy mã chỉ xác thực lần đầu tiên để lấy access token xong lưu vào đâu đó tuỳ thích

3/ các lần sau không cần nữa ... chờ ngày mail qua một ngày tôi thử đi thử lại xem sao xong tính tiếp

4/ hai hàm cơ bản khoãng 40 dòng code là xong ...

vài thông tin cơ bản sau thôi còn code trên Tools nào thì tuỳ chỉnh lại trên Tools đó là xong

1726797468006.png
 
có lẻ keo xác thực chỉ lần đầu đúng nghĩa hơn vì Tôi sử dụng File *.JSON nên nó quá đơn giản luôn

1/ Người dùng tạo APIKeys

2/ Tải File *.JSON

3/ chạy mã chỉ xác thực lần đầu tiên để lấy access token xong lưu vào đâu đó tuỳ thích

3/ các lần sau không cần nữa ... chờ ngày mail qua một ngày tôi thử đi thử lại xem sao xong tính tiếp

4/ hai hàm cơ bản khoãng 40 dòng code là xong ...

vài thông tin cơ bản sau thôi còn code trên Tools nào thì tuỳ chỉnh lại trên Tools đó là xong

View attachment 304091
Nếu dùng Service Account thì đơn giản hơn nhưng chỉ giới hạn trong phạm vi một tài khoản, muốn thay người dùng khác thì phải dùng JSON từ chính người dùng đó, cách này phù hợp với ứng dụng dạng daemon hơn.
 
Nếu dùng Service Account thì đơn giản hơn nhưng chỉ giới hạn trong phạm vi một tài khoản, muốn thay người dùng khác thì phải dùng JSON từ chính người dùng đó, cách này phù hợp với ứng dụng dạng daemon hơn.
1726798387851.png

Thêm mục tôi khoanh đó thành số 1000 xem sao ???!!!
 
1726798770780.png

Nếu có khó khăn gì bên ta luôn có Em ChatGPT tuyệt đỉnh rồi ... còn ta chỉ tư duy logis xong lắp ráp lại là xong
 
cách tạo một Service Accounts + API Keys vài thao tác chuột là xong rồi sử dụng hoài ...

quá đơn giản chỉ một ngày định hình khung xong ... thong thả và thư giản thử các kiểu một thời gian xem xét xong xuất một Hàm API bao quát nhật là xong

chuyển qua giai đoạn liên kết Google Sheets với máy chủ Web Server lấy dữ liệu từ PC lên Google Sheet ... này mới đang chơi

còn vài trò vặt kia chơi cho vui thôi
 
Lần chỉnh sửa cuối:
sau một ngày thử lại Token cũ OK và 3 máy = OK

quá đơn giản ... thử đi thử lại sau 1 tuần nếu ok tiến hành viết hàm bao quát nhất xong xuất API thì từ VBA cứ thế Call
 
Nó sang tới Google Drive rồi .. xong sẻ qua Gmail luôn cho đủ bộ .. đang dò list File Google Drive

cần quái gì uỷ quyền .. cho dù có cam kết gì đi nữa thì cũng làm tâm lý người dùng lo lắng

Không biết đặt tâm lý người sử dụng vào mình xem rồi biết thôi ...
y trang tâm lý khoá cửa xong gửi chìa khoá tay hàng xóm
_)()(-,,,,,,,


1726925570391.png
 
Lần chỉnh sửa cuối:
Mấy năm trước, Google bắt đầu chặn yêu cầu ủy quyền thông qua webview hoặc webview được nhúng vào WinForm, đại loại là thường trả về lỗi "This browser or app may not be secure". Mình có tra cứu trên GitHub và tìm thấy đoạn code mẫu C# .NET này của Google, nhìn chung logic như sau:
  1. Tạo uri yêu cầu ủy quyền.
  2. Tạo redirect uri (tức là uri chuyển hướng sau khi thực hiện xong ủy quyền) để theo dõi.
  3. Dùng lớp (class) HttpListener để theo dõi lưu lượng HTTP từ redirect uri trên.
  4. Tạo tiến trình (process) với uri yêu cầu ủy quyền để Windows mở bằng trình duyệt mặc định.
  5. Khi người dùng thực hiện ủy quyền xong (thành công hay thất bại) sẽ trả về redirect uri, HttpListener bắt được thông tin từ redirect uri và gửi thông báo đến trình duyệt rằng người dùng có thể đóng trình duyệt lại.
  6. Tiến hành phân tích thông tin nhận được, trao đổi access token nếu có authorization code, thông báo lỗi nếu có.
View attachment 304087
OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs
Cái này mình không rõ Delphi có thư viện nào tương tự như vậy không, gửi bạn tham khảo thử.
Rảnh tôi thử nhúng xem nhưng mọi cái đã bị chặn sạch kể cả TWebBrowser có lẻ do bảo mật tài khoản nên Google chặn lại

nhưng có rất nhiều cách mà không cần nhúng .. chỉ cần File JSON xong người dùng bấm nút là xong

Tới khúc này rồi mọi cái tự động sạch ... Viết máy chủ cho nó tự động lấy mã xác thực của Google xong lấy Token luôn


1727231203870.png
 
Rảnh tôi thử nhúng xem nhưng mọi cái đã bị chặn sạch kể cả TWebBrowser có lẻ do bảo mật tài khoản nên Google chặn lại

nhưng có rất nhiều cách mà không cần nhúng .. chỉ cần File JSON xong người dùng bấm nút là xong

Tới khúc này rồi mọi cái tự động sạch ... Viết máy chủ cho nó tự động lấy mã xác thực của Google xong lấy Token luôn


View attachment 304247
Nói chung là Google đã chặn hết các thể loại từ trình duyệt cho đến webview. Bạn có thể xem chi tiết ở đây trên Github: [UPDATE] Google Auth Flows and WebView2 #3828
Theo hướng dẫn từ Google mà tôi đã đề cập, tôi đã dựa vào đó để viết một script nhỏ xin quyền gửi email từ người dùng để gửi một email đơn giản bằng Gmail API. Script này được viết bằng PowerShell mặc dù hướng dẫn viết bằng C#, nhưng C# chính là đứa đẻ ra PowerShell nên viết code vẫn chạy tốt.
Script này chạy tốt trên Windows, nếu muốn chạy trên phiên bản PowerShell của Linux thì phải sửa biến môi trường $env: cho phù hợp với đường dẫn của Linux với thay đổi cách xác định kiểu MIME.
Mã:
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
1727237166836.png
 
Lần chỉnh sửa cuối:
Nói chung là Google đã chặn hết các thể loại từ trình duyệt cho đến webview. Bạn có thể xem chi tiết ở đây trên Github: [UPDATE] Google Auth Flows and WebView2 #3828 Theo hướng dẫn từ Google mà tôi đã đề cập, tôi đã dựa vào đó để viết một script nhỏ xin quyền gửi email từ người dùng để gửi một email đơn giản bằng Gmail API. Script này được viết bằng PowerShell mặc dù hướng dẫn viết bằng C#, nhưng C# chính là đứa đẻ ra PowerShell nên viết code vẫn chạy tốt. Script này chạy tốt trên Windows, nếu muốn chạy trên phiên bản PowerShell của Linux thì phải sửa biến môi trường $env: cho phù hợp với đường dẫn của Linux với thay đổi cách xác định kiểu MIME.
Mã:
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("
$($Text)
") $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 = @"
[HEADING=1]Test Mail[/HEADING]
This is a test mail.
"@ Send-GmailMessage -From "abc@gmail.com" -To "bcd@yahoo.com.vn" -Subject "Test Mail" -Body $HtmlBody -BodyFormat "Html" -AccessToken $AccessToken -HasAttach $false
View attachment 304251

sao rồi ... thấy bài trang trước có hỏi hổ trợ AccessToken có cần Mạnh hổ trợ thêm cho một hàm đơn giản lắm chỉ 1 tham số là File JSON là xong thôi

Mã:
Declare PtrSafe Function RetrieveAccessTokenFromJSON Lib "GoogleSheets64.dll" _
            (ByRef OAuthJsonPath As Variant) As Variant


Sub AccessToken_OAuthJsonPath()
    Dim OAuthJsonPath As Variant
    Dim AccessToken As Variant
  
    OAuthJsonPath = "C:\Google OAuth2\Desktop client 2.json"
    AccessToken = RetrieveAccessTokenFromJSON(OAuthJsonPath)
  
    Debug.Print AccessToken
End Sub

Muốn ghi dữ liệu lên Google Sheet chỉ cần một hàm trên là đủ rồi xong từ VBA viết mã sử dụng Http của Windows là xong thôi

Làm gì nhiều vẻ chuyện mệt ra ... còn nhanh thì viết Mã trong Delphi ghi dữ liệu lên Google Sheets
 
Lần chỉnh sửa cuối:
1727357794071.png

đơn giản lắm chỉ có thế thôi mà ....................--=0--=0--=0
 
Chốt lại chủ đề này quá đơn giản lại xếp vào ngăn kéo hay xó thôi ... hết trò chơi rồi chuyển game mới

ai có gì cần cứ hỏi tôi xem xét nếu khả năng có thể tôi sẻ hổ trợ

1/ Hàm lấy dữ liệu trả về các phương thức thuộc tính của ADODB Recordset từ đó người dùng tuỳ chỉnh các kiểu = xong
Mã:
Declare PtrSafe Function GoogleSheetAsRecordsetEx Lib "GoogleSheets64.dll" _
            (ByRef SheetId As Variant, _
             ByRef APIKey As Variant, _
             ByRef SheetName As Variant, _
             Optional ByVal ThreadCount As LongPtr = 0) As Variant

2/ Hổ trợ hàm lấy AccessToken để ghi dữ liệu vào Google Sheet = Xong ...
người dùng chỉ cần có thế xong sử dụng Http của Window ghi dữ liệu các kiểu lên Google Sheets tuỳ biến

Mã:
Declare PtrSafe Function RetrieveAccessTokenFromJSON Lib "GoogleSheets64.dll" _
            (ByRef OAuthJsonPath As Variant) As Variant


Sub AccessToken_OAuthJsonPath()
    Dim OAuthJsonPath As Variant
    Dim accessToken As Variant
   
    OAuthJsonPath = "C:\Google OAuth2\Desktop client 2.json"
    accessToken = RetrieveAccessTokenFromJSON(OAuthJsonPath)
   
    Debug.Print accessToken
End Sub
3/ Hàm Làm mới AccessToken = xong

Cơ bản ba hàm trên thôi là người dùng ôm cột múa lửa các kiểu trên VBA rồi ..
Nhưng sẻ chậm hơn xử lý mọi cái trong Delphi


4/ sẻ hổ trợ thêm hàm chuyển Data Excel thành JSON để ghi dữ liệu vào Google Sheets ( Mục này viết trên VBA khoãng 10 dòng code là xong ... nhưng chạy chậm thôi )

Mã:
Private Declare PtrSafe Function ConvertRangeToJSON Lib "GoogleSheets64.dll" _
    (ByVal Data As Variant) As Variant

Ai cần gì hay có ý kiến gì cứ mạnh dạn nêu .. nếu khả năng có thể Tôi sẻ hổ trợ
 
Status
Không mở trả lời sau này.
Web KT

Bài viết mới nhất

Back
Top Bottom