使用ServiceAccount与Google API Python发送Gmail遇400错误求助
补充说明
接受原答案后发现,我的问题针对免费Gmail账号(此前不知道付费与免费账号存在差异),而原答案适用于付费账号(该答案本身正确)。此答案让我了解到两者差异,并指引我找到适配自身情况的正确方案——使用应用专用密码。
问题详情
尝试通过ServiceAccount调用Google API发送邮件时,遇到如下错误:
An error occurred: <HttpError 400 when requesting https://gmail.googleapis.com/gmail/v1/users/me/drafts?alt=json returned "Precondition check failed.". Details: "[{'message': 'Precondition check failed.', 'domain': 'global', 'reason': 'failedPrecondition'}]">
所用代码
from __future__ import print_function import os.path from google.auth.transport.requests import Request from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError from oauth2client.service_account import ServiceAccountCredentials import base64 from email.message import EmailMessage # If modifying these scopes, delete the file token.json. SCOPES = ['https://mail.google.com/'] def main(): """Shows basic usage of the Gmail API. Lists the user's Gmail labels. """ creds = None creds = ServiceAccountCredentials.from_json_keyfile_name( """path_to_cred_file.json""", SCOPES) try: # Call the Gmail API service = build('gmail', 'v1', credentials=creds) message = EmailMessage() message.set_content('This is automated draft mail') message['To'] = 'somemail@gmail.com' message['From'] = 'somemail@gmail.com' message['Subject'] = 'Automated draft' # encoded message encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode() create_message = { 'message': { 'raw': encoded_message } } # pylint: disable=E1101 draft = service.users().drafts().create(userId="me", body=create_message).execute() except HttpError as error: # TODO(developer) - Handle errors from gmail API. print(f'An error occurred: {error}') if __name__ == '__main__': main()
解决方案
这个错误的核心原因是免费Gmail账号不支持服务账号(ServiceAccount)直接调用Gmail API,服务账号仅对Google Workspace(付费)账号开放,且需要配置域范围委派。免费账号需使用以下两种方案之一:
方案1:切换到OAuth 2.0授权码流程
修改代码,通过用户授权的方式获取API访问权限,替代服务账号:
from __future__ import print_function import os.path from google.auth.transport.requests import Request from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build from googleapiclient.errors import HttpError import base64 from email.message import EmailMessage # 修改权限范围后需删除token.json SCOPES = ['https://mail.google.com/'] def main(): creds = None # 读取已保存的凭据(首次运行后自动生成) if os.path.exists('token.json'): from google.oauth2.credentials import Credentials creds = Credentials.from_authorized_user_file('token.json', SCOPES) # 无有效凭据时触发用户登录授权 if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: # 替换为你的OAuth客户端密钥文件路径 flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES) creds = flow.run_local_server(port=0) # 保存凭据供后续使用 with open('token.json', 'w') as token: token.write(creds.to_json()) try: service = build('gmail', 'v1', credentials=creds) message = EmailMessage() message.set_content('这是自动生成的草稿邮件') message['To'] = '目标邮箱@gmail.com' message['From'] = '你的免费Gmail账号@gmail.com' message['Subject'] = '自动草稿' encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode() create_message = { 'message': { 'raw': encoded_message } } draft = service.users().drafts().create(userId="me", body=create_message).execute() print(f"草稿创建成功,ID: {draft['id']}") except HttpError as error: print(f'发生错误: {error}') if __name__ == '__main__': main()
注意:需在Google Cloud控制台创建OAuth客户端ID,下载credentials.json文件。
方案2:使用应用专用密码(需启用两步验证)
若你的免费Gmail账号已启用两步验证,可生成应用专用密码,通过SMTP发送邮件:
import smtplib from email.message import EmailMessage def send_email(): msg = EmailMessage() msg.set_content('这是测试邮件内容') msg['Subject'] = '测试邮件' msg['From'] = '你的免费Gmail账号@gmail.com' msg['To'] = '目标邮箱@gmail.com' # 使用应用专用密码登录SMTP服务器 with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp: smtp.login('你的免费Gmail账号@gmail.com', '你的应用专用密码') smtp.send_message(msg) print("邮件发送成功") if __name__ == '__main__': send_email()
注意:应用专用密码需在Google账号安全设置中生成。
内容的提问来源于stack exchange,提问作者yaodav

