python获取outlook邮件

2021年10月16日 1164点热度 0人点赞 18条评论

问题描述

最近想搞个 python 获取 outlook 邮箱邮件的程序,这里介绍 imap 客户端和使用 Microsoft Graph Api 两种获取outlook邮件的方法。方法2更优雅。注意本文中的操作需要全局管理员。

20220707 更新 oauth 方法获取 access_token。

IMAP客户端

首先开启接收邮件账户的imap功能。

使用全局管理员登录 https://admin.microsoft.com/ ,左侧菜单选择用户、活跃用户,然后点击接收邮件的用户,右侧弹出面板中点击邮件、管理电子邮件应用。选中 IMAP 后保存(其他选中项不用关闭)。

编写 python 代码获取邮件,其中 mail_id 是由服务端提供,可以通过记录 mail_id 来判断是否为新邮件。

import imaplib
import email

imap_server = "outlook.office365.com"
username = ""
password = ""

client = imaplib.IMAP4_SSL(imap_server) #SSL
#conn = imaplib.IMAP4(imap_server)

client.login(user=username, password=password) #登录
client.select('INBOX') #选择目录
_, query = client.search(None, 'ALL') #查询邮件
mail_id_list = [item.decode('utf8') for item in query[0].split()] #获取邮件ID列表
for mail_id in mail_id_list:
    _, data = client.fetch(str(mail_id), '(RFC822)') #根据MailID获取邮件内容
    mail = email.message_from_string(data[0][1].decode()) #解析邮件为email对象
    print(mail) #然后可以从email对象中获取邮件信息,建议搜索文档查看

Microsoft Graph Api获取邮件

2.1 首先使用全局管理员登录 https://portal.azure.com/ ,进入 Azure Active Directory 控制面板,选择左侧菜单应用注册,点击新注册。填写名称并选择账户类型后点击注册。

2.2 回到应用概述可以查看应用程序(客户端) ID(client_id)。

2.3 点击左侧证书和密码、新客户端密码,选择截止期限、添加。

2.4 保存客户端密码的值(client_secret)。

2.5 点击左侧 API 权限,添加权限,Microsoft Graph,应用程序权限,搜索 Mail 后选中读取邮件相关权限。

2.6 然后点击 代表xxx授予管理员同意。

2.7 安装 Office365-REST-Python-Client 库。

pip install Office365-REST-Python-Client
pip install pytz

2.8 编写代码获取邮件。

import msal
from office365.graph_client import GraphClient

tenant_id='xxx.onmicrosoft.com' #工作账户系统分配的域名 xxx.onmicrosoft.com
client_id='' #2.2中获取的client_id
client_secret='' #2.4中保存的client_secret
mail_address='' #接收邮件的邮箱

def acquire_token():
    authority_url = f'https://login.microsoftonline.com/{tenant_id}'
    app = msal.ConfidentialClientApplication(
        authority=authority_url,
        client_id=f'{client_id}',
        client_credential=f'{client_secret}'
    )
    token = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
    return token

client = GraphClient(acquire_token)
messages = client.users[mail_address].mail_folders['INBOX'].messages.top(10).get().execute_query()

for mail in messages:
    print(mail.properties) #邮件内容

参考 Use query parameters to customize responses - Microsoft Graph | Microsoft Docs 配置筛选。

参考 Use query parameters to customize responses - Microsoft Graph | Microsoft Docs 配置排序。

修改 top(10) 来获取指定数量的邮件。

使用 oauth2.0 获取 access_token

方法2中使用 msal 获取的 access_token 是对工作账户内所有账号都有权限的,可能会有安全隐私等问题。使用 oauth 获取的 access_token 可以仅对登陆的账号有效,应用注册时也无需配置权限。

3.1 注册应用

重定向 url 选择 web,地址填写 http://localhost:5000/authorize

3.2 保存 client_id

3.3 创建并保存客户端密码

客户端密码的值为 client_secret。

3.4 查看保存 tenant_id

tenant_id 可以是 https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Properties 页面中的租户 ID 。

或者是 https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Domains 页面中的以 .onmicrosoft.com 为后缀的域名。

3.5 登录账号获取 access_token 和 refresh_token

安装 flask authlib

pip install flask
pip install authlib

创建 login.py,修改 tanant_id client_id client_secret 参数。

from authlib.integrations.flask_client import OAuth
from flask import Flask, url_for
import json

