django-post-office


Namedjango-post-office JSON
Version 3.8.0 PyPI version JSON
download
home_pagehttps://github.com/ui/django-post_office
SummaryA Django app to monitor and send mail asynchronously, complete with template support.
upload_time2023-10-22 08:36:44
maintainer
docs_urlNone
authorSelwin Ong
requires_python
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI
coveralls test coverage No coveralls.
            # Django Post Office

Django Post Office is a simple app to send and manage your emails in
Django. Some awesome features are:

-   Designed to scale, handles millions of emails efficiently
-   Allows you to send email asynchronously
-   Multi backend support
-   Supports HTML email
-   Supports inlined images in HTML email
-   Supports database based email templates
-   Supports multilingual email templates (i18n)
-   Built in scheduling support
-   Works well with task queues like [RQ](http://python-rq.org) or
    [Celery](http://www.celeryproject.org)
-   Uses multiprocessing and threading to send a large number of emails in parallel

## Dependencies

-   [django \>= 2.2](https://djangoproject.com/)
-   [jsonfield](https://github.com/rpkilby/jsonfield)
-   [bleach](https://bleach.readthedocs.io/)

With this optional dependency, HTML emails are nicely rendered
inside the Django admin backend. Without this library, all HTML tags
will otherwise be stripped for security reasons.

## Installation

[![Build Status](https://github.com/ui/django-post_office/actions/workflows/test.yml/badge.svg)](https://github.com/ui/django-post_office/actions)
[![PyPI](https://img.shields.io/pypi/pyversions/django-post_office.svg)]()
[![PyPI version](https://img.shields.io/pypi/v/django-post_office.svg)](https://pypi.python.org/pypi/django-post_office)
[![PyPI](https://img.shields.io/pypi/l/django-post_office.svg)]()


```sh
pip install django-post_office
```

Add `post_office` to your INSTALLED_APPS in django's `settings.py`:

```python
INSTALLED_APPS = (
    # other apps
    "post_office",
)
```

Run `migrate`:

```sh
python manage.py migrate
```

Set `post_office.EmailBackend` as your `EMAIL_BACKEND` in Django's `settings.py`:

```python
EMAIL_BACKEND = 'post_office.EmailBackend'
```

## Quickstart

Send a simple email is really easy:

```python
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>!',
)
```

If you want to use templates, ensure that Django's admin interface is
enabled. Create an `EmailTemplate` instance via `admin` and do the
following:

```python
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'},
)
```

The above command will put your email on the queue so you can use the
command in your webapp without slowing down the request/response cycle
too much. To actually send them out, run
`python manage.py send_queued_mail`. You can schedule this management
command to run regularly via cron:

    * * * * * (/usr/bin/python manage.py send_queued_mail >> send_mail.log 2>&1)

## Usage

### mail.send()

`mail.send` is the most important function in this library, it takes
these arguments:

| Argument | Required | Description |
| --- | --- | --- |
| recipients | Yes | List of recipient email addresses |
| sender | No | Defaults to `settings.DEFAULT_FROM_EMAIL`, display name like `John <john@a.com>` is allowed |
| subject | No | Email subject (if `template` is not specified) |
| message | No | Email content (if `template` is not specified) |
| html_message | No | HTML content (if `template` is not specified) |
| template | No | `EmailTemplate` instance or name of template |
| language | No | Language in which you want to send the email in (if you have multilingual email templates). |
| cc | No | List of emails, will appear in `cc` field |
| bcc | No | List of emails, will appear in `bcc` field |
| attachments | No | Email attachments - a dict where the keys are the filenames and the values are files, file-like-objects or path to file |
| context | No | A dict, used to render templated email |
| headers | No | A dictionary of extra headers on the message |
| scheduled_time | No | A date/datetime object indicating when the email should be sent |
| expires_at | No | If specified, mails that are not yet sent won't be delivered after this date. |
| priority | No | `high`, `medium`, `low` or `now` (sent immediately) |
| backend | No | Alias of the backend you want to use, `default` will be used if not specified. |
| render_on_delivery | No | Setting this to `True` causes email to be lazily rendered during delivery. `template` is required when `render_on_delivery` is True. With this option, the full email content is never stored in the DB. May result in significant space savings if you're sending many emails using the same template. |

Here are a few examples.

If you just want to send out emails without using database templates.
You can call the `send` command without the `template` argument.

```python
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` is also task queue friendly. Passing `now` as priority
into `send_mail` will deliver the email right away (instead of queuing
it), regardless of how many emails you have in your queue:

```python
from post_office import mail

mail.send(
    ['recipient1@example.com'],
    'from@example.com',
    template='welcome_email',
    context={'foo': 'bar'},
    priority='now',
)
```

This is useful if you already use something like [django-rq](https://github.com/ui/django-rq) to send emails
asynchronously and only need to store email related activities and logs.

If you want to send an email with attachments:

```python
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'},
    }
)
```

### Template Tags and Variables

`post-office` supports Django's template tags and variables. For
example, if you put `Hello, {{ name }}` in the subject line and pass in
`{'name': 'Alice'}` as context, you will get `Hello, Alice` as subject:

```python
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?'
```

### Multilingual Email Templates

You can easily create email templates in various different languages.
For example:

```python
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!'
)
```

Sending an email using template in a non default language is similarly easy:

```python
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
)
```

### Inlined Images

Often one wants to render images inside a template, which are attached
as inlined `MIMEImage` to the outgoing email. This requires a slightly
modified Django Template Engine, keeping a list of inlined images, which
later will be added to the outgoing message.

First we must add a special Django template backend to our list of template engines:

```python
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',
            ]
        }
    }
]
```

then we must tell Post-Office to use this template engine:

```python
POST_OFFICE = {
    'TEMPLATE_ENGINE': 'post_office',
}
```

In templates used to render HTML for emails add

```
{% load post_office %}

<p>... somewhere in the body ...</p>
<img src="{% inline_image 'path/to/image.png' %}" />
```

Here the templatetag named `inline_image` is used to keep track of
inlined images. It takes a single parameter. This can either be the
relative path to an image file located in one of the `static`
directories, or the absolute path to an image file, or an image-file
object itself. Templates rendered using this templatetag, render a
reference ID for each given image, and store these images inside the
context of the adopted template engine. Later on, when the rendered
template is passed to the mailing library, those images will be
transferred to the email message object as `MIMEImage`-attachments.

To send an email containing both, a plain text body and some HTML with
inlined images, use the following code snippet:

```python
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()
```

To send an email containing HTML with inlined images, but without a
plain text body, use this code snippet:

```python
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()
```

### Custom Email Backends

By default, `post_office` uses django's `smtp.EmailBackend`. If you want
to use a different backend, you can do so by configuring `BACKENDS`.

For example if you want to use [django-ses](https://github.com/hmarr/django-ses):

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'BACKENDS': {
        'default': 'smtp.EmailBackend',
        'ses': 'django_ses.SESBackend',
    }
}
```

You can then choose what backend you want to use when sending mail:

```python
# 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',
)
```

### Management Commands

-   `send_queued_mail` - send queued emails, those aren't successfully
    sent will be marked as `failed`. Accepts the following arguments:

  | Argument | Description |
  | --- | --- |
  |`--processes` or `-p` | Number of parallel processes to send email. Defaults to 1 |
  | `--lockfile` or `-L` | Full path to file used as lock file. Defaults to `/tmp/post_office.lock` |


-   `cleanup_mail` - delete all emails created before an X number of
    days (defaults to 90).

| Argument | Description |
| --- | --- |
| `--days` or `-d` | Email older than this argument will be deleted. Defaults to 90 |
| `--delete-attachments` | Flag to delete orphaned attachment records and files on disk. If not specified, attachments won't be deleted. |

You may want to set these up via cron to run regularly:

    * * * * * (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)


## Settings

This section outlines all the settings and configurations that you can
put in Django's `settings.py` to fine tune `post-office`'s behavior.


### Batch Size

If you may want to limit the number of emails sent in a batch (
useful in a low memory environment), use the `BATCH_SIZE` argument to
limit the number of queued emails fetched in one batch. `BATCH_SIZE` defaults to 100.

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'BATCH_SIZE': 100,
}
```

Version 3.8 introduces a companion setting called `BATCH_DELIVERY_TIMEOUT`. This setting
specifies the maximum time allowed for each batch to be delivered. Defaults to 180.

If you send a large number of emails in a single batch on a slow connection, consider increasing this number.

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'BATCH_DELIVERY_TIMEOUT': 180,
}
```

### Default Priority

The default priority for emails is `medium`, but this can be altered by
setting `DEFAULT_PRIORITY`. Integration with asynchronous email backends
(e.g. based on Celery) becomes trivial when set to `now`.

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'DEFAULT_PRIORITY': 'now',
}
```

### Override Recipients

Defaults to `None`. This option is useful if you want to redirect all
emails to specified a few email for development purposes.

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'OVERRIDE_RECIPIENTS': ['to@example.com', 'to2@example.com'],
}
```

### Message-ID

The SMTP standard requires that each email contains a unique [Message-ID](https://tools.ietf.org/html/rfc2822#section-3.6.4). Typically the Message-ID consists of two parts separated by the `@`
symbol: The left part is a generated pseudo random number. The right
part is a constant string, typically denoting the full qualified domain
name of the sending server.

By default, **Django** generates such a Message-ID during email
delivery. Since **django-post_office** keeps track of all delivered
emails, it can be very useful to create and store this Message-ID while
creating each email in the database. This identifier then can be looked
up in the Django admin backend.

To enable this feature, add this to your Post-Office settings:

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'MESSAGE_ID_ENABLED': True,
}
```

It can further be fine tuned, using for instance another full qualified
domain name:

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'MESSAGE_ID_ENABLED': True,
    'MESSAGE_ID_FQDN': 'example.com',
}
```

Otherwise, if `MESSAGE_ID_FQDN` is unset (the default),
**django-post_office** falls back to the DNS name of the server, which
is determined by the network settings of the host.

### Retry

Not activated by default. You can automatically requeue failed email deliveries.
You can also configure failed deliveries to be retried after a specific time interval.

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'MAX_RETRIES': 4,
    'RETRY_INTERVAL': datetime.timedelta(minutes=15),  # Schedule to be retried 15 minutes later
}
```

### Log Level

Logs are stored in the database and is browsable via Django admin.
The default log level is 2 (logs both successful and failed deliveries)
This behavior can be changed by setting `LOG_LEVEL`.

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'LOG_LEVEL': 1, # Log only failed deliveries
}
```

The different options are:

* `0` logs nothing
* `1` logs only failed deliveries
* `2` logs everything (both successful and failed delivery attempts)

### Sending Order

The default sending order for emails is `-priority`, but this can be
altered by setting `SENDING_ORDER`. For example, if you want to send
queued emails in FIFO order :

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'SENDING_ORDER': ['created'],
}
```

### Context Field Serializer

If you need to store complex Python objects for deferred rendering (i.e.
setting `render_on_delivery=True`), you can specify your own context
field class to store context variables. For example if you want to use
[django-picklefield](https://github.com/gintas/django-picklefield/tree/master/src/picklefield):

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'CONTEXT_FIELD_CLASS': 'picklefield.fields.PickledObjectField',
}
```

`CONTEXT_FIELD_CLASS` defaults to `django.db.models.JSONField`.

### Logging

You can configure `post-office`'s logging from Django's `settings.py`.
For example:

```python
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"
        },
    },
}
```

### Threads

`post-office` >= 3.0 allows you to use multiple threads to dramatically
speed up the speed at which emails are sent. By default, `post-office`
uses 5 threads per process. You can tweak this setting by changing
`THREADS_PER_PROCESS` setting.

This may dramatically increase the speed of bulk email delivery,
depending on which email backends you use. In my tests, multi threading
speeds up email backends that use HTTP based (REST) delivery mechanisms
but doesn't seem to help SMTP based backends.

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'THREADS_PER_PROCESS': 10,
}
```

Performance
-----------

### Caching

if Django's caching mechanism is configured, `post_office` will cache
`EmailTemplate` instances . If for some reason you want to disable
caching, set `POST_OFFICE_CACHE` to `False` in `settings.py`:

```python
## 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()` is much more performant (generates less database queries)
when sending a large number of emails. `send_many()` is almost identical
to `mail.send()`, with the exception that it accepts a list of keyword
arguments that you'd usually pass into `mail.send()`:

```python
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)
```

Attachments are not supported with `mail.send_many()`.

## Running Tests

To run the test suite:

```python
`which django-admin` test post_office --settings=post_office.test_settings --pythonpath=.
```

You can run the full test suite for all supported versions of Django and Python with:

```python
tox
```

or:

```python
python setup.py test
```


## Integration with Celery

If your Django project runs in a Celery enabled configuration, you can use its worker to send out
queued emails. Compared to the solution with cron (see above), or the solution with uWSGI timers
(see below) this setup has the big advantage that queued emails are send *immediately* after they
have been added to the mail queue. The delivery is still performed in a separate and asynchronous
task, which prevents sending emails during the request/response-cycle.

If you [configured Celery](https://docs.celeryproject.org/en/latest/userguide/application.html)
in your project and started the
[Celery worker](https://docs.celeryproject.org/en/latest/userguide/workers.html),
you should see something such as:

```
--------------- 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
```

Delivering emails through the Celery worker must be explicitly enabled:

```python
# Put this in settings.py
POST_OFFICE = {
    ...
    'CELERY_ENABLED': True,
}
```

Emails will then be delivered immediately after they have been queued. In order to make this happen,
the project's `celery.py` setup shall invoke the
[autodiscoverttasks](https://docs.celeryproject.org/en/latest/reference/celery.html#celery.Celery.autodiscover_tasks)
function. In case of a temporary delivery failure, we might want retrying to send those emails by a
periodic task. This can be scheduled with a simple
[Celery beat configuration](https://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#entries),
for instance through

```python
app.conf.beat_schedule = {
    'send-queued-mail': {
        'task': 'post_office.tasks.send_queued_mail',
        'schedule': 600.0,
    },
}
```

The email queue now will be processed every 10 minutes. If you are using
[Django Celery Beat](https://django-celery-beat.readthedocs.io/en/latest/),
then use the Django-Admin backend and add a periodic tasks for `post_office.tasks.send_queued_mail`.

Depending on your policy, you may also want to remove expired emails from the queue. This can be
done by adding another periodic tasks for `post_office.tasks.cleanup_mail`, which may run once a
week or month.


## Integration with uWSGI

If setting up Celery is too daunting and you use
[uWSGI](https://uwsgi-docs.readthedocs.org/en/latest/) as application
server, then uWSGI decorators can act as a poor men's scheduler. Just
add this short snipped to the project's `wsgi.py` file:

```python
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")
```

Alternatively you can also use the decorator
`@uwsgidecorators.cron(minute, hour, day, month, weekday)`. This will
schedule a task at specific times. Use `-1` to signal any time, it
corresponds to the `*` in cron.

Please note that `uwsgidecorators` are available only, if the
application has been started with **uWSGI**. However, Django's internal
`./manange.py runserver` also access this file, therefore wrap the block
into an exception handler as shown above.

This configuration can be useful in environments, such as Docker
containers, where you don't have a running cron-daemon.

## Signals

Each time an email is added to the mail queue, Post Office emits a
special [Django
signal](https://docs.djangoproject.com/en/stable/topics/signals/).
Whenever a third party application wants to be informed about this
event, it shall connect a callback function to the Post Office's signal
handler `email_queued`, for instance:

```python
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)))
```

The Emails objects added to the queue are passed as list to the callback
handler.


## Changelog

Full changelog can be found [here](https://github.com/ui/django-post_office/blob/master/CHANGELOG.md).

Created and maintained by the cool guys at [Stamps](https://stamps.co.id), Indonesia's most elegant
CRM/loyalty platform.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/ui/django-post_office",
    "name": "django-post-office",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "",
    "author": "Selwin Ong",
    "author_email": "selwin.ong@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/0e/57/3c29d7c8205c53b4f950897f53032bf14364386d9d8c3297670cefb9cbd0/django-post_office-3.8.0.tar.gz",
    "platform": null,
    "description": "# Django Post Office\n\nDjango Post Office is a simple app to send and manage your emails in\nDjango. Some awesome features are:\n\n-   Designed to scale, handles millions of emails efficiently\n-   Allows you to send email asynchronously\n-   Multi backend support\n-   Supports HTML email\n-   Supports inlined images in HTML email\n-   Supports database based email templates\n-   Supports multilingual email templates (i18n)\n-   Built in scheduling support\n-   Works well with task queues like [RQ](http://python-rq.org) or\n    [Celery](http://www.celeryproject.org)\n-   Uses multiprocessing and threading to send a large number of emails in parallel\n\n## Dependencies\n\n-   [django \\>= 2.2](https://djangoproject.com/)\n-   [jsonfield](https://github.com/rpkilby/jsonfield)\n-   [bleach](https://bleach.readthedocs.io/)\n\nWith this optional dependency, HTML emails are nicely rendered\ninside the Django admin backend. Without this library, all HTML tags\nwill otherwise be stripped for security reasons.\n\n## Installation\n\n[![Build Status](https://github.com/ui/django-post_office/actions/workflows/test.yml/badge.svg)](https://github.com/ui/django-post_office/actions)\n[![PyPI](https://img.shields.io/pypi/pyversions/django-post_office.svg)]()\n[![PyPI version](https://img.shields.io/pypi/v/django-post_office.svg)](https://pypi.python.org/pypi/django-post_office)\n[![PyPI](https://img.shields.io/pypi/l/django-post_office.svg)]()\n\n\n```sh\npip install django-post_office\n```\n\nAdd `post_office` to your INSTALLED_APPS in django's `settings.py`:\n\n```python\nINSTALLED_APPS = (\n    # other apps\n    \"post_office\",\n)\n```\n\nRun `migrate`:\n\n```sh\npython manage.py migrate\n```\n\nSet `post_office.EmailBackend` as your `EMAIL_BACKEND` in Django's `settings.py`:\n\n```python\nEMAIL_BACKEND = 'post_office.EmailBackend'\n```\n\n## Quickstart\n\nSend a simple email is really easy:\n\n```python\nfrom post_office import mail\n\nmail.send(\n    'recipient@example.com', # List of email addresses also accepted\n    'from@example.com',\n    subject='My email',\n    message='Hi there!',\n    html_message='Hi <strong>there</strong>!',\n)\n```\n\nIf you want to use templates, ensure that Django's admin interface is\nenabled. Create an `EmailTemplate` instance via `admin` and do the\nfollowing:\n\n```python\nfrom post_office import mail\n\nmail.send(\n    'recipient@example.com', # List of email addresses also accepted\n    'from@example.com',\n    template='welcome_email', # Could be an EmailTemplate instance or name\n    context={'foo': 'bar'},\n)\n```\n\nThe above command will put your email on the queue so you can use the\ncommand in your webapp without slowing down the request/response cycle\ntoo much. To actually send them out, run\n`python manage.py send_queued_mail`. You can schedule this management\ncommand to run regularly via cron:\n\n    * * * * * (/usr/bin/python manage.py send_queued_mail >> send_mail.log 2>&1)\n\n## Usage\n\n### mail.send()\n\n`mail.send` is the most important function in this library, it takes\nthese arguments:\n\n| Argument | Required | Description |\n| --- | --- | --- |\n| recipients | Yes | List of recipient email addresses |\n| sender | No | Defaults to `settings.DEFAULT_FROM_EMAIL`, display name like `John <john@a.com>` is allowed |\n| subject | No | Email subject (if `template` is not specified) |\n| message | No | Email content (if `template` is not specified) |\n| html_message | No | HTML content (if `template` is not specified) |\n| template | No | `EmailTemplate` instance or name of template |\n| language | No | Language in which you want to send the email in (if you have multilingual email templates). |\n| cc | No | List of emails, will appear in `cc` field |\n| bcc | No | List of emails, will appear in `bcc` field |\n| attachments | No | Email attachments - a dict where the keys are the filenames and the values are files, file-like-objects or path to file |\n| context | No | A dict, used to render templated email |\n| headers | No | A dictionary of extra headers on the message |\n| scheduled_time | No | A date/datetime object indicating when the email should be sent |\n| expires_at | No | If specified, mails that are not yet sent won't be delivered after this date. |\n| priority | No | `high`, `medium`, `low` or `now` (sent immediately) |\n| backend | No | Alias of the backend you want to use, `default` will be used if not specified. |\n| render_on_delivery | No | Setting this to `True` causes email to be lazily rendered during delivery. `template` is required when `render_on_delivery` is True. With this option, the full email content is never stored in the DB. May result in significant space savings if you're sending many emails using the same template. |\n\nHere are a few examples.\n\nIf you just want to send out emails without using database templates.\nYou can call the `send` command without the `template` argument.\n\n```python\nfrom post_office import mail\n\nmail.send(\n    ['recipient1@example.com'],\n    'from@example.com',\n    subject='Welcome!',\n    message='Welcome home, {{ name }}!',\n    html_message='Welcome home, <b>{{ name }}</b>!',\n    headers={'Reply-to': 'reply@example.com'},\n    scheduled_time=date(2014, 1, 1),\n    context={'name': 'Alice'},\n)\n```\n\n`post_office` is also task queue friendly. Passing `now` as priority\ninto `send_mail` will deliver the email right away (instead of queuing\nit), regardless of how many emails you have in your queue:\n\n```python\nfrom post_office import mail\n\nmail.send(\n    ['recipient1@example.com'],\n    'from@example.com',\n    template='welcome_email',\n    context={'foo': 'bar'},\n    priority='now',\n)\n```\n\nThis is useful if you already use something like [django-rq](https://github.com/ui/django-rq) to send emails\nasynchronously and only need to store email related activities and logs.\n\nIf you want to send an email with attachments:\n\n```python\nfrom django.core.files.base import ContentFile\nfrom post_office import mail\n\nmail.send(\n    ['recipient1@example.com'],\n    'from@example.com',\n    template='welcome_email',\n    context={'foo': 'bar'},\n    priority='now',\n    attachments={\n        'attachment1.doc': '/path/to/file/file1.doc',\n        'attachment2.txt': ContentFile('file content'),\n        'attachment3.txt': {'file': ContentFile('file content'), 'mimetype': 'text/plain'},\n    }\n)\n```\n\n### Template Tags and Variables\n\n`post-office` supports Django's template tags and variables. For\nexample, if you put `Hello, {{ name }}` in the subject line and pass in\n`{'name': 'Alice'}` as context, you will get `Hello, Alice` as subject:\n\n```python\nfrom post_office.models import EmailTemplate\nfrom post_office import mail\n\nEmailTemplate.objects.create(\n    name='morning_greeting',\n    subject='Morning, {{ name|capfirst }}',\n    content='Hi {{ name }}, how are you feeling today?',\n    html_content='Hi <strong>{{ name }}</strong>, how are you feeling today?',\n)\n\nmail.send(\n    ['recipient@example.com'],\n    'from@example.com',\n    template='morning_greeting',\n    context={'name': 'alice'},\n)\n\n# This will create an email with the following content:\nsubject = 'Morning, Alice',\ncontent = 'Hi alice, how are you feeling today?'\ncontent = 'Hi <strong>alice</strong>, how are you feeling today?'\n```\n\n### Multilingual Email Templates\n\nYou can easily create email templates in various different languages.\nFor example:\n\n```python\ntemplate = EmailTemplate.objects.create(\n    name='hello',\n    subject='Hello world!',\n)\n\n# Add an Indonesian version of this template:\nindonesian_template = template.translated_templates.create(\n    language='id',\n    subject='Halo Dunia!'\n)\n```\n\nSending an email using template in a non default language is similarly easy:\n\n```python\nmail.send(\n    ['recipient@example.com'],\n    'from@example.com',\n    template=template, # Sends using the default template\n)\n\nmail.send(\n    ['recipient@example.com'],\n    'from@example.com',\n    template=template,\n    language='id', # Sends using Indonesian template\n)\n```\n\n### Inlined Images\n\nOften one wants to render images inside a template, which are attached\nas inlined `MIMEImage` to the outgoing email. This requires a slightly\nmodified Django Template Engine, keeping a list of inlined images, which\nlater will be added to the outgoing message.\n\nFirst we must add a special Django template backend to our list of template engines:\n\n```python\nTEMPLATES = [\n    {\n        ...\n    }, {\n        'BACKEND': 'post_office.template.backends.post_office.PostOfficeTemplates',\n        'APP_DIRS': True,\n        'DIRS': [],\n        'OPTIONS': {\n            'context_processors': [\n                'django.contrib.auth.context_processors.auth',\n                'django.template.context_processors.debug',\n                'django.template.context_processors.i18n',\n                'django.template.context_processors.media',\n                'django.template.context_processors.static',\n                'django.template.context_processors.tz',\n                'django.template.context_processors.request',\n            ]\n        }\n    }\n]\n```\n\nthen we must tell Post-Office to use this template engine:\n\n```python\nPOST_OFFICE = {\n    'TEMPLATE_ENGINE': 'post_office',\n}\n```\n\nIn templates used to render HTML for emails add\n\n```\n{% load post_office %}\n\n<p>... somewhere in the body ...</p>\n<img src=\"{% inline_image 'path/to/image.png' %}\" />\n```\n\nHere the templatetag named `inline_image` is used to keep track of\ninlined images. It takes a single parameter. This can either be the\nrelative path to an image file located in one of the `static`\ndirectories, or the absolute path to an image file, or an image-file\nobject itself. Templates rendered using this templatetag, render a\nreference ID for each given image, and store these images inside the\ncontext of the adopted template engine. Later on, when the rendered\ntemplate is passed to the mailing library, those images will be\ntransferred to the email message object as `MIMEImage`-attachments.\n\nTo send an email containing both, a plain text body and some HTML with\ninlined images, use the following code snippet:\n\n```python\nfrom django.core.mail import EmailMultiAlternatives\nfrom django.template.loader import get_template\n\nsubject, body = \"Hello\", \"Plain text body\"\nfrom_email, to_email = \"no-reply@example.com\", \"john@example.com\"\nemail_message = EmailMultiAlternatives(subject, body, from_email, [to_email])\ntemplate = get_template('email-template-name.html', using='post_office')\ncontext = {...}\nhtml = template.render(context)\nemail_message.attach_alternative(html, 'text/html')\ntemplate.attach_related(email_message)\nemail_message.send()\n```\n\nTo send an email containing HTML with inlined images, but without a\nplain text body, use this code snippet:\n\n```python\nfrom django.core.mail import EmailMultiAlternatives\nfrom django.template.loader import get_template\n\nsubject, from_email, to_email = \"Hello\", \"no-reply@example.com\", \"john@example.com\"\ntemplate = get_template('email-template-name.html', using='post_office')\ncontext = {...}\nhtml = template.render(context)\nemail_message = EmailMultiAlternatives(subject, html, from_email, [to_email])\nemail_message.content_subtype = 'html'\ntemplate.attach_related(email_message)\nemail_message.send()\n```\n\n### Custom Email Backends\n\nBy default, `post_office` uses django's `smtp.EmailBackend`. If you want\nto use a different backend, you can do so by configuring `BACKENDS`.\n\nFor example if you want to use [django-ses](https://github.com/hmarr/django-ses):\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'BACKENDS': {\n        'default': 'smtp.EmailBackend',\n        'ses': 'django_ses.SESBackend',\n    }\n}\n```\n\nYou can then choose what backend you want to use when sending mail:\n\n```python\n# If you omit `backend_alias` argument, `default` will be used\nmail.send(\n    ['recipient@example.com'],\n    'from@example.com',\n    subject='Hello',\n)\n\n# If you want to send using `ses` backend\nmail.send(\n    ['recipient@example.com'],\n    'from@example.com',\n    subject='Hello',\n    backend='ses',\n)\n```\n\n### Management Commands\n\n-   `send_queued_mail` - send queued emails, those aren't successfully\n    sent will be marked as `failed`. Accepts the following arguments:\n\n  | Argument | Description |\n  | --- | --- |\n  |`--processes` or `-p` | Number of parallel processes to send email. Defaults to 1 |\n  | `--lockfile` or `-L` | Full path to file used as lock file. Defaults to `/tmp/post_office.lock` |\n\n\n-   `cleanup_mail` - delete all emails created before an X number of\n    days (defaults to 90).\n\n| Argument | Description |\n| --- | --- |\n| `--days` or `-d` | Email older than this argument will be deleted. Defaults to 90 |\n| `--delete-attachments` | Flag to delete orphaned attachment records and files on disk. If not specified, attachments won't be deleted. |\n\nYou may want to set these up via cron to run regularly:\n\n    * * * * * (cd $PROJECT; python manage.py send_queued_mail --processes=1 >> $PROJECT/cron_mail.log 2>&1)\n    0 1 * * * (cd $PROJECT; python manage.py cleanup_mail --days=30 --delete-attachments >> $PROJECT/cron_mail_cleanup.log 2>&1)\n\n\n## Settings\n\nThis section outlines all the settings and configurations that you can\nput in Django's `settings.py` to fine tune `post-office`'s behavior.\n\n\n### Batch Size\n\nIf you may want to limit the number of emails sent in a batch (\nuseful in a low memory environment), use the `BATCH_SIZE` argument to\nlimit the number of queued emails fetched in one batch. `BATCH_SIZE` defaults to 100.\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'BATCH_SIZE': 100,\n}\n```\n\nVersion 3.8 introduces a companion setting called `BATCH_DELIVERY_TIMEOUT`. This setting\nspecifies the maximum time allowed for each batch to be delivered. Defaults to 180.\n\nIf you send a large number of emails in a single batch on a slow connection, consider increasing this number.\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'BATCH_DELIVERY_TIMEOUT': 180,\n}\n```\n\n### Default Priority\n\nThe default priority for emails is `medium`, but this can be altered by\nsetting `DEFAULT_PRIORITY`. Integration with asynchronous email backends\n(e.g. based on Celery) becomes trivial when set to `now`.\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'DEFAULT_PRIORITY': 'now',\n}\n```\n\n### Override Recipients\n\nDefaults to `None`. This option is useful if you want to redirect all\nemails to specified a few email for development purposes.\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'OVERRIDE_RECIPIENTS': ['to@example.com', 'to2@example.com'],\n}\n```\n\n### Message-ID\n\nThe SMTP standard requires that each email contains a unique [Message-ID](https://tools.ietf.org/html/rfc2822#section-3.6.4). Typically the Message-ID consists of two parts separated by the `@`\nsymbol: The left part is a generated pseudo random number. The right\npart is a constant string, typically denoting the full qualified domain\nname of the sending server.\n\nBy default, **Django** generates such a Message-ID during email\ndelivery. Since **django-post_office** keeps track of all delivered\nemails, it can be very useful to create and store this Message-ID while\ncreating each email in the database. This identifier then can be looked\nup in the Django admin backend.\n\nTo enable this feature, add this to your Post-Office settings:\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'MESSAGE_ID_ENABLED': True,\n}\n```\n\nIt can further be fine tuned, using for instance another full qualified\ndomain name:\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'MESSAGE_ID_ENABLED': True,\n    'MESSAGE_ID_FQDN': 'example.com',\n}\n```\n\nOtherwise, if `MESSAGE_ID_FQDN` is unset (the default),\n**django-post_office** falls back to the DNS name of the server, which\nis determined by the network settings of the host.\n\n### Retry\n\nNot activated by default. You can automatically requeue failed email deliveries.\nYou can also configure failed deliveries to be retried after a specific time interval.\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'MAX_RETRIES': 4,\n    'RETRY_INTERVAL': datetime.timedelta(minutes=15),  # Schedule to be retried 15 minutes later\n}\n```\n\n### Log Level\n\nLogs are stored in the database and is browsable via Django admin.\nThe default log level is 2 (logs both successful and failed deliveries)\nThis behavior can be changed by setting `LOG_LEVEL`.\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'LOG_LEVEL': 1, # Log only failed deliveries\n}\n```\n\nThe different options are:\n\n* `0` logs nothing\n* `1` logs only failed deliveries\n* `2` logs everything (both successful and failed delivery attempts)\n\n### Sending Order\n\nThe default sending order for emails is `-priority`, but this can be\naltered by setting `SENDING_ORDER`. For example, if you want to send\nqueued emails in FIFO order :\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'SENDING_ORDER': ['created'],\n}\n```\n\n### Context Field Serializer\n\nIf you need to store complex Python objects for deferred rendering (i.e.\nsetting `render_on_delivery=True`), you can specify your own context\nfield class to store context variables. For example if you want to use\n[django-picklefield](https://github.com/gintas/django-picklefield/tree/master/src/picklefield):\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'CONTEXT_FIELD_CLASS': 'picklefield.fields.PickledObjectField',\n}\n```\n\n`CONTEXT_FIELD_CLASS` defaults to `django.db.models.JSONField`.\n\n### Logging\n\nYou can configure `post-office`'s logging from Django's `settings.py`.\nFor example:\n\n```python\nLOGGING = {\n    \"version\": 1,\n    \"disable_existing_loggers\": False,\n    \"formatters\": {\n        \"post_office\": {\n            \"format\": \"[%(levelname)s]%(asctime)s PID %(process)d: %(message)s\",\n            \"datefmt\": \"%d-%m-%Y %H:%M:%S\",\n        },\n    },\n    \"handlers\": {\n        \"post_office\": {\n            \"level\": \"DEBUG\",\n            \"class\": \"logging.StreamHandler\",\n            \"formatter\": \"post_office\"\n        },\n        # If you use sentry for logging\n        'sentry': {\n            'level': 'ERROR',\n            'class': 'raven.contrib.django.handlers.SentryHandler',\n        },\n    },\n    'loggers': {\n        \"post_office\": {\n            \"handlers\": [\"post_office\", \"sentry\"],\n            \"level\": \"INFO\"\n        },\n    },\n}\n```\n\n### Threads\n\n`post-office` >= 3.0 allows you to use multiple threads to dramatically\nspeed up the speed at which emails are sent. By default, `post-office`\nuses 5 threads per process. You can tweak this setting by changing\n`THREADS_PER_PROCESS` setting.\n\nThis may dramatically increase the speed of bulk email delivery,\ndepending on which email backends you use. In my tests, multi threading\nspeeds up email backends that use HTTP based (REST) delivery mechanisms\nbut doesn't seem to help SMTP based backends.\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'THREADS_PER_PROCESS': 10,\n}\n```\n\nPerformance\n-----------\n\n### Caching\n\nif Django's caching mechanism is configured, `post_office` will cache\n`EmailTemplate` instances . If for some reason you want to disable\ncaching, set `POST_OFFICE_CACHE` to `False` in `settings.py`:\n\n```python\n## All cache key will be prefixed by post_office:template:\n## To turn OFF caching, you need to explicitly set POST_OFFICE_CACHE to False in settings\nPOST_OFFICE_CACHE = False\n\n## Optional: to use a non default cache backend, add a \"post_office\" entry in CACHES\nCACHES = {\n    'post_office': {\n        'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',\n        'LOCATION': '127.0.0.1:11211',\n    }\n}\n```\n\n### send_many()\n\n`send_many()` is much more performant (generates less database queries)\nwhen sending a large number of emails. `send_many()` is almost identical\nto `mail.send()`, with the exception that it accepts a list of keyword\narguments that you'd usually pass into `mail.send()`:\n\n```python\nfrom post_office import mail\n\nfirst_email = {\n    'sender': 'from@example.com',\n    'recipients': ['alice@example.com'],\n    'subject': 'Hi!',\n    'message': 'Hi Alice!'\n}\nsecond_email = {\n    'sender': 'from@example.com',\n    'recipients': ['bob@example.com'],\n    'subject': 'Hi!',\n    'message': 'Hi Bob!'\n}\nkwargs_list = [first_email, second_email]\n\nmail.send_many(kwargs_list)\n```\n\nAttachments are not supported with `mail.send_many()`.\n\n## Running Tests\n\nTo run the test suite:\n\n```python\n`which django-admin` test post_office --settings=post_office.test_settings --pythonpath=.\n```\n\nYou can run the full test suite for all supported versions of Django and Python with:\n\n```python\ntox\n```\n\nor:\n\n```python\npython setup.py test\n```\n\n\n## Integration with Celery\n\nIf your Django project runs in a Celery enabled configuration, you can use its worker to send out\nqueued emails. Compared to the solution with cron (see above), or the solution with uWSGI timers\n(see below) this setup has the big advantage that queued emails are send *immediately* after they\nhave been added to the mail queue. The delivery is still performed in a separate and asynchronous\ntask, which prevents sending emails during the request/response-cycle.\n\nIf you [configured Celery](https://docs.celeryproject.org/en/latest/userguide/application.html)\nin your project and started the\n[Celery worker](https://docs.celeryproject.org/en/latest/userguide/workers.html),\nyou should see something such as:\n\n```\n--------------- celery@halcyon.local v4.0 (latentcall)\n--- ***** -----\n-- ******* ---- [Configuration]\n- *** --- * --- . broker:      amqp://guest@localhost:5672//\n- ** ---------- . app:         __main__:0x1012d8590\n- ** ---------- . concurrency: 8 (processes)\n- ** ---------- . events:      OFF (enable -E to monitor this worker)\n- ** ----------\n- *** --- * --- [Queues]\n-- ******* ---- . celery:      exchange:celery(direct) binding:celery\n--- ***** -----\n\n[tasks]\n. post_office.tasks.cleanup_expired_mails\n. post_office.tasks.send_queued_mail\n```\n\nDelivering emails through the Celery worker must be explicitly enabled:\n\n```python\n# Put this in settings.py\nPOST_OFFICE = {\n    ...\n    'CELERY_ENABLED': True,\n}\n```\n\nEmails will then be delivered immediately after they have been queued. In order to make this happen,\nthe project's `celery.py` setup shall invoke the\n[autodiscoverttasks](https://docs.celeryproject.org/en/latest/reference/celery.html#celery.Celery.autodiscover_tasks)\nfunction. In case of a temporary delivery failure, we might want retrying to send those emails by a\nperiodic task. This can be scheduled with a simple\n[Celery beat configuration](https://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#entries),\nfor instance through\n\n```python\napp.conf.beat_schedule = {\n    'send-queued-mail': {\n        'task': 'post_office.tasks.send_queued_mail',\n        'schedule': 600.0,\n    },\n}\n```\n\nThe email queue now will be processed every 10 minutes. If you are using\n[Django Celery Beat](https://django-celery-beat.readthedocs.io/en/latest/),\nthen use the Django-Admin backend and add a periodic tasks for `post_office.tasks.send_queued_mail`.\n\nDepending on your policy, you may also want to remove expired emails from the queue. This can be\ndone by adding another periodic tasks for `post_office.tasks.cleanup_mail`, which may run once a\nweek or month.\n\n\n## Integration with uWSGI\n\nIf setting up Celery is too daunting and you use\n[uWSGI](https://uwsgi-docs.readthedocs.org/en/latest/) as application\nserver, then uWSGI decorators can act as a poor men's scheduler. Just\nadd this short snipped to the project's `wsgi.py` file:\n\n```python\nfrom django.core.wsgi import get_wsgi_application\n\napplication = get_wsgi_application()\n\n# add this block of code\ntry:\n    import uwsgidecorators\n    from django.core.management import call_command\n\n    @uwsgidecorators.timer(10)\n    def send_queued_mail(num):\n        \"\"\"Send queued mail every 10 seconds\"\"\"\n        call_command('send_queued_mail', processes=1)\n\nexcept ImportError:\n    print(\"uwsgidecorators not found. Cron and timers are disabled\")\n```\n\nAlternatively you can also use the decorator\n`@uwsgidecorators.cron(minute, hour, day, month, weekday)`. This will\nschedule a task at specific times. Use `-1` to signal any time, it\ncorresponds to the `*` in cron.\n\nPlease note that `uwsgidecorators` are available only, if the\napplication has been started with **uWSGI**. However, Django's internal\n`./manange.py runserver` also access this file, therefore wrap the block\ninto an exception handler as shown above.\n\nThis configuration can be useful in environments, such as Docker\ncontainers, where you don't have a running cron-daemon.\n\n## Signals\n\nEach time an email is added to the mail queue, Post Office emits a\nspecial [Django\nsignal](https://docs.djangoproject.com/en/stable/topics/signals/).\nWhenever a third party application wants to be informed about this\nevent, it shall connect a callback function to the Post Office's signal\nhandler `email_queued`, for instance:\n\n```python\nfrom django.dispatch import receiver\nfrom post_office.signals import email_queued\n\n@receiver(email_queued)\ndef my_callback(sender, emails, **kwargs):\n    print(\"Added {} mails to the sending queue\".format(len(emails)))\n```\n\nThe Emails objects added to the queue are passed as list to the callback\nhandler.\n\n\n## Changelog\n\nFull changelog can be found [here](https://github.com/ui/django-post_office/blob/master/CHANGELOG.md).\n\nCreated and maintained by the cool guys at [Stamps](https://stamps.co.id), Indonesia's most elegant\nCRM/loyalty platform.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "A Django app to monitor and send mail asynchronously, complete with template support.",
    "version": "3.8.0",
    "project_urls": {
        "Homepage": "https://github.com/ui/django-post_office"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "28f22940f3d705b97275cbb2946bf2b641e176d19986353ed4bda06272d1384f",
                "md5": "82cf6140e97bc8fc58f326a22717887e",
                "sha256": "21ae03dd6d09036ae96469c5418305aa39855985f29b21374c7a013b2ff47098"
            },
            "downloads": -1,
            "filename": "django_post_office-3.8.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "82cf6140e97bc8fc58f326a22717887e",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 85235,
            "upload_time": "2023-10-22T08:36:41",
            "upload_time_iso_8601": "2023-10-22T08:36:41.998604Z",
            "url": "https://files.pythonhosted.org/packages/28/f2/2940f3d705b97275cbb2946bf2b641e176d19986353ed4bda06272d1384f/django_post_office-3.8.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "0e573c29d7c8205c53b4f950897f53032bf14364386d9d8c3297670cefb9cbd0",
                "md5": "34c6ae7c51405bb81c1019fa3eabdf13",
                "sha256": "0df8a3595d8b6088a933d66d984787172e75dd009fa57bb439c6587ea5746df4"
            },
            "downloads": -1,
            "filename": "django-post_office-3.8.0.tar.gz",
            "has_sig": false,
            "md5_digest": "34c6ae7c51405bb81c1019fa3eabdf13",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 63287,
            "upload_time": "2023-10-22T08:36:44",
            "upload_time_iso_8601": "2023-10-22T08:36:44.656526Z",
            "url": "https://files.pythonhosted.org/packages/0e/57/3c29d7c8205c53b4f950897f53032bf14364386d9d8c3297670cefb9cbd0/django-post_office-3.8.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-10-22 08:36:44",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "ui",
    "github_project": "django-post_office",
    "travis_ci": true,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "django-post-office"
}
        
Elapsed time: 0.14090s