使用 SMTP 使用我的 Gmail 帐户发送电子邮件的范围XOAUTH2范围混淆

Scopes confusion using SMTP to send email using my Gmail account with XOAUTH2

提问人:Booboo 提问时间:11/15/2023 最后编辑:Linda Lawton - DaImToBooboo 更新时间:11/15/2023 访问量:37

问:

我的应用程序有一个现有模块,我用于发送电子邮件,该模块访问 SMTP 服务器并使用用户(电子邮件地址)和密码进行授权。现在我正在尝试使用Gmail使用我的Gmail帐户做同样的事情,为了论证,我们说是[email protected](实际上是不同的东西)。

首先,我创建了一个 Gmail 应用程序。在有点令人困惑的同意屏幕上,我开始添加“敏感”或“受限”的范围。如果我想让应用程序“生产”,我被告知它必须经过验证过程,并且我必须提供某些文档。这不适合我,因为我,这个帐户的所有者,只是为了以编程方式发送电子邮件而尝试连接到它。我他们为桌面应用程序创建了凭据并将其下载到文件credentials.json

接下来,我使用以下代码获取了一个访问令牌:

from google_auth_oauthlib.flow import InstalledAppFlow

SCOPES = ['https://mail.google.com/']

def get_initial_credentials(*, token_path, credentials_path):
        flow = InstalledAppFlow.from_client_secrets_file(credentials_path, SCOPES)
        creds = flow.run_local_server(port=0)

        with open(token_path, 'w') as f:
            f.write(creds.to_json())

if __name__ == '__main__':
    get_initial_credentials(token_path='token.json', credentials_path='credentials.json')

浏览器窗口打开,说这不是一个经过验证的应用程序,我有机会“回到安全”,但我点击了高级链接并最终获得了我的令牌。

然后,我尝试使用以下代码发送电子邮件:

import smtplib
from email.mime.text import MIMEText

import base64
import json

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow

SCOPES = ['https://www.googleapis.com/auth/gmail.send']

def get_credentials(token_path):

    with open(token_path) as f:
        creds = Credentials.from_authorized_user_info(json.load(f), SCOPES)

    if not creds.valid:
        creds.refresh(Request())
        with open(token_path, 'w') as f:
            f.write(creds.to_json())

    return creds


def generate_OAuth2_string(access_token):
    auth_string = f'user=booboo\1auth=Bearer {access_token}\1\1'
    return base64.b64encode(auth_string.encode('utf-8')).decode('ascii')

message = MIMEText('I need lots of help!', "plain")
message["From"] = '[email protected]'
message["To"] = '[email protected]'
message["Subject"] = 'Help needed with Gmail'

creds = get_credentials('token.json')
xoauth_string = generate_OAuth2_string(creds.token)

with smtplib.SMTP('smtp.gmail.com', 587) as conn:
    conn.starttls()
    conn.docmd('AUTH', 'XOAUTH2 ' + xoauth_string)
    conn.sendmail('booboo', ['[email protected]'], message.as_string())

这可行,但请注意,我使用了不同的作用域 https://www.googleapis.com/auth/gmail.send 而不是用于获取初始访问令牌的 https://mail.google.com/

然后,我编辑了应用程序以添加范围 https://www.googleapis.com/auth/gmail.send。这需要我将应用程序置于测试模式。我不理解添加“测试用户”的部分,也就是说,我不知道我可以/应该在这里输入什么。然后,我生成了新的凭据和新的令牌,如上所述。然后,当我发送电子邮件时,我看到(调试已打开):

...
reply: b'535-5.7.8 Username and Password not accepted. Learn more at\r\n'
reply: b'535 5.7.8  https://support.google.com/mail/?p=BadCredentials l19-20020ac84a93000000b0041b016faf7esm2950068qtq.58 - gsmtp\r\n'
reply: retcode (535); Msg: b'5.7.8 Username and Password not accepted. Learn more at\n5.7.8  https://support.google.com/mail/?p=BadCredentials l19-20020ac84a93000000b0041b016faf7esm2950068qtq.58 - gsmtp'
...

但我从未发送过密码,而是发送了XOAUTH2授权字符串。我不知道这是否是因为我没有添加测试用户而发生的。就其价值而言,我不相信这个新令牌已经过期,因此它没有刷新。

我没有尝试过,但是如果我将应用程序设置为“生产”,它会起作用吗?同样,我不想使用Gmail进行整个验证过程。不幸的是,除了我想定义一个范围更受限制的应用程序并使用它之外,我没有一个具体的问题,但如果不经过此验证,这似乎是不可能的。有什么建议吗?