tenant_id = ''
client_id = ''
client_secret = ''

app = Flask(__name__)

app.secret_key = 'mc9gyQRGEumAu5XKgk42oESC6ZBYv3U6'
app.config['SESSION_TYPE'] = 'filesystem'

oauth = OAuth(app)

oauth.register(
    name='azure',
    client_id=client_id,
    client_secret=client_secret,
    access_token_url=f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token',
    authorize_url=f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize',
    client_kwargs={
        'scope': 'offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.ReadBasic https://graph.microsoft.com/Mail.ReadWrite'},
)


@app.route('/')
def login():
    redirect_uri = url_for('authorize', _external=True)

    return oauth.azure.authorize_redirect(redirect_uri)


@app.route('/authorize')
def authorize():
    token = oauth.azure.authorize_access_token()
    print(token)
    with open("token.json", "w", encoding="utf8") as f:
        f.write(json.dumps(token)) # token 保存到了 token.json 文件中

    return token


app.run(host='localhost', port=5000, debug=True)

运行 login.py ,浏览器打开 http://localhost:5000/ 跳转到登陆页面,登录获取邮件的账号,并点击接收许可。

python login.py

成功后网页、控制台都会输出 token 信息,也会保存到 token.json 文件,可自行编写保存 token 的方法,比如保存到文件或数据库。

登录成功后可以 ctrl + c 关闭 login.py 进程,该 token 可以通过 refresh_token 更新 token 以达到长期使用的效果,所以只需要登录这一次。

3.6 使用 Microsoft Graph Api 获取邮件

创建 mail.py,修改 tanant_id client_id client_secret 参数。可以修改 save_token get_token 函数自定义 token 保存读取的方法。

import json
import requests
import time

from authlib.integrations.requests_client import OAuth2Session

tenant_id = ''
client_id = ''
client_secret = ''


def save_token(token):
    # 自定义保存token的方法
    with open('token.json', 'w', encoding='utf8') as f:
        f.write(json.dumps(token))


def load_token():
    # 自定义读取token的方法
    with open('token.json', 'r', encoding='utf8') as f:
        token = json.loads(f.read())
        return token


def get_token():
    token = load_token()

    if token['expires_at'] > int(time.time()):

        return token
    else:
        client = OAuth2Session(
            client_id=client_id,
            client_secret=client_secret,
            scope='offline_access https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.ReadBasic https://graph.microsoft.com/Mail.ReadWrite'
        )
        new_token = client.refresh_token(
            f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token',
            refresh_token=token['refresh_token']
        )

        save_token(token)

        return new_token


token = get_token()

print(token)

response = requests.get(f'https://graph.microsoft.com/v1.0/me/messages?%24top=1&%24skip=0', headers={
    'Authorization': 'Bearer %s' % token['access_token']
})

print(response.content.decode('utf8'))

# 也可以使用 Office365-REST-Python-Client 库获取邮件。
# pip install Office365-REST-Python-Client 
from office365.graph_client import GraphClient
client = GraphClient(get_token)
messages = client.me.mail_folders['INBOX'].messages.get().execute_query()

for mail in messages:
    print(mail.properties)

KAMINO

这个人很懒,什么都没留下

