.. _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, 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 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).