python oauth-2.0 gmail google-oauth smtp-auth

评论

0赞 Linda Lawton - DaImTo 11/15/2023
看看这个 youtu.be/t1U3pwIBAIM?si=1Wbnw-3fwvZFI0jC

答:

1赞 Linda Lawton - DaImTo 11/15/2023 #1

好的,首先,这将是一个单用户应用程序。作为开发人员,您将是唯一使用它的人,而您只是以编程方式发送电子邮件,就可以开始澄清一些事情。

  1. 您无需验证此应用程序。是的,您只需要像以前一样通过该屏幕。不用担心。not a verified application
  2. 通过单击按钮在生产环境中设置应用程序。将使你能够请求不会过期的刷新令牌。你想要这个。同样,请忽略显示您需要验证不需要的应用的屏幕。send to production
  3. 测试用户,只要仅使用创建项目的用户登录,就不需要测试用户。忽略它。
  4. 它只是您使用应用程序使用范围https://mail.google.com/
  5. 不要担心将范围添加到 google cloud 项目,它只是为了验证。重要的是代码中的内容。

好了,一切都清楚了。

您现在有两个选择。

  1. XOauth2,这就是你现在正在做的事情。
  2. 应用程序密码。只需在您的Google帐户上创建一个应用程序密码,然后使用它来代替您的实际Google密码,您现有的代码就可以了。我如何使用 Python 发送电子邮件。在2.2分钟内平坦!

Xoauth2

如果你想使用 XOauth2,那么你可以使用 Google api python 客户端库来帮助你获取所需的授权令牌

#   To install the Google client library for Python, run the following command:
#   pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib


from __future__ import print_function

import base64
import os.path
import smtplib
from email.mime.text import MIMEText

import google.auth.exceptions
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.errors import HttpError

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://mail.google.com/']

# usr token storage

USER_TOKENS = 'token.json'

# application credentials

CREDENTIALS = 'C:\YouTube\dev\credentials.json'


def getToken() -> str:
    """ Gets a valid Google access token with the mail scope permissions. """

    creds = None

    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists(USER_TOKENS):
        try:
            creds = Credentials.from_authorized_user_file(USER_TOKENS, SCOPES)
            creds.refresh(Request())
        except google.auth.exceptions.RefreshError as error:
            # if refresh token fails, reset creds to none.
            creds = None
            print(f'An error occurred: {error}')
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                CREDENTIALS, SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open(USER_TOKENS, 'w') as token:
            token.write(creds.to_json())

    try:

        return creds.token
    except HttpError as error:
        # TODO(developer) - Handle errors from authorization request.
        print(f'An error occurred: {error}')


def generate_oauth2_string(username, access_token, as_base64=False) -> str:

    # creating the authorization string needed by the auth server.
    #auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)

    auth_string = 'user=' + username + '\1auth=Bearer ' + access_token + '\1\1'
    if as_base64:
        auth_string = base64.b64encode(auth_string.encode('ascii')).decode('ascii')
    return auth_string


def send_email(host, port, subject, msg, sender, recipients):
    access_token = getToken()
    auth_string = generate_oauth2_string(sender, access_token, as_base64=True)

    msg = MIMEText(msg)
    msg['Subject'] = subject
    msg['From'] = sender
    msg['To'] = ', '.join(recipients)

    server = smtplib.SMTP(host, port)
    server.starttls()
    server.docmd('AUTH', 'XOAUTH2 ' + auth_string)
    server.sendmail(sender, recipients, msg.as_string())
    server.quit()


def main():
    host = "smtp.gmail.com"
    port = 587

    user = "[email protected]"

    subject = "Test email Oauth2"
    msg = "Hello world"
    sender = user
    recipients = [user]
    send_email(host, port, subject, msg, sender, recipients)


if __name__ == '__main__':
    main()

评论

0赞 Booboo 11/15/2023
我的XOAUTH2代码不包括使用该模块获取新访问令牌的逻辑,因为我不是使用此软件的最终用户,因此我指望刷新访问令牌永远不会有问题。因此,设置应用程序密码是要走的路。我设置了两步验证,但现在在屏幕上的任何地方都看不到添加应用程式密码的选项。根据 Google 的说法,在 3 个原因中,似乎只有一个是可能的:您登录了工作、学校或其他组织帐户。什么会使它成为工作或组织帐户?InstalledAppFlow
1赞 Booboo 11/15/2023
更新信息:当我创建帐户时,我可能确实表明它是用于工作或业务的。我关闭了“业务功能”。我最终找到了可以添加应用程序密码的位置,并且它起作用了!