2018-02-26 16:48:52 -08:00
|
|
|
import requests
|
2024-04-24 09:50:42 -04:00
|
|
|
import six
|
2018-02-26 16:48:52 -08:00
|
|
|
from django.conf import settings
|
|
|
|
from django.core.mail.backends.base import BaseEmailBackend
|
|
|
|
from django.core.mail.message import sanitize_address
|
|
|
|
from django.utils.encoding import force_text
|
|
|
|
from requests.packages.urllib3.filepost import encode_multipart_formdata
|
|
|
|
|
2024-04-24 09:43:56 -04:00
|
|
|
__version__ = "0.8.0"
|
2018-02-26 16:48:52 -08:00
|
|
|
version = __version__
|
|
|
|
|
|
|
|
|
|
|
|
# A mapping of smtp headers to API key names, along
|
|
|
|
# with a callable to transform them somehow (if nec.)
|
|
|
|
#
|
|
|
|
# https://documentation.mailgun.com/user_manual.html#sending-via-smtp
|
|
|
|
# https://documentation.mailgun.com/api-sending.html#sending
|
|
|
|
#
|
|
|
|
# structure is SMTP_HEADER: (api_name, data_transform_function)
|
|
|
|
HEADERS_MAP = {
|
2024-04-24 09:43:56 -04:00
|
|
|
"X-Mailgun-Tag": ("o:tag", lambda x: x),
|
|
|
|
"X-Mailgun-Campaign-Id": ("o:campaign", lambda x: x),
|
|
|
|
"X-Mailgun-Dkim": ("o:dkim", lambda x: x),
|
|
|
|
"X-Mailgun-Deliver-By": ("o:deliverytime", lambda x: x),
|
|
|
|
"X-Mailgun-Drop-Message": ("o:testmode", lambda x: x),
|
|
|
|
"X-Mailgun-Track": ("o:tracking", lambda x: x),
|
|
|
|
"X-Mailgun-Track-Clicks": ("o:tracking-clicks", lambda x: x),
|
|
|
|
"X-Mailgun-Track-Opens": ("o:tracking-opens", lambda x: x),
|
|
|
|
"X-Mailgun-Variables": lambda v_k: (("v:%s" % v_k[0]), v_k[1]),
|
2018-02-26 16:48:52 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class MailgunAPIError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class MailgunBackend(BaseEmailBackend):
|
2024-04-24 09:43:56 -04:00
|
|
|
"""A Django Email backend that uses mailgun."""
|
2018-02-26 16:48:52 -08:00
|
|
|
|
|
|
|
def __init__(self, fail_silently=False, *args, **kwargs):
|
2024-04-24 09:43:56 -04:00
|
|
|
access_key, server_name = (kwargs.pop("access_key", None), kwargs.pop("server_name", None))
|
2018-02-26 16:48:52 -08:00
|
|
|
|
2024-04-24 09:43:56 -04:00
|
|
|
super(MailgunBackend, self).__init__(fail_silently=fail_silently, *args, **kwargs)
|
2018-02-26 16:48:52 -08:00
|
|
|
|
|
|
|
try:
|
2024-04-24 09:43:56 -04:00
|
|
|
self._access_key = access_key or getattr(settings, "MAILGUN_ACCESS_KEY")
|
|
|
|
self._server_name = server_name or getattr(settings, "MAILGUN_SERVER_NAME")
|
2018-02-26 16:48:52 -08:00
|
|
|
except AttributeError:
|
|
|
|
if fail_silently:
|
|
|
|
self._access_key, self._server_name = None
|
|
|
|
else:
|
|
|
|
raise
|
|
|
|
|
|
|
|
self._api_url = "https://api.mailgun.net/v3/%s/" % self._server_name
|
|
|
|
self._headers_map = HEADERS_MAP
|
|
|
|
|
|
|
|
def open(self):
|
2024-04-24 09:43:56 -04:00
|
|
|
"""Stub for open connection, all sends are done over HTTP POSTs"""
|
2018-02-26 16:48:52 -08:00
|
|
|
pass
|
|
|
|
|
|
|
|
def close(self):
|
2024-04-24 09:43:56 -04:00
|
|
|
"""Close any open HTTP connections to the API server."""
|
2018-02-26 16:48:52 -08:00
|
|
|
pass
|
|
|
|
|
|
|
|
def _map_smtp_headers_to_api_parameters(self, email_message):
|
|
|
|
"""
|
|
|
|
Map the values passed in SMTP headers to API-ready
|
|
|
|
2-item tuples present in HEADERS_MAP
|
|
|
|
|
|
|
|
header values must be a single string or list or tuple of strings
|
|
|
|
|
|
|
|
:return: 2-item tuples of the form (api_name, api_values)
|
|
|
|
"""
|
|
|
|
api_data = []
|
|
|
|
for smtp_key, api_transformer in six.iteritems(self._headers_map):
|
|
|
|
data_to_transform = email_message.extra_headers.pop(smtp_key, None)
|
|
|
|
if data_to_transform is not None:
|
|
|
|
if isinstance(data_to_transform, (list, tuple)):
|
|
|
|
# map each value in the tuple/list
|
|
|
|
for data in data_to_transform:
|
|
|
|
api_data.append((api_transformer[0], api_transformer[1](data)))
|
|
|
|
elif isinstance(data_to_transform, dict):
|
|
|
|
for data in six.iteritems(data_to_transform):
|
|
|
|
api_data.append(api_transformer(data))
|
|
|
|
else:
|
|
|
|
# we only have one value
|
|
|
|
api_data.append((api_transformer[0], api_transformer[1](data_to_transform)))
|
|
|
|
return api_data
|
|
|
|
|
|
|
|
def _send(self, email_message):
|
|
|
|
"""A helper method that does the actual sending."""
|
|
|
|
if not email_message.recipients():
|
|
|
|
return False
|
|
|
|
from_email = sanitize_address(email_message.from_email, email_message.encoding)
|
|
|
|
|
2024-04-24 09:43:56 -04:00
|
|
|
to_recipients = [sanitize_address(addr, email_message.encoding) for addr in email_message.to]
|
2018-02-26 16:48:52 -08:00
|
|
|
|
|
|
|
try:
|
|
|
|
post_data = []
|
2024-04-24 09:43:56 -04:00
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"to",
|
|
|
|
(",".join(to_recipients)),
|
|
|
|
)
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
if email_message.bcc:
|
2024-04-24 09:43:56 -04:00
|
|
|
bcc_recipients = [
|
|
|
|
sanitize_address(addr, email_message.encoding) for addr in email_message.bcc
|
|
|
|
]
|
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"bcc",
|
|
|
|
(",".join(bcc_recipients)),
|
|
|
|
)
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
if email_message.cc:
|
|
|
|
cc_recipients = [sanitize_address(addr, email_message.encoding) for addr in email_message.cc]
|
2024-04-24 09:43:56 -04:00
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"cc",
|
|
|
|
(",".join(cc_recipients)),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"text",
|
|
|
|
email_message.body,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"subject",
|
|
|
|
email_message.subject,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"from",
|
|
|
|
from_email,
|
|
|
|
)
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
# get our recipient variables if they were passed in
|
2024-04-24 09:43:56 -04:00
|
|
|
recipient_variables = email_message.extra_headers.pop("recipient_variables", None)
|
2018-02-26 16:48:52 -08:00
|
|
|
if recipient_variables is not None:
|
2024-04-24 09:43:56 -04:00
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"recipient-variables",
|
|
|
|
recipient_variables,
|
|
|
|
)
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
|
|
|
|
for name, value in self._map_smtp_headers_to_api_parameters(email_message):
|
2024-04-24 09:43:56 -04:00
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
name,
|
|
|
|
value,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
if hasattr(email_message, "alternatives") and email_message.alternatives:
|
2018-02-26 16:48:52 -08:00
|
|
|
for alt in email_message.alternatives:
|
2024-04-24 09:43:56 -04:00
|
|
|
if alt[1] == "text/html":
|
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"html",
|
|
|
|
alt[0],
|
|
|
|
)
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
break
|
|
|
|
|
|
|
|
# Map Reply-To header if present
|
|
|
|
try:
|
2024-04-24 09:43:56 -04:00
|
|
|
if hasattr(email_message, "reply_to"):
|
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"h:Reply-To",
|
|
|
|
", ".join(map(force_text, email_message.reply_to)),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
elif "Reply-To" in email_message.extra_headers:
|
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"h:Reply-To",
|
|
|
|
email_message.extra_headers["Reply-To"],
|
|
|
|
)
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
if email_message.attachments:
|
|
|
|
for attachment in email_message.attachments:
|
2024-04-24 09:43:56 -04:00
|
|
|
post_data.append(
|
|
|
|
(
|
|
|
|
"attachment",
|
|
|
|
(
|
|
|
|
attachment[0],
|
|
|
|
attachment[1],
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
content, header = encode_multipart_formdata(post_data)
|
2024-04-24 09:43:56 -04:00
|
|
|
headers = {"Content-Type": header}
|
2018-02-26 16:48:52 -08:00
|
|
|
else:
|
|
|
|
content = post_data
|
|
|
|
headers = None
|
|
|
|
|
2024-04-24 09:43:56 -04:00
|
|
|
response = requests.post(
|
|
|
|
self._api_url + "messages", auth=("api", self._access_key), data=content, headers=headers
|
|
|
|
)
|
2018-02-26 16:48:52 -08:00
|
|
|
except:
|
|
|
|
if not self.fail_silently:
|
|
|
|
raise
|
|
|
|
return False
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
if not self.fail_silently:
|
|
|
|
raise MailgunAPIError(response)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
def send_messages(self, email_messages):
|
|
|
|
"""Sends one or more EmailMessage objects and returns the number of
|
|
|
|
email messages sent.
|
|
|
|
"""
|
|
|
|
if not email_messages:
|
|
|
|
return
|
|
|
|
|
|
|
|
num_sent = 0
|
|
|
|
for message in email_messages:
|
|
|
|
if self._send(message):
|
|
|
|
num_sent += 1
|
|
|
|
|
2024-04-24 09:43:56 -04:00
|
|
|
return num_sent
|