• Home
  • History
  • Annotate
Name Date Size #Lines LOC

..03-May-2022-

post_office/H03-May-2022-5,2104,108

.gitignoreH A D04-Dec-2020467 4133

.travis.ymlH A D04-Dec-20201 KiB5141

AUTHORS.rstH A D04-Dec-2020312 1612

CHANGELOG.mdH A D04-Dec-20209 KiB256210

MANIFEST.inH A D04-Dec-2020210 87

README.mdH A D04-Dec-202024.2 KiB815639

setup.pyH A D04-Dec-20202.3 KiB7664

tox.iniH A D04-Dec-2020556 2924

README.md

1# Django Post Office
2
3Django Post Office is a simple app to send and manage your emails in
4Django. Some awesome features are:
5
6-   Allows you to send email asynchronously
7-   Multi backend support
8-   Supports HTML email
9-   Supports inlined images in HTML email
10-   Supports database based email templates
11-   Supports multilingual email templates (i18n)
12-   Built in scheduling support
13-   Works well with task queues like [RQ](http://python-rq.org) or
14    [Celery](http://www.celeryproject.org)
15-   Uses multiprocessing (and threading) to send a large number of
16    emails in parallel
17
18## Dependencies
19
20-   [django \>= 2.2](https://djangoproject.com/)
21-   [jsonfield](https://github.com/rpkilby/jsonfield)
22
23### Optional Dependency
24
25-   [bleach](https://bleach.readthedocs.io/)
26
27With this optional dependency, HTML emails are nicely rendered
28inside the Django admin backend. Without this library, all HTML tags
29will otherwise be stripped for security reasons.
30
31## Installation
32
33[![Build
34Status](https://travis-ci.org/ui/django-post_office.png?branch=master)](https://travis-ci.org/ui/django-post_office) [![PyPI version](https://img.shields.io/pypi/v/django-post_office.svg)](https://pypi.org/project/django-post_office/) ![Software license](https://img.shields.io/pypi/l/django-post_office.svg)
35
36Install from PyPI (or [manually download from PyPI](http://pypi.python.org/pypi/django-post_office)):
37
38```sh
39pip install django-post_office
40```
41
42Add `post_office` to your INSTALLED_APPS in django's `settings.py`:
43
44```python
45INSTALLED_APPS = (
46    # other apps
47    "post_office",
48)
49```
50
51Run `migrate`:
52
53```sh
54python manage.py migrate
55```
56
57Set `post_office.EmailBackend` as your `EMAIL_BACKEND` in Django's `settings.py`:
58
59```python
60EMAIL_BACKEND = 'post_office.EmailBackend'
61```
62
63## Quickstart
64
65Send a simple email is really easy:
66
67```python
68from post_office import mail
69
70mail.send(
71    'recipient@example.com', # List of email addresses also accepted
72    'from@example.com',
73    subject='My email',
74    message='Hi there!',
75    html_message='Hi <strong>there</strong>!',
76)
77```
78
79If you want to use templates, ensure that Django's admin interface is
80enabled. Create an `EmailTemplate` instance via `admin` and do the
81following:
82
83```python
84from post_office import mail
85
86mail.send(
87    'recipient@example.com', # List of email addresses also accepted
88    'from@example.com',
89    template='welcome_email', # Could be an EmailTemplate instance or name
90    context={'foo': 'bar'},
91)
92```
93
94The above command will put your email on the queue so you can use the
95command in your webapp without slowing down the request/response cycle
96too much. To actually send them out, run
97`python manage.py send_queued_mail`. You can schedule this management
98command to run regularly via cron:
99
100    * * * * * (/usr/bin/python manage.py send_queued_mail >> send_mail.log 2>&1)
101
102## Usage
103
104### mail.send()
105
106`mail.send` is the most important function in this library, it takes
107these arguments:
108
109| Argument | Required | Description |
110| --- | --- | --- |
111| recipients | Yes | List of recipient email addresses |
112| sender | No | Defaults to `settings.DEFAULT_FROM_EMAIL`, display name like `John <john@a.com>` is allowed |
113| subject | No | Email subject (if `template` is not specified) |
114| message | No | Email content (if `template` is not specified) |
115| html_message | No | HTML content (if `template` is not specified) |
116| template | No | `EmailTemplate` instance or name of template |
117| language | No | Language in which you want to send the email in (if you have multilingual email templates). |
118| cc | No | List of emails, will appear in `cc` field |
119| bcc | No | List of emails, will appear in `bcc` field |
120| attachments | No | Email attachments - a dict where the keys are the filenames and the values are files, file-like-objects or path to file |
121| context | No | A dict, used to render templated email |
122| headers | No | A dictionary of extra headers on the message |
123| scheduled_time | No | A date/datetime object indicating when the email should be sent |
124| expires_at | No | If specified, mails that are not yet sent won't be delivered after this date. |
125| priority | No | `high`, `medium`, `low` or `now` (sent immediately) |
126| backend | No | Alias of the backend you want to use, `default` will be used if not specified. |
127| 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. |
128
129Here are a few examples.
130
131If you just want to send out emails without using database templates.
132You can call the `send` command without the `template` argument.
133
134```python
135from post_office import mail
136
137mail.send(
138    ['recipient1@example.com'],
139    'from@example.com',
140    subject='Welcome!',
141    message='Welcome home, {{ name }}!',
142    html_message='Welcome home, <b>{{ name }}</b>!',
143    headers={'Reply-to': 'reply@example.com'},
144    scheduled_time=date(2014, 1, 1),
145    context={'name': 'Alice'},
146)
147```
148
149`post_office` is also task queue friendly. Passing `now` as priority
150into `send_mail` will deliver the email right away (instead of queuing
151it), regardless of how many emails you have in your queue:
152
153```python
154from post_office import mail
155
156mail.send(
157    ['recipient1@example.com'],
158    'from@example.com',
159    template='welcome_email',
160    context={'foo': 'bar'},
161    priority='now',
162)
163```
164
165This is useful if you already use something like [django-rq](https://github.com/ui/django-rq) to send emails
166asynchronously and only need to store email related activities and logs.
167
168If you want to send an email with attachments:
169
170```python
171from django.core.files.base import ContentFile
172from post_office import mail
173
174mail.send(
175    ['recipient1@example.com'],
176    'from@example.com',
177    template='welcome_email',
178    context={'foo': 'bar'},
179    priority='now',
180    attachments={
181        'attachment1.doc': '/path/to/file/file1.doc',
182        'attachment2.txt': ContentFile('file content'),
183        'attachment3.txt': {'file': ContentFile('file content'), 'mimetype': 'text/plain'},
184    }
185)
186```
187
188### Template Tags and Variables
189
190`post-office` supports Django's template tags and variables. For
191example, if you put `Hello, {{ name }}` in the subject line and pass in
192`{'name': 'Alice'}` as context, you will get `Hello, Alice` as subject:
193
194```python
195from post_office.models import EmailTemplate
196from post_office import mail
197
198EmailTemplate.objects.create(
199    name='morning_greeting',
200    subject='Morning, {{ name|capfirst }}',
201    content='Hi {{ name }}, how are you feeling today?',
202    html_content='Hi <strong>{{ name }}</strong>, how are you feeling today?',
203)
204
205mail.send(
206    ['recipient@example.com'],
207    'from@example.com',
208    template='morning_greeting',
209    context={'name': 'alice'},
210)
211
212# This will create an email with the following content:
213subject = 'Morning, Alice',
214content = 'Hi alice, how are you feeling today?'
215content = 'Hi <strong>alice</strong>, how are you feeling today?'
216```
217
218### Multilingual Email Templates
219
220You can easily create email templates in various different languanges.
221For example:
222
223```python
224template = EmailTemplate.objects.create(
225    name='hello',
226    subject='Hello world!',
227)
228
229# Add an Indonesian version of this template:
230indonesian_template = template.translated_templates.create(
231    language='id',
232    subject='Halo Dunia!'
233)
234```
235
236Sending an email using template in a non default languange is similarly easy:
237
238```python
239mail.send(
240    ['recipient@example.com'],
241    'from@example.com',
242    template=template, # Sends using the default template
243)
244
245mail.send(
246    ['recipient@example.com'],
247    'from@example.com',
248    template=template,
249    language='id', # Sends using Indonesian template
250)
251```
252
253### Inlined Images
254
255Often one wants to render images inside a template, which are attached
256as inlined `MIMEImage` to the outgoing email. This requires a slightly
257modified Django Template Engine, keeping a list of inlined images, which
258later will be added to the outgoing message.
259
260First we must add a special Django template backend to our list of template engines:
261
262```python
263TEMPLATES = [
264    {
265        ...
266    }, {
267        'BACKEND': 'post_office.template.backends.post_office.PostOfficeTemplates',
268        'APP_DIRS': True,
269        'DIRS': [],
270        'OPTIONS': {
271            'context_processors': [
272                'django.contrib.auth.context_processors.auth',
273                'django.template.context_processors.debug',
274                'django.template.context_processors.i18n',
275                'django.template.context_processors.media',
276                'django.template.context_processors.static',
277                'django.template.context_processors.tz',
278                'django.template.context_processors.request',
279            ]
280        }
281    }
282]
283```
284
285then we must tell Post-Office to use this template engine:
286
287```python
288POST_OFFICE = {
289    'TEMPLATE_ENGINE': 'post_office',
290}
291```
292
293In templates used to render HTML for emails add
294
295```
296{% load post_office %}
297
298<p>... somewhere in the body ...</p>
299<img src="{% inline_image 'path/to/image.png' %}" />
300```
301
302Here the templatetag named `inline_image` is used to keep track of
303inlined images. It takes a single parameter. This can either be the
304relative path to an image file located in one of the `static`
305directories, or the absolute path to an image file, or an image-file
306object itself. Templates rendered using this templatetag, render a
307reference ID for each given image, and store these images inside the
308context of the adopted template engine. Later on, when the rendered
309template is passed to the mailing library, those images will be
310transferred to the email message object as `MIMEImage`-attachments.
311
312To send an email containing both, a plain text body and some HTML with
313inlined images, use the following code snippet:
314
315```python
316from django.core.mail import EmailMultiAlternatives
317
318subject, body = "Hello", "Plain text body"
319from_email, to_email = "no-reply@example.com", "john@example.com"
320email_message = EmailMultiAlternatives(subject, body, from_email, [to_email])
321template = get_template('email-template-name.html', using='post_office')
322context = {...}
323html = template.render(context)
324email_message.attach_alternative(html, 'text/html')
325template.attach_related(email_message)
326email_message.send()
327```
328
329To send an email containing HTML with inlined images, but without a
330plain text body, use this code snippet:
331
332```python
333from django.core.mail import EmailMultiAlternatives
334
335subject, from_email, to_email = "Hello", "no-reply@example.com", "john@example.com"
336template = get_template('email-template-name.html', using='post_office')
337context = {...}
338html = template.render(context)
339email_message = EmailMultiAlternatives(subject, html, from_email, [to_email])
340email_message.content_subtype = 'html'
341template.attach_related(email_message)
342email_message.send()
343```
344
345### Custom Email Backends
346
347By default, `post_office` uses django's `smtp.EmailBackend`. If you want
348to use a different backend, you can do so by configuring `BACKENDS`.
349
350For example if you want to use [django-ses](https://github.com/hmarr/django-ses):
351
352```python
353# Put this in settings.py
354POST_OFFICE = {
355    ...
356    'BACKENDS': {
357        'default': 'smtp.EmailBackend',
358        'ses': 'django_ses.SESBackend',
359    }
360}
361```
362
363You can then choose what backend you want to use when sending mail:
364
365```python
366# If you omit `backend_alias` argument, `default` will be used
367mail.send(
368    ['recipient@example.com'],
369    'from@example.com',
370    subject='Hello',
371)
372
373# If you want to send using `ses` backend
374mail.send(
375    ['recipient@example.com'],
376    'from@example.com',
377    subject='Hello',
378    backend='ses',
379)
380```
381
382### Management Commands
383
384-   `send_queued_mail` - send queued emails, those aren't successfully
385    sent will be marked as `failed`. Accepts the following arguments:
386
387  | Argument | Description |
388  | --- | --- |
389  |`--processes` or `-p` | Number of parallel processes to send email. Defaults to 1 |
390  | `--lockfile` or `-L` | Full path to file used as lock file. Defaults to `/tmp/post_office.lock` |
391
392
393-   `cleanup_mail` - delete all emails created before an X number of
394    days (defaults to 90).
395
396| Argument | Description |
397| --- | --- |
398| `--days` or `-d` | Email older than this argument will be deleted. Defaults to 90 |
399| `--delete-attachments` | Flag to delete orphaned attachment records and files on disk. If not specified, attachments won't be deleted. |
400
401You may want to set these up via cron to run regularly:
402
403    * * * * * (cd $PROJECT; python manage.py send_queued_mail --processes=1 >> $PROJECT/cron_mail.log 2>&1)
404    0 1 * * * (cd $PROJECT; python manage.py cleanup_mail --days=30 --delete-attachments >> $PROJECT/cron_mail_cleanup.log 2>&1)
405
406
407## Settings
408
409This section outlines all the settings and configurations that you can
410put in Django's `settings.py` to fine tune `post-office`'s behavior.
411
412
413### Batch Size
414
415If you may want to limit the number of emails sent in a batch (sometimes
416useful in a low memory environment), use the `BATCH_SIZE` argument to
417limit the number of queued emails fetched in one batch.
418
419```python
420# Put this in settings.py
421POST_OFFICE = {
422    ...
423    'BATCH_SIZE': 50,
424}
425```
426
427### Default Priority
428
429The default priority for emails is `medium`, but this can be altered by
430setting `DEFAULT_PRIORITY`. Integration with asynchronous email backends
431(e.g. based on Celery) becomes trivial when set to `now`.
432
433```python
434# Put this in settings.py
435POST_OFFICE = {
436    ...
437    'DEFAULT_PRIORITY': 'now',
438}
439```
440
441### Override Recipients
442
443Defaults to `None`. This option is useful if you want to redirect all
444emails to specified a few email for development purposes.
445
446```python
447# Put this in settings.py
448POST_OFFICE = {
449    ...
450    'OVERRIDE_RECIPIENTS': ['to@example.com', 'to2@example.com'],
451}
452```
453
454### Message-ID
455
456The 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 `@`
457symbol: The left part is a generated pseudo random number. The right
458part is a constant string, typically denoting the full qualified domain
459name of the sending server.
460
461By default, **Django** generates such a Message-ID during email
462delivery. Since **django-post_office** keeps track of all delivered
463emails, it can be very useful to create and store this Message-ID while
464creating each email in the database. This identifier then can be looked
465up in the Django admin backend.
466
467To enable this feature, add this to your Post-Office settings:
468
469```python
470# Put this in settings.py
471POST_OFFICE = {
472    ...
473    'MESSAGE_ID_ENABLED': True,
474}
475```
476
477It can further be fine tuned, using for instance another full qualified
478domain name:
479
480```python
481# Put this in settings.py
482POST_OFFICE = {
483    ...
484    'MESSAGE_ID_ENABLED': True,
485    'MESSAGE_ID_FQDN': 'example.com',
486}
487```
488
489Otherwise, if `MESSAGE_ID_FQDN` is unset (the default),
490**django-post_office** falls back to the DNS name of the server, which
491is determined by the network settings of the host.
492
493### Retry
494
495Not activated by default. You can automatically requeue failed email deliveries.
496You can also configure failed deliveries to be retried after a specific time interval.
497
498```python
499# Put this in settings.py
500POST_OFFICE = {
501    ...
502    'MAX_RETRIES': 4,
503    'RETRY_INTERVAL': datetime.timedelta(minutes=15),  # Schedule to be retried 15 minutes later
504}
505```
506
507### Log Level
508
509Logs are stored in the database and is browseable via Django admin.
510The default log level is 2 (logs both successful and failed deliveries)
511This behavior can be changed by setting `LOG_LEVEL`.
512
513```python
514# Put this in settings.py
515POST_OFFICE = {
516    ...
517    'LOG_LEVEL': 1, # Log only failed deliveries
518}
519```
520
521The different options are:
522
523* `0` logs nothing
524* `1` logs only failed deliveries
525* `2` logs everything (both successful and failed delivery attempts)
526
527### Sending Order
528
529The default sending order for emails is `-priority`, but this can be
530altered by setting `SENDING_ORDER`. For example, if you want to send
531queued emails in FIFO order :
532
533```python
534# Put this in settings.py
535POST_OFFICE = {
536    ...
537    'SENDING_ORDER': ['created'],
538}
539```
540
541### Context Field Serializer
542
543If you need to store complex Python objects for deferred rendering (i.e.
544setting `render_on_delivery=True`), you can specify your own context
545field class to store context variables. For example if you want to use
546[django-picklefield](https://github.com/gintas/django-picklefield/tree/master/src/picklefield):
547
548```python
549# Put this in settings.py
550POST_OFFICE = {
551    ...
552    'CONTEXT_FIELD_CLASS': 'picklefield.fields.PickledObjectField',
553}
554```
555
556`CONTEXT_FIELD_CLASS` defaults to `jsonfield.JSONField`.
557
558### Logging
559
560You can configure `post-office`'s logging from Django's `settings.py`.
561For example:
562
563```python
564LOGGING = {
565    "version": 1,
566    "disable_existing_loggers": False,
567    "formatters": {
568        "post_office": {
569            "format": "[%(levelname)s]%(asctime)s PID %(process)d: %(message)s",
570            "datefmt": "%d-%m-%Y %H:%M:%S",
571        },
572    },
573    "handlers": {
574        "post_office": {
575            "level": "DEBUG",
576            "class": "logging.StreamHandler",
577            "formatter": "post_office"
578        },
579        # If you use sentry for logging
580        'sentry': {
581            'level': 'ERROR',
582            'class': 'raven.contrib.django.handlers.SentryHandler',
583        },
584    },
585    'loggers': {
586        "post_office": {
587            "handlers": ["post_office", "sentry"],
588            "level": "INFO"
589        },
590    },
591}
592```
593
594### Threads
595
596`post-office` >= 3.0 allows you to use multiple threads to dramatically
597speed up the speed at which emails are sent. By default, `post-office`
598uses 5 threads per process. You can tweak this setting by changing
599`THREADS_PER_PROCESS` setting.
600
601This may dramatically increase the speed of bulk email delivery,
602depending on which email backends you use. In my tests, multi threading
603speeds up email backends that use HTTP based (REST) delivery mechanisms
604but doesn't seem to help SMTP based backends.
605
606```python
607# Put this in settings.py
608POST_OFFICE = {
609    ...
610    'THREADS_PER_PROCESS': 10,
611}
612```
613
614Performance
615-----------
616
617### Caching
618
619if Django's caching mechanism is configured, `post_office` will cache
620`EmailTemplate` instances . If for some reason you want to disable
621caching, set `POST_OFFICE_CACHE` to `False` in `settings.py`:
622
623```python
624## All cache key will be prefixed by post_office:template:
625## To turn OFF caching, you need to explicitly set POST_OFFICE_CACHE to False in settings
626POST_OFFICE_CACHE = False
627
628## Optional: to use a non default cache backend, add a "post_office" entry in CACHES
629CACHES = {
630    'post_office': {
631        'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
632        'LOCATION': '127.0.0.1:11211',
633    }
634}
635```
636
637### send_many()
638
639`send_many()` is much more performant (generates less database queries)
640when sending a large number of emails. `send_many()` is almost identical
641to `mail.send()`, with the exception that it accepts a list of keyword
642arguments that you'd usually pass into `mail.send()`:
643
644```python
645from post_office import mail
646
647first_email = {
648    'sender': 'from@example.com',
649    'recipients': ['alice@example.com'],
650    'subject': 'Hi!',
651    'message': 'Hi Alice!'
652}
653second_email = {
654    'sender': 'from@example.com',
655    'recipients': ['bob@example.com'],
656    'subject': 'Hi!',
657    'message': 'Hi Bob!'
658}
659kwargs_list = [first_email, second_email]
660
661mail.send_many(kwargs_list)
662```
663
664Attachments are not supported with `mail.send_many()`.
665
666## Running Tests
667
668To run the test suite:
669
670```python
671`which django-admin.py` test post_office --settings=post_office.test_settings --pythonpath=.
672```
673
674You can run the full test suite for all supported versions of Django and Python with:
675
676```python
677tox
678```
679
680or:
681
682```python
683python setup.py test
684```
685
686
687## Integration with Celery
688
689If your Django project runs in a Celery enabled configuration, you can
690use its worker to send out queued emails. Compared to the solution with
691cron (see above), or the solution with uWSGI timers (see below) this
692setup has the big advantage that queued emails are send *immediately*
693after they have been added to the mail queue. The delivery is still
694performed in a separate and asynchronous task, which prevents sending
695emails during the request/response-cycle.
696
697If you [configured Celery](https://docs.celeryproject.org/en/latest/userguide/application.html)
698in your project and started the [Celery worker](https://docs.celeryproject.org/en/latest/userguide/workers.html),
699you should see something such as:
700
701```
702--------------- celery@halcyon.local v4.0 (latentcall)
703--- ***** -----
704-- ******* ---- [Configuration]
705- *** --- * --- . broker:      amqp://guest@localhost:5672//
706- ** ---------- . app:         __main__:0x1012d8590
707- ** ---------- . concurrency: 8 (processes)
708- ** ---------- . events:      OFF (enable -E to monitor this worker)
709- ** ----------
710- *** --- * --- [Queues]
711-- ******* ---- . celery:      exchange:celery(direct) binding:celery
712--- ***** -----
713
714[tasks]
715. post_office.tasks.cleanup_expired_mails
716. post_office.tasks.send_queued_mail
717```
718
719Emails will now be delivered by the Celery worker, immediately after
720they have been queued. In order to make this happen, the project's
721`celery.py` setup shall invoke the
722[autodiscoverttasks](https://docs.celeryproject.org/en/latest/reference/celery.html#celery.Celery.autodiscover_tasks)
723function. There is no need to otherwise configure Post Office for
724integrating with Celery. However, in case of a temporary delivery
725failure, we might want retrying to send those emails by a periodic task.
726This can be done by a simple [Celery beat
727configuration](https://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html#entries),
728for instance through
729
730```python
731app.conf.beat_schedule = {
732    'send-queued-mail': {
733        'task': 'post_office.tasks.send_queued_mail',
734        'schedule': 600.0,
735    },
736}
737```
738
739This will send queued emails every 10 minutes. If you are using [Django
740Celery Beat](https://django-celery-beat.readthedocs.io/en/latest/),
741then use the Django-Admin backend and add a
742periodic taks for `post_office.tasks.send_queued_mail`.
743
744Depending on your policy, you may also want to remove expired emails
745from the queue. This can be done by adding another periodic taks for
746`post_office.tasks.cleanup_mail`, which may run once a week or month.
747
748## Integration with uWSGI
749
750If setting up Celery is too daunting and you use
751[uWSGI](https://uwsgi-docs.readthedocs.org/en/latest/) as application
752server, then uWSGI decorators can act as a poor men's scheduler. Just
753add this short snipped to the project's `wsgi.py` file:
754
755```python
756from django.core.wsgi import get_wsgi_application
757
758application = get_wsgi_application()
759
760# add this block of code
761try:
762    import uwsgidecorators
763    from django.core.management import call_command
764
765    @uwsgidecorators.timer(10)
766    def send_queued_mail(num):
767        """Send queued mail every 10 seconds"""
768        call_command('send_queued_mail', processes=1)
769
770except ImportError:
771    print("uwsgidecorators not found. Cron and timers are disabled")
772```
773
774Alternatively you can also use the decorator
775`@uwsgidecorators.cron(minute, hour, day, month, weekday)`. This will
776schedule a task at specific times. Use `-1` to signal any time, it
777corresponds to the `*` in cron.
778
779Please note that `uwsgidecorators` are available only, if the
780application has been started with **uWSGI**. However, Django's internal
781`./manange.py runserver` also access this file, therefore wrap the block
782into an exception handler as shown above.
783
784This configuration can be useful in environments, such as Docker
785containers, where you don't have a running cron-daemon.
786
787## Signals
788
789Each time an email is added to the mail queue, Post Office emits a
790special [Django
791signal](https://docs.djangoproject.com/en/stable/topics/signals/).
792Whenever a third party application wants to be informed about this
793event, it shall connect a callback function to the Post Office's signal
794handler `email_queued`, for instance:
795
796```python
797from django.dispatch import receiver
798from post_office.signals import email_queued
799
800@receiver(email_queued)
801def my_callback(sender, emails, **kwargs):
802    print("Added {} mails to the sending queue".format(len(emails)))
803```
804
805The Emails objects added to the queue are passed as list to the callback
806handler.
807
808
809## Changelog
810
811Full changelog can be found [here](https://github.com/ui/django-post_office/blob/master/CHANGELOG.md).
812
813Created and maintained by the cool guys at [Stamps](https://stamps.co.id), Indonesia's most elegant
814CRM/loyalty platform.
815