Django 6.1:发送邮件

Django 为 Python 的 email 和 smtplib 模块提供封装,简化邮件编写与发送。Django 的邮件框架还支持替换投递机制:开发时可以把邮件输出到控制台或文件,生产环境则可以使用 SMTP 服务器或邮件服务提供商。(email · smtplib)

这些代码位于 django.core.mail 模块。

快速示例

使用 send_mail() 进行简单的邮件发送。例如,发送纯文本消息:(send_mail())

from django.core.mail import send_mail

send_mail(
    "Subject here",
    "Here is the message.",
    "from@example.com",
    ["to@example.com"],
)

需要更多邮件发送功能时,请使用 EmailMessage 或 EmailMultiAlternatives 类。例如,要利用特定模板和自定义邮件头,发送同时包含 HTML 与纯文本版本的多部分邮件,可以采用下面的方法:(EmailMessage · EmailMultiAlternatives)

from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string

# First, render the plain text content.
text_content = render_to_string(
    "templates/emails/my_email.txt",
    context={"my_variable": 42},
)

# Secondly, render the HTML content.
html_content = render_to_string(
    "templates/emails/my_email.html",
    context={"my_variable": 42},
)

# Then, create a multipart email instance.
msg = EmailMultiAlternatives(
    subject="Subject here",
    body=text_content,
    from_email="from@example.com",
    to=["to@example.com"],
    headers={"List-Unsubscribe": "<mailto:unsub@example.com>"},
)

# Lastly, attach the HTML content to the email instance and send.
msg.attach_alternative(html_content, "text/html")
msg.send()

配置邮件

新建 Django 项目默认不配置真实邮件发送。通过 startproject 创建的项目会把邮件打印到控制台,方便开发;如果没有定义 MAILERS 设置,则会出现 MailerDoesNotExist 错误。(startproject · MAILERS)

使用 MAILERS 设置告诉 Django 如何发送邮件。例如,通过本机运行的 SMTP 服务器发送:(MAILERS)

MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "localhost",
        },
    },
}

Django 将邮件发送过程抽象为“邮件后端”类。“邮件后端”章节列出了 Django 自带的后端。(邮件后端)

上例使用 Django 的 SMTP 邮件后端,通过标准 SMTP 协议发送。这一后端适用于许多生产配置,包括你自己基础设施中的 SMTP 服务器,以及大多数商业邮件服务提供商(ESP)。此外,还有第三方邮件后端,可以直接对接 ESP 的 API,或添加其他发送功能。(SMTP email backend · SMTP · third-party email backends)

开发或测试期间,通常根本不希望发送真实邮件。Django 的测试运行器会自动覆盖 MAILERS 配置,改用内存邮件后端。这既防止测试用例发送真实邮件,又让测试能够访问原本准备发送的消息。“为了开发配置邮件”章节讨论了其他方法。(automatically overrides · MAILERS · memory email
backend
· 为了开发配置邮件)

Django 6.1 的变更:在较早版本中,Django 默认通过 localhost 上运行的 SMTP 服务器发送邮件,使用如今已经弃用的 EMAIL_BACKEND 及相关设置。(EMAIL_BACKEND)

自 6.1 版本起弃用:在 Django 2028 之前,如果没有定义 MAILERS,仍采用旧行为:Django 默认使用 localhost 上的 SMTP 服务器,但会发出弃用警告。从 Django 2028 开始,未定义 MAILERS 就尝试发送邮件会导致 MailerDoesNotExist 错误。(MAILERS)

现有项目可以在 settings.py 中添加 MAILERS,提前启用新行为。请参阅“将邮件迁移到 mailers”。(MAILERS · Migrating email to mailers)

多个 mailer

有时不同类型的邮件需要采用不同发送方式,例如内部与外部邮件;不同地区用户使用不同 SMTP 服务器;事务通知与批量营销邮件使用不同服务。

MAILERS 设置可以定义多个邮件配置。例如:(MAILERS)

import os

MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "smtp.example.net",
            "use_tls": True,
            "username": os.environ["EMAIL_ACCOUNT_ID"],
            "password": os.environ["EMAIL_API_KEY"],
        },
    },
    "notifications": {
        "BACKEND": "example.third.party.EmailBackend",
        "OPTIONS": {
            "api_key": os.environ["THIRD_PARTY_API_KEY"],
            "region": "eu",
        },
    },
    "admin": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "localhost",
        },
    },
}

这里定义了三个 mailer 配置:

  • default 通过 smtp.example.net 上的 SMTP 服务器发送,连接使用 TLS 保护。它从环境变量读取账号 ID 与 API 密钥,用作 SMTP 身份验证的用户名与密码。许多 SMTP 服务采用这类验证方式的变体。
  • notifications 通过一个假设的商业邮件服务发送,使用直接连接该服务 API 的第三方 EmailBackend。如何查找真实的社区维护邮件后端包,请参阅“第三方后端”。(Third-party backends)
  • admin 通过 localhost 上运行的 SMTP 服务器发送,无需其他选项。

采用此配置后,可以向 Django 的邮件发送函数传入 using 参数,指定某个 mailer 配置:(email sending functions)

from django.core.mail import send_mail

send_mail(
    "Account activated",
    "Congratulations, you're all ready to use our Django app!",
    "from@example.com",
    ["user@example.com"],
    using="notifications",
)

未指定 using 时,Django 使用 default 配置所定义的 mailer。

对于替你发送邮件的可复用应用或 Django 功能,可能会提供选择特定 mailer 的选项。例如,Django 日志系统的 AdminEmailHandler 允许在 using 选项中指定 mailer 配置。(AdminEmailHandler)

发送消息

django.core.mail 提供方便发送邮件的函数,也提供类来构建和发送包含附件及多种内容类型的复杂邮件。

