EmailMessage 在使用 python 的电子邮件和 smtplib 包发送的电子邮件中未正确显示
EmailMessage doesn't show correctly in the sent emails with python's email and smtplib packages
我的邮件可以正确发送,但在收件人邮件中显示不正确。它看起来像这样:
To: =?utf-8?b?..?= <....com> MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="===============5404281335870522242=="
--===============5404281335870522242== Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64
5bCK5pWs55qE5a2U6LaF5YW...
--===============5404281335870522242== Content-Type: image/png Content-Transfer-Encoding: base64 Content-Disposition: attachment;
filename="user.png" MIME-Version: 1.0
iVBORw0KGgo...
除Subject
和From
行(显示在To
之后)外,MIME字符串直接显示,所有正文均为纯文本。
这是我的代码:
import smtplib
import ssl
import mimetypes
from pathlib import Path
from email.message import EmailMessage
from email.utils import formataddr
import time
class EmailSender:
PORT = 465
CONTEXT = ssl.create_default_context()
def __init__(
self,
username,
password,
host,
):
self.username = username
self.password = password
self.host = host
self.mails = []
def _add_name_header(self, name="", mail_addr=""):
if name:
return formataddr((name, mail_addr))
else:
return mail_addr
def add_mail(
self,
from_email="",
from_name="",
to_email="",
to_name="",
subject="",
message_txt="",
files=None,
):
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = self._add_name_header(from_name, from_email)
msg["To"] = self._add_name_header(to_name, to_email)
msg.set_content(message_txt)
if not files is None:
for file_obj in files:
if file_obj.exists():
file = str(file_obj)
ctype, encoding = mimetypes.guess_type(file)
if ctype is None or encoding is not None:
# No guess could be made, or the file is encoded (compressed), so use a generic bag-of-bits type.
ctype = "application/octet-stream"
maintype, subtype = ctype.split("/", 1)
with file_obj.open("rb") as fp:
msg.add_attachment(
fp.read(),
maintype=maintype,
subtype=subtype,
filename=file_obj.name,
)
self.mails.append(msg)
def send(self, time_interval=1):
with smtplib.SMTP_SSL(
host=self.host, port=self.PORT, context=self.CONTEXT
) as server:
try:
server.login(user=self.username, password=self.password)
except Exception as e:
# Need process errors
raise e
for msg in self.mails:
server.send_message(msg)
time.sleep(time_interval)
而我只是这样做:
sender = EmailSender(
username, password, host="smtp.163.com"
)
files = list(Path("D:/").glob("*.pdf"))
sender.add_mail(
from_email, from_name, to_email, to_name, subject, message_txt, files=None
)
sender.send(time_interval=10)
我是问题的 OP。我刚刚自己解决了这个问题,我会分享解决方案。
TLNR:我的邮件中使用了 Non-Ascii 个字符,因此请使用 msg = EmailMessage(EmailPolicy(utf8=True))
而不是 msg = EmailMessage()
。
我误解了 SMTP.send_message
文档中的这些句子:
If any of the addresses in from_addr and to_addrs contain non-ASCII
characters and the server does not advertise SMTPUTF8 support, an
SMTPNotSupported error is raised. Otherwise the Message is serialized
with a clone of its policy with the utf8 attribute set to True, and
SMTPUTF8 and BODY=8BITMIME are added to mail_options.
由于我在地址中添加了 non-ASCII header,我相信 smtplib
会自动为我使用 utf8 策略。但是在文件 smtplib.py
中我看到了这个:
if from_addr is None:
# Some code
from_addr = email.utils.getaddresses([from_addr])[0][1]
if to_addrs is None:
# Some code
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
# Some code
international = False
try:
"".join([from_addr, *to_addrs]).encode("ascii")
except UnicodeEncodeError:
# Some code
international = True
也就是说,该函数只检查地址部分是否有 non-ASCII 个字符,但不检查 header 个名称。
在那之后,消息被当作纯 ASCII 内容处理,这可能没问题,我不知道为什么,但是如此,前后插入了许多额外的 /r
个字符To:xxx
行,这可能会让 smtp 服务器认为这是一个分隔符?并最终导致了问题。
我的邮件可以正确发送,但在收件人邮件中显示不正确。它看起来像这样:
To: =?utf-8?b?..?= <....com> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="===============5404281335870522242=="
--===============5404281335870522242== Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64
5bCK5pWs55qE5a2U6LaF5YW...
--===============5404281335870522242== Content-Type: image/png Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="user.png" MIME-Version: 1.0
iVBORw0KGgo...
除Subject
和From
行(显示在To
之后)外,MIME字符串直接显示,所有正文均为纯文本。
这是我的代码:
import smtplib
import ssl
import mimetypes
from pathlib import Path
from email.message import EmailMessage
from email.utils import formataddr
import time
class EmailSender:
PORT = 465
CONTEXT = ssl.create_default_context()
def __init__(
self,
username,
password,
host,
):
self.username = username
self.password = password
self.host = host
self.mails = []
def _add_name_header(self, name="", mail_addr=""):
if name:
return formataddr((name, mail_addr))
else:
return mail_addr
def add_mail(
self,
from_email="",
from_name="",
to_email="",
to_name="",
subject="",
message_txt="",
files=None,
):
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = self._add_name_header(from_name, from_email)
msg["To"] = self._add_name_header(to_name, to_email)
msg.set_content(message_txt)
if not files is None:
for file_obj in files:
if file_obj.exists():
file = str(file_obj)
ctype, encoding = mimetypes.guess_type(file)
if ctype is None or encoding is not None:
# No guess could be made, or the file is encoded (compressed), so use a generic bag-of-bits type.
ctype = "application/octet-stream"
maintype, subtype = ctype.split("/", 1)
with file_obj.open("rb") as fp:
msg.add_attachment(
fp.read(),
maintype=maintype,
subtype=subtype,
filename=file_obj.name,
)
self.mails.append(msg)
def send(self, time_interval=1):
with smtplib.SMTP_SSL(
host=self.host, port=self.PORT, context=self.CONTEXT
) as server:
try:
server.login(user=self.username, password=self.password)
except Exception as e:
# Need process errors
raise e
for msg in self.mails:
server.send_message(msg)
time.sleep(time_interval)
而我只是这样做:
sender = EmailSender(
username, password, host="smtp.163.com"
)
files = list(Path("D:/").glob("*.pdf"))
sender.add_mail(
from_email, from_name, to_email, to_name, subject, message_txt, files=None
)
sender.send(time_interval=10)
我是问题的 OP。我刚刚自己解决了这个问题,我会分享解决方案。
TLNR:我的邮件中使用了 Non-Ascii 个字符,因此请使用 msg = EmailMessage(EmailPolicy(utf8=True))
而不是 msg = EmailMessage()
。
我误解了 SMTP.send_message
文档中的这些句子:
If any of the addresses in from_addr and to_addrs contain non-ASCII characters and the server does not advertise SMTPUTF8 support, an SMTPNotSupported error is raised. Otherwise the Message is serialized with a clone of its policy with the utf8 attribute set to True, and SMTPUTF8 and BODY=8BITMIME are added to mail_options.
由于我在地址中添加了 non-ASCII header,我相信 smtplib
会自动为我使用 utf8 策略。但是在文件 smtplib.py
中我看到了这个:
if from_addr is None:
# Some code
from_addr = email.utils.getaddresses([from_addr])[0][1]
if to_addrs is None:
# Some code
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
# Some code
international = False
try:
"".join([from_addr, *to_addrs]).encode("ascii")
except UnicodeEncodeError:
# Some code
international = True
也就是说,该函数只检查地址部分是否有 non-ASCII 个字符,但不检查 header 个名称。
在那之后,消息被当作纯 ASCII 内容处理,这可能没问题,我不知道为什么,但是如此,前后插入了许多额外的 /r
个字符To:xxx
行,这可能会让 smtp 服务器认为这是一个分隔符?并最终导致了问题。