-
Notifications
You must be signed in to change notification settings - Fork 126
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Closes #8
- Loading branch information
Showing
7 changed files
with
303 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,4 +10,5 @@ Sending email | |
anymail_additions | ||
templates | ||
tracking | ||
signals | ||
exceptions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
.. _signals: | ||
|
||
Pre- and post-send signals | ||
========================== | ||
|
||
Anymail provides :ref:`pre-send <pre-send-signal>` and :ref:`post-send <post-send-signal>` | ||
signals you can connect to trigger actions whenever messages are sent through an Anymail backend. | ||
|
||
Be sure to read Django's `listening to signals`_ docs for information on defining | ||
and connecting signal receivers. | ||
|
||
.. _listening to signals: | ||
https://docs.djangoproject.com/en/stable/topics/signals/#listening-to-signals | ||
|
||
|
||
.. _pre-send-signal: | ||
|
||
Pre-send signal | ||
--------------- | ||
|
||
You can use Anymail's :data:`~anymail.signals.pre_send` signal to examine | ||
or modify messages before they are sent. | ||
For example, you could implement your own email suppression list: | ||
|
||
.. code-block:: python | ||
from anymail.exceptions import AnymailCancelSend | ||
from anymail.signals import pre_send | ||
from django.dispatch import receiver | ||
from email.utils import parseaddr | ||
from your_app.models import EmailBlockList | ||
@receiver(pre_send) | ||
def filter_blocked_recipients(sender, message, **kwargs): | ||
# Cancel the entire send if the from_email is blocked: | ||
if not ok_to_send(message.from_email): | ||
raise AnymailCancelSend("Blocked from_email") | ||
# Otherwise filter the recipients before sending: | ||
message.to = [addr for addr in message.to if ok_to_send(addr)] | ||
message.cc = [addr for addr in message.cc if ok_to_send(addr)] | ||
def ok_to_send(addr): | ||
# This assumes you've implemented an EmailBlockList model | ||
# that holds emails you want to reject... | ||
name, email = parseaddr(addr) # just want the <email> part | ||
try: | ||
EmailBlockList.objects.get(email=email) | ||
return False # in the blocklist, so *not* OK to send | ||
except EmailBlockList.DoesNotExist: | ||
return True # *not* in the blocklist, so OK to send | ||
Any changes you make to the message in your pre-send signal receiver | ||
will be reflected in the ESP send API call, as shown for the filtered | ||
"to" and "cc" lists above. Note that this will modify the original | ||
EmailMessage (not a copy)---be sure this won't confuse your sending | ||
code that created the message. | ||
|
||
If you want to cancel the message altogether, your pre-send receiver | ||
function can raise an :exc:`~anymail.signals.AnymailCancelSend` exception, | ||
as shown for the "from_email" above. This will silently cancel the send | ||
without raising any other errors. | ||
|
||
|
||
.. data:: anymail.signals.pre_send | ||
|
||
Signal delivered before each EmailMessage is sent. | ||
|
||
Your pre_send receiver must be a function with this signature: | ||
|
||
.. function:: def my_pre_send_handler(sender, message, **kwargs): | ||
|
||
(You can name it anything you want.) | ||
|
||
:param class sender: | ||
The Anymail backend class processing the message. | ||
This parameter is required by Django's signal mechanism, | ||
and despite the name has nothing to do with the *email message's* sender. | ||
(You generally won't need to examine this parameter.) | ||
:param ~django.core.mail.EmailMessage message: | ||
The message being sent. If your receiver modifies the message, those | ||
changes will be reflected in the ESP send call. | ||
:param str esp_name: | ||
The name of the ESP backend in use (e.g., "SendGrid" or "Mailgun"). | ||
:param \**kwargs: | ||
Required by Django's signal mechanism (to support future extensions). | ||
:raises: | ||
:exc:`anymail.exceptions.AnymailCancelSend` if your receiver wants | ||
to cancel this message without causing errors or interrupting a batch send. | ||
|
||
|
||
|
||
.. _post-send-signal: | ||
|
||
Post-send signal | ||
---------------- | ||
|
||
You can use Anymail's :data:`~anymail.signals.post_send` signal to examine | ||
messages after they are sent. This is useful to centralize handling of | ||
the :ref:`sent status <esp-send-status>` for all messages. | ||
|
||
For example, you could implement your own ESP logging dashboard | ||
(perhaps combined with Anymail's :ref:`event-tracking webhooks <event-tracking>`): | ||
|
||
.. code-block:: python | ||
from anymail.signals import post_send | ||
from django.dispatch import receiver | ||
from your_app.models import SentMessage | ||
@receiver(post_send) | ||
def log_sent_message(sender, message, status, esp_name, **kwargs): | ||
# This assumes you've implemented a SentMessage model for tracking sends. | ||
# status.recipients is a dict of email: status for each recipient | ||
for email, recipient_status in status.recipients.items(): | ||
SentMessage.objects.create( | ||
esp=esp_name, | ||
message_id=recipient_status.message_id, # might be None if send failed | ||
email=email, | ||
subject=message.subject, | ||
status=recipient_status.status, # 'sent' or 'rejected' or ... | ||
) | ||
.. data:: anymail.signals.post_send | ||
|
||
Signal delivered after each EmailMessage is sent. | ||
|
||
If you register multiple post-send receivers, Anymail will ensure that | ||
all of them are called, even if one raises an error. | ||
|
||
Your post_send receiver must be a function with this signature: | ||
|
||
.. function:: def my_post_send_handler(sender, message, status, esp_name, **kwargs): | ||
|
||
(You can name it anything you want.) | ||
|
||
:param class sender: | ||
The Anymail backend class processing the message. | ||
This parameter is required by Django's signal mechanism, | ||
and despite the name has nothing to do with the *email message's* sender. | ||
(You generally won't need to examine this parameter.) | ||
:param ~django.core.mail.EmailMessage message: | ||
The message that was sent. You should not modify this in a post-send receiver. | ||
:param ~anymail.message.AnymailStatus status: | ||
The normalized response from the ESP send call. (Also available as | ||
:attr:`message.anymail_status <anymail.message.AnymailMessage.anymail_status>`.) | ||
:param str esp_name: | ||
The name of the ESP backend in use (e.g., "SendGrid" or "Mailgun"). | ||
:param \**kwargs: | ||
Required by Django's signal mechanism (to support future extensions). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
from django.dispatch import receiver | ||
|
||
from anymail.backends.test import TestBackend | ||
from anymail.exceptions import AnymailCancelSend, AnymailRecipientsRefused | ||
from anymail.message import AnymailRecipientStatus | ||
from anymail.signals import pre_send, post_send | ||
|
||
from .test_general_backend import TestBackendTestCase | ||
|
||
|
||
class TestPreSendSignal(TestBackendTestCase): | ||
"""Test Anymail's pre_send signal""" | ||
|
||
def test_pre_send(self): | ||
"""Pre-send receivers invoked for each message, before sending""" | ||
@receiver(pre_send, weak=False) | ||
def handle_pre_send(sender, message, esp_name, **kwargs): | ||
self.assertEqual(self.get_send_count(), 0) # not sent yet | ||
self.assertEqual(sender, TestBackend) | ||
self.assertEqual(message, self.message) | ||
self.assertEqual(esp_name, "Test") # the TestBackend's ESP is named "Test" | ||
self.receiver_called = True | ||
self.addCleanup(pre_send.disconnect, receiver=handle_pre_send) | ||
|
||
self.receiver_called = False | ||
self.message.send() | ||
self.assertTrue(self.receiver_called) | ||
self.assertEqual(self.get_send_count(), 1) # sent now | ||
|
||
def test_modify_message_in_pre_send(self): | ||
"""Pre-send receivers can modify message""" | ||
@receiver(pre_send, weak=False) | ||
def handle_pre_send(sender, message, esp_name, **kwargs): | ||
message.to = [email for email in message.to if not email.startswith('bad')] | ||
message.body += "\nIf you have received this message in error, ignore it" | ||
self.addCleanup(pre_send.disconnect, receiver=handle_pre_send) | ||
|
||
self.message.to = ['legit@example.com', 'bad@example.com'] | ||
self.message.send() | ||
params = self.get_send_params() | ||
self.assertEqual([email.email for email in params['to']], # params['to'] is ParsedEmail list | ||
['legit@example.com']) | ||
self.assertRegex(params['text_body'], | ||
r"If you have received this message in error, ignore it$") | ||
|
||
def test_cancel_in_pre_send(self): | ||
"""Pre-send receiver can cancel the send""" | ||
@receiver(pre_send, weak=False) | ||
def cancel_pre_send(sender, message, esp_name, **kwargs): | ||
raise AnymailCancelSend("whoa there") | ||
self.addCleanup(pre_send.disconnect, receiver=cancel_pre_send) | ||
|
||
self.message.send() | ||
self.assertEqual(self.get_send_count(), 0) # send API not called | ||
|
||
|
||
class TestPostSendSignal(TestBackendTestCase): | ||
"""Test Anymail's post_send signal""" | ||
|
||
def test_post_send(self): | ||
"""Post-send receiver called for each message, after sending""" | ||
@receiver(post_send, weak=False) | ||
def handle_post_send(sender, message, status, esp_name, **kwargs): | ||
self.assertEqual(self.get_send_count(), 1) # already sent | ||
self.assertEqual(sender, TestBackend) | ||
self.assertEqual(message, self.message) | ||
self.assertEqual(status.status, {'sent'}) | ||
self.assertEqual(status.message_id, 1) # TestBackend default message_id | ||
self.assertEqual(status.recipients['to@example.com'].status, 'sent') | ||
self.assertEqual(status.recipients['to@example.com'].message_id, 1) | ||
self.assertEqual(esp_name, "Test") # the TestBackend's ESP is named "Test" | ||
self.receiver_called = True | ||
self.addCleanup(post_send.disconnect, receiver=handle_post_send) | ||
|
||
self.receiver_called = False | ||
self.message.send() | ||
self.assertTrue(self.receiver_called) | ||
|
||
def test_post_send_exception(self): | ||
"""All post-send receivers called, even if one throws""" | ||
@receiver(post_send, weak=False) | ||
def handler_1(sender, message, status, esp_name, **kwargs): | ||
raise ValueError("oops") | ||
self.addCleanup(post_send.disconnect, receiver=handler_1) | ||
|
||
@receiver(post_send, weak=False) | ||
def handler_2(sender, message, status, esp_name, **kwargs): | ||
self.handler_2_called = True | ||
self.addCleanup(post_send.disconnect, receiver=handler_2) | ||
|
||
self.handler_2_called = False | ||
with self.assertRaises(ValueError): | ||
self.message.send() | ||
self.assertTrue(self.handler_2_called) | ||
|
||
def test_rejected_recipients(self): | ||
"""Post-send receiver even if AnymailRecipientsRefused is raised""" | ||
@receiver(post_send, weak=False) | ||
def handle_post_send(sender, message, status, esp_name, **kwargs): | ||
self.receiver_called = True | ||
self.addCleanup(post_send.disconnect, receiver=handle_post_send) | ||
|
||
self.message.test_response = { | ||
'recipient_status': { | ||
'to@example.com': AnymailRecipientStatus(message_id=None, status='rejected') | ||
} | ||
} | ||
|
||
self.receiver_called = False | ||
with self.assertRaises(AnymailRecipientsRefused): | ||
self.message.send() | ||
self.assertTrue(self.receiver_called) |