Python - 如何发送 html 电子邮件并附上带有 Python 的图片?

Python - How to send an html email and attach an image with Python?

我收到一个 "error: ValueError: Cannot convert mixed to alternative."

当我插入打开的图像和 msg.add_attachment 块时出现此错误(在 btw #### #### 中突出显示)。没有它,代码运行良好。我需要将电子邮件作为 html 和图片附件发送。

import os
import imghdr
from email.message import EmailMessage
import smtplib


EMAIL_ADDRESS = os.environ.get('EMAIL-USER')
EMAIL_PASSWORD = os.environ.get('EMAIL-PASS')

Message0 = "HelloWorld1"
Message1 = "HelloWorld2"

msg = EmailMessage()
msg['Subject'] = 'Hello WORLD'
msg['From'] = EMAIL_ADDRESS
msg['To'] = EMAIL_ADDRESS

msg.set_content('This is a plain text email, see HTML format')

########################################################################
with open('screenshot.png', 'rb') as f:
    file_data = f.read()
    file_type = imghdr.what(f.name)
    file_name = f.name

msg.add_attachment(file_data, maintype='image', subtype=file_type, filename=file_name)
#########################################################################

msg.add_alternative("""\
    <!DOCTYPE html>
    <html>
        <body>
            <h1 style="color:Blue;">Hello World</h1>
                {Message0}
                {Message1}
        </body>
    </html>
    """.format(**locals()), subtype='html')

with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
    smtp.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
    smtp.send_message(msg)
    print("email sent")

对于最终结果,我需要能够通过 Python 发送电子邮件并附加图像。

电子邮件可以由单个部分组成,也可以是多部分邮件。 如果是多部分消息,通常是 multipart/alternativemultipart/mixed.

  • multipart/alternative 表示同一内容有 2 个或更多版本(例如纯文本和 html)
  • multipart/mixed用于需要将多个不同的内容打包在一起(例如一封邮件和一个附件)

当使用 multipart 时实际发生的是电子邮件由一个 "multipart" 容器组成,其中包含其他部分,例如对于 text+html 是这样的:

  • multipart/alternative 部分
    • text/plain部分
    • text/html部分

如果是带附件的邮件,你可以这样写:

  • multipart/mixed 部分
    • text/plain部分
    • image/png部分

因此,容器是 mixedalternative,但不能同时是两者。那么,如何兼得呢?您可以嵌套它们,例如:

  • multipart/mixed 部分
    • multipart/alternative 部分
      • text/plain部分
      • text/html部分
    • image/png部分

所以,现在您有一封包含消息和附件的电子邮件,并且该消息既有纯文本也有 html。


现在,在代码中,这是基本思想:

msg = EmailMessage()
msg['Subject'] = 'Subject'
msg['From'] = 'from@email'
msg['To'] = 'to@email'

msg.set_content('This is a plain text')
msg.add_attachment(b'xxxxxx', maintype='image', subtype='png', filename='image.png')

# Now there are plain text and attachment.
# HTML should be added as alternative to the plain text part:

text_part, attachment_part = msg.iter_parts()
text_part.add_alternative("<p>html contents</p>", subtype='html')

顺便说一句,您可以这样查看每个部分的内容:

>>> plain_text_part, html_text_part = text_part.iter_parts()
>>> print(plain_text_part)
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit

This is a plain text

>>> print(html_text_part)
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0

<p>html contents</p>

>>> print(attachment_part)
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="image.png"
MIME-Version: 1.0

eHh4eHh4

>>> print(msg)
Subject: Subject
From: from@email
To: to@email
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="===============2219697219721248811=="

--===============2219697219721248811==
Content-Type: multipart/alternative;
 boundary="===============5680305804901241482=="

--===============5680305804901241482==
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit

This is a plain text

--===============5680305804901241482==
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0

<p>html contents</p>

--===============5680305804901241482==--

--===============2219697219721248811==
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="image.png"
MIME-Version: 1.0

eHh4eHh4

--===============2219697219721248811==--

这是@zvone 提供的示例的补充示例。这里的区别是我想要一封有 2 个版本的邮件。 plain-text 中的一个带有图像作为附件。另一部分是带有嵌入图像的 html。

所以邮件的布局是这样的:

  • multipart/mixed part
    • multipart/alternative part
      • multipart/related part(html 与 img)
        • text/html part
        • image/png part
      • text/plain part(纯文本)
    • image/png part(附件)

plain-text版本:textfile.txt

Hello World

html版本:mail_content.html

<html>
  <head></head>
  <body>
    <h1>Hello World</h1>
    <img src="cid:{graph_example_img_cid}" />
  </body>
</html>

当然还有 Python 代码(针对 Python 3.10 进行了测试)。图像的嵌入遵循最新 Python Standard Library of email.

中的示例

Python代码:main.py

# var
mail_server: str = "****smtp****"
user: str = "****@****.com"
me = user
you = user

# imports
import smtplib # mail actual sending function
import imghdr # And imghdr to find the types of our images
from email.message import EmailMessage
from email.utils import make_msgid
import datetime

# Open a plain text file for reading.  For this example, assume that
# the text file contains only ASCII characters.
textfile_filepath: str = "textfile.txt"
with open(textfile_filepath) as fp:
    # Create a text/plain message
    msg = EmailMessage()
    msg.set_content(fp.read())

# me == the sender's email address
# you == the recipient's email address
msg['Subject'] = 'Test Email with 2 versions'
msg['From'] = me
msg['To'] = you

# attachements
# Open the files in binary mode.  Use imghdr to figure out the
# MIME subtype for each specific image.
image_filepath: str = "test_image.png"
with open(image_filepath, 'rb') as fp:
    img_data = fp.read()
    msg.add_attachment(img_data, maintype='image', subtype=imghdr.what(None, img_data))

text_part, attachment_part = msg.iter_parts()

## HTML alternative using Content ID
html_content_filepath: str = "mail_content.html"
graph_cid = make_msgid()
with open(html_content_filepath) as fp:
    # Create a text/plain message
    #msg = MIMEText(fp.read())
    raw_html: str = fp.read()
    # note that we needed to peel the <> off the msgid for use in the html.
    html_content: str = raw_html.format(graph_example_img_cid=graph_cid[1:-1])

    # add to email message 
    text_part.add_alternative(html_content,subtype='html')

# Now add the related image to the html part.
with open(image_filepath, 'rb') as img:
    text_part, html_part = text_part.iter_parts()
    html_part.add_related(img.read(), 'image', 'jpeg', cid=graph_cid)

# Send the message via our own SMTP server, but don't include the envelope header.
s = smtplib.SMTP(mail_server)
s.sendmail(me, [you], msg.as_string())
s.quit()

# log
current_time = datetime.datetime.now()
print("{} >>> Email sent (From: {}, To: {})".format(
    current_time.isoformat(), me, you
))