问题描述
最近想搞个 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)
文章评论
我其实需要实现的也只是通过python获取outlook的邮件。也有研究过您提到的oauth的方法,有太多的参数不知道怎么获取,也不知道怎么配置,就放弃了这个方法
@Eason 本文中也更新了使用oauth获取token的方法,这个可以在登陆的时候授予权限,并不需要在后台配置很多东西,可能会更方便一些。这篇文章也算是完善了很多,感谢您的访问!
@KAMINO 感谢大佬提供的支持,我这边先尝试去调一下。后续有问题再请教您~
"code":"InvalidAuthenticationToken","message":"Access token is empty.
您知道这是哪一步出错吗
@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 里可以看到。
@KAMINO 我debug过这个token,没发现有报错信息,也能正常体现出一个token串。唯一不太确定的就是这个tenant_id,具体是哪个,我找network的去查找对应信息,给了我很多不同的参数。也尝试过用租户ID。也是响应json里面有"code":"InvalidAuthenticationToken","message":"Access token is empty.这个报错的。
@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'))
@KAMINO 我现在都用的这个租户ID。
这个png图片看不到的
我请求过报这个{"error":{"code":"NoPermissionsInAccessToken","message":" token包含没有权限,或者权限不能被理解"
@Eason 我debug过这个token,没发现有报错信息,也能正常体现出一个token串。唯一不太确定的就是这个tenant_id,具体是哪个,我找network的去查找对应信息,给了我很多不同的参数。也尝试过用租户ID。也是响应json里面有"code":"InvalidAuthenticationToken","message":"Access token is empty.这个报错的。
@Eason 问题越来越复杂了哈哈哈,我觉得 token 也没有问题,NoPermissionsInAccessToken 这个错误大概是因为没有配置好API权限,看一下是不是2.6条中没有点击 "授予管理员同意"。
@KAMINO 哈哈哈不瞒您说,我研究这个东西,已经搞了2周了。我也是这样猜测的所以在联系IT同事帮忙确认一下权限问题
AttributeError: 'MailFolder' object has no attribute 'top'
请问大佬,报这个错怎么处理
@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()
@KAMINO 感谢!再请问一下tenant_id='xxx.onmicrosoft.com' #工作账户系统分配的域名 xxx.onmicrosoft.com。这里指的是什么参数,是您2.2图中的tenant ID吗
@Eason tenant_id 是系统分配的域名,我用的是 E5 订阅,可以在 https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/Domains 看到 .onmicrosoft.com 后缀的域名。
@KAMINO 如果tenant_ID是域名的话,这行代码是不是有问题? authority_url = f'https://login.microsoftonline.com/{tenant_id}'
@Eason 如果tenant_ID是域名的话,这行代码是不是有问题? authority_url = f'https://login.microsoftonline.com/{tenant_id}'
@Eason 还有请问你是在做一个 web 应用还是控制台应用,这个获取 token 的方法不是很好,获取到的 token 是全局可用的,能获取到所有用户的邮件,如果密钥信息泄露可能会有安全问题。更好的方法是搭建一个 oauth 服务器来获取 token,用这种方法获取到的 token 可以只对获取邮件的账号有效。