mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Move common message attrs into base backend
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
|
||||
from ..exceptions import AnymailError, AnymailRequestsAPIError, AnymailSerializationError, AnymailUnsupportedFeature
|
||||
from ..utils import Attachment, ParsedEmail, UNSET, combine, last
|
||||
from .._version import __version__
|
||||
from ..exceptions import AnymailError, AnymailRequestsAPIError, AnymailSerializationError, AnymailImproperlyConfigured
|
||||
|
||||
|
||||
class AnymailBaseBackend(BaseEmailBackend):
|
||||
@@ -12,6 +15,10 @@ class AnymailBaseBackend(BaseEmailBackend):
|
||||
Base Anymail email backend
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AnymailBaseBackend, self).__init__(*args, **kwargs)
|
||||
self.send_defaults = getattr(settings, "ANYMAIL_SEND_DEFAULTS", {})
|
||||
|
||||
def open(self):
|
||||
"""
|
||||
Open and persist a connection to the ESP's API, and whether
|
||||
@@ -109,9 +116,144 @@ class AnymailBaseBackend(BaseEmailBackend):
|
||||
|
||||
Can raise AnymailUnsupportedFeature for unsupported options in message.
|
||||
"""
|
||||
raise NotImplementedError("%s.%s must implement build_message_payload" %
|
||||
encoding = message.encoding
|
||||
payload = self.get_base_payload(message)
|
||||
|
||||
# Standard EmailMessage features:
|
||||
self.set_payload_from_email(payload, ParsedEmail(message.from_email, encoding), message)
|
||||
for recipient_type in ["to", "cc", "bcc"]:
|
||||
recipients = getattr(message, recipient_type, [])
|
||||
if recipients:
|
||||
emails = [ParsedEmail(address, encoding) for address in recipients]
|
||||
self.add_payload_recipients(payload, recipient_type, emails, message)
|
||||
self.set_payload_subject(payload, message.subject, message)
|
||||
|
||||
if hasattr(message, "reply_to"):
|
||||
emails = [ParsedEmail(address, encoding) for address in message.reply_to]
|
||||
self.set_payload_reply_to(payload, emails, message)
|
||||
if hasattr(message, "extra_headers"):
|
||||
self.add_payload_headers(payload, message.extra_headers, message)
|
||||
|
||||
if message.content_subtype == "html":
|
||||
self.set_payload_html_body(payload, message.body, message)
|
||||
else:
|
||||
self.set_payload_text_body(payload, message.body, message)
|
||||
|
||||
if hasattr(message, "alternatives"):
|
||||
for (content, mimetype) in message.alternatives:
|
||||
self.add_payload_alternative(payload, content, mimetype, message)
|
||||
|
||||
str_encoding = encoding or settings.DEFAULT_CHARSET
|
||||
for attachment in message.attachments:
|
||||
self.add_payload_attachment(payload, Attachment(attachment, str_encoding), message)
|
||||
|
||||
# Anymail additions:
|
||||
metadata = self.get_anymail_merged_attr(message, "metadata") # merged: changes semantics from Djrill!
|
||||
if metadata is not UNSET:
|
||||
self.set_payload_metadata(payload, metadata, message)
|
||||
send_at = self.get_anymail_attr(message, "send_at")
|
||||
if send_at is not UNSET:
|
||||
self.set_payload_send_at(payload, send_at, message)
|
||||
tags = self.get_anymail_merged_attr(message, "tags") # merged: changes semantics from Djrill!
|
||||
if tags is not UNSET:
|
||||
self.set_payload_tags(payload, tags, message)
|
||||
track_clicks = self.get_anymail_attr(message, "track_clicks")
|
||||
if track_clicks is not UNSET:
|
||||
self.set_payload_track_clicks(payload, track_clicks, message)
|
||||
track_opens = self.get_anymail_attr(message, "track_opens")
|
||||
if track_opens is not UNSET:
|
||||
self.set_payload_track_opens(payload, track_opens, message)
|
||||
|
||||
# ESP-specific fallback:
|
||||
self.add_payload_esp_options(payload, message)
|
||||
|
||||
return payload
|
||||
|
||||
def get_anymail_attr(self, message, attr):
|
||||
default_value = self.send_defaults.get(attr, UNSET)
|
||||
message_value = getattr(message, attr, UNSET)
|
||||
return last(default_value, message_value)
|
||||
|
||||
def get_anymail_merged_attr(self, message, attr):
|
||||
default_value = self.send_defaults.get(attr, UNSET)
|
||||
message_value = getattr(message, attr, UNSET)
|
||||
return combine(default_value, message_value)
|
||||
|
||||
def unsupported_feature(self, feature):
|
||||
# future: check settings.ANYMAIL_UNSUPPORTED_FEATURE_ERRORS
|
||||
raise AnymailUnsupportedFeature("%s does not support %s" % (self.esp_name, feature))
|
||||
|
||||
#
|
||||
# Payload construction
|
||||
#
|
||||
|
||||
def get_base_payload(self, message):
|
||||
raise NotImplementedError("%s.%s must implement init_base_payload" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def set_payload_from_email(self, payload, email, message):
|
||||
raise NotImplementedError("%s.%s must implement set_payload_from" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def add_payload_recipients(self, payload, recipient_type, emails, message):
|
||||
for email in emails:
|
||||
self.add_payload_recipient(payload, recipient_type, email, message)
|
||||
|
||||
def add_payload_recipient(self, payload, recipient_type, email, message):
|
||||
raise NotImplementedError("%s.%s must implement add_payload_recipient" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def set_payload_subject(self, payload, subject, message):
|
||||
raise NotImplementedError("%s.%s must implement set_payload_subject" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def set_payload_reply_to(self, payload, emails, message):
|
||||
raise NotImplementedError("%s.%s must implement set_payload_reply_to" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def add_payload_headers(self, payload, headers, message):
|
||||
raise NotImplementedError("%s.%s must implement add_payload_heeaders" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def set_payload_text_body(self, payload, body, message):
|
||||
raise NotImplementedError("%s.%s must implement set_payload_text_body" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def set_payload_html_body(self, payload, body, message):
|
||||
raise NotImplementedError("%s.%s must implement set_payload_html_body" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def add_payload_alternative(self, payload, content, mimetype, message):
|
||||
raise NotImplementedError("%s.%s must implement add_payload_alternative" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def add_payload_attachment(self, payload, attachment, message):
|
||||
raise NotImplementedError("%s.%s must implement add_payload_attachment" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
# Anymail-specific payload construction
|
||||
def set_payload_metadata(self, payload, metadata, message):
|
||||
self.unsupported_feature("metadata")
|
||||
|
||||
def set_payload_send_at(self, payload, send_at, message):
|
||||
self.unsupported_feature("send_at")
|
||||
|
||||
def set_payload_tags(self, payload, tags, message):
|
||||
self.unsupported_feature("tags")
|
||||
|
||||
def set_payload_track_clicks(self, payload, track_clicks, message):
|
||||
self.unsupported_feature("track_clicks")
|
||||
|
||||
def set_payload_track_opens(self, payload, track_opens, message):
|
||||
self.unsupported_feature("track_opens")
|
||||
|
||||
# ESP-specific payload construction
|
||||
def add_payload_esp_options(self, payload, message):
|
||||
raise NotImplementedError("%s.%s must implement add_payload_esp_options" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
#
|
||||
|
||||
def post_to_esp(self, payload, message):
|
||||
"""Post payload to ESP send API endpoint, and return the raw response.
|
||||
|
||||
@@ -121,7 +263,7 @@ class AnymailBaseBackend(BaseEmailBackend):
|
||||
|
||||
Can raise AnymailAPIError (or derived exception) for problems posting to the ESP
|
||||
"""
|
||||
raise NotImplementedError("%s.%s must implement build_message_payload" %
|
||||
raise NotImplementedError("%s.%s must implement post_to_esp" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def deserialize_response(self, response, payload, message):
|
||||
@@ -129,7 +271,7 @@ class AnymailBaseBackend(BaseEmailBackend):
|
||||
|
||||
Can raise AnymailAPIError (or derived exception) if response is unparsable
|
||||
"""
|
||||
raise NotImplementedError("%s.%s must implement build_message_payload" %
|
||||
raise NotImplementedError("%s.%s must implement deserialize_response" %
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
def validate_response(self, parsed_response, response, payload, message):
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import mimetypes
|
||||
from base64 import b64encode
|
||||
from datetime import date, datetime
|
||||
from email.mime.base import MIMEBase
|
||||
from email.utils import parseaddr
|
||||
try:
|
||||
from urlparse import urljoin # python 2
|
||||
except ImportError:
|
||||
from urllib.parse import urljoin # python 3
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
||||
|
||||
from ..exceptions import (AnymailImproperlyConfigured,
|
||||
AnymailRequestsAPIError, AnymailRecipientsRefused,
|
||||
AnymailUnsupportedFeature)
|
||||
from ..exceptions import (AnymailImproperlyConfigured, AnymailRequestsAPIError,
|
||||
AnymailRecipientsRefused, AnymailUnsupportedFeature)
|
||||
|
||||
from .base import AnymailRequestsBackend
|
||||
|
||||
@@ -36,47 +30,24 @@ class MandrillBackend(AnymailRequestsBackend):
|
||||
except AttributeError:
|
||||
raise AnymailImproperlyConfigured("Set MANDRILL_API_KEY in settings.py to use Anymail Mandrill backend")
|
||||
|
||||
self.global_settings = {}
|
||||
# Djrill compat! MANDRILL_SETTINGS
|
||||
try:
|
||||
self.global_settings.update(settings.MANDRILL_SETTINGS)
|
||||
self.send_defaults.update(settings.MANDRILL_SETTINGS)
|
||||
except AttributeError:
|
||||
pass # no MANDRILL_SETTINGS setting
|
||||
except (TypeError, ValueError): # e.g., not enumerable
|
||||
raise AnymailImproperlyConfigured("MANDRILL_SETTINGS must be a dict or mapping")
|
||||
|
||||
# Djrill compat! MANDRILL_SUBACCOUNT
|
||||
try:
|
||||
self.global_settings["subaccount"] = settings.MANDRILL_SUBACCOUNT
|
||||
self.send_defaults["subaccount"] = settings.MANDRILL_SUBACCOUNT
|
||||
except AttributeError:
|
||||
pass # no MANDRILL_SUBACCOUNT setting
|
||||
|
||||
self.global_settings = self.send_defaults
|
||||
self.ignore_recipient_status = getattr(settings, "MANDRILL_IGNORE_RECIPIENT_STATUS", False)
|
||||
self.session = None
|
||||
|
||||
def build_message_payload(self, message):
|
||||
"""Modify payload to add all message-specific options for Mandrill send call.
|
||||
|
||||
payload is a dict that will become the Mandrill send data
|
||||
message is an EmailMessage, possibly with additional Mandrill-specific attrs
|
||||
|
||||
Can raise AnymailUnsupportedFeature for unsupported options in message.
|
||||
"""
|
||||
msg_dict = self._build_standard_message_dict(message)
|
||||
self._add_mandrill_options(message, msg_dict)
|
||||
if getattr(message, 'alternatives', None):
|
||||
self._add_alternatives(message, msg_dict)
|
||||
self._add_attachments(message, msg_dict)
|
||||
|
||||
payload = {
|
||||
"key": self.api_key,
|
||||
"message": msg_dict,
|
||||
}
|
||||
if hasattr(message, 'template_name'):
|
||||
payload['template_name'] = message.template_name
|
||||
payload['template_content'] = \
|
||||
self._expand_merge_vars(getattr(message, 'template_content', {}))
|
||||
self._add_mandrill_toplevel_options(message, payload)
|
||||
return payload
|
||||
|
||||
def get_api_url(self, payload, message):
|
||||
"""Return the correct Mandrill API url for sending payload
|
||||
|
||||
@@ -115,48 +86,88 @@ class MandrillBackend(AnymailRequestsBackend):
|
||||
# Payload construction
|
||||
#
|
||||
|
||||
def _build_standard_message_dict(self, message):
|
||||
"""Create a Mandrill send message struct from a Django EmailMessage.
|
||||
|
||||
Builds the standard dict that Django's send_mail and send_mass_mail
|
||||
use by default. Standard text email messages sent through Django will
|
||||
still work through Mandrill.
|
||||
|
||||
Raises AnymailUnsupportedFeature for any standard EmailMessage
|
||||
features that cannot be accurately communicated to Mandrill.
|
||||
"""
|
||||
sender = sanitize_address(message.from_email, message.encoding)
|
||||
from_name, from_email = parseaddr(sender)
|
||||
|
||||
to_list = self._make_mandrill_to_list(message, message.to, "to")
|
||||
to_list += self._make_mandrill_to_list(message, message.cc, "cc")
|
||||
to_list += self._make_mandrill_to_list(message, message.bcc, "bcc")
|
||||
|
||||
content = "html" if message.content_subtype == "html" else "text"
|
||||
msg_dict = {
|
||||
content: message.body,
|
||||
"to": to_list
|
||||
def get_base_payload(self, message):
|
||||
return {
|
||||
"key": self.api_key,
|
||||
"message": {},
|
||||
}
|
||||
|
||||
if not getattr(message, 'use_template_from', False):
|
||||
msg_dict["from_email"] = from_email
|
||||
if from_name:
|
||||
msg_dict["from_name"] = from_name
|
||||
def set_payload_from_email(self, payload, email, message):
|
||||
if not getattr(message, "use_template_from", False): # Djrill compat!
|
||||
payload["message"]["from_email"] = email.email
|
||||
if email.name:
|
||||
payload["message"]["from_name"] = email.name
|
||||
|
||||
if not getattr(message, 'use_template_subject', False):
|
||||
msg_dict["subject"] = message.subject
|
||||
def add_payload_recipient(self, payload, recipient_type, email, message):
|
||||
assert recipient_type in ["to", "cc", "bcc"]
|
||||
to_list = payload["message"].setdefault("to", [])
|
||||
to_list.append({"email": email.email, "name": email.name, "type": recipient_type})
|
||||
|
||||
if hasattr(message, 'reply_to'):
|
||||
reply_to = [sanitize_address(addr, message.encoding) for addr in message.reply_to]
|
||||
msg_dict["headers"] = {'Reply-To': ', '.join(reply_to)}
|
||||
# Note: An explicit Reply-To header will override the reply_to attr below
|
||||
# (matching Django's own behavior)
|
||||
def set_payload_subject(self, payload, subject, message):
|
||||
if not getattr(message, "use_template_subject", False): # Djrill compat!
|
||||
payload["message"]["subject"] = subject
|
||||
|
||||
if message.extra_headers:
|
||||
msg_dict["headers"] = msg_dict.get("headers", {})
|
||||
msg_dict["headers"].update(message.extra_headers)
|
||||
def set_payload_reply_to(self, payload, emails, message):
|
||||
reply_to = ", ".join([email.address for email in emails])
|
||||
payload["message"].setdefault("headers", {})["Reply-To"] = reply_to
|
||||
|
||||
return msg_dict
|
||||
def add_payload_headers(self, payload, headers, message):
|
||||
payload["message"].setdefault("headers", {}).update(headers)
|
||||
|
||||
def set_payload_text_body(self, payload, body, message):
|
||||
payload["message"]["text"] = body
|
||||
|
||||
def set_payload_html_body(self, payload, body, message):
|
||||
payload["message"]["html"] = body
|
||||
|
||||
def add_payload_alternative(self, payload, content, mimetype, message):
|
||||
if mimetype != 'text/html':
|
||||
raise AnymailUnsupportedFeature(
|
||||
"Invalid alternative mimetype '%s'. "
|
||||
"Mandrill only accepts plain text and html emails."
|
||||
% mimetype,
|
||||
email_message=message)
|
||||
|
||||
if "html" in payload["message"]:
|
||||
raise AnymailUnsupportedFeature(
|
||||
"Too many alternatives attached to the message. "
|
||||
"Mandrill only accepts plain text and html emails.",
|
||||
email_message=message)
|
||||
|
||||
payload["message"]["html"] = content
|
||||
|
||||
def add_payload_attachment(self, payload, attachment, message):
|
||||
key = "images" if attachment.inline else "attachments"
|
||||
payload["message"].setdefault(key, []).append({
|
||||
"type": attachment.mimetype,
|
||||
"name": attachment.name or "",
|
||||
"content": attachment.b64content
|
||||
})
|
||||
|
||||
def set_payload_metadata(self, payload, metadata, message):
|
||||
payload["message"]["metadata"] = metadata
|
||||
|
||||
def set_payload_send_at(self, payload, send_at, message):
|
||||
payload["send_at"] = self.encode_date_for_mandrill(send_at)
|
||||
|
||||
def set_payload_tags(self, payload, tags, message):
|
||||
payload["message"]["tags"] = tags
|
||||
|
||||
def set_payload_track_clicks(self, payload, track_clicks, message):
|
||||
payload["message"]["track_clicks"] = track_clicks
|
||||
|
||||
def set_payload_track_opens(self, payload, track_opens, message):
|
||||
payload["message"]["track_opens"] = track_opens
|
||||
|
||||
def add_payload_esp_options(self, payload, message):
|
||||
self._add_mandrill_options(message, payload["message"])
|
||||
if hasattr(message, 'template_name'):
|
||||
payload['template_name'] = message.template_name
|
||||
payload['template_content'] = \
|
||||
self._expand_merge_vars(getattr(message, 'template_content', {}))
|
||||
self._add_mandrill_toplevel_options(message, payload)
|
||||
|
||||
# unported
|
||||
|
||||
def _add_mandrill_toplevel_options(self, message, api_params):
|
||||
"""Extend api_params to include Mandrill global-send options set on message"""
|
||||
@@ -170,35 +181,19 @@ class MandrillBackend(AnymailRequestsBackend):
|
||||
if hasattr(message, attr):
|
||||
api_params[attr] = getattr(message, attr)
|
||||
|
||||
# Mandrill attributes that require conversion:
|
||||
if hasattr(message, 'send_at'):
|
||||
api_params['send_at'] = self.encode_date_for_mandrill(message.send_at)
|
||||
# setting send_at in global_settings wouldn't make much sense
|
||||
|
||||
def _make_mandrill_to_list(self, message, recipients, recipient_type="to"):
|
||||
"""Create a Mandrill 'to' field from a list of emails.
|
||||
|
||||
Parses "Real Name <address@example.com>" format emails.
|
||||
Sanitizes all email addresses.
|
||||
"""
|
||||
parsed_rcpts = [parseaddr(sanitize_address(addr, message.encoding))
|
||||
for addr in recipients]
|
||||
return [{"email": to_email, "name": to_name, "type": recipient_type}
|
||||
for (to_name, to_email) in parsed_rcpts]
|
||||
|
||||
def _add_mandrill_options(self, message, msg_dict):
|
||||
"""Extend msg_dict to include Mandrill per-message options set on message"""
|
||||
# Mandrill attributes that can be copied directly:
|
||||
mandrill_attrs = [
|
||||
'from_name', # overrides display name parsed from from_email above
|
||||
'important',
|
||||
'track_opens', 'track_clicks', 'auto_text', 'auto_html',
|
||||
'auto_text', 'auto_html',
|
||||
'inline_css', 'url_strip_qs',
|
||||
'tracking_domain', 'signing_domain', 'return_path_domain',
|
||||
'merge_language',
|
||||
'tags', 'preserve_recipients', 'view_content_link', 'subaccount',
|
||||
'preserve_recipients', 'view_content_link', 'subaccount',
|
||||
'google_analytics_domains', 'google_analytics_campaign',
|
||||
'metadata']
|
||||
]
|
||||
|
||||
for attr in mandrill_attrs:
|
||||
if attr in self.global_settings:
|
||||
@@ -243,98 +238,6 @@ class MandrillBackend(AnymailRequestsBackend):
|
||||
return [{'name': name, 'content': vardict[name]}
|
||||
for name in sorted(vardict.keys())]
|
||||
|
||||
def _add_alternatives(self, message, msg_dict):
|
||||
"""
|
||||
There can be only one! ... alternative attachment, and it must be text/html.
|
||||
|
||||
Since mandrill does not accept image attachments or anything other
|
||||
than HTML, the assumption is the only thing you are attaching is
|
||||
the HTML output for your email.
|
||||
"""
|
||||
if len(message.alternatives) > 1:
|
||||
raise AnymailUnsupportedFeature(
|
||||
"Too many alternatives attached to the message. "
|
||||
"Mandrill only accepts plain text and html emails.",
|
||||
email_message=message)
|
||||
|
||||
(content, mimetype) = message.alternatives[0]
|
||||
if mimetype != 'text/html':
|
||||
raise AnymailUnsupportedFeature(
|
||||
"Invalid alternative mimetype '%s'. "
|
||||
"Mandrill only accepts plain text and html emails."
|
||||
% mimetype,
|
||||
email_message=message)
|
||||
|
||||
msg_dict['html'] = content
|
||||
|
||||
def _add_attachments(self, message, msg_dict):
|
||||
"""Extend msg_dict to include any attachments in message"""
|
||||
if message.attachments:
|
||||
str_encoding = message.encoding or settings.DEFAULT_CHARSET
|
||||
mandrill_attachments = []
|
||||
mandrill_embedded_images = []
|
||||
for attachment in message.attachments:
|
||||
att_dict, is_embedded = self._make_mandrill_attachment(attachment, str_encoding)
|
||||
if is_embedded:
|
||||
mandrill_embedded_images.append(att_dict)
|
||||
else:
|
||||
mandrill_attachments.append(att_dict)
|
||||
if len(mandrill_attachments) > 0:
|
||||
msg_dict['attachments'] = mandrill_attachments
|
||||
if len(mandrill_embedded_images) > 0:
|
||||
msg_dict['images'] = mandrill_embedded_images
|
||||
|
||||
def _make_mandrill_attachment(self, attachment, str_encoding=None):
|
||||
"""Returns EmailMessage.attachments item formatted for sending with Mandrill.
|
||||
|
||||
Returns mandrill_dict, is_embedded_image:
|
||||
mandrill_dict: {"type":..., "name":..., "content":...}
|
||||
is_embedded_image: True if the attachment should instead be handled as an inline image.
|
||||
|
||||
"""
|
||||
# Note that an attachment can be either a tuple of (filename, content,
|
||||
# mimetype) or a MIMEBase object. (Also, both filename and mimetype may
|
||||
# be missing.)
|
||||
is_embedded_image = False
|
||||
if isinstance(attachment, MIMEBase):
|
||||
name = attachment.get_filename()
|
||||
content = attachment.get_payload(decode=True)
|
||||
mimetype = attachment.get_content_type()
|
||||
# Treat image attachments that have content ids as embedded:
|
||||
if attachment.get_content_maintype() == "image" and attachment["Content-ID"] is not None:
|
||||
is_embedded_image = True
|
||||
name = attachment["Content-ID"]
|
||||
else:
|
||||
(name, content, mimetype) = attachment
|
||||
|
||||
# Guess missing mimetype from filename, borrowed from
|
||||
# django.core.mail.EmailMessage._create_attachment()
|
||||
if mimetype is None and name is not None:
|
||||
mimetype, _ = mimetypes.guess_type(name)
|
||||
if mimetype is None:
|
||||
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
||||
|
||||
# b64encode requires bytes, so let's convert our content.
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
if isinstance(content, unicode):
|
||||
# Python 2.X unicode string
|
||||
content = content.encode(str_encoding)
|
||||
except NameError:
|
||||
# Python 3 doesn't differentiate between strings and unicode
|
||||
# Convert python3 unicode str to bytes attachment:
|
||||
if isinstance(content, str):
|
||||
content = content.encode(str_encoding)
|
||||
|
||||
content_b64 = b64encode(content)
|
||||
|
||||
mandrill_attachment = {
|
||||
'type': mimetype,
|
||||
'name': name or "",
|
||||
'content': content_b64.decode('ascii'),
|
||||
}
|
||||
return mandrill_attachment, is_embedded_image
|
||||
|
||||
@classmethod
|
||||
def encode_date_for_mandrill(cls, dt):
|
||||
"""Format a date or datetime for use as a Mandrill API date field
|
||||
|
||||
@@ -614,7 +614,7 @@ class DjrillRecipientsRefusedTests(DjrillBackendMockAPITestCase):
|
||||
self.assertEqual(sent, 1) # refused message is included in sent count
|
||||
|
||||
|
||||
@override_settings(MANDRILL_SETTINGS={
|
||||
@override_settings(ANYMAIL_SEND_DEFAULTS={
|
||||
'from_name': 'Djrill Test',
|
||||
'important': True,
|
||||
'track_opens': True,
|
||||
@@ -632,7 +632,7 @@ class DjrillRecipientsRefusedTests(DjrillBackendMockAPITestCase):
|
||||
'return_path_domain': 'example.com',
|
||||
'google_analytics_domains': ['example.com/test'],
|
||||
'google_analytics_campaign': ['UA-00000000-1'],
|
||||
'metadata': ['djrill'],
|
||||
'metadata': {'feature': 'global', 'plus': 'that'},
|
||||
'merge_language': 'mailchimp',
|
||||
'global_merge_vars': {'TEST': 'djrill'},
|
||||
'async': True,
|
||||
@@ -670,7 +670,7 @@ class DjrillMandrillGlobalFeatureTests(DjrillBackendMockAPITestCase):
|
||||
self.assertEqual(data['message']['return_path_domain'], 'example.com')
|
||||
self.assertEqual(data['message']['google_analytics_domains'], ['example.com/test'])
|
||||
self.assertEqual(data['message']['google_analytics_campaign'], ['UA-00000000-1'])
|
||||
self.assertEqual(data['message']['metadata'], ['djrill'])
|
||||
self.assertEqual(data['message']['metadata'], {'feature': 'global', 'plus': 'that'})
|
||||
self.assertEqual(data['message']['merge_language'], 'mailchimp')
|
||||
self.assertEqual(data['message']['global_merge_vars'],
|
||||
[{'name': 'TEST', 'content': 'djrill'}])
|
||||
@@ -702,7 +702,7 @@ class DjrillMandrillGlobalFeatureTests(DjrillBackendMockAPITestCase):
|
||||
self.message.return_path_domain = "override.example.com"
|
||||
self.message.google_analytics_domains = ['override.example.com']
|
||||
self.message.google_analytics_campaign = ['UA-99999999-1']
|
||||
self.message.metadata = ['override']
|
||||
self.message.metadata = {'feature': 'message', 'also': 'this'}
|
||||
self.message.merge_language = 'handlebars'
|
||||
self.message.async = False
|
||||
self.message.ip_pool = "Bulk Pool"
|
||||
@@ -716,7 +716,7 @@ class DjrillMandrillGlobalFeatureTests(DjrillBackendMockAPITestCase):
|
||||
self.assertFalse(data['message']['auto_html'])
|
||||
self.assertFalse(data['message']['inline_css'])
|
||||
self.assertFalse(data['message']['url_strip_qs'])
|
||||
self.assertEqual(data['message']['tags'], ['override'])
|
||||
self.assertEqual(data['message']['tags'], ['djrill', 'override']) # tags are merged
|
||||
self.assertFalse(data['message']['preserve_recipients'])
|
||||
self.assertFalse(data['message']['view_content_link'])
|
||||
self.assertEqual(data['message']['subaccount'], 'override')
|
||||
@@ -725,7 +725,8 @@ class DjrillMandrillGlobalFeatureTests(DjrillBackendMockAPITestCase):
|
||||
self.assertEqual(data['message']['return_path_domain'], 'override.example.com')
|
||||
self.assertEqual(data['message']['google_analytics_domains'], ['override.example.com'])
|
||||
self.assertEqual(data['message']['google_analytics_campaign'], ['UA-99999999-1'])
|
||||
self.assertEqual(data['message']['metadata'], ['override'])
|
||||
# metadata is merged:
|
||||
self.assertEqual(data['message']['metadata'], {'feature': 'message', 'also': 'this', 'plus': 'that'})
|
||||
self.assertEqual(data['message']['merge_language'], 'handlebars')
|
||||
self.assertEqual(data['message']['global_merge_vars'],
|
||||
[{'name': 'TEST', 'content': 'djrill'}])
|
||||
|
||||
135
anymail/utils.py
Normal file
135
anymail/utils.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import mimetypes
|
||||
from base64 import b64encode
|
||||
from email.mime.base import MIMEBase
|
||||
from email.utils import parseaddr
|
||||
|
||||
import six
|
||||
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
||||
|
||||
|
||||
UNSET = object() # Used as non-None default value
|
||||
|
||||
|
||||
def combine(*args):
|
||||
"""
|
||||
Combines all non-UNSET args, by shallow merging mappings and concatenating sequences
|
||||
|
||||
>>> combine({'a': 1, 'b': 2}, UNSET, {'b': 3, 'c': 4}, UNSET)
|
||||
{'a': 1, 'b': 3, 'c': 4}
|
||||
>>> combine([1, 2], UNSET, [3, 4], UNSET)
|
||||
[1, 2, 3, 4]
|
||||
>>> combine({'a': 1}, None, {'b': 2}) # None suppresses earlier args
|
||||
{'b': 2}
|
||||
>>> combine()
|
||||
UNSET
|
||||
|
||||
"""
|
||||
result = UNSET
|
||||
for value in args:
|
||||
if value is None:
|
||||
# None is a request to suppress any earlier values
|
||||
result = UNSET
|
||||
elif value is not UNSET:
|
||||
if result is UNSET:
|
||||
try:
|
||||
result = value.copy() # will shallow merge if dict-like
|
||||
except AttributeError:
|
||||
result = value # will concatenate if sequence-like
|
||||
else:
|
||||
try:
|
||||
result.update(value) # shallow merge if dict-like
|
||||
except AttributeError:
|
||||
result += value # concatenate if sequence-like
|
||||
return result
|
||||
|
||||
|
||||
def last(*args):
|
||||
"""Returns the last of its args which is not UNSET.
|
||||
|
||||
(Essentially `combine` without the merge behavior)
|
||||
|
||||
>>> last(1, 2, UNSET, 3, UNSET, UNSET)
|
||||
3
|
||||
>>> last(1, 2, None, UNSET) # None suppresses earlier args
|
||||
UNSET
|
||||
>>> last()
|
||||
UNSET
|
||||
|
||||
"""
|
||||
for value in reversed(args):
|
||||
if value is None:
|
||||
# None is a request to suppress any earlier values
|
||||
return UNSET
|
||||
elif value is not UNSET:
|
||||
return value
|
||||
return UNSET
|
||||
|
||||
|
||||
class ParsedEmail(object):
|
||||
"""A sanitized, full email address with separate name and email properties"""
|
||||
|
||||
def __init__(self, address, encoding):
|
||||
self.address = sanitize_address(address, encoding)
|
||||
self._name = None
|
||||
self._email = None
|
||||
|
||||
def _parse(self):
|
||||
if self._email is None:
|
||||
self._name, self._email = parseaddr(self.address)
|
||||
|
||||
def __str__(self):
|
||||
return self.address
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
self._parse()
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
self._parse()
|
||||
return self._email
|
||||
|
||||
|
||||
class Attachment(object):
|
||||
"""A normalized EmailMessage.attachments item with additional functionality
|
||||
|
||||
Normalized to have these properties:
|
||||
name: attachment filename; may be empty string; will be Content-ID for inline attachments
|
||||
content
|
||||
mimetype: the content type; guessed if not explicit
|
||||
inline: bool, True if attachment has a Content-ID header
|
||||
"""
|
||||
|
||||
def __init__(self, attachment, encoding):
|
||||
# Note that an attachment can be either a tuple of (filename, content, mimetype)
|
||||
# or a MIMEBase object. (Also, both filename and mimetype may be missing.)
|
||||
self._attachment = attachment
|
||||
self.encoding = encoding # should we be checking attachment["Content-Encoding"] ???
|
||||
self.inline = False
|
||||
|
||||
if isinstance(attachment, MIMEBase):
|
||||
self.name = attachment.get_filename()
|
||||
self.content = attachment.get_payload(decode=True)
|
||||
self.mimetype = attachment.get_content_type()
|
||||
# Treat image attachments that have content ids as inline:
|
||||
if attachment.get_content_maintype() == "image" and attachment["Content-ID"] is not None:
|
||||
self.inline = True
|
||||
self.name = attachment["Content-ID"]
|
||||
else:
|
||||
(self.name, self.content, self.mimetype) = attachment
|
||||
|
||||
# Guess missing mimetype from filename, borrowed from
|
||||
# django.core.mail.EmailMessage._create_attachment()
|
||||
if self.mimetype is None and self.name is not None:
|
||||
self.mimetype, _ = mimetypes.guess_type(self.name)
|
||||
if self.mimetype is None:
|
||||
self.mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
||||
|
||||
@property
|
||||
def b64content(self):
|
||||
"""Content encoded as a base64 ascii string"""
|
||||
content = self.content
|
||||
if isinstance(content, six.text_type):
|
||||
content = content.encode(self.encoding)
|
||||
return b64encode(content).decode("ascii")
|
||||
Reference in New Issue
Block a user