文章评论

  • Eason

    我其实需要实现的也只是通过python获取outlook的邮件。也有研究过您提到的oauth的方法,有太多的参数不知道怎么获取,也不知道怎么配置,就放弃了这个方法

    2022年7月7日
    • KAMINO

      @Eason 本文中也更新了使用oauth获取token的方法,这个可以在登陆的时候授予权限,并不需要在后台配置很多东西,可能会更方便一些。这篇文章也算是完善了很多,感谢您的访问!

      2022年7月8日
      • Eason

        @KAMINO 感谢大佬提供的支持,我这边先尝试去调一下。后续有问题再请教您~

        2022年7月8日
  • Eason

    "code":"InvalidAuthenticationToken","message":"Access token is empty.
    您知道这是哪一步出错吗

    2022年7月7日
    • KAMINO

      @Eason 不如先看一下 acquire_token 函数的返回值,应该会有错误信息。
      token = acquire_token()
      print(token)
      https://i.bmp.ovh/imgs/2022/07/07/3abc92fbab0efa77.png

      tenant_id 填写 .onmicrosoft.com 后缀的域名应该是没有问题的,authority_url 就是 https://login.microsoftonline.com/xxx.onmicrosoft.com

      如果域名不行的话可以试试租户ID,在 https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Properties 里可以看到。

      2022年7月7日
      • Eason

        @KAMINO 我debug过这个token,没发现有报错信息,也能正常体现出一个token串。唯一不太确定的就是这个tenant_id,具体是哪个,我找network的去查找对应信息,给了我很多不同的参数。也尝试过用租户ID。也是响应json里面有"code":"InvalidAuthenticationToken","message":"Access token is empty.这个报错的。

        2022年7月7日
        • KAMINO

          @Eason 根据 msal 的文档( https://github.com/vgrem/Office365-REST-Python-Client#working-with-outlook-api ) tenant_id 是域名或者租户ID都可以。

          获取到 token 直接用 requests 请求官方api 试试。

          https://i.bmp.ovh/imgs/2022/07/07/653442f61dc5d5af.png

          token = acquire_token()

          response = requests.get(f'https://graph.microsoft.com/v1.0/users/{mail_address}/messages?%24top=1&%24skip=0', headers={
          'Authorization': 'Bearer %s' % token['access_token']
          })

          print(response.content.decode('utf8'))

          2022年7月7日
          • Eason

            @KAMINO 我现在都用的这个租户ID。
            这个png图片看不到的
            我请求过报这个{"error":{"code":"NoPermissionsInAccessToken","message":" token包含没有权限,或者权限不能被理解"

            2022年7月7日
    • Eason

      @Eason 我debug过这个token,没发现有报错信息,也能正常体现出一个token串。唯一不太确定的就是这个tenant_id,具体是哪个,我找network的去查找对应信息,给了我很多不同的参数。也尝试过用租户ID。也是响应json里面有"code":"InvalidAuthenticationToken","message":"Access token is empty.这个报错的。

      2022年7月7日
      • KAMINO

        @Eason 问题越来越复杂了哈哈哈,我觉得 token 也没有问题,NoPermissionsInAccessToken 这个错误大概是因为没有配置好API权限,看一下是不是2.6条中没有点击 "授予管理员同意"。

        2022年7月7日
        • Eason

          @KAMINO 哈哈哈不瞒您说,我研究这个东西,已经搞了2周了。我也是这样猜测的所以在联系IT同事帮忙确认一下权限问题

          2022年7月7日
  • Eason

    AttributeError: 'MailFolder' object has no attribute 'top'
    请问大佬,报这个错怎么处理

    2022年7月7日
    • KAMINO

      @Eason 非常抱歉,这个是我写错了,应该为
      messages = client.users[mail_address].mail_folders['INBOX'].messages.top(10).get().execute_query()
      top(10) 是指 https://docs.microsoft.com/en-us/graph/query-parameters#odata-system-query-options 中的 $top,获取10条数据。配合 skip(10) 可以完成分页。
      messages = client.users[mail_address].mail_folders['INBOX'].messages.top(10).skip(10).get().execute_query()
      当然也可以都不加。
      messages = client.users[mail_address].mail_folders['INBOX'].messages.get().execute_query()

      2022年7月7日
      • Eason

        @KAMINO 感谢!再请问一下tenant_id='xxx.onmicrosoft.com' #工作账户系统分配的域名 xxx.onmicrosoft.com。这里指的是什么参数,是您2.2图中的tenant ID吗

        2022年7月7日
        • KAMINO

          @Eason tenant_id 是系统分配的域名,我用的是 E5 订阅,可以在 https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Domains 看到 .onmicrosoft.com 后缀的域名。

          2022年7月7日
          • Eason

            @KAMINO 如果tenant_ID是域名的话,这行代码是不是有问题? authority_url = f'https://login.microsoftonline.com/{tenant_id}'

            2022年7月7日
        • Eason

          @Eason 如果tenant_ID是域名的话,这行代码是不是有问题? authority_url = f'https://login.microsoftonline.com/{tenant_id}'

          2022年7月7日
          • KAMINO

            @Eason 还有请问你是在做一个 web 应用还是控制台应用,这个获取 token 的方法不是很好,获取到的 token 是全局可用的,能获取到所有用户的邮件,如果密钥信息泄露可能会有安全问题。更好的方法是搭建一个 oauth 服务器来获取 token,用这种方法获取到的 token 可以只对获取邮件的账号有效。

            2022年7月7日