PHPMailer + Gmail OAuth2 寄信
小蛙有個維護的系統使用 PHPMailer 透過 Gmail 帳號密碼發送郵件,近期收到 Google 寄來的通知「自 2024 年 9 月 30 日起,應用程式必須採用 OAuth 才能存取 Google Workspace 帳戶。系統將不再支援使用密碼 (應用程式密碼除外) 的存取行為。」為了避免哪一天突然通通都只支援 OAuth,趁現在有一點時間趕快來改一下。
PHPMailer + Gmail OAuth 發信大概有幾個步驟:
- 安裝必要套件:使用 composer 在網站上安裝 phpmailer/phpmailer 及 league/oauth2-google 套件。
- 建立 OAuth 2.0 用戶端 ID 憑證:到 Goolge Cloud 建立一個專案,並開啟 Gmail API 與建立一個 OAuth 2.0 用戶端 ID 憑證。
- 取得 Refresh Token:設定
get_oauth_token.php
通過 OAuth 驗證,取得 Refresh Token。 - 使用 PHPMailer 寄信:使用 PHPMailer 搭配前幾步得到的資訊寄送郵件。
安裝必要套件
需使用 composer 安裝必要套件,沒有 composer 的話可參考這邊。
composer require phpmailer/phpmailer composer require league/oauth2-google
建立 OAuth 2.0 用戶端 ID 憑證
啟用 Gmail API
進入到 Google Cloud,點擊左上方專案下拉選單 -> 新增專案 -> 輸入專案名稱,只要自己能辨識的即可,下面若有機構、位置,也都必須填寫上去
專案建立完成後進入該專案,左側選擇「已啟用的 API 和服務」,點擊「啟用 API 和服務」
找到並啟用 Gmail API
建立 OAuth 用戶端 ID 憑證
成功啟用 Gmail API 後,左側進入「憑證」選單 -> 點擊「建立憑證」 -> OAuth 用戶端 ID
第一次建立需要先設定同意畫面,點擊「設定同意畫面」
我們的目的是完成至少一次 OAuth 驗證取得 Refresh Token,這邊選「內部」就可以了
應用程式名稱、使用者支援電子郵件及開發人員聯絡資訊為必填項目,其餘部份視情況填寫,填寫完成後點擊「儲存並繼續」。
第二步範圍部份直接留空,點擊「儲存並繼續」
OAuth 同意畫面建立完成
再回到「憑證」->「建立憑證」->「OAuth 用戶端 ID」
應用程式類型選擇「網頁應用程式」,名稱為必填,可以跟小蛙一樣輸入 PHPMailer,只要能辨識即可。「已授權的重新導向 URI」填入網站下 phpmailer 套件下的 get_oauth_token.php
路徑 (也可以把它拉出來到外層比較好處理的地方),小蛙這邊的例子是
https://xx.xxx/vendor/phpmailer/phpmailer/get_oauth_token.php
建立完成後會得到用戶端編號及用戶端密鑰,先存起來等一下馬上會用到。
取得 Refresh Token
找到 PHPMailer 下的 get_oauth_token.php
(預設應該會在網頁目錄下的 vender/phpmailer/phpmailer/
),PHPMailer 原始附帶的檔案會一直卡在 Provider missing 或 Invalid state,沒辦法正確取得 Refresh Token,因此小蛙對 get_oauth_token.php
做了一些修改,確保可以拿到 token。
<?php /** * PHPMailer - PHP email creation and transport class. * PHP Version 5.5 * @package PHPMailer * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project * @author Marcus Bointon (Synchro/coolbru) <[email protected]> * @author Jim Jagielski (jimjag) <[email protected]> * @author Andy Prevost (codeworxtech) <[email protected]> * @author Brent R. Matzelle (original founder) * @copyright 2012 - 2020 Marcus Bointon * @copyright 2010 - 2012 Jim Jagielski * @copyright 2004 - 2009 Andy Prevost * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License * @note This program is distributed in the hope that it will be useful - WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. */ /** * Get an OAuth2 token from an OAuth2 provider. * * Install this script on your server so that it's accessible * as [https/http]://<yourdomain>/<folder>/get_oauth_token.php * e.g.: http://localhost/phpmailer/get_oauth_token.php * * Ensure dependencies are installed with 'composer install' * * Set up an app in your Google/Yahoo/Microsoft account * * Set the script address as the app's redirect URL * If no refresh token is obtained when running this file, * revoke access to your app and run the script again. */ namespace PHPMailer\PHPMailer; // 用來看錯誤訊息以及刪除 opcache ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); @opcache_reset(); /** * Aliases for League Provider Classes * Make sure you have added these to your composer.json and run `composer install` * Plenty to choose from here: * @see https://oauth2-client.thephpleague.com/providers/thirdparty/ */ //@see https://github.com/thephpleague/oauth2-google use League\OAuth2\Client\Provider\Google; // 根據自己的 vendor 路徑進行調整 require '../../autoload.php'; session_start(); // 改成剛剛在 Google Cloud 上設定的值 $providerName = 'Google'; // 用戶端編號 $clientId = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com'; // 用戶端密碼 $clientSecret = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // 已授權的重新導向 URI $redirectUri = 'https://xxx.xxxx.xxx/get_oauth_token.php'; $_SESSION['provider'] = $providerName; $_SESSION['clientId'] = $clientId; $_SESSION['clientSecret'] = $clientSecret; $params = [ 'clientId' => $clientId, 'clientSecret' => $clientSecret, 'redirectUri' => $redirectUri, 'accessType' => 'offline' ]; $options = []; $provider = null; switch ($providerName) { case 'Google': $provider = new Google($params); $options = [ 'scope' => [ 'https://mail.google.com/' ] ]; break; } if (null === $provider) { exit('Provider missing'); } if (!isset($_GET['code'])) { //If we don't have an authorization code then get one $authUrl = $provider->getAuthorizationUrl($options); $_SESSION['oauth2state'] = $provider->getState(); header('Location: ' . $authUrl); exit; //Check given state against previously stored one to mitigate CSRF attack // 這段不註解掉的話,會一直卡在 Invalid state,沒辦法拿到正確的 token /*} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { error_log("3333"); error_log("state 3333 " . $_SESSION['oauth2state']); //unset($_SESSION['oauth2state']); //unset($_SESSION['provider']); exit('Invalid state');*/ } else { //unset($_SESSION['provider']); //Try to get an access token (using the authorization code grant) $token = $provider->getAccessToken( 'authorization_code', [ 'code' => $_GET['code'] ] ); //Use this to interact with an API on the users behalf //Use this to get a new access token if the old one expires echo 'Refresh Token: ', htmlspecialchars($token->getRefreshToken()); }
將內容貼上後,開啟瀏覽器連到該網址 (就是已授權的重新導向 URI) 開始進入 OAuth 登入流程,選擇要登入的帳號
確認登入
信任應用程式
最後會印出 Refresh Token: xxxxxxxxxx,這個 token 一定要存好,少了這個 PHPMailer 就沒辦法透過 OAuth 驗證寄信喔,原本以為 get_oauth_token.php
每次執行應該 “有機會” 去換一個可使用的 Token 回來,不過再執行一次會發現 Refresh Token: 變成空白。
到這邊就成功取得可以使用的 Token 了,回到 PHPMailer 寄信程式吧!
使用 PHPMailer 寄信
複製下面的檔案,視自身情況修改幾個地方:
- require(‘./vendor/autoload.php’);:將位置指向正確的路徑。
- $client_id:前面步驟取得的用戶端編號。
- $client_secret:前面步驟取得的用戶端密碼。
- $refresh_token:前面步驟取得的 Token。
- $email:前面步驟通過 OAuth 的信箱。
- 信件相關之寄件人、收件人、主旨、內容 … 等。
<?php // 用來看錯誤訊息以及刪除 opcache ini_set('display_errors', 1); ini_set('display_startup_errors', 1); error_reporting(E_ALL); @opcache_reset(); // 根據自己的路徑修改 composer 的載入位置 require('./vendor/autoload.php'); use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\SMTP; use PHPMailer\PHPMailer\Exception; use PHPMailer\PHPMailer\OAuth; use League\OAuth2\Client\Provider\Google; // 用戶端編號 $client_id = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com'; // 用戶端密碼 $client_secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // 從前一個步驟取得的 Token $refresh_token = '1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxg'; // OAuth 的信箱 $email = 'xxxxxxx@xxxxxxx'; $mail = new PHPMailer(true); $provider = new Google( [ 'clientId' => $client_id, 'clientSecret' => $client_secret, ] ); $mail->setOAuth( new OAuth( [ 'provider' => $provider, 'clientId' => $client_id, 'clientSecret' => $client_secret, 'refreshToken' => $refresh_token, 'userName' => $email, ] ) ); $mail->isSMTP(); $mail->SMTPDebug = 3; // 如果需要顯示debug內容,測完確定沒問題請改成 0 $mail->SMTPAuth = true; $mail->SMTPSecure = 'tls'; $mail->AuthType = 'XOAUTH2'; $mail->Host = "smtp.gmail.com"; $mail->Port = 587; $mail->CharSet = "utf-8"; $mail->setFrom('寄件人信箱', '寄件人名稱'); // 寄件人 $mail->addAddress('收件人信箱', '收件人名稱'); // 收件人 $mail->isHTML(true); // 寄送 HTML 格式郵件 $mail->Subject = "郵件主旨"; // 主旨 $mail->Body = "<Strong>測試</Strong>"; // 信件內容 $mail->AltBody = "測試"; // 無法顯示 HTML 時的內容 $mail->send(); ?>
一切就緒後連到該寄信的 PHP 頁面,就可以成功把信寄出囉!
延伸閱讀:Google 試算表批量發送 Email (免費版每日 50 封)