# OAuth2认证+谷歌客户端库调用gmail api处理业务

By [gaohongxiang](https://paragraph.com/@gaohongxiang-2) · 2023-02-13

---

OAuth2认证+谷歌客户端库调用gmail api处理业务
==============================

需求：使用程序来自动化处理邮箱，比如获取指定邮件、获取验证码等业务

两个方案

1.  模拟点击，但使用方式不打开网页来处理。这种可控因素多，效率低，访问外网速度慢，需要时间比较久
    
2.  走协议，访问gmail api 来获取
    

建议程序使用走协议的方案，不可控因素少，效率高。

> POP协议和gmail客户端开启邮件访问

授权访问授权，授权方式

1.  用户名+密码+开启安全性较低的应用的访问权限，从2020.05.30开始，google已不再支持此权限
    
2.  用户名+应用密码+开启二次验证，比较麻烦
    
3.  用户名+OAuth2验证
    

建议使用用户名+OAuth2验证的方式

编程层次也存在分析解决方案

1.  使用python编写，smtplib库发送邮件。imaplib库接收邮件。垃圾邮件库，处理比较垃圾库
    
2.  使用，qq等库，但是现在imbox，yagmail其他人写邮件的库，方便好用的邮箱2验证，gmail邮箱不好用，其他邮箱可以用这个库。
    
3.  谷歌开发的库，google\_auth\_oauthlib（OAuth2验证）、google-api-python-client（谷歌客户端库）
    

综上，解决方案为：

> python程序走协议通过用户名+OAuth2的验证方式，使用google\_auth\_oauthlib、google-api-python-client等谷歌客户端库来访问gmail api，实现业务需求。

以下为官方示例教程

*   使用 OAuth 2.0 访问 Google API：[https://developers.google.com/identity/protocols/oauth2](https://developers.google.com/identity/protocols/oauth2)
    
*   快速入门：[https://developers.google.com/gmail/api/quickstart/python](https://developers.google.com/gmail/api/quickstart/python)
    

具体流程如下

1.  Google API Console 获取 OAuth 2.0 从客户端连接。
    
2.  您的客户端应用程序从 Google 授权服务器请求访问令牌，从响应中获取令牌，
    
3.  的业务通知发送到您要访问的 Google API，验证后处理。
    

为了更好地理解自己的逻辑，官方提供了将 OAuth 2.0 与 Google 使用结合的互动式视频（包括使用您的客户端连接的选项）：[https://developers.google.com/authplayground/](https://developers.google.com/oauthplayground/)

Google API Console 获取 OAuth 2.0 客户端请求 从
---------------------------------------

谷歌API解析地址：[https://console.google.com/](https://console.cloud.google.com/)

1、创建并选择项目

![](https://storage.googleapis.com/papyrus_images/fa0731a1c12a9f27d80393c876ea050703d741ba4cf19447257572b5edbddfab.jpg)

2、创建证书

![](https://storage.googleapis.com/papyrus_images/3fe6cad2b10b0333f86473670859c8afd39e559525b6210abd39b2cb17a21ae3.jpg)

证书下载完整搜索项目目录下，可以重命名为`client_secret.json`

3、编辑OAuth同意画面，添加测试用户

![](https://storage.googleapis.com/papyrus_images/d6ede7f8ea6f980c7c938ecffc062ff5ae7160bebfcc72579ac2db36993bc663.jpg)

4、启用gmail api

![](https://storage.googleapis.com/papyrus_images/f558a9ee21faefa6787eb190ac553a52d4a32a3dd36917f98ac1482039e32162.jpg)

客户端应用从 Google 授权服务器请求访问令牌，从响应中添加令牌
----------------------------------

### 安装谷歌客户端库

      pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
    

官方库github地址

*   [https://github.com/googleapis/google-auth-library-python-oauthlib](https://github.com/googleapis/google-auth-library-python-oauthlib)
    
*   [https://github.com/googleapis/google-api-python-client](https://github.com/googleapis/google-api-python-client)
    

### 生成并存储备用标记

    from google_auth_oauthlib.flow import Flow
    import google.oauth2.credentials
    from googleapiclient.discovery import build
    import html2text
    import base64
    import json, sys, os 
    sys.path.append(os.getcwd()) # 根目录
    # 导入模块都从根目录开始写
    from accounts.adspower import *
    
    class OAuth2Unit(AdsPowerUnit):
        
        def __init__(self, ads_id):
            super(OAuth2Unit, self).__init__(ads_id)
    
        def create_refresh_token(self, gmail):
            """所有邮箱授权，生成刷新令牌，保存起来，以后直接用
            """
            # # 使用谷歌API的客户秘密文件创建流程
            flow = Flow.from_client_secrets_file(
                './client_secret.json',
                # 阅读、撰写、发送和永久删除你在Gmail的所有邮件
                scopes=['openid','https://mail.google.com/', 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'],
                redirect_uri='urn:ietf:wg:oauth:2.0:oob')
    
            # 告诉用户要去授权的URL
            auth_url, _ = flow.authorization_url(
                # 离线访问所请求范围的权限，可以刷新访问令牌，而无需重新提示用户授予权限
                access_type='offline',
                # 增量授权，建议打开
                include_granted_scopes='true')
            
            # 利用selenium自动获取code，这样可以全自动
            url = format(auth_url)
            self.driver.get(url)
            try:
                WebDriverWait(self.driver,10).until(EC.visibility_of_element_located((By.XPATH, "//div[text()='"+gmail+"']"))).click()
                WebDriverWait(self.driver,10).until(EC.visibility_of_element_located((By.XPATH, "//div/div/div/div/div/div/div/div/div/div/div/div[2]"))).click()
                time.sleep(2)
                # 勾选gmail权限
                WebDriverWait(self.driver,10).until(lambda x:self.driver.find_elements(by=By.CSS_SELECTOR, value="input"))[3].click()
                WebDriverWait(self.driver,10).until(lambda x:self.driver.find_elements(by=By.CSS_SELECTOR, value="button"))[2].click()
            except Exception as e:
                pass
            code = WebDriverWait(self.driver,10).until(EC.visibility_of_element_located((By.TAG_NAME, "textarea"))).get_attribute("value")
            print(code)
            # # 手动复制url得到code再粘贴到终端的方式。
            # # 终端获得的url复制到新邮箱所在adspower浏览器，新邮箱同意授权
            # print('Please go to this URL: {}'.format(auth_url))
            # # 用户得到的授权码填入此处，来获取访问令牌。
            # code = input('Enter the authorization code: ')
    
            flow.fetch_token(code=code)
            # credentials包含token（访问令牌）、refresh_token（刷新令牌）、client_id、client_secret等信息
            credentials = flow.credentials
    
            # 将刷新令牌保存到json文件
            # json.load()：将已编码的 JSON 字符串解码为 Python 对象
            # json.dump()：将 Python 对象编码成 JSON 字符串
            new_refresh_token = {gmail: credentials.refresh_token}
            with open('./accounts/mail/credential_tokens.json', 'r') as f:
                content = json.load(f)
                old_refresh_token = content['refresh_token']
                refresh_token = {**old_refresh_token, **new_refresh_token}
                content.update({"refresh_token": refresh_token})
    
            with open('./accounts/mail/credential_tokens.json', 'w') as f:
                json.dump(content, f, indent=4, ensure_ascii=False)
    
    if __name__ == '__main__':
    
        oauth = OAuth2Unit('adspower_id')
        # 这个谷歌邮箱是添加进OAuth同意屏幕的测试用户
        oauth.create_refresh_token("gmail_address")
    

此段代码结合了adspower指纹浏览器使用，实现完全自动化。如果不需要，把adspower相关部分删掉，使用手动复制url得到code再粘贴到终端的方式来生成刷新令牌（refresh\_token）

授权范围（scopes）决定了应用的权限，具体请参考：[https://developers.google.com/gmail/api/auth/scopes?hl=en](https://developers.google.com/gmail/api/auth/scopes?hl=en)

将刷新令牌保存到`credential_tokens.json`文件中备用，下次应用访问此用户就不用征求意见了。文件格式如下

    {
        "client_id": "xxx",
        "client_secret": "xxx",
        "token_uri": "https://oauth2.googleapis.com/token",
        "refresh_token": {
            "xxx@gmail.com": "xxx",
            "yyy@gmail.com": "yyy"
        }
    }
    

相关方法示例

*   google\_auth\_oauthlib.flow module：[https://google-auth-oauthlib.readthedocs.io/en/latest/reference/google\_auth\_oauthlib.flow.html#google\_auth\_oauthlib.flow.Flow.from\_client\_secrets\_file](https://google-auth-oauthlib.readthedocs.io/en/latest/reference/google_auth_oauthlib.flow.html#google_auth_oauthlib.flow.Flow.from_client_secrets_file)
    
*   credentials示例：[https://google-auth.readthedocs.io/en/latest/reference/google.oauth2.credentials.html#google.oauth2.credentials.Credentials](https://google-auth.readthedocs.io/en/latest/reference/google.oauth2.credentials.html#google.oauth2.credentials.Credentials)
    

将该令牌发送到Google Gmail API，完成业务
----------------------------

    class GmailUnit():
        def __init__(self, gmail, file_='./credential_tokens.json'):
            """获取授权,调用google gmail api
            """
            
            data = json.load(open(file_,'r',encoding="utf-8"))
            credentials = google.oauth2.credentials.Credentials(
                token=None,
                refresh_token=data['refresh_token'][gmail],
                client_id=data['client_id'],
                client_secret=data['client_secret'],
                token_uri=data['token_uri']
                )
            self.service = build('gmail', 'v1', credentials=credentials)
    
        def get_messages_by_query(self, user_id='me', label_ids=['INBOX'], query=''):
            try:
                response = self.service.users().messages().list(userId=user_id, labelIds=label_ids, q=query).execute()
                messages = []
                if 'messages' in response:
                    messages.extend(response['messages'])
                while 'nextPageToken' in response:
                    page_token = response['nextPageToken']
                    response = self.service.users().messages().list(userId=user_id, labelIds=label_ids, q=query, pageToken=page_token).execute()
                    messages.extend(response['messages'])
                # print(messages)
                # print(len(messages))
    
                for message in messages:
                    msg = self.service.users().messages().get(userId=user_id, id=message['id']).execute()
                    # print(msg)
                    # 正文
                    msg = msg['payload']['body']['data']
                    # print(msg)
                    # base64url解码
                    msg = base64.urlsafe_b64decode(msg).decode('utf-8')
                    print(msg)
            except Exception as e:
                print('An error occurred: %s' % e)
    
    if __name__ == '__main__':
    
        gmail = GmailUnit("xxx@gmail.com")
        #user_id='me', label_ids=['INBOX'], query=''
        gmail.get_messages_by_query(query='from:xxx subject:xxx')
    

\_\_init\_\_函数从`credential_tokens.json`文件中读取数据，获取授权

get\_messages\_by\_query函数调用gmail api处理业务

具体业务查看官方gmail api文档

*   google gmail api：[https://developers.google.com/gmail/api/reference/rest?apix=true、https://developers.google.com/resources/api-libraries/documentation/gmail/v1/python/latest/index.html](https://developers.google.com/gmail/api/reference/rest?apix=true%E3%80%81https://developers.google.com/resources/api-libraries/documentation/gmail/v1/python/latest/index.html)
    
*   可用于 Gmail 的搜索运算符（查询条件）：[https://support.google.com/mail/answer/7190?hl=en](https://support.google.com/mail/answer/7190?hl=en)
    

文件结构如下

    client_secret.json  # 下载的OAuth 2.0 客户端凭据
    credential_tokens.json  # 存放refresh_token等信息，获取授权用
    mail.py # OAuth2Unit获取refresh_token，GmailUnit处理业务
    adspower.py # adspower指纹浏览器相关，用不到请忽略，文末列出相关代码
    

其他参考示例

*   Python 读取gmail代码示例: [https://justcode.ikeepstudying.com/2019/09/python-读取gmail-python-搜索gmail-python操作gmail-how-to-access-gmail-using-python/](https://justcode.ikeepstudying.com/2019/09/python-%E8%AF%BB%E5%8F%96gmail-python-%E6%90%9C%E7%B4%A2gmail-python%E6%93%8D%E4%BD%9Cgmail-how-to-access-gmail-using-python/)
    
*   操作方法：在 Python 中使用 OAuth2 和 Gmail发送HTML邮件: [https://blog.macuyiko.com/post/2016/how-to-send-html-mails-with-oauth2-and-gmail-in-python.html](https://blog.macuyiko.com/post/2016/how-to-send-html-mails-with-oauth2-and-gmail-in-python.html)
    
*   Google OAuth 2.0 中文认证指南：[https://wiki.jikexueyuan.com/project/google-oauth-2/](https://wiki.jikexueyuan.com/project/google-oauth-2/)
    

adspower相关代码

    from selenium import webdriver
    from selenium.webdriver.chrome.service import Service
    from selenium.webdriver.chrome.options import Options
    from selenium.webdriver.support.wait import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver.common.action_chains import ActionChains
    from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
    import requests,time
    
    class AdsPowerUnit(object):
        """selenium操作adspower指纹浏览器
        """
    
        def __init__(self, ads_id):
            """启动浏览器（webdriver为adspower自带的，不必单独下载）
    
            通过adspower提供的api和自己的浏览器id来启动浏览器
    
            Attributes：
                ads_id:adspower浏览器id
            """
            self.ads_id = ads_id
            status = 'http://local.adspower.com:50325/status'
            open_url = "http://local.adspower.net:50325/api/v1/browser/start?user_id=" + self.ads_id
            self.close_url = "http://local.adspower.net:50325/api/v1/browser/stop?user_id=" + self.ads_id
    
            resp = requests.get(open_url).json()
            if resp["code"] != 0:
                print(resp["msg"])
                print("please check ads_id")
    
            
            # 启动浏览器后在返回值中拿到对应的Webdriver的路径resp["data"]["webdriver"]
            chrome_driver = Service(str(resp["data"]["webdriver"]))
    
            # selenium启动的chrome浏览器是一个空白的浏览器。chromeOptions是一个配置chrome启动属性的类，用来配置参数。
            chrome_options = webdriver.ChromeOptions()
            # adspower提供的debug接口,用于执行selenium自动化
            chrome_options.add_experimental_option("debuggerAddress", resp["data"]["ws"]["selenium"])
            # add_extension设置应用扩展。但是注意，adspower的扩展不能通过代码，是通过在客户端的应用程序里上传的，会应用到所有浏览器
            # chrome_options.add_extension("./metamask.crx")
            chrome_options.add_argument('--disable-gpu')
    
            #get直接返回，再也不等待界面加载完成
            desired_capabilities = DesiredCapabilities.CHROME
            desired_capabilities["pageLoadStrategy"] = "none"
    
            self.driver = webdriver.Chrome(service=chrome_driver, options=chrome_options)
           
            # 全屏
            self.driver.maximize_window()
    
            self.close_other_windows()
    
        def close_other_windows(self):
            """关闭无关窗口，只留当前窗口
            理论上下面代码会保留当前窗口，句柄没错。但是实际窗口却没有达到预期。不清楚具体原因。后续再研究。目前能做到的就是只保留一个窗口
            """
            current_handle = self.driver.current_window_handle
            all_handles = self.driver.window_handles
            for handle in all_handles:
                self.driver.switch_to.window(handle)
                if handle != current_handle:
                    self.driver.close()
    
        def quit(self):
            """关闭浏览器
            """
            time.sleep(5)
            self.driver.quit()
            requests.get(self.close_url)

---

*Originally published on [gaohongxiang](https://paragraph.com/@gaohongxiang-2/oauth2-gmail-api)*