注意

通过 django.core.mail 发送的邮件的字符编码由 DEFAULT_CHARSET 设置项指定。(DEFAULT_CHARSET)

send_mail()

send_mail(subject, message, from_email, recipient_list, *, fail_silently=False, auth_user=None, auth_password=None, connection=None, html_message=None)[source]

django.core.mail.send_mail() 发送一封邮件。

参数 subject, message, from_email 和 recipient_list 是必须的。

  • subject: 一个字符串。

  • message: 一个字符串。

  • from_email :字符串。如果为 None ,Django 将使用 DEFAULT_FROM_EMAIL 设置的值。

    (DEFAULT_FROM_EMAIL)

  • recipient_list: 一个字符串列表,每项都是一个邮箱地址。recipient_list 中的每个成员都可以在邮件的 "收件人:" 中看到其他的收件人。

以下参数可选,使用时必须作为关键字参数传入。

  • fail_silently:布尔值,默认为 False。设置为 True 时,send_mail() 会抑制发送过程中的某些错误。具体忽略哪些异常,取决于所用邮件后端。
  • auth_user: 可选的用户名,用于验证登陆 SMTP 服务器。 若未提供,Django 会使用 EMAIL_HOST_USER 指定的值。

    (EMAIL_HOST_USER)

  • auth_password: 可选的密码,用于验证登陆 SMTP 服务器。若未提供, Django 会使用 EMAIL_HOST_PASSWORD 指定的值。

    (EMAIL_HOST_PASSWORD)

  • connection: 可选参数,发送邮件使用的后端。若未指定,则使用默认的后端。查询 邮件后端 文档获取更多细节。

    (邮件后端)

  • html_message: 若提供了 html_message,会使邮件成为 multipart/alternative 的实例, message 的内容类型则是 text/plain ,并且 html_message 的内容类型是 text/html 。

  • using:可选的 MAILERS 别名,用于选择发送邮件的配置。未指定时使用默认 mailer 配置。(MAILERS)

使用 using 参数时,不允许同时传入 fail_silently、auth_user、auth_password 或 connection。

返回值会是成功发送的信息的数量(只能是 0 或 1 ,因为同时只能发送一条消息)。

自 6.0 版本起弃用:将 fail_silently 及其后的参数作为位置参数传入的方式已弃用。

自 6.1 版本起弃用:fail_silently、auth_user、auth_password 和 connection 参数已弃用。多数情况下,可以配置适当的 MAILERS 并使用 using 代替。请参阅“替换 fail_silently”“替换 auth_user 和 auth_password”及“替换 get_connection() 和 connection 参数”。(MAILERS · Replacing fail_silently · Replacing auth_user and auth_password · Replacing get_connection() and connection arguments)

增加了 using 参数。

旧版本在同时提供 connection 时会忽略 fail_silently=True、auth_user 和 auth_password。现在这种调用会抛出 TypeError。

send_mass_mail()

send_mass_mail(datatuple, *, fail_silently=False, auth_user=None, auth_password=None, connection=None, using=None)[source]

django.core.mail.send_mass_mail() 用于批量发送邮件。

datatuple 是一个元组,形式如下:

(subject, message, from_email, recipient_list)

fail_silently、auth_user、auth_password 和 connection 的作用与 send_mail() 中相同。使用时必须作为关键字参数传入,并且不能与 using 同时使用。(send_mail())

关键字参数 using 是可选的 MAILERS 别名,用于选择发送配置。未指定时使用默认 mailer。(MAILERS)

datatuple 中每个独立元素都会生成一封独立邮件。与 send_mail() 一样,同一 recipient_list 中的收件人都能在邮件的 To 字段中看到其他地址。(send_mail())

举个例子,以下代码会向两个不同的收件人列表发送两封不同的邮件,却复用了同一条连接:

message1 = (
    "Subject here",
    "Here is the message",
    "from@example.com",
    ["first@example.com", "other@example.com"],
)
message2 = (
    "Another Subject",
    "Here is another message",
    "from@example.com",
    ["second@test.com"],
)
send_mass_mail((message1, message2))

返回值是成功发送的消息的数量。

自 6.0 版本起弃用:将 fail_silently 及其后的参数作为位置参数传入的方式已弃用。

自 6.1 版本起弃用:fail_silently、auth_user、auth_password 和 connection 参数已弃用。多数情况下,可以配置适当的 MAILERS 并使用 using 代替。请参阅“替换 fail_silently”“替换 auth_user 和 auth_password”及“替换 get_connection() 和 connection 参数”。(MAILERS · Replacing fail_silently · Replacing auth_user and auth_password · Replacing get_connection() and connection arguments)

增加了 using 参数。

旧版本在同时提供 connection 时会忽略 fail_silently=True、auth_user 和 auth_password。现在这种调用会抛出 TypeError。

send_mass_mail() 与 send_mail() 的区别

send_mass_mail() 与反复调用 send_mail() 的主要区别在于:send_mail() 每次执行都会打开邮件服务器连接,而 send_mass_mail() 用同一条连接发送全部消息,因此效率略高。(send_mass_mail() · send_mail())

向 send_mail() 提供多个收件地址会发送一封邮件,john@example.com 和 jane@example.com 都会出现在 To 字段中:(send_mail())

send_mail(
    "Subject",
    "Message.",
    "from@example.com",
    ["john@example.com", "jane@example.com"],
)

send_mass_mail() 为 datatuple 的每个元素分别发送一封消息,因此 john@example.com 和 jane@example.com 各自收到一封独立邮件:(send_mass_mail())

datatuple = (
    ("Subject", "Message.", "from@example.com", ["john@example.com"]),
    ("Subject", "Message.", "from@example.com", ["jane@example.com"]),
)
send_mass_mail(datatuple)

mail_admins()

mail_admins(subject, message, *, fail_silently=False, connection=None, html_message=None, using=None)[source]

django.core.mail.mail_admins() 是定义在 ADMINS 配置项中,用于向网站所有者快速发送邮件。(ADMINS)

mail_admins() 在主题前面添加 EMAIL_SUBJECT_PREFIX 指定的前缀,默认是 "[Django] " 。(EMAIL_SUBJECT_PREFIX)

邮件头的 "发件人:" 由 SERVER_EMAIL 配置项指定。(SERVER_EMAIL)

创建这个方法是为了方便和可读性。

若提供了 html_message,会使邮件成为 multipart/alternative 的实例, message 的内容类型则是 text/plain ,并且 html_message 的内容类型是 text/html 。

关键字参数 using 是可选的 MAILERS 别名,用于选择发送配置。未指定时使用默认 mailer。(MAILERS)

自 6.0 版本起弃用:将 fail_silently 及其后的参数作为位置参数传入的方式已弃用。

自 6.1 版本起弃用:fail_silently 和 connection 参数已弃用。多数情况下,可以配置适当的 MAILERS 并使用 using 代替。请参阅“替换 fail_silently”及“替换 get_connection() 和 connection 参数”。(MAILERS · Replacing fail_silently · Replacing get_connection() and connection arguments)

增加了 using 参数。

旧版本在同时提供 connection 时会忽略 fail_silently=True。现在这种调用会抛出 TypeError。

mail_managers()

mail_managers(subject, message, *, fail_silently=False, connection=None, html_message=None, using=None)[source]

django.core.mail.mail_managers() 类似 mail_admins(),但它向 MANAGERS 指定的管理员们发送邮件。(MANAGERS)

关键字参数 using 是可选的 MAILERS 别名,用于选择发送配置。未指定时使用默认 mailer。(MAILERS)

自 6.0 版本起弃用:将 fail_silently 及其后的参数作为位置参数传入的方式已弃用。

自 6.1 版本起弃用:fail_silently 和 connection 参数已弃用。多数情况下,可以配置适当的 MAILERS 并使用 using 代替。请参阅“替换 fail_silently”及“替换 get_connection() 和 connection 参数”。(MAILERS · Replacing fail_silently · Replacing get_connection() and connection arguments)

增加了 using 参数。

旧版本在同时提供 connection 时会忽略 fail_silently=True。现在这种调用会抛出 TypeError。

EmailMessage 类

Django 的 send_mail() 和 send_mass_mail() 实际上是利用 EmailMessage 类实现的轻量封装。(send_mail() · send_mass_mail() · EmailMessage)

send_mail() 及相关封装函数并未开放 EmailMessage 的全部功能。如果需要密送收件人、文件附件或多部分邮件等高级功能,就必须直接创建 EmailMessage 实例。(EmailMessage · send_mail())

注意

这是设计上的选择。send_mail() 及相关函数最初是 Django 提供的唯一接口,但它们接受的参数列表逐渐增长。改用更加面向对象的邮件设计,并仅为向后兼容保留原有函数,是合理的选择。(send_mail())

EmailMessage 负责创建邮件消息本身,邮件后端则负责发送。(EmailMessage · email backend)

为方便使用,EmailMessage 提供 send() 方法发送单封邮件。需要发送多封时,邮件后端 API 提供另一种方式。(EmailMessage · send() · provides an alternative)

class EmailMessage[source]

EmailMessage 类使用以下参数初始化。所有参数都可选,而且可以在调用 send() 之前随时设置。(send())

前四个参数既可以按位置传入,也可以作为关键字参数传入;按位置传入时必须遵循给定顺序:

  • subject: 邮件的主题。

  • body: 邮件内容,需要为纯文本格式。

  • from_email:发件地址。既支持 fred@example.com,也支持带显示名的形式,例如 "Fred" <fred@example.com>,详见“邮件地址格式”。省略时使用 DEFAULT_FROM_EMAIL 设置。(Formatting email addresses · DEFAULT_FROM_EMAIL)
  • to: 一个包含收件人地址的列表或元组。

以下参数使用时必须按关键字传入:

  • cc: 一个包含收件人地址的列表或元组,指定“抄送”对象。

  • bcc:发送邮件时用作密送收件人的地址列表或元组。
  • reply_to: 一个包含收件人地址的列表或元组,指定“回复”对象。

  • attachments:消息附件列表。每项可以是 MIMEPart 或 EmailAttachment 实例,也可以是包含 filename、content、mimetype 的元组。Django 6.0 的变更:新增了 attachments 列表对 MIMEPart 对象的支持。自 6.0 起弃用:attachments 对 Python 旧版 MIMEBase 对象的支持已弃用,请改用 MIMEPart。(MIMEPart · EmailAttachment · MIMEBase)
  • headers: 一个字典,包含邮件中额外的头信息。字典的关键字是头的名称,值为头的值。需要由调用者确保头名和值的正确性。对应的属性是 extra_headers 。

  • connection:邮件后端实例。使用 send_messages() 时忽略此参数。自 6.1 起弃用:connection 参数已弃用。请改为定义具有所需连接选项的 MAILERS 配置,然后调用 EmailMessage.send(using="…"),传入该配置别名。参阅“将邮件迁移到 mailers”。(email backend · send_messages() · MAILERS · EmailMessage.send(using="…") · Migrating email to mailers)

自 6.0 版本起弃用:除前四个参数外,将其他参数作为位置参数传入的方式已弃用。

例如:

from django.core.mail import EmailMessage

email = EmailMessage(
    subject="Hello",
    body="Body goes here",
    from_email="from@example.com",
    to=["to1@example.com", "to2@example.com"],
    bcc=["bcc@example.com"],
    reply_to=["another@example.com"],
    headers={"Message-ID": "foo"},
)

这个类拥有以下方法:

send(fail_silently=False, *, using=None)[source]

发送消息。成功发送返回 1,否则返回 0。收件人列表为空时返回 0,不会抛出异常。

可选关键字参数 using 指定发送邮件使用的 MAILERS 别名。不提供时使用默认 mailer 配置。(MAILERS)

如果构建邮件时指定了已经弃用的 connection,就使用该连接。同时提供 connection 和 using 会报错。

如果已经弃用的关键字参数 fail_silently 为 True,则忽略发送过程中某些依赖后端的异常。同时提供 fail_silently 和 using 会报错。

增加了 using 参数。

旧版本在同时提供 connection 时会忽略 fail_silently=True。现在这种调用会抛出 TypeError。

自 6.1 版本起弃用:fail_silently 参数已弃用。替代方式见“替换 fail_silently”。(Replacing fail_silently)

message(*, policy=email.policy.default)[source]

构建并返回表示待发送消息的 Python email.message.EmailMessage 对象。(email.message.EmailMessage)

关键字参数 policy 用于指定更新和序列化消息表示形式的规则集,必须是 email.policy.Policy 对象,默认为 email.policy.default。某些情况下可能需要 SMTP、SMTPUTF8 或自定义策略。例如,SMTP 邮件后端使用 SMTP 策略,保证采用 SMTP 协议要求的 \r\n 行结束符。(email.policy.Policy · email.policy.default · SMTP · SMTPUTF8 · SMTP email backend)

如果要扩展 Django 的 EmailMessage 类,通常需要重写此方法,将所需内容放入 Python EmailMessage 对象。(EmailMessage)

Django 6.0 的变更:新增了 policy 关键字参数,返回类型也更新为 EmailMessage 实例。(EmailMessage)

recipients()[source]

返回消息全部收件人的列表,无论他们记录在 to、cc 还是 bcc 属性中。子类化时可能需要重写这个方法,因为发送消息时必须把完整收件人列表告知 SMTP 服务器。如果你的类新增了指定收件人的方式,也必须通过这个方法返回这些收件人。

attach(filename, content, mimetype)[source]
attach(mimepart)

创建新附件并加入消息。attach() 有两种调用方式:

  • 可以传入三个参数:filename、content 和 mimetype。filename 是附件在邮件中显示的文件名;content 是附件包含的数据;mimetype 是可选 MIME 类型。省略 mimetype 时,根据文件名猜测 MIME 内容类型。例如:message.attach("design.png", img_data, "image/png")。如果指定 message/rfc822,content 可以是 django.core.mail.EmailMessage、Python 的 email.message.EmailMessage 或 email.message.Message。对于以 text/ 开头的 mimetype,内容应为字符串。二进制数据会尝试用 UTF-8 解码;如果失败,MIME 类型改为 application/octet-stream,数据本身不会修改。
    message.attach("design.png", img_data, "image/png")
    

    (django.core.mail.EmailMessage · email.message.EmailMessage · email.message.Message)

  • 对于需要额外邮件头或参数的附件,可以给 attach() 传入一个 Python MIMEPart 对象,它会直接附加到最终消息中。例如,下面附加带 Content-ID 的内联图片。Python 的 email.contentmanager.set_content() 文档说明了 MIMEPart.set_content() 支持的参数。Django 6.0 的变更:增加 MIMEPart 附件支持。自 6.0 起弃用:email.mime.base.MIMEBase 附件支持已弃用,请改用 MIMEPart。
    import email.utils
    from email.message import MIMEPart
    from django.core.mail import EmailMultiAlternatives
    
    message = EmailMultiAlternatives(...)
    image_data_bytes = ...  # Load image as bytes
    
    # Create a random Content-ID, including angle brackets
    cid = email.utils.make_msgid()
    inline_image = email.message.MIMEPart()
    inline_image.set_content(
        image_data_bytes,
        maintype="image",
        subtype="png",  # or "jpeg", etc. depending on the image type
        disposition="inline",
        cid=cid,
    )
    message.attach(inline_image)
    # Refer to Content-ID in HTML without angle brackets
    message.attach_alternative(f'… <img src="cid:{cid[1:-1]}"> …', "text/html")
    

    (MIMEPart · email.contentmanager.set_content() · email.mime.base.MIMEBase)

attach_file(path, mimetype=None)[source]

利用文件系统中的文件创建附件。传入文件路径以及可选 MIME 类型。省略 MIME 类型时,根据文件名猜测。可以这样使用:

message.attach_file("/images/weather_map.png")

对于以 text/ 开头的 MIME 类型,二进制数据的处理与 attach() 相同。(attach())

class EmailAttachment

用于存储邮件附件的具名元组。

具名元组包含以下字段:

  • filename

  • content

  • mimetype

发送可选的内容类型。

发送多个内容版本

在邮件中包含多个内容版本可能很有用,典型例子是同时发送纯文本与 HTML 版本。Django 的邮件库可以通过 EmailMultiAlternatives 类实现。(EmailMultiAlternatives)

class EmailMultiAlternatives[source]

这是 EmailMessage 的子类,允许通过 attach_alternative() 为邮件正文增加其他版本。它直接继承 EmailMessage 的所有方法,包括初始化方法。(EmailMessage · attach_alternative())

alternatives

EmailAlternative 具名元组的列表,尤其适合在测试中使用:(EmailAlternative)

self.assertEqual(len(msg.alternatives), 1)
self.assertEqual(msg.alternatives[0].content, html_content)
self.assertEqual(msg.alternatives[0].mimetype, "text/html")

替代内容只能通过 attach_alternative() 添加,或传入构造函数。(attach_alternative())

attach_alternative(content, mimetype)[source]

在电子邮件中附加消息正文的替代表示。

例如,要发送文本和 HTML 组合,你可以这样写:

from django.core.mail import EmailMultiAlternatives

subject = "hello"
from_email = "from@example.com"
to = "to@example.com"
text_content = "This is an important message."
html_content = "<p>This is an <strong>important</strong> message.</p>"
msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg.attach_alternative(html_content, "text/html")
msg.send()
body_contains(text)[source]

返回布尔值,表示指定文本是否同时包含在邮件正文及所有附加的 text/* MIME 替代内容中。

测试邮件时这可能很有用。例如:

def test_contains_email_content(self):
    subject = "Hello World"
    from_email = "from@example.com"
    to = "to@example.com"
    msg = EmailMultiAlternatives(subject, "I am content.", from_email, [to])
    msg.attach_alternative("<p>I am content.</p>", "text/html")

    self.assertIs(msg.body_contains("I am content"), True)
    self.assertIs(msg.body_contains("<p>I am content.</p>"), False)
class EmailAlternative

用于存储邮件内容替代版本的具名元组。

具名元组包含以下字段:

  • content

  • mimetype

更新默认内容类型

默认情况下,EmailMessage 的 body 参数使用 text/plain MIME 类型。保留默认值是良好做法,因为这样无论收件人使用什么邮件客户端,都能阅读邮件。但如果确信收件人可以处理其他内容类型,可以通过 EmailMessage 的 content_subtype 属性改变主内容类型。主类型始终为 text,但子类型可以改变。例如:(EmailMessage)

msg = EmailMessage(subject, html_content, from_email, [to])
msg.content_subtype = "html"  # Main content is now text/html
msg.send()

安全地发送邮件

任何能够发送邮件的公开网站,最终都会遇到利用它发送垃圾邮件、钓鱼邮件或其他恶意内容的企图。全面讨论邮件发送漏洞超出了 Django 文档范围,但网上有许多参考资料。以下两项适合作为起点:

  • 普林斯顿大学关于防止网页表单邮件滥用的指南。虽然这是为校内 Drupal Site Builder 用户编写的参考资料,但几乎全部建议也适用于 Django 或其他 Web 框架构建的网站。(preventing email abuse in web forms)
  • OWASP 的《身份系统中的电子邮件验证与核验速查表》。它主要讨论认证场景中的邮件使用。许多建议已由 django.contrib.auth 及其他 Django 功能处理,但速率限制、保障邮件地址更改流程等事项仍由开发者负责。(Email Validation and Verification in Identity Systems Cheat Sheet · django.contrib.auth)

如果网站可以向未经验证的地址发送邮件,尤其需要认真考虑邮件可能如何被滥用,并采取缓解措施。订阅通讯、抄送或自动回复发件人的联系表单,以及“分享本页”等功能,都是容易被攻击的目标。

邮件地址格式

邮件地址除了 user@domain 形式外,还可以带“友好”的显示名。例如,可以在 DEFAULT_FROM_EMAIL 中包含公司名称:(DEFAULT_FROM_EMAIL)

DEFAULT_FROM_EMAIL = '"Example, Inc." <contact@example.com>'

Example, Inc. 周围的双引号不可省略,否则逗号会被解释为两个地址的分隔符。像上例那样手工写出的固定地址是安全的,但从可变部分,尤其是不可信输入拼接地址,需要更加谨慎。

警告

绝不要使用字符串格式化,从可变部分构造邮件地址。例如 f'"{name}" <{email}>' 是不安全的。

邮件地址头与 HTML、SQL 类似,有复杂的语法规则。因此,拼接字符串构造邮件头会产生注入漏洞。即便已经验证 email 的格式,攻击者仍可能利用 name 部分注入更多地址。(validated)

应始终使用专门用于格式化邮件地址、经过充分测试的库,例如 Python 的 email.headerregistry.Address 类。它替代旧版 formataddr(),后者不支持国际化域名。(email.headerregistry.Address · formataddr())

例如,向用户发送邮件时包含用户全名,下面的 user 是默认 User 模型实例:(User)

from django.core.mail import send_mail
from email.headerregistry import Address


def send_mail_to_user(user, subject, body, from_email=None):
    # Safely create an email address with the user's name.
    # (addr_spec is the technical term for the user@domain address.)
    address = Address(
        display_name=user.get_full_name(),
        addr_spec=user.email,
    )
    send_mail(subject, body, from_email, [address])

如示例所示,Django 内置邮件后端支持在任意地址字段直接使用 Address 对象。许多自定义及第三方后端也支持。如果某个特定后端因此产生 TypeError 或其他问题,可以调用 str(address),将其转换为安全且格式正确的字符串。(Address)

防止头注入

邮件头注入是一种安全攻击:攻击者操纵邮件头,改变原定发件人、收件人、主题,甚至可能改变整封可见邮件正文。(Email header injection)

一种邮件头注入利用地址头语法。根据用户输入构建邮件地址时,你必须负责防止它,详见上面的“邮件地址格式”。(Formatting email addresses)

另一种可能更熟悉的攻击是 CRLF 注入,利用回车和换行字符插入额外邮件头。Django 会在尝试发送消息时检查各邮件头字段:如果出现这些字符,则抛出 ValueError,从而防止这种攻击。(ValueError)

Django 6.0 的变更:旧版本对某些无效邮件头抛出 django.core.mail.BadHeaderError,现在已替换为 ValueError。

Django 的 CRLF 防护依赖于 EmailMessage.message() 使用 Python 的现代 EmailPolicy。未调用该函数,或者调用时使用旧版 compat32 策略的自定义后端,必须自行实现 CRLF 注入防护。(EmailPolicy · EmailMessage.message() · compat32)

高效发送多封消息

创建和关闭 SMTP 连接(或其它网络连接)是一项耗时的进程。如果你有很多封邮件要发送,复用连接就显得很有意义,而不是在每次发送邮件时创建和关闭连接。

有两种方式要求邮件后端复用连接。两者都需要从 mail.mailers 获取后端实例,并使用后端 API。(mail.mailers · backend's API)

第一种方式是使用后端的 send_messages()。该方法接受 EmailMessage 或其子类实例列表,并通过同一连接发送所有消息。(EmailMessage)

例如,get_notification_emails() 函数返回一个 EmailMessage 对象列表,代表要定期发送的邮件。可以一次调用 send_messages() 发送它们:(EmailMessage)

from django.core import mail

email_list = get_notification_emails()
# Use the default mailer. You could substitute
# mail.mailers["alias"] for a specific mailer.
backend = mail.mailers.default
backend.send_messages(email_list)

此例中,send_messages() 打开后端连接,发送消息列表,然后关闭连接。send_mass_mail() 就是这样实现的。(send_mass_mail())

第二种方式是使用后端的 open() 和 close() 手工控制连接。如果连接已经打开,send_messages() 就不会打开或关闭它。因此,手工打开连接后,你可以自行控制何时关闭。例如:

from django.core import mail

# Use the "notifications" mailer configuration.
backend = mail.mailers["notifications"]

# Manually open the connection.
backend.open()

# Construct an email message. (Passing None as the third argument
# uses settings.DEFAULT_FROM_EMAIL as the "From:" address.)
email1 = mail.EmailMessage("Hi", "Message", None, ["to1@example.com"])
# Send the email. The connection was already open, so send_messages()
# leaves it open after sending.
backend.send_messages([email1])

# Construct and send two more messages. The connection is still open.
email2 = mail.EmailMessage("Hi", "Message", None, ["to2@example.com"])
email3 = mail.EmailMessage("Hi", "Message", None, ["to3@example.com"])
backend.send_messages([email2, email3])

# Because we opened it, we need to manually close the connection.
backend.close()

手工打开后端连接时,你必须确保它被关闭。上例实际上有一个问题:发送邮件期间发生异常时,连接不会关闭。可以通过 try-finally 修复,但更好的方法是将后端实例用作上下文管理器,它会按需自动调用 open() 和 close()。

下面与前例等价,但使用后端作为上下文管理器,避免错误发生后连接仍保持打开:(context manager)

from django.core import mail

# Use mail.mailers[...] as a context manager.
with mail.mailers["notifications"] as backend:
    # The backend connection is automatically opened inside the context.
    email1 = mail.EmailMessage("Hi", "Message", None, ["to1@example.com"])
    backend.send_messages([email1])

    # The connection is still open, and is reused for the second send.
    email2 = mail.EmailMessage("Hi", "Message", None, ["to2@example.com"])
    email3 = mail.EmailMessage("Hi", "Message", None, ["to3@example.com"])
    backend.send_messages([email2, email3])

# After exiting the context (either normally or because of an error),
# the backend connection is automatically closed.

邮件后端

发送邮件的动作是由邮件后端执行的。

Django 自带多个邮件后端。除 SMTP 后端外,它们主要用于测试和开发。如果内置后端不能满足需要,还可以使用第三方包。也可以继承某个内置后端修改行为,甚至自行编写后端。(third-party packages · write your own email
backend
)

SMTP 后端

SMTP 邮件后端连接 SMTP 服务器发送邮件。使用时将 BACKEND 设为 django.core.mail.backends.smtp.EmailBackend。(BACKEND)

SMTP 后端支持以下 OPTIONS:(OPTIONS)

  • host(必填):SMTP 服务器主机名或 IP 地址。
  • port:SMTP 主机连接端口。省略时,根据 use_tls 和 use_ssl 使用相应协议标准端口:TLS 为 587,SSL 为 465,非加密连接为 25。
  • username 和 password:服务器要求 SMTP 身份验证时设置,即 SMTP AUTH 凭据,有时称为 SMTP 登录。用户名虽然通常是邮件地址,但不能与默认 From 地址混淆;后者由 DEFAULT_FROM_EMAIL 和 SERVER_EMAIL 定义。(DEFAULT_FROM_EMAIL · SERVER_EMAIL)
  • use_tls 或 use_ssl:将其中一个设为 True,以安全协议连接服务器。use_tls 使用显式 TLS;use_ssl 使用 SSL,即隐式 TLS。
  • ssl_certfile 和 ssl_keyfile:如果 SMTP 服务器 SSL/TLS 连接需要客户端证书认证,用这些选项指定 PEM 格式证书链及私钥文件路径。如果证书文件包含私钥,可以省略独立密钥文件。这些选项不用于私有 CA 或自签名 SMTP 服务器证书,见下文对应章节。注意,它们不会执行证书有效性检查,而是传给底层 SSL 连接。证书链与私钥的处理详见 Python 的 ssl.SSLContext.wrap_socket() 文档。(Private and self-signed SMTP server certificates · ssl.SSLContext.wrap_socket())
  • timeout:SMTP 连接及其他阻塞操作的超时时间,单位为秒。未指定时取 socket.getdefaulttimeout();默认无超时,即 None,意味着 SMTP 操作可能无限期阻塞。(socket.getdefaulttimeout())
  • fail_silently:设为 True 后,发送时忽略某些错误。打开 SMTP 连接时忽略所有 OSError,与服务器通信时忽略 smtplib.SMTPException。它会同时抑制暂时网络故障及严重配置问题,但并非忽略全部错误;消息序列化问题不会被静默忽略。该选项用于向后兼容,不推荐用于典型场景。(OSError · smtplib.SMTPException)

举例:

MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": "smtp.example.net",
            "use_tls": True,
            "username": "my-app",
            "password": os.environ["MY_APP_SMTP_PASSWORD"],
            "timeout": 10,
        },
    },
}

自 6.1 版本起弃用:未定义 MAILERS 时,Django 将 SMTP 后端作为默认 mailer,即默认 EMAIL_BACKEND,连接 localhost 的 25 端口。Django 7.0 将移除此行为,不再提供默认 mailer 配置。(MAILERS · EMAIL_BACKEND)

未定义 MAILERS 而使用 SMTP 后端时,上述选项分别取自已经弃用的 EMAIL_HOST、EMAIL_PORT、EMAIL_HOST_USER、EMAIL_HOST_PASSWORD、EMAIL_USE_TLS、EMAIL_USE_SSL、EMAIL_SSL_KEYFILE、EMAIL_SSL_CERTFILE 和 EMAIL_TIMEOUT 设置。没有与 fail_silently 选项对应的设置。(MAILERS · EMAIL_HOST · EMAIL_PORT · EMAIL_HOST_USER · EMAIL_HOST_PASSWORD · EMAIL_USE_TLS · EMAIL_USE_SSL · EMAIL_SSL_KEYFILE · EMAIL_SSL_CERTFILE · EMAIL_TIMEOUT)

class backends.smtp.EmailBackend

不建议直接实例化 EmailBackend 类,请通过 mailers 获取后端实例。(mailers)

不经过 mailers 而直接构造 SMTP EmailBackend 时,它以关键字参数接受上述选项,默认值取自对应的已弃用 EMAIL_* 设置。host 非必填,默认为 localhost;port 默认是 25,即使 use_tls 或 use_ssl 为 True 也如此。(mailers)

已经定义 MAILERS 时,尝试直接创建 SMTP EmailBackend 会抛出 AttributeError。(MAILERS)

自 6.1 版本起弃用:Django 2028 将不再支持直接构造 EmailBackend 实例。使用未记录的调用方式,默认参数处理会与较早版本不同。

私有及自签名 SMTP 服务器证书

如果 SMTP 服务器使用私有证书颁发机构(CA)的 SSL 证书,应把该 CA 根证书加入客户端,即运行 Django 的机器的系统 CA 证书包。服务器使用自签名证书时,也应将该证书加入客户端系统 CA 证书包,以便信任它。SMTP 后端的 ssl_certfile 选项不能用于 CA 根证书或自签名证书。

请按平台说明添加系统 CA 证书。如果不能或不想修改系统证书包,也可以使用 OpenSSL 的 SSL_CERT_FILE 或 SSL_CERT_DIR 环境变量指定自定义证书包。

对于更复杂场景,可以继承 SMTP 后端,通过 ssl.SSLContext.load_verify_locations() 向它的 ssl_context 添加根证书。(ssl.SSLContext.load_verify_locations())

控制台后端

控制台后端不会发送真实邮件,而是将原本要发送的邮件写入标准输出。将 BACKEND 设为 django.core.mail.backends.console.EmailBackend 即可使用。(BACKEND)

控制台后端支持以下 OPTIONS:(OPTIONS)

  • stream:接收输出的类流对象,默认为 stdout。
  • fail_silently:设为 True 后,忽略向流写入消息时的全部错误,包括序列化错误。该选项用于向后兼容,不推荐使用。

该后端不是为了在生产环境使用的——出于方便的目的,让你在开发阶段使用。

Django 6.1 的变更:startproject 创建的设置文件现在会定义 MAILERS,以控制台后端作为默认配置。(startproject · MAILERS)

文件后端

文件后端将邮件写入文件。每次在该后端打开新会话时都会新建文件。将 BACKEND 设为 django.core.mail.backends.filebased.EmailBackend 即可使用。(BACKEND)

文件后端支持以下 OPTIONS:(OPTIONS)

  • file_path(必填):写入文件的目录,可以是字符串或 pathlib.Path 对象。如果目录不存在,文件后端会尝试创建。(pathlib.Path)
  • fail_silently:设为 True 后,忽略向文件写入消息时的全部错误,包括序列化错误,但不忽略确保目标目录存在时的错误。该选项用于向后兼容,不推荐使用。

该后端不是为了在生产环境使用的——出于方便的目的,让你在开发阶段使用。

自 6.1 版本起弃用:未定义 MAILERS 而使用文件后端时,file_path 选项取自 EMAIL_FILE_PATH 设置。(MAILERS · EMAIL_FILE_PATH)

内存后端

locmem 后端把消息保存在 django.core.mail 的特殊属性中。首次发送消息时创建 outbox 属性;它是一个列表,每封原本要发送的消息对应一个 EmailMessage 实例。outbox 中的消息带有 sent_using 属性,标识发送所用的 MAILERS 别名。(EmailMessage · MAILERS)

使用内存后端时,将 BACKEND 设为 django.core.mail.backends.locmem.EmailBackend。它不支持任何 OPTIONS。(BACKEND · OPTIONS)

Django 测试运行器会自动切换到此后端进行测试。(automatically switches to this backend for testing)

该后端不是为了在生产环境使用的——出于方便的目的,让你在开发阶段使用。

Django 6.1 的变更:outbox 中的消息新增了 sent_using 属性。

虚拟后端

顾名思义,虚拟后端不对消息做任何处理。将 BACKEND 设为 django.core.mail.backends.dummy.EmailBackend 即可使用。它不支持任何 OPTIONS。(BACKEND · OPTIONS)

该后端不是为了在生产环境使用的——出于方便的目的,让你在开发阶段使用。

第三方后端

还有社区维护的解决方案!

Django 拥有活跃的生态。社区生态页面介绍了一些邮件后端,Django Packages 的 Email 列表中还有更多选择。(Community Ecosystem · Email grid)

可用的第三方邮件后端能够:

  • 直接对接商业邮件服务提供商的 API,这些 API 经常具有 SMTP 不支持的额外功能。
  • 把邮件发送转移到异步任务队列。
  • 为其他邮件后端增加功能,例如强制执行禁止发送列表,或记录已发送消息。
  • 提供开发与调试工具,例如沙箱捕获和浏览器内邮件预览。

自定义邮件后端

需要改变邮件发送方式时,可以编写自己的后端。使用时,将 BACKEND 设为后端类的 Python 导入路径,OPTIONS 设为该后端 __init__() 支持的关键字参数。(BACKEND · OPTIONS)

自定义后端应继承 django.core.mail.backends.base 模块中的 BaseEmailBackend,并实现 send_messages(email_messages)。该方法接收 EmailMessage 实例列表,返回成功投递的消息数量。如果后端具有持久会话或连接的概念,还应实现 open() 和 close()。参考实现见 smtp.EmailBackend。(EmailMessage)

获取邮件后端的一个实例

django.core.mail 中的 mailers 工厂返回邮件后端实例。

mailers

可以通过类似字典的 django.core.mail.mailers 对象,访问 MAILERS 中配置的 mailer:(MAILERS)

>>> from django.core.mail import mailers
>>> mailers["notifications"]

指定键未定义时,抛出 MailerDoesNotExist;其他配置问题抛出 InvalidMailer。

mailers.default

默认 mailer 可以通过快捷属性 django.core.mail.mailers.default 访问:

>>> from django.core.mail import mailers
>>> mailers.default

这等价于 mailers["default"]。如果未配置默认 mailer,会抛出 MailerDoesNotExist。

自 6.1 版本起弃用:未定义 MAILERS 时,mailers.default 会根据已弃用的 EMAIL_BACKEND 及相关设置创建后端实例,以兼容 Django 6.0 及更早版本。(MAILERS · EMAIL_BACKEND)

这一行为及相关设置将在 Django 2028 中移除。

Django 6.1 新增。

get_connection(backend=None, *, fail_silently=False, **kwargs)[source]

已弃用的 django.core.mail.get_connection() 创建并返回邮件后端实例。行为取决于 MAILERS 设置及调用方式。(MAILERS)

如果已经定义 MAILERS:(MAILERS)

  • 不带参数调用 get_connection(),返回 mailers.default。(mailers.default)
  • 只传入 fail_silently 或其他关键字参数调用 get_connection(…),会创建 MAILERS["default"] 的实例,并将这些关键字参数加入默认 mailer 的 OPTIONS。(OPTIONS)
  • 传入后端导入路径调用 get_connection(backend, …) 会报错。

如果未定义 MAILERS:(MAILERS)

  • 不带参数调用 get_connection(),返回 EMAIL_BACKEND 指定的后端实例。(EMAIL_BACKEND)
  • 指定 backend 参数时,实例化该后端。
  • 关键字参数 fail_silently 为 True 时,邮件发送过程中某些依赖后端的异常会被静默忽略。
  • 其他关键字参数直接传给后端构造函数。

自 6.0 版本起弃用:将 fail_silently 按位置传入的方式已弃用。

自 6.1 版本起弃用:get_connection() 已弃用,将在 Django 7.0 中移除。请改用 mailers[alias]。迁移建议见“替换 get_connection() 和 connection 参数”。(mailers[alias] · Replacing get_connection() and connection arguments)

邮件后端 API

邮件后端实例具有以下方法:

  • open() 创建一个发送邮件的长连接。

  • close() 关闭当前发送邮件的连接。

  • send_messages(email_messages) 发送 EmailMessage 对象列表。如果连接未打开,该调用会隐式打开连接,并在发送后关闭;如果连接已打开,发送后仍保持打开。(EmailMessage)

后端实例也可以作为上下文管理器,按需自动调用 open() 和 close()。示例见“高效发送多封消息”。(Sending many messages efficiently)

为了开发配置邮件

曾经有很多次,你并不想 Django 真的发送邮件。举个例子,在开发网站时,你可能并不期望发送成千上万封邮件——但你想要确保这些邮件将会在正确的时间,包含正确的内容,发送给正确的人。

在本地开发中配置电子邮件的最简单方法是使用 console 电子邮件后端。该后端将所有电子邮件重定向到 stdout,允许您查看邮件的内容。(console)

文件 邮件后端在开发时也很有用——这个后端将每次 SMTP 连接的内容输出至一个文件,你可以在你闲暇时查看这个文件。(文件)

另一种方法是使用模拟 SMTP 服务器,在本机接收邮件并显示到终端,但不真正发送。aiosmtpd 包可以实现:(aiosmtpd)

python -m pip install "aiosmtpd >= 1.4.5"

python -m aiosmtpd -n -l localhost:8025

该命令启动一个最小 SMTP 服务器,监听 localhost 的 8025 端口。服务器把全部邮件头及正文打印到标准输出。然后只需相应设置 SMTP 后端 OPTIONS 中的 host 与 port。SMTP 服务器选项的详细讨论见 aiosmtpd 文档。(OPTIONS · aiosmtpd)

关于发送邮件的单元测试资料,参见测试文档中 邮件服务 章节。(邮件服务)


来源:Django 6.1:发送邮件。Django Software Foundation 与文档贡献者。依据 Django 文档 BSD 许可,保留6.1版本范围。 正文原已有中文,保留全文并汉化未译段落。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容