一个用于监控和异步发送邮件的Django应用,支持模板。
项目描述
Django Post Office
Django Post Office是一个简单的应用,用于在Django中发送和管理您的电子邮件。一些惊人的特性包括
- 设计用于扩展,高效处理数百万封电子邮件
- 允许您异步发送电子邮件
- 支持多后端
- 支持HTML电子邮件
- 支持HTML电子邮件中的内联图片
- 支持基于数据库的电子邮件模板
- 支持多语言电子邮件模板(i18n)
- 内置调度支持
- 与RQ或Celery等任务队列配合良好
- 使用多进程和多线程并行发送大量电子邮件
依赖项
使用这个可选依赖项,HTML 电子邮件可以在 Django 管理后端中很好地渲染。如果没有这个库,出于安全原因,所有 HTML 标签都将被删除。
安装
pip install django-post_office
将 post_office
添加到 django 的 settings.py
中的 INSTALLED_APPS
INSTALLED_APPS = (
# other apps
"post_office",
)
运行 migrate
python manage.py migrate
在 Django 的 settings.py
中将 post_office.EmailBackend
设置为 EMAIL_BACKEND
EMAIL_BACKEND = 'post_office.EmailBackend'
快速入门
发送简单的电子邮件非常容易
from post_office import mail
mail.send(
'recipient@example.com', # List of email addresses also accepted
'from@example.com',
subject='My email',
message='Hi there!',
html_message='Hi <strong>there</strong>!',
)
如果您想使用模板,请确保 Django 的管理界面已启用。通过 admin
创建一个 EmailTemplate
实例,并进行以下操作
from post_office import mail
mail.send(
'recipient@example.com', # List of email addresses also accepted
'from@example.com',
template='welcome_email', # Could be an EmailTemplate instance or name
context={'foo': 'bar'},
)
上述命令将您的电子邮件放入队列中,这样您可以在不太多地减慢请求/响应周期的情况下使用该命令。要实际发送它们,请运行 python manage.py send_queued_mail
。您可以通过 cron 定期安排此管理命令的运行。
* * * * * (/usr/bin/python manage.py send_queued_mail >> send_mail.log 2>&1)
用法
mail.send()
mail.send
是此库中最重要的功能,它接受以下参数
参数 | 必需 | 描述 |
---|---|---|
recipients | 是 | 收件人电子邮件地址列表 |
sender | 否 | 默认为 settings.DEFAULT_FROM_EMAIL ,允许显示名称如 John <john@a.com> |
subject | 否 | 电子邮件主题(如果没有指定 template ) |
message | 否 | 电子邮件内容(如果没有指定 template ) |
html_message | 否 | HTML 内容(如果没有指定 template ) |
template | 否 | EmailTemplate 实例或模板名称 |
language | 否 | 您想要发送电子邮件的语言(如果您有多语言电子邮件模板)。 |
cc | 否 | 电子邮件列表,将显示在 cc 字段中 |
bcc | 否 | 电子邮件列表,将显示在 bcc 字段中 |
attachments | 否 | 电子邮件附件 - 键是文件名,值是文件、文件-like 对象或文件路径 |
context | 否 | 用于渲染模板电子邮件的字典 |
headers | 否 | 消息的额外头部的字典 |
scheduled_time | 否 | 表示电子邮件应发送的日期/日期时间的对象 |
expires_at | 否 | 如果指定,未发送的邮件在指定日期之后不会交付。 |
priority | 否 | high 、medium 、low 或 now (立即发送) |
backend | 否 | 您想要使用的后端的别名,如果没有指定,则使用 default 。 |
render_on_delivery | 否 | 将此设置为 True 会导致电子邮件在交付期间被懒加载渲染。当 render_on_delivery 为 True 时,需要 template 。使用此选项,电子邮件的完整内容永远不会存储在数据库中。如果您使用相同的模板发送许多电子邮件,可能会节省大量空间。 |
以下是一些示例。
如果您只想发送电子邮件而不使用数据库模板,您可以在没有 template
参数的情况下调用 send
命令。
from post_office import mail
mail.send(
['recipient1@example.com'],
'from@example.com',
subject='Welcome!',
message='Welcome home, {{ name }}!',
html_message='Welcome home, <b>{{ name }}</b>!',
headers={'Reply-to': 'reply@example.com'},
scheduled_time=date(2014, 1, 1),
context={'name': 'Alice'},
)
post_office
也对任务队列友好。将 now
作为 send_mail
中的优先级传递将立即交付电子邮件(而不是排队),无论您的队列中有多少封电子邮件。
from post_office import mail
mail.send(
['recipient1@example.com'],
'from@example.com',
template='welcome_email',
context={'foo': 'bar'},
priority='now',
)
如果您已经使用像 django-rq 这样的东西异步发送电子邮件,并且只需要存储与电子邮件相关的活动和日志,这将很有用。
如果您想发送带有附件的电子邮件
from django.core.files.base import ContentFile
from post_office import mail
mail.send(
['recipient1@example.com'],
'from@example.com',
template='welcome_email',
context={'foo': 'bar'},
priority='now',
attachments={
'attachment1.doc': '/path/to/file/file1.doc',
'attachment2.txt': ContentFile('file content'),
'attachment3.txt': {'file': ContentFile('file content'), 'mimetype': 'text/plain'},
}
)
模板标签和变量
post-office
支持 Django 的模板标签和变量。例如,如果您在主题行中放入 Hello, {{ name }}
,并将 {'name': 'Alice'}
作为上下文传入,您将得到 Hello, Alice
作为主题。
from post_office.models import EmailTemplate
from post_office import mail
EmailTemplate.objects.create(
name='morning_greeting',
subject='Morning, {{ name|capfirst }}',
content='Hi {{ name }}, how are you feeling today?',
html_content='Hi <strong>{{ name }}</strong>, how are you feeling today?',
)
mail.send(
['recipient@example.com'],
'from@example.com',
template='morning_greeting',
context={'name': 'alice'},
)
# This will create an email with the following content:
subject = 'Morning, Alice',
content = 'Hi alice, how are you feeling today?'
content = 'Hi <strong>alice</strong>, how are you feeling today?'
多语言电子邮件模板
您可以在多种不同的语言中轻松创建电子邮件模板。例如:
template = EmailTemplate.objects.create(
name='hello',
subject='Hello world!',
)
# Add an Indonesian version of this template:
indonesian_template = template.translated_templates.create(
language='id',
subject='Halo Dunia!'
)
使用非默认语言的模板发送电子邮件同样简单
mail.send(
['recipient@example.com'],
'from@example.com',
template=template, # Sends using the default template
)
mail.send(
['recipient@example.com'],
'from@example.com',
template=template,
language='id', # Sends using Indonesian template
)
内联图片
通常,人们希望在模板中渲染图片,这些图片作为内联的MIMEImage
附加到要发送的电子邮件中。这需要稍微修改Django模板引擎,保留内联图片列表,稍后这些图片将被添加到要发送的消息中。
首先,我们必须将一个特殊的Django模板后端添加到我们的模板引擎列表中
TEMPLATES = [
{
...
}, {
'BACKEND': 'post_office.template.backends.post_office.PostOfficeTemplates',
'APP_DIRS': True,
'DIRS': [],
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.template.context_processors.request',
]
}
}
]
然后,我们必须告诉Post-Office使用此模板引擎
POST_OFFICE = {
'TEMPLATE_ENGINE': 'post_office',
}
在用于渲染电子邮件HTML的模板中添加
{% load post_office %}
<p>... somewhere in the body ...</p>
<img src="{% inline_image 'path/to/image.png' %}" />
在这里,名为inline_image
的模板标签用于跟踪内联图片。它接受一个参数。这个参数可以是位于某个static
目录中的图片文件的相对路径,或者是图片文件的绝对路径,或者是一个图片文件对象本身。使用此模板标签渲染的模板,将为每个给定的图片渲染一个引用ID,并将这些图片存储在所采用的模板引擎的上下文中。稍后,当渲染的模板传递给邮件库时,那些图片将以MIMEImage
附件的形式转移到电子邮件消息对象中。
要发送包含纯文本正文和带内联图片的HTML的电子邮件,请使用以下代码片段
from django.core.mail import EmailMultiAlternatives
from django.template.loader import get_template
subject, body = "Hello", "Plain text body"
from_email, to_email = "no-reply@example.com", "john@example.com"
email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
template = get_template('email-template-name.html', using='post_office')
context = {...}
html = template.render(context)
email_message.attach_alternative(html, 'text/html')
template.attach_related(email_message)
email_message.send()
要发送包含带内联图片的HTML但不含纯文本正文的电子邮件,请使用此代码片段
from django.core.mail import EmailMultiAlternatives
from django.template.loader import get_template
subject, from_email, to_email = "Hello", "no-reply@example.com", "john@example.com"
template = get_template('email-template-name.html', using='post_office')
context = {...}
html = template.render(context)
email_message = EmailMultiAlternatives(subject, html, from_email, [to_email])
email_message.content_subtype = 'html'
template.attach_related(email_message)
email_message.send()
自定义电子邮件后端
默认情况下,post_office
使用Django的smtp.EmailBackend
。如果您想使用不同的后端,可以通过配置BACKENDS
来实现。
例如,如果您想使用django-ses
# Put this in settings.py
POST_OFFICE = {
...
'BACKENDS': {
'default': 'smtp.EmailBackend',
'ses': 'django_ses.SESBackend',
}
}
然后,您可以选择在发送邮件时使用哪个后端
# If you omit `backend_alias` argument, `default` will be used
mail.send(
['recipient@example.com'],
'from@example.com',
subject='Hello',
)
# If you want to send using `ses` backend
mail.send(
['recipient@example.com'],
'from@example.com',
subject='Hello',
backend='ses',
)
管理命令
send_queued_mail
- 发送队列中的电子邮件,那些未成功发送的将标记为failed
。接受以下参数:
参数 | 描述 |
---|---|
--processes 或 -p |
发送电子邮件的并行进程数。默认为1 |
--lockfile 或 -L |
用作锁文件的完整路径。默认为/tmp/post_office.lock |
cleanup_mail
- 删除在X天之前创建的所有电子邮件(默认为90天)。
参数 | 描述 |
---|---|
--days 或 -d |
大于此参数的电子邮件将被删除。默认为90 |
--delete-attachments |
标志以删除孤立的附件记录和磁盘上的文件。如果未指定,则不会删除附件。 |
您可能希望通过cron定期运行这些设置
* * * * * (cd $PROJECT; python manage.py send_queued_mail --processes=1 >> $PROJECT/cron_mail.log 2>&1)
0 1 * * * (cd $PROJECT; python manage.py cleanup_mail --days=30 --delete-attachments >> $PROJECT/cron_mail_cleanup.log 2>&1)
设置
本节概述了您可以在Django的settings.py
中设置的设置和配置,以微调post-office
的行为。
批量大小
如果您可能想要限制批量中发送的电子邮件数量(在低内存环境中很有用),请使用BATCH_SIZE
参数来限制一次批量中检索的队列电子邮件数量。BATCH_SIZE
默认为100。
# Put this in settings.py
POST_OFFICE = {
...
'BATCH_SIZE': 100,
}
3.8版本引入了一个名为BATCH_DELIVERY_TIMEOUT
的配套设置。此设置指定了每个批量交付允许的最大时间,这对于防止交付过程永不终止的情况很有用。默认为180。
如果您在慢速连接上一次性发送大量电子邮件,请考虑增加此数字。
# Put this in settings.py
POST_OFFICE = {
...
'BATCH_DELIVERY_TIMEOUT': 180,
}
默认优先级
电子邮件的默认优先级是medium
,但可以通过设置DEFAULT_PRIORITY
来更改。当设置为now
时,与异步电子邮件后端(例如基于Celery的后端)的集成变得简单。
# Put this in settings.py
POST_OFFICE = {
...
'DEFAULT_PRIORITY': 'now',
}
锁文件名称
默认的锁定文件名为 post_office
,但可以通过在配置中设置 LOCK_FILE_NAME
来更改。
# Put this in settings.py
POST_OFFICE = {
...
'LOCK_FILE_NAME': 'custom_lock_file',
}
覆盖收件人
默认为 None
。如果您想将所有电子邮件重定向到几个指定的电子邮件以供开发使用,此选项很有用。
# Put this in settings.py
POST_OFFICE = {
...
'OVERRIDE_RECIPIENTS': ['to@example.com', 'to2@example.com'],
}
消息-ID
SMTP标准要求每封电子邮件都包含一个唯一的 消息-ID。通常,消息-ID 由两个部分组成,由 @
符号分隔:左侧是一个生成的伪随机数。右侧是一个常量字符串,通常表示发送服务器的完全限定域名。
默认情况下,Django 在发送电子邮件时生成这样的消息-ID。由于 django-post_office 跟踪所有已发送的电子邮件,因此在数据库中创建和存储每个电子邮件的消息-ID 非常有用。然后可以在 Django 管理后端中查找此标识符。
要启用此功能,请将以下内容添加到 Post-Office 设置中
# Put this in settings.py
POST_OFFICE = {
...
'MESSAGE_ID_ENABLED': True,
}
可以通过使用另一个完全限定域名来进一步调整
# Put this in settings.py
POST_OFFICE = {
...
'MESSAGE_ID_ENABLED': True,
'MESSAGE_ID_FQDN': 'example.com',
}
否则,如果未设置 MESSAGE_ID_FQDN
(默认值),则 django-post_office 将回退到服务器的 DNS 名称,该名称由主机的网络设置确定。
重试
默认情况下未激活。您可以自动重新队列失败的电子邮件投递。您还可以配置在特定时间间隔后重试失败的投递。
# Put this in settings.py
POST_OFFICE = {
...
'MAX_RETRIES': 4,
'RETRY_INTERVAL': datetime.timedelta(minutes=15), # Schedule to be retried 15 minutes later
}
日志级别
日志存储在数据库中,可以通过 Django 管理进行浏览。默认日志级别为 2(记录成功和失败的投递)通过设置 LOG_LEVEL
可以更改此行为。
# Put this in settings.py
POST_OFFICE = {
...
'LOG_LEVEL': 1, # Log only failed deliveries
}
不同的选项是
0
不记录任何内容1
只记录失败的投递2
记录所有内容(成功和失败的投递尝试)
发送顺序
电子邮件的默认发送顺序为 -priority
,但可以通过设置 SENDING_ORDER
来更改。例如,如果您想按先进先出顺序发送队列中的电子邮件
# Put this in settings.py
POST_OFFICE = {
...
'SENDING_ORDER': ['created'],
}
上下文字段序列化器
如果您需要存储用于延迟渲染的复杂 Python 对象(即设置 render_on_delivery=True
),您可以指定自己的上下文字段类以存储上下文变量。例如,如果您想使用 django-picklefield
# Put this in settings.py
POST_OFFICE = {
...
'CONTEXT_FIELD_CLASS': 'picklefield.fields.PickledObjectField',
}
CONTEXT_FIELD_CLASS
默认为 django.db.models.JSONField
。
日志记录
您可以从 Django 的 settings.py
中配置 post-office
的日志记录。例如
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"post_office": {
"format": "[%(levelname)s]%(asctime)s PID %(process)d: %(message)s",
"datefmt": "%d-%m-%Y %H:%M:%S",
},
},
"handlers": {
"post_office": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "post_office"
},
# If you use sentry for logging
'sentry': {
'level': 'ERROR',
'class': 'raven.contrib.django.handlers.SentryHandler',
},
},
'loggers': {
"post_office": {
"handlers": ["post_office", "sentry"],
"level": "INFO"
},
},
}
线程
post-office
>= 3.0 允许您使用多个线程来显著提高发送电子邮件的速度。默认情况下,post-office
使用每个进程 5 个线程。您可以通过更改 THREADS_PER_PROCESS
设置来调整此设置。
这可能会根据您使用的电子邮件后端显著提高批量电子邮件投递的速度。在我的测试中,多线程可以加快使用基于 HTTP(REST)的投递机制的电子邮件后端的速度,但似乎对基于 SMTP 的后端没有帮助。
# Put this in settings.py
POST_OFFICE = {
...
'THREADS_PER_PROCESS': 10,
}
性能
缓存
如果配置了 Django 的缓存机制,则 post_office
将缓存 EmailTemplate
实例。如果您出于某种原因想禁用缓存,请将 settings.py
中的 POST_OFFICE_CACHE
设置为 False
。
## All cache key will be prefixed by post_office:template:
## To turn OFF caching, you need to explicitly set POST_OFFICE_CACHE to False in settings
POST_OFFICE_CACHE = False
## Optional: to use a non default cache backend, add a "post_office" entry in CACHES
CACHES = {
'post_office': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': '127.0.0.1:11211',
}
}
send_many()
send_many()
在发送大量电子邮件时性能更高(生成的数据库查询更少)。send_many()
几乎与 mail.send()
相同,唯一的区别是它接受一个关键字参数列表,您通常将其传递给 mail.send()
from post_office import mail
first_email = {
'sender': 'from@example.com',
'recipients': ['alice@example.com'],
'subject': 'Hi!',
'message': 'Hi Alice!'
}
second_email = {
'sender': 'from@example.com',
'recipients': ['bob@example.com'],
'subject': 'Hi!',
'message': 'Hi Bob!'
}
kwargs_list = [first_email, second_email]
mail.send_many(kwargs_list)
不支持使用 mail.send_many()
来添加附件。
运行测试
要运行测试套件
`which django-admin` test post_office --settings=post_office.test_settings --pythonpath=.
您可以使用以下命令运行Django和Python所有支持版本的完整测试套件:
tox
或者
python setup.py test
Celery集成
如果您的Django项目在启用Celery的配置中运行,您可以使用其工作进程发送排队中的电子邮件。与上面提到的cron解决方案或uWSGI定时器解决方案相比,这种设置的最大优点是排队中的电子邮件在添加到邮件队列后立即发送。立即发送,而交付仍然在单独的异步任务中完成,这可以防止在请求/响应周期中发送电子邮件。
如果您已在项目中配置了Celery并启动了Celery工作进程,您应该看到类似以下的内容:
--------------- celery@halcyon.local v4.0 (latentcall)
--- ***** -----
-- ******* ---- [Configuration]
- *** --- * --- . broker: amqp://guest@localhost:5672//
- ** ---------- . app: __main__:0x1012d8590
- ** ---------- . concurrency: 8 (processes)
- ** ---------- . events: OFF (enable -E to monitor this worker)
- ** ----------
- *** --- * --- [Queues]
-- ******* ---- . celery: exchange:celery(direct) binding:celery
--- ***** -----
[tasks]
. post_office.tasks.cleanup_expired_mails
. post_office.tasks.send_queued_mail
必须明确启用通过Celery工作进程发送电子邮件
# Put this in settings.py
POST_OFFICE = {
...
'CELERY_ENABLED': True,
}
然后,电子邮件将在排队后立即发送。为了实现这一点,项目的celery.py
设置应调用autodiscover_tasks函数。如果发生临时交付失败,我们可以通过周期性任务尝试重新发送这些电子邮件。这可以通过简单的Celery beat配置进行安排,例如
app.conf.beat_schedule = {
'send-queued-mail': {
'task': 'post_office.tasks.send_queued_mail',
'schedule': 600.0,
},
}
现在,电子邮件队列将每10分钟处理一次。如果您使用Django Celery Beat,则使用Django-Admin后端,并为post_office.tasks.send_queued_mail
添加周期性任务。
根据您的策略,您还可能想从队列中删除过期的电子邮件。这可以通过为post_office.tasks.cleanup_mail
添加另一个周期性任务来完成,该任务可能每周或每月运行一次。
与uWSGI集成
如果设置Celery过于复杂,并且您使用uWSGI作为应用程序服务器,那么uWSGI装饰器可以作为穷人的调度器。只需将以下简短代码段添加到项目的wsgi.py
文件中即可
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# add this block of code
try:
import uwsgidecorators
from django.core.management import call_command
@uwsgidecorators.timer(10)
def send_queued_mail(num):
"""Send queued mail every 10 seconds"""
call_command('send_queued_mail', processes=1)
except ImportError:
print("uwsgidecorators not found. Cron and timers are disabled")
或者您也可以使用装饰器@uwsgidecorators.cron(minute, hour, day, month, weekday)
。这将安排在特定时间执行任务。使用-1
表示任何时间,它与cron中的*
相对应。
请注意,仅在应用已使用uWSGI启动时,才可使用uwsgidecorators
。然而,Django的内部./manage.py runserver
也访问此文件,因此将代码块包装在上述异常处理器中。
此配置在Docker容器等环境中非常有用,在这些环境中您没有运行cron守护进程。
信号
每次将电子邮件添加到邮件队列时,Post Office都会发出一个特殊的Django信号。每当第三方应用程序想要了解此事件时,它应将回调函数连接到Post Office的信号处理器email_queued
,例如
from django.dispatch import receiver
from post_office.signals import email_queued
@receiver(email_queued)
def my_callback(sender, emails, **kwargs):
print("Added {} mails to the sending queue".format(len(emails)))
添加到队列中的电子邮件对象作为列表传递给回调处理器。
变更日志
完整的变更日志可以在这里找到。
由印度尼西亚最优雅的CRM/忠诚度平台Stamps的酷家伙们创建和维护。