背景
最近研究了microsoft graph,azure sdk for python,masl。用来发送、查询邮件。文档的零散、庞大、详尽对开发学习形成巨大的阻力。
关联图
选择一种认证、鉴权方式
- MSAL、microsoft graph api通常返回一个access_token
- azure identity返回一个包含了token和各种信息的对象;
操作应用
- 拿着access_token去请求microsoft graph的REST api操作各种应用
- 拿着access_token拼成xoauth2,使用语言包的api连接邮箱
- azure identity返回的对象,通过sdk提供的各种方法操作应用;
通用环境设置
python虚拟环境
开启一个python虚拟环境
python3.12 -m venv {虚拟环境名字}
该命令会在执行的地方创建一个名为
{虚拟环境名字}
的文件夹,一般包括这些个文件:guorong.huang@{主机名} pythonvenv-mail % ls -l total 8 drwxr-xr-x 14 guorong.huang staff 448 7 31 12:07 bin drwxr-xr-x 3 guorong.huang staff 96 7 31 12:06 include drwxr-xr-x 3 guorong.huang staff 96 7 31 12:06 lib -rw-r--r-- 1 guorong.huang staff 317 7 31 12:06 pyvenv.cfg
- 命令在那里执行都可以,本着方便管理的目的,要么创建一个文件夹,所有创建虚拟环境的命令都在里面执行,要么在项目根目录下执行创建命令
使用虚拟环境 - Way 1
source {虚拟环境目录}/{虚拟环境名字}/bin/activate
这样就开启、激活了虚拟环境,注意到用户名前多了虚拟环境名
guorong.huang@{主机名} mail % source pythonvenv-mail/bin/activate (pythonvenv-mail) guorong.huang@{主机名} mail %
- 使用
pip list
能看到全局环境安装的包都没了,现在只列出虚拟环境内的包 - 现在使用
pip install {package}
就是只在虚拟环境里安装 - 现在执行
python3 pythonFileName.py
就是在虚拟环境下运行代码
关闭
deactivate
注意到用户名前的虚拟环境名消失
guorong.huang@{主机名} mail % source pythonvenv-mail/bin/activate (pythonvenv-mail) guorong.huang@{主机名} mail % deactivate guorong.huang@{主机名} mail %
使用虚拟环境 - Way 2
在开启虚拟环境,安装好包以后,可以在代码里将包路径纳入系统环境变量里。如果直接在环境变量里改,那等于是全局安装了。
import sys
sys.path.append('/{虚拟环境目录}/{虚拟环境名字}/lib/python3.12/site-packages')
修改ide环境
不修改会导致vscode不能正确识别包,导致报错和无法自动联想
如何选择:
修改vscode的设置:
导出、导入一个虚拟环境/创建一个新的一样的虚拟环境
参考这个:https://www.reddit.com/r/learnpython/comments/pyoc7v/transfer...
#on the existing one
pip freeze > requirements.txt
#on the new one
pip install -r requirements.txt
开启ssl链接
不像使用http请求,使用request
或者urllib.request
可以主动关闭ssl验证。我暂时没找到使用sdk能关闭ssl的选项。
典型的ssl验证失败报错:
azure.core.exceptions.ServiceRequestError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1000)
更新证书
参考:https://support.pyxll.com/hc/en-gb/articles/18949393753363--S...
以下操作在全局或者虚拟均可
安装pip-system-certs:pip install pip-system-certs
更新certifi,这3种命令都可以试试:使用国内镜像;指定target;
1135 pip3.12 install --upgrade certifi
1138 pip3.12 install --upgrade certifi -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com
1139 pip3.12 install --upgrade certifi -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.com --target=./pythonvenv-mail/lib/python3.12/site-packages
查找证书位置
import certifi
print(certifi.where())
添加环境变量
代码内添加:
import os
# os.environ['SSL_CERT_FILE'] = '/{虚拟环境目录}/{虚拟环境名字}/lib/python3.12/site-packages/certifi/cacert.pem'
当然也可以直接在系统变量里添加,视情况而定:.zprofile
或.zshrc
或.zbashrc
或.bash_profile
:
echo `SSL_CERT_FILE="/{虚拟环境目录}/{虚拟环境名字}/lib/python3.12/site-packages/certifi/cacert.pem"` >> ~/.zprofile
source ~/.zprofile
实施 - Road 1
这是第一条走通的路,我的路线:azure identity➡️microsoft graph➡️graph sdk for python
安装sdk
官方文档:https://learn.microsoft.com/en-us/graph/sdks/sdk-installation
就一句话:pip install msgraph-sdk
我的全局环境不知道哪里有毛病,所以开一个python的虚拟环境安装sdk
虚拟环境不虚拟,就是和全局环境隔离开来的环境,只安装项目所需的包
使用不同认证方式创建带有token的对象
官方文档:https://learn.microsoft.com/en-us/graph/sdks/choose-authentic...
这里直接复制黏贴官方文档里的代码就ok,有几点注意事项如下:
scopes
可以直接写权限,例如这样写scopes = ['IMAP.AccessAsUser.All','Mail.Read','Mail.ReadWrite','Mail.Send']
或者还可以使用url自动获取全部权,不同区域graph的url是不一样的,参见:https://learn.microsoft.com/en-us/graph/deployments
scopes = ['https://microsoftgraph.chinacloudapi.cn/.default']
我使用的
UsernamePasswordCredential
因为公司限制的,但其中会有报错:azure.core.exceptions.ClientAuthenticationError: Authentication failed: AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'.
要求使用
'client_assertion' or 'client_secret'
作为请求参数,查看UsernamePasswordCredential
文档:https://learn.microsoft.com/en-us/python/api/azure-identity/a...是没有这两个参数的。千辛万苦找到这篇问答:https://github.com/Azure/azure-sdk-for-python/issues/35386可以添加client_credential
指定:credential = UsernamePasswordCredential( tenant_id=tenant_id, client_id=client_id, client_credential=client_secret, username=username, password=password, authority='login.chinacloudapi.cn', )
不同地区的azure使用不同的
authority
地址。参见:https://learn.microsoft.com/en-us/python/api/azure-identity/a...。如下两种方式均可:credential = UsernamePasswordCredential( {其他参数} authority='login.chinacloudapi.cn', )
from azure.identity import AzureAuthorityHosts credential = UsernamePasswordCredential( {其他参数} authority=AzureAuthorityHosts.AZURE_CHINA, )
更换gragh api请求的endpoint
我们的路径是通过sdk操作应用,sdk后台请求的endpoint默认是Microsoft Graph global service的,参见:https://learn.microsoft.com/en-us/graph/sdks/national-clouds?...。
需要在创建认证对象前,添加request adapter
,将其修改为中国区endpoint。这篇有很多参考价值:https://github.com/microsoftgraph/msgraph-sdk-python/issues/672
auth_provider = AzureIdentityAuthenticationProvider(credential, scopes=scopes)
adapter = GraphRequestAdapter(auth_provider)
adapter.base_url = 'https://microsoftgraph.chinacloudapi.cn/v1.0'
graph_client = GraphServiceClient(request_adapter=adapter, scopes=scopes)
发送邮件
构建邮件主体
https://learn.microsoft.com/en-us/graph/api/user-sendmail?vie...
request_body = SendMailPostRequestBody(
message = Message(
subject = "Meet for lunch?",
body = ItemBody(
content_type = BodyType.Text,
content = "The new cafeteria is open.",
),
to_recipients = [
Recipient(
email_address = EmailAddress(
address = "recipient1@gg.com",
),
),
# Recipient(
# email_address = EmailAddress(
# address = "recipient2@gg.com",
# ),
# ),
],
# cc_recipients = [
# Recipient(
# email_address = EmailAddress(
# address = "cc_recipient@gg.com",
# ),
# ),
# ],
),
# save_to_sent_items = False,
)
使用asyncio/await发送邮件
asyncio文档:https://docs.python.org/zh-cn/3.12/library/asyncio-task.html
async def some_async_function():
response = await graph_client.me.send_mail.post(request_body)
print(response)
asyncio.run(some_async_function())
完整示例/需要导入的包
import sys
# sys.path.append('{虚拟环境包路径}')
import os
os.environ['SSL_CERT_FILE'] = '{证书路径}' # 指定证书文件路径
import tracemalloc
tracemalloc.start()
import asyncio
import certifi
print(certifi.where())
from azure.identity.aio import ClientSecretCredential
from azure.identity import UsernamePasswordCredential
from msgraph import GraphServiceClient
from msgraph import GraphRequestAdapter
from azure.identity import AzureAuthorityHosts
from kiota_authentication_azure.azure_identity_authentication_provider import AzureIdentityAuthenticationProvider
from msgraph.generated.users.item.send_mail.send_mail_post_request_body import SendMailPostRequestBody
from msgraph.generated.models.message import Message
from msgraph.generated.models.item_body import ItemBody
from msgraph.generated.models.body_type import BodyType
from msgraph.generated.models.recipient import Recipient
from msgraph.generated.models.email_address import EmailAddress
scopes = ['https://microsoftgraph.chinacloudapi.cn/.default']
# Values from app registration
client_id = "{client_id}"
client_secret = "{client_secret}"
tenant_id = "{tenant_id}"
username = '{邮箱用户名}'
password = '{邮箱密码}'
credential = UsernamePasswordCredential(
tenant_id=tenant_id,
client_id=client_id,
client_credential=client_secret,
username=username,
password=password,
authority='login.chinacloudapi.cn',
)
auth_provider = AzureIdentityAuthenticationProvider(credential, scopes=scopes)
adapter = GraphRequestAdapter(auth_provider)
adapter.base_url = 'https://microsoftgraph.chinacloudapi.cn/v1.0'
graph_client = GraphServiceClient(request_adapter=adapter, scopes=scopes)
request_body = SendMailPostRequestBody(
message = Message(
subject = "Meet for lunch?",
body = ItemBody(
content_type = BodyType.Text,
content = "The new cafeteria is open.",
),
to_recipients = [
Recipient(
email_address = EmailAddress(
address = "recipient1@gg.com",
),
),
# Recipient(
# email_address = EmailAddress(
# address = "recipient2@gg.com",
# ),
# ),
],
# cc_recipients = [
# Recipient(
# email_address = EmailAddress(
# address = "cc_recipient@gg.com",
# ),
# ),
# ],
),
# save_to_sent_items = False,
)
async def some_async_function():
response = await graph_client.me.send_mail.post(request_body)
print(response)
asyncio.run(some_async_function())
tracemalloc.stop()
实施 - Road 2
今天走通了另外一条路线:MSAL➡️microsoft graph➡️REST api
完整示例/需要导入的包
这条路线更加简洁,我觉得直接上代码就行了:
# coding: utf-8
import os
os.environ['SSL_CERT_FILE'] = '{证书路径}' # 指定证书文件路径
import json
import urllib.request
import base64
import requests
import msal
# Values from app registration
client_id = "{client_id}"
client_secret = "{client_secret}"
tenant_id = "{tenant_id}"
authority = f"https://login.chinacloudapi.cn/{tenant_id}"
scope = ["https://microsoftgraph.chinacloudapi.cn/.default"]
# Get an access token
app = msal.ConfidentialClientApplication(authority=authority, client_id=client_id, client_credential=client_secret)
result = app.acquire_token_by_username_password({邮箱用户名},{邮箱密码},scope)
# result = app.acquire_token_for_client(scope)
access_token = result['access_token']
print(json.dumps(result, indent=4))
# Send the email
endpoint = f'https://microsoftgraph.chinacloudapi.cn/v1.0/me/sendMail'
email_data = {
"message": {
"subject": "test email from graph",
"body": {
"contentType": "Text",
"content": "well well well",
},
"toRecipients": [
{
"emailAddress": {
"address": "{接受人邮箱地址}"
}
}
]
}
}
headers = {
'Content-Type': 'application/json',
"Authorization": f"Bearer {access_token}",
}
response = requests.post(endpoint, json=email_data, headers=headers)
if response.status_code == 202:
print("Email sent successfully")
else:
print(f"Failed to send email: {response.status_code}")
print(response.text)
print(response.headers)
- 需要安装的包:msal、request(可选)
- 如何安装包可以参考
#实施 - Road 1➡️##安装SDK➡️###开启一个python虚拟环境
msal.ConfidentialClientApplication
和app.acquire_token_by_username_password
的文档:app.acquire_token_by_username_password
是一种token获取方式,有很多其他方式可以参见上一条中的文档REST api
可以参见这个文档:https://learn.microsoft.com/zh-cn/graph/api/user-sendmail?vie...。文档中有各种各样的api用来操作各种各样的应用,不局限于邮件,选择示例中的http
方式请求,就能够随心所欲的玩耍了- 最终的发送邮件也是使用http请求,使用
request包
或者urllib.request
都可以
实施 - Road 3 - 失败
之前尝试的路线:microsoft graph api➡️smtp➡️package for python
这是一个失败的例子,我认为是因为服务端关闭了smtp认证,或者权限没开。
完整示例/需要导入的包/报错
# coding: utf-8
import sys
import os
os.environ['SSL_CERT_FILE'] = '{证书路径}' # 指定证书文件路径
import json
import copy
import string
import urllib.parse
import urllib.request
from datetime import datetime, timedelta
import logging
import ssl
from argparse import ArgumentParser
import base64
import requests
import smtplib
import email
from email.header import Header
from email.mime.text import MIMEText
from html.parser import HTMLParser
from html.entities import name2codepoint
import re
import webbrowser
# #ssl._create_default_https_context = ssl._create_unverified_context
# #context = ssl._create_unverified_context
# context = ssl.create_default_context()
# context.check_hostname=False
# context.verify_mode=ssl.CERT_NONE
# Values from app registration
client_id = "{client_id}"
client_secret = "{client_secret}"
tenant_id = "{tenant_id}"
url = f'https://login.partner.microsoftonline.cn/{tenant_id}/oauth2/token'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'login.partner.microsoftonline.cn',
}
datas = {
"client_id": client_id,
"client_secret": client_secret,
"grant_type": "password",
"resource": "https://partner.outlook.cn",
"username": "{邮箱用户名}",
"password": "{邮箱密码}",
}
# token_r = requests.post(url, headers=headers, data=datas)
# print(token_r)
data = urllib.parse.urlencode(datas)
Data = data.encode('ascii')
# print(data)
req = urllib.request.Request(url=url, data=Data, headers=headers, method="POST")
# print(req)
response = urllib.request.urlopen(req)
# response = urllib.request.urlopen(req)
responseJson = json.loads(response.read().decode("utf-8"))
print(json.dumps(responseJson, indent=4))
# XOAUTH2认证字符串拼接
bearerString = "user={邮箱用户名}\x01auth=Bearer " + responseJson['access_token'] + "\x01\x01"
bearBase64 = base64.b64encode(bearerString.encode('ascii')).decode('ascii')
# 连接邮件服务器
smtp_server = 'smtp.partner.outlook.cn'
# smtp_port = 465
smtp_port = 587
subject = "plain text by smtp client"
FROM = "{发件人地址}"
TO = "{收件人地址}"
message = MIMEText('plain text body', 'plain', 'utf-8')
message['Subject'] = Header(subject, 'utf-8')
message['From'] = Header(FROM, 'utf-8')
message['To'] = Header(TO, 'utf-8')
smtp_connection = smtplib.SMTP(smtp_server, smtp_port)
smtp_connection.set_debuglevel(2)
smtp_connection.ehlo()
smtp_connection.starttls()
smtp_connection.ehlo()
# XOAUTH2方式登陆
smtp_connection.docmd('AUTH', "XOAUTH2 " + bearBase64)
smtp_connection.sendmail(FROM, [TO], message.as_string())
print(smtp_connection.esmtp_features)
smtp_connection.quit()
报错示例:
09:35:20.798392 reply: b'535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Tenant. Visit https://aka.ms/smtp_auth_disabled for more information. [ZQ0PR01CA0019.CHNPR01.prod.partner.outlook.cn 2024-08-06T01:35:20.764Z 08DCB55624A149F7]\r\n'
- 所以我觉得是因为被限了,但是问了人家人家打马虎眼,无法证实。
- 可以参考这篇文档:https://mp.weixin.qq.com/s?__biz=MzU0MzUxMzU2NA==&mid=2247485...
- 管理员如何开启,看这篇官方文档:https://learn.microsoft.com/en-us/exchange/clients-and-mobile...
实施 - Road 4
今天又走通了一条新路线:microsoft graph api➡️microsoft graph➡️REST api
完整示例/需要导入的包
# coding: utf-8
import os
os.environ['SSL_CERT_FILE'] = '{证书路径}' # 指定证书文件路径
import json
import urllib.parse
import urllib.request
import requests
# Values from app registration
client_id = "{client_id}"
client_secret = "{client_secret}"
tenant_id = "{tenant_id}"
url = f'https://login.chinacloudapi.cn/{tenant_id}/oauth2/token'
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Host': 'login.partner.microsoftonline.cn',
}
datas = {
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://microsoftgraph.chinacloudapi.cn/.default",
"grant_type": "password",
"resource": "https://microsoftgraph.chinacloudapi.cn",
"username": "{邮箱用户名}",
"password": "{邮箱密码}",
}
data = urllib.parse.urlencode(datas)
Data = data.encode('ascii')
# print(data)
req = urllib.request.Request(url=url, data=Data, headers=headers, method="POST")
# print(req)
response = urllib.request.urlopen(req)
# response = urllib.request.urlopen(req)
responseJson = json.loads(response.read().decode("utf-8"))
print(json.dumps(responseJson, indent=4))
endpoint = f'https://microsoftgraph.chinacloudapi.cn/v1.0/me/sendMail'
email_data = {
"message": {
"subject": "test email from graph",
"body": {
"contentType": "Text",
"content": "well well well",
},
"toRecipients": [
{
"emailAddress": {
"address": "{接受人邮箱地址}"
}
}
]
}
}
headers = {
'Content-Type': 'application/json',
"Authorization": f"Bearer {responseJson['access_token']}",
}
response = requests.post(endpoint, json=email_data, headers=headers)
if response.status_code == 202:
print("Email sent successfully")
else:
print(f"Failed to send email: {response.status_code}")
print(response.text)
print(response.headers)
- 翻到一篇极好的文档,囊括了OAUTH2.0所有的认证方式:https://learn.microsoft.com/en-us/entra/identity-platform/v2-...
- 在按照文档造轮子的时候,出现这样的报错:
urllib.error.HTTPError: HTTP Error 400: Bad Request
。因为请求需要指定aud
/resource
但文档中没有写,像这样指定:"resource": "https://partner.outlook.cn",
之后可能会有这样的会有这样的报错:
{"error":{"code":"InvalidAuthenticationToken","message":"Access token validation failure. Invalid audience.","innerError":{"date":"2024-08-07T02:26:29","request-id":"ac793dc9-6ca0-463b-a536-0bf6feb7c5fa","client-request-id":"ac793dc9-6ca0-463b-a536-0bf6feb7c5fa"}}}
前半句
Access token validation failure.
字面意思access_token有问题,但实际没有问题。重点在后半句Invalid audience.
,啥是audience?进入网站:https://jwt.ms。这个网站可以用来翻译access_token,翻译出来各种信息:
观察到第一行的aud
字段在代码里使用resource
指定的,这个地址明显不能通达,修改为中国区地址解决报错:"resource": "https://microsoftgraph.chinacloudapi.cn",
REST api
的用法可以参考其他使用REST api
的 实施 - Road
参考
- 解决Python安装库时出现的Requirement already satisfied问题:https://blog.csdn.net/qq_16906867/article/details/105558288
- 使用pip报错:Could not fetch URL https://pypi.org/simple/selenium/: There was a problem confirming the ss:https://blog.csdn.net/zkbaba/article/details/109188004
- Resolving SSLCertVerificationError: certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)’))) Ensuring Secure API Connections in Python:https://medium.com/@vkmauryavk/resolving-sslcertverificatione...
- msgraph-sdk-python源码:https://github.com/microsoftgraph/msgraph-sdk-python/blob/main/msgraph/graph_service_client.py
- 解决 Microsoft Graph 授权错误:https://learn.microsoft.com/zh-cn/graph/resolve-auth-errors?v...
启发:
- 【Azure Developer】使用Microsoft Graph API创建用户时候遇见“401 : Unauthorized”“403 : Forbidden”:https://www.cnblogs.com/lulight/p/14350649.html
- 【Azure Developer】Azure Graph SDK获取用户列表的问题: SDK中GraphServiceClient如何指向中国区的Endpoint:https://microsoftgraph.chinacloudapi.cn/v1.0:https://www.cnblogs.com/lulight/p/14638876.html
- SMTP Authentication error while while sending mail from outlook using python language:https://stackoverflow.com/questions/60910104/smtp-authentication-error-while-while-sending-mail-from-outlook-using-python-lan
- Force user to select account on Authorization MS Graph:https://stackoverflow.com/questions/68813511/force-user-to-select-account-on-authorization-ms-graph
python文档:
- http.client --- HTTP 协议客户端:https://docs.python.org/zh-cn/3/library/http.client.html#http...
- 协程与任务:https://docs.python.org/zh-cn/3.12/library/asyncio-task.html
- webbrowser — Convenient web-browser controller:https://docs.python.org/3/library/webbrowser.html
- 【Python】Http Post请求四种请求体的Python实现:https://www.cnblogs.com/Detector/p/9404391.html
- Python 运行时警告:启用 tracemalloc 查看对象分配跟踪:https://www.imooc.com/article/339410
smtp连接:
- What SMTP Authentication Is and Why You Can’t Ignore It:https://mailtrap.io/blog/smtp-auth/
- smtplib --- SMTP 协议客户端:https://docs.python.org/zh-cn/3/library/smtplib.html
- python如何发送邮件?smtplib库介绍!:https://www.w3cschool.cn/article/98812600.html
- python smtp 密码认证 smtplib python教程:https://blog.51cto.com/u_16213718/10410056
- python发邮件详解,smtplib和email模块详解:https://cloud.tencent.com/developer/article/2147621
- Smtp Oauth With Python:https://www.cnblogs.com/CQman/p/17020556.html
- Sending mail from Python using SMTP:https://stackoverflow.com/questions/64505/sending-mail-from-python-using-smtp
- Python办公自动化 -- Python发送电子邮件和Outlook的集成:https://blog.csdn.net/u014740628/article/details/135061728
- python smtplib源码:https://github.com/python/cpython/blob/3.12/Lib/smtplib.py#L720
实施2 - Road 2:
- Microsoft Graph using MSAL with Python and Delegated Permissions:https://blog.darrenjrobinson.com/microsoft-graph-using-msal-w...
- Interactive MSAL AAD Delegated AuthN.py:https://gist.github.com/darrenjrobinson/34dc0925724426823c79f46397d950b9
- Microsoft Graph API auth error: "Access token validation failure. Invalid audience":https://stackoverflow.com/questions/65221354/microsoft-graph-api-auth-error-access-token-validation-failure-invalid-audien
- Announcing OAuth 2.0 support for IMAP and SMTP AUTH protocols in Exchange Online:https://techcommunity.microsoft.com/t5/exchange-team-blog/ann...
- Cert Expired Error when using Graph API python example:https://learn.microsoft.com/en-us/answers/questions/1616892/c...
- Get access on behalf of a user:https://learn.microsoft.com/en-us/graph/auth-v2-user?view=gra...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。