本文档详细介绍了如何通过编程方式调用 Google Gmail API 来发送电子邮件。整个过程不依赖于 SMTP,而是使用现代的 OAuth 2.0 授权流程和 REST API 调用,这使得它可以在任何网络环境(包括限制了 SMTP 端口的云服务器)下工作。
目录
- 前置条件
- [第一步:启用 API 和创建 OAuth 凭据](#第一步:启用 API 和创建 OAuth 凭据)
- [第二步:获取授权 Token](#第二步:获取授权 Token)
- 第三步:发送邮件
- 附录:常见问题(FAQ)
1. 前置条件
在开始之前,请确保您拥有:
- 一个 Google 账户 :任何标准的
@gmail.com账户或 Google Workspace 账户都可以。这个账户将是发送邮件的账户。 - 一个 Google Cloud Platform (GCP) 项目:这是调用所有 Google API 的管理中心。即使您的代码不运行在 GCP 上,也必须拥有一个 GCP 项目来启用 API 和创建凭据。如果您没有,可以免费创建一个。
- Python 环境:本地或服务器上需要安装 Python 3。
2. 第一步:启用 API 和创建 OAuth 凭据
此步骤的目标是在您的 GCP 项目中创建一个"应用身份证",即 credentials.json 文件。
- 登录 GCP 控制台 :访问 Google Cloud Console 并选择您的项目。
- 启用 Gmail API :
- 在顶部的搜索栏中输入
Gmail API。 - 在搜索结果中选择 "Gmail API" 并点击 "启用" 按钮。
- 在顶部的搜索栏中输入
- 创建 OAuth 客户端 ID :
- 导航到 "API 和服务" > "凭据" 页面。
- 点击 "+ 创建凭据" 并选择 "OAuth 客户端 ID"。
- 如果系统提示您配置"OAuth 同意屏幕",请按以下步骤操作:
- 选择 "外部" (External)。
- 填写必要的应用信息(应用名称、用户支持电子邮件等)。
- 在"测试用户"步骤中,添加您将要用来发邮件的那个 Gmail 账户。
- 保存并继续。
- 返回创建凭据页面,在 "应用类型" (Application type) 中选择 "桌面应用" (Desktop app)。
- 给它一个名称(例如,"Gmail Sender Script")。
- 点击 "创建"。
- 下载凭据文件 :
- 创建成功后,会弹出一个窗口显示您的客户端 ID 和密钥。请不要复制它们。
- 点击右侧的 "下载 JSON" 按钮。
- 将下载的文件重命名为
credentials.json并将其保存在您的项目文件夹中。此文件非常机密,切勿泄露。
3. 第二步:获取授权 Token
此步骤的目标是让您的 Gmail 账户授权给上一步创建的应用,并生成一把"钥匙",即 token.json 文件。这是一个一次性的本地操作。
-
创建
get_token.py脚本 :在与
credentials.json相同的文件夹中,创建以下 Python 脚本:python# get_token.py import os.path from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from google_auth_oauthlib.flow import InstalledAppFlow # 定义需要的权限范围,https://mail.google.com/ 是最高权限 SCOPES = ["https://mail.google.com/"] def main(): """ 引导用户完成 OAuth 2.0 授权流程并生成 token.json。 """ creds = None # 如果 token.json 已存在,则先加载它 if os.path.exists("token.json"): creds = Credentials.from_authorized_user_file("token.json", SCOPES) # 如果没有有效的凭据,则启动新的授权流程 if not creds or not creds.valid: # 如果 token 已过期且有 refresh_token,则刷新它 if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: # 否则,从 credentials.json 启动新的网页授权流程 flow = InstalledAppFlow.from_client_secrets_file( "credentials.json", SCOPES) creds = flow.run_local_server(port=0) # 将新获取的凭据写入 token.json 文件 with open("token.json", "w") as token: token.write(creds.to_json()) print("token.json 文件已成功生成或刷新。") if __name__ == "__main__": main() -
安装必要的库 :
在终端中运行以下命令:
bashpip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib -
运行脚本 :
在终端中运行脚本:
bashpython get_token.py -
在浏览器中完成授权:
- 脚本会自动在您的浏览器中打开一个 Google 登录页面。
- 登录您希望用来发邮件的 Gmail 账户。
- 您可能会看到一个"此应用未经 Google 验证"的警告。这是正常的,因为这是您自己的应用。请点击 "高级" > "转至...(不安全)"。
- 在下一个页面中,点击 "允许",授予权限。
- 授权成功后,您会看到 "The authentication flow has completed" 的消息,并且终端会显示 "token.json 文件已成功生成"。
现在,您的项目文件夹中应该有 credentials.json 和 token.json 两个文件了。
4. 第三步:发送邮件
这是最终发送邮件的脚本。我们使用 Python 内置的 urllib 库,因为它在有网络代理的环境下表现更稳定。
-
创建
send_email_raw_api.py脚本 :在同一个文件夹中,创建以下脚本:
python# send_email_raw_api.py import json import base64 import logging import urllib.request from email.mime.text import MIMEText from urllib.error import URLError, HTTPError # 配置日志记录 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def get_access_token(proxy_url=None): """使用 refresh_token 获取新的 access_token""" logging.info("尝试刷新 access_token...") try: with open('token.json', 'r') as f: token_data = json.load(f) with open('credentials.json', 'r') as f: # 兼容从 GCP 下载的两种 JSON 格式 creds_data = json.load(f).get('installed') or json.load(f).get('web') params = { 'client_id': creds_data['client_id'], 'client_secret': creds_data['client_secret'], 'refresh_token': token_data['refresh_token'], 'grant_type': 'refresh_token' } data = json.dumps(params).encode('utf-8') req = urllib.request.Request( creds_data['token_uri'], data=data, headers={'Content-Type': 'application/json'} ) # 配置代理(如果提供) if proxy_url: proxy_handler = urllib.request.ProxyHandler({'https': proxy_url, 'http': proxy_url}) opener = urllib.request.build_opener(proxy_handler) else: opener = urllib.request.build_opener() with opener.open(req, timeout=30) as response: result = json.loads(response.read().decode('utf-8')) logging.info("成功刷新 access_token。") return result['access_token'] except Exception as e: logging.error(f"刷新 access_token 失败: {e}", exc_info=True) return None def send_email_raw(access_token, to, subject, message_text, proxy_url=None): """使用原始 HTTP 请求和 access_token 发送邮件""" logging.info("使用原始 API 请求发送邮件...") try: message = MIMEText(message_text, 'plain', 'utf-8') message['to'] = to message['from'] = 'me' message['subject'] = subject raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode() api_url = 'https://www.googleapis.com/gmail/v1/users/me/messages/send' headers = { 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json' } body = {'raw': raw_message} data = json.dumps(body).encode('utf-8') req = urllib.request.Request(api_url, data=data, headers=headers, method='POST') # 配置代理(如果提供) if proxy_url: proxy_handler = urllib.request.ProxyHandler({'https': proxy_url, 'http': proxy_url}) opener = urllib.request.build_opener(proxy_handler) else: opener = urllib.request.build_opener() logging.info(f"向 {api_url} 发送 POST 请求...") with opener.open(req, timeout=30) as response: response_data = json.loads(response.read().decode('utf-8')) logging.info(f"邮件发送成功!Message ID: {response_data.get('id')}") return response_data except (URLError, HTTPError) as e: logging.error(f"发送邮件时发生网络错误: {e}", exc_info=True) if hasattr(e, 'read'): logging.error(f"错误详情: {e.read().decode()}") except Exception as e: logging.error(f"发送邮件时发生未知错误: {e}", exc_info=True) return None if __name__ == '__main__': # --- 配置 --- RECIPIENT_EMAIL = "收件人邮箱@example.com" EMAIL_SUBJECT = "来自应用的 API 调用问候" EMAIL_BODY = "这是一封通过纯 Python urllib 库调用 Gmail API 发送的邮件。" # 如果需要通过代理发送,请设置代理地址,否则设为 None # PROXY_ADDRESS = "http://127.0.0.1:7890" PROXY_ADDRESS = None # --- 配置结束 --- # 1. 获取最新的 access_token access_token = get_access_token(proxy_url=PROXY_ADDRESS) # 2. 如果成功获取,则发送邮件 if access_token: send_email_raw( access_token=access_token, to=RECIPIENT_EMAIL, subject=EMAIL_SUBJECT, message_text=EMAIL_BODY, proxy_url=PROXY_ADDRESS ) -
修改配置并运行:
- 打开
send_email_raw_api.py文件。 - 在文件底部的
if __name__ == '__main__':部分,修改RECIPIENT_EMAIL,EMAIL_SUBJECT, 和EMAIL_BODY的值。 - 如果您需要通过代理发送,请修改
PROXY_ADDRESS。 - 保存文件并在终端运行:
bashpython send_email_raw_api.py - 打开
如果一切顺利,您将在终端看到成功发送的日志,并且收件人会收到邮件。
5. 附录:常见问题(FAQ)
-
Q: 为什么必须要有 GCP 项目?
- A: GCP 项目是您作为"开发者"的身份标识。所有 Google API 的启用、凭据创建和用量管理都必须依托于一个 GCP 项目。
-
Q:
credentials.json和token.json有什么区别?请用通俗的比喻解释。- A: 当然,这是一个核心概念。我们可以用一个"机器人管家"的例子来理解:
-
credentials.json是什么?------ 机器人的"出厂说明书 & 身份证"- 这个文件是在您的 GCP 工厂里生成的,它定义了您的机器人管家是谁。
- 它包含了客户端 ID (相当于机器人的身份证号 ,是公开的)和客户端密钥 (相当于机器人的出厂机密代码,是绝对私密的)。
- 这份"说明书"是永久有效的,它只代表机器人的身份,但本身没有任何权力。任何不认识这个机器人的家庭都不会让它进门。
-
token.json是什么?------ 特定家庭的"门禁卡"- 这个文件是在您(家庭主人)亲自"面试"过机器人,并同意授权后,才颁发给这个特定机器人的"门禁卡"。
- 它包含了
access_token(一张临时通行证 ,可能一小时就过期)和refresh_token(一张长期身份卡)。 - 当机器人需要进门工作时,它会出示"临时通行证" (
access_token)。如果通行证过期了,它就会出示它的"长期身份卡" (refresh_token) 和自己的"身份证" (credentials.json),去物业处(Google)换一张新的"临时通行证"。
-
它们如何协同工作?
- 您的代码(机器人)在工作时,必须同时携带 自己的"身份证" (
credentials.json) 和您家颁发的"门禁卡" (token.json)。 - 如果门禁卡里的临时通行证过期了,它就需要用门禁卡里的长期身份卡加上自己身份证上的机密代码,才能换到新的临时通行证。
- 总结:
credentials.json是"我是谁",token.json是"我被谁授权可以做什么"。两者缺一不可。
- 您的代码(机器人)在工作时,必须同时携带 自己的"身份证" (
-
- A: 当然,这是一个核心概念。我们可以用一个"机器人管家"的例子来理解:
-
Q: 我可以把这两个 JSON 文件提交到 Git 仓库吗?
- A: 绝对不能! 这两个文件都是高度机密的,泄露它们等同于泄露了您应用乃至用户邮箱的控制权。请务必将它们添加到
.gitignore文件中。
- A: 绝对不能! 这两个文件都是高度机密的,泄露它们等同于泄露了您应用乃至用户邮箱的控制权。请务必将它们添加到
-
Q:
token.json会过期吗?- A: 是的,
token.json里的access_token通常只有1小时的有效期。但它包含的refresh_token有效期很长。我们提供的脚本会自动使用refresh_token去换取新的access_token,所以您几乎无感。
- A: 是的,