Commit Graph

76 Commits

Author SHA1 Message Date
medmunds
07fbeac6bd Feature: Add envelope_sender
New EmailMessage attribute `envelope_sender` controls ESP's sender,
sending domain, or return path where supported:

* Mailgun: overrides SENDER_DOMAIN on individual message
  (domain portion only)
* Mailjet: becomes `Sender` API param
* Mandrill: becomes `return_path_domain` API param
  (domain portion only)
* SparkPost: becomes `return_path` API param
* Other ESPs: not believed to be supported

Also support undocumented Django SMTP backend behavior, where envelope
sender is given by `message.from_email` when
`message.extra_headers["From"]` is set. Fixes #91.
2018-02-26 18:42:19 -08:00
medmunds
bd9d92f5a0 Cleanup: centralize Reply-To header handling; case-insensitive headers
Django allows setting the reply address with either message.reply_to
or message.extra_headers["Reply-To"]. If both are supplied, the extra
headers version takes precedence. (See EmailMessage.message().)

Several Anymail backends had duplicate logic to handle conflicting
properties. Move that logic into the base Payload.

(Also prepares for common handling of extra_headers['From'], later.)

Related changes:

* Use CaseInsensitiveDict for processing extra_headers.
  This is potentially a breaking change, but any code that was trying
  to send multiple headers differing only in case was likely already
  broken. (Email header field names are case-insensitive, per RFC-822.)

* Handle CaseInsensitiveDict in RequestsPayload.serialize_json().
  (Several backends had duplicate code for handling this, too.)

* Fixes SparkPost backend, which had been incorrectly treating
  message.reply_to and message.extra_headers['Reply-To'] differently.
2018-02-26 12:25:57 -08:00
medmunds
ec0ee336a2 Cleanup: Avoid Python 3.7 deprecation warning on 'async' keyword
Fixes #92
2018-02-26 10:24:08 -08:00
Rignon Noël
dc2b4b4e7a Add SendinBlue backend
Add support for sending transactional email through SendinBlue. (Thanks to @RignonNoel.)

Partially implements #84. (Tracking webhooks will be a separate PR. SendinBlue doesn't support inbound handling.)
2018-02-26 09:46:10 -08:00
medmunds
1a6086f2b5 Security: rename WEBHOOK_AUTHORIZATION --> WEBHOOK_SECRET
This fixes a low severity security issue affecting Anymail v0.2--v1.3.

Django error reporting includes the value of your Anymail
WEBHOOK_AUTHORIZATION setting. In a properly-configured deployment,
this should not be cause for concern. But if you have somehow exposed
your Django error reports (e.g., by mis-deploying with DEBUG=True or by
sending error reports through insecure channels), anyone who gains
access to those reports could discover your webhook shared secret. An
attacker could use this to post fabricated or malicious Anymail
tracking/inbound events to your app, if you are using those Anymail
features.

The fix renames Anymail's webhook shared secret setting so that
Django's error reporting mechanism will [sanitize][0] it.

If you are using Anymail's event tracking and/or inbound webhooks, you
should upgrade to this release and change "WEBHOOK_AUTHORIZATION" to
"WEBHOOK_SECRET" in the ANYMAIL section of your settings.py. You may
also want to [rotate the shared secret][1] value, particularly if you
have ever exposed your Django error reports to untrusted individuals.

If you are only using Anymail's EmailBackends for sending email and
have not set up Anymail's webhooks, this issue does not affect you.

The old WEBHOOK_AUTHORIZATION setting is still allowed in this release,
but will issue a system-check warning when running most Django
management commands. It will be removed completely in a near-future
release, as a breaking change.

Thanks to Charlie DeTar (@yourcelf) for responsibly reporting this
security issue through private channels.

[0]: https://docs.djangoproject.com/en/stable/ref/settings/#debug
[1]: https://anymail.readthedocs.io/en/1.4/tips/securing_webhooks/#use-a-shared-authorization-secret
2018-02-08 11:38:15 -08:00
Mike Edmunds
b57eb94f64 Add inbound mail handling
Add normalized event, signal, and webhooks for inbound mail.

Closes #43
Closes #86
2018-02-02 10:38:53 -08:00
Charlie DeTar
771d4040df Un-hardcode message_id in test backend; add console backend
* Un-hardcode status message_id in test backend

For the test EmailBackend, get message ID's based on array position in
`mail.outbox`, so that tests can predict the message ID.


* Add a console backend for use in development

Adds an EmailBackend derived from both Anymail's test backend and
Django's console backend, to provide anymail statuses and signal
handling while printing messages to the console.  For use during
development on localhost.

Closes #87
2018-01-28 12:25:05 -08:00
medmunds
09def30868 Add timeout to all Requests calls
Use a default timeout of 30 seconds for all requests, and add a
REQUESTS_TIMEOUT Anymail setting to override.

(I'm making a judgement call that this is not a breaking change in the
real world, and not bumping the major version. Theoretically, it could
affect you if your network somehow takes >30s to connect to your ESP,
but eventually succeeds. If so, set REQUESTS_TIMEOUT to None to restore
the earlier behavior.)

Fixes #80.
2018-01-17 14:36:50 -08:00
medmunds
5fb46952c6 Tests: fix MockRequestsBackend.get_api_call_arg edge cases
get_api_call_arg had incorrectly returned None if a kwarg was passed
to the mocked function with a False-y value (e.g., [] or {})

get_api_call_json had only considered data param, ignoring json param
requests added a while back
2018-01-17 13:53:06 -08:00
medmunds
7e908184ed Postmark: support "clicked" tracking events
Handle Postmark's new click webhook.

Closes #78
2017-11-02 11:48:02 -07:00
medmunds
9acf6501b5 Utils: Finish ParsedEmail --> EmailAddress conversion
Within an EmailAddress (previously ParsedEmail object), properties
now match Python 3.6 email.headerregistry.Address naming:

* .email --> .addr_spec
* .name --> .display_name
* .localpart --> .username

(Completes work started in 386668908423d1d4eade90cf7a21a546a1e96514;
this updates remaining uses of old names and removes them.)
2017-10-27 17:53:13 -07:00
medmunds
bb68f3dd6d Mailgun: fix event/metadata param extraction in tracking webhook
Mailgun merges user-variables (metadata) into the webhook post data
interspersed with the actual event params. This can lead to ambiguity
interpreting post data.

To extract metadata from an event, Anymail had been attempting to avoid
that ambiguity by instead using X-Mailgun-Variables fields found in the
event's message-headers param. But message-headers isn't included in
some tracking events (opened, clicked, unsubscribed), resulting in
empty metadata for those events. (#76)

Also, conflicting metadata keys could confuse Anymail's Mailgun event
parsing, leading to unexpected values in the normalized event. (#77)

This commit:
* Cleans up Anymail's tracking webhook to be explicit about which
  multi-value params it uses, avoiding conflicts with metadata keys.
  Fixes #77.
* Extracts metadata from post params for opened, clicked and
  unsubscribed events. All unknown event params are assumed to be
  metadata. Fixes #76.
* Documents a few metadata key names where it's impossible (or likely
  to be unreliable) for Anymail to extract metadata from the post data.

For reference, the order of params in the Mailgun's post data *appears*
to be (from live testing):
* For the timestamp, token and signature params, any user-variable with
  the same name appears *before* the corresponding event data.
* For all other params, any user-variable with the same name as a
  Mailgun event param appears *after* the Mailgun data.
2017-10-27 13:26:37 -07:00
medmunds
636c8a5d80 Tests: move sample files into separate subdir 2017-10-25 19:50:31 -07:00
medmunds
3866689084 Utils: convert internal ParsedEmail to documented EmailAddress
Update internal-use ParsedEmail to be more like Python 3.6+
email.headerregistry.Address, and remove "internal use only"
recommendation.

(Prep for exposing inbound email headers in a convenient form.
Old names remain temporarily available for internal use;
should clean up at some point.)
2017-10-25 19:50:31 -07:00
medmunds
fe097ce4b4 Utils: add parse_rfc2822date
(Useful for interpreting date header in inbound messages)
2017-10-25 19:50:31 -07:00
medmunds
68de868ebf Tests: prep for string_concat deprecation in Django 2.1
Test either or both of the lazy string builders `string_concat`
and `format_lazy`, depending on which is/are available in the
Django being tested.
2017-10-11 15:34:11 -07:00
medmunds
9e1f9fbcdc Tests: Test Django 2.0a 2017-10-11 15:30:39 -07:00
medmunds
8cad1a6367 Keep flake happy on int/long test code 2017-09-14 11:54:30 -07:00
medmunds
3b9cb963ef SendGrid: convert long to str in headers, metadata
SendGrid requires extra headers and metadata values be strings.
Anymail has always coerced int and float; this treats Python 2's
`long` integer type the same.

Fixes #74
2017-09-14 11:45:17 -07:00
medmunds
a67e174f38 Remove deprecated <ESP>Backend names
(Deprecated with warnings since v0.8)
2017-09-08 16:42:28 -07:00
medmunds
2faa5f96cb Clean up and document Anymail's test EmailBackend
* Change Anymail's test EmailBackend to collect sent messages in
  django.core.mail.outbox, same as Django's own locmem EmailBackend.
  (So Django's test runner will automatically clear accumulated mail
  between test cases.)

* Rename EmailMessage `test_response` attr to `anymail_test_response`
  to avoid conflicts, and record merged ESP send params in
  new `anymail_send_params` attr.

* Add docs

Closes #36.
2017-09-01 13:13:25 -07:00
medmunds
d6d8044066 Mailjet: support tracking webhooks 2017-07-13 11:27:00 -07:00
medmunds
fc59707133 Webhooks: default tracking event tags=[], metadata={}
If a tracking event doesn't contain tags or metadata, set the event
record fields to tags=[] or metadata={} to simplify checking values.

Closes #67
2017-06-30 16:56:36 -07:00
Peter Wu
e39614e5a5 Mailjet backend support (#66)
Add Mailjet backend. (Docs and webhooks to follow.)

Thanks to @Lekensteyn and @calvin.
2017-06-30 16:33:53 -07:00
medmunds
ba6ccdb13a Mailgun: handle x.y.z status code in webhooks
Mailgun sometimes (though not usually) gives the 'code' event
field as an RFC-3463 extended SMTP status code. Handle either
format.

Fixes #62
2017-05-22 11:09:26 -07:00
medmunds
bb54806dcf Tests: update settings for Django 1.11 final
Regenerate test settings.py using released Django 1.11.
(Had version from alpha Django 1.11a1. Only comments changed.)
2017-04-19 12:49:02 -07:00
medmunds
6b6793016e Mailgun, SparkPost: support multiple from_email addresses
[RFC-5322 allows](https://tools.ietf.org/html/rfc5322#section-3.6.2)
multiple addresses in the From header.

Django's SMTP backend supports this, as a single comma-separated
string (*not* a list of strings like the recipient params):

    from_email='one@example.com, two@example.com'
    to=['one@example.com', 'two@example.com']

Both Mailgun and SparkPost support multiple From addresses
(and Postmark accepts them, though truncates to the first one
on their end). For compatibility with Django -- and because
Anymail attempts to support all ESP features -- Anymail now
allows multiple From addresses, too, for ESPs that support it.

Note: as a practical matter, deliverability with multiple
From addresses is pretty bad. (Google outright rejects them.)

This change also reworks Anymail's internal ParsedEmail object,
and approach to parsing addresses, for better consistency with
Django's SMTP backend and improved error messaging.

In particular, Django (and now Anymail) allows multiple email
addresses in a single recipient string:

    to=['one@example.com', 'two@example.com, three@example.com']
    len(to) == 2  # but there will be three recipients

Fixes #60
2017-04-19 12:43:33 -07:00
medmunds
8bdc67939a Flake8 clean 2017-04-16 11:43:13 -07:00
medmunds
853a7cd31d Cleanup forwarded email attachment test
... prep for flake8
2017-04-15 12:46:19 -07:00
Luke Plant
7d7448011b Fixed crasher when sending rfc822 messages as attachments. (#59) 2017-04-15 11:45:32 -07:00
medmunds
09bec9e463 Improve error when reply_to isn't list.
Issue a better error message if message.reply_to
is set to a single string.

(Would also like to do this for to, cc, and bcc,
but Django core EmailMessage.recipients is called
and stumbles over thoses cases before Anymail's
backend gets involved.)

Fixes #57
2017-04-04 11:57:11 -07:00
medmunds
3dd05ae882 Mailgun: update integration tests
Track change to Mailgun's events API, which
no longer includes message recipients.

(Only affected check for successful send
in the integration tests; Anymail doesn't
use the events API outside test code.)

Fixes #58
2017-04-04 10:33:03 -07:00
medmunds
23800657b8 Support Python 3.6 with Django 1.11-alpha
Only real problem is in json serialization tests:
Python 3.6 [changed][1] the json serialization
error message to use the object's class name
rather than its repr. E.g.:
  "Decimal('19.99') is not JSON serializable"
becomes:
  "Object of type 'Decimal' is not JSON serializable"

Update tests that looked for specific serialization
error message to just look for the word "Decimal"
instead. (Works with all Python versions.)

[1]: https://bugs.python.org/issue26623
2017-01-22 11:34:57 -08:00
medmunds
56d2b53c2b Use specific ESP name in error messages.
Change  (e.g.,) "ESP API response 400"
to "Mailgun API response 400".
2017-01-22 10:21:19 -08:00
medmunds
79288603fb Rename EmailBackends for Django consistency
* **Future breaking change:**
  Rename all Anymail backends to just `EmailBackend`,
  matching Django's naming convention.
  (E.g., switch to "anymail.backends.mailgun.EmailBackend"
  rather than "anymail.backends.mailgun.MailgunBackend".)

  The old names still work, but will issue a DeprecationWarning
  and will be removed in some future release.

  (Apologies for this change; the old naming convention was
  a holdover from Djrill, and I wanted consistency with
  other Django EmailBackends before hitting 1.0.)

Fixes #49.
2017-01-20 15:47:37 -08:00
medmunds
bff01b440a Fix webhook tests for Django 1.11-alpha
Django 1.11 appears to enforce ALLOWED_HOSTS
(more strictly) during tests.
2017-01-19 19:22:16 -08:00
medmunds
0ba5d1d4ad Mandrill: include auth in webhook signature calc
Mandrill's webhook signature calculation uses the
*exact url* Mandrill is posting to. If HTTP basic
auth is also used, that auth is included in the url.

Anymail was using Django's request.build_absolute_uri,
which doesn't include HTTP basic auth. Anymail now
includes the auth in the calculation, if it was present
in the request.

This should eliminate the need to use the
ANYMAIL_MANDRILL_WEBHOOK_URL override,
if Django's SECURE_PROXY_SSL_HEADER and
USE_X_FORWARDED_HOST (and/or
USE_X_FORWARDED_PROTO) settings are correct
for your server.

(The calculated url is now also included in
the validation failure error message, to aid
debugging.)

Fixes #48
2017-01-19 19:01:36 -08:00
medmunds
0eab2172d2 Tests: add Django 1.11-alpha
(including on Python 3.6)

Allow failures on prerelease Django tests.
2017-01-19 14:54:59 -08:00
Mike Edmunds
e568e50d0c SendGrid: update to new v3 send API (#50)
SendGrid: update to v3 send API

**SendGrid:** **[possibly-breaking]** Update SendGrid backend to newer Web API v3. This should be a transparent change for most projects. Exceptions: if you use SendGrid username/password auth, esp_extra with "x-smtpapi", or multiple Reply-To addresses, please review the [porting notes](http://anymail.readthedocs.io/en/latest/esps/sendgrid/#sendgrid-v3-upgrade).

Closes #28
2017-01-19 14:29:15 -08:00
Mike Edmunds
edf2a3ddcf Handle Django lazy strings.
In BasePayload, ensure any Django ugettext_lazy
(or similar) are converted to real strings before
handing off to ESP code. This resolves problems where
calling code expects it can use lazy strings "anywhere",
but non-Django code (requests, ESP packages) don't
always handle them correctly.

* Add utils helpers for lazy objects (is_lazy, force_non_lazy*)
* Add lazy object handling to utils.Attachment
* Add lazy object handling converters to BasePayload attr
  processing where appropriate. (This ends up varying by
  the expected attribute type.)

Fixes #34.
2016-12-30 15:48:08 -05:00
medmunds
146afbaf3b Simplify Mandrill webhook validation handshake.
Anymail was requiring Mandrill's webhook authentication key for the initial webhook url validation request from Mandrill, but Mandrill doesn't issue the key until that validation request succeeds.

* Defer complaining about missing Mandrill webhook key until actual event post.
* Document the double-deploy process required to set up Mandrill webhooks.

Fixes #46.
2016-12-16 14:24:46 -08:00
medmunds
52596394cc Support Postmark delivery event webhook.
Add support for Postmark's recently-released [delivery tracking webhook] to Anymail's normailized status event handling. The existing Anymail tracking webhook URL can be copied to "Delivery webhook" in your Postmark outbound server settings.

Closes #45.
2016-12-15 14:24:10 -08:00
medmunds
d0596d100b Raise error for invalidly-formatted email addresses.
A message's `from_email` and each address in its `to`, `cc`, and `bcc` lists must contain exactly one email address. Previous code would silently ignore additional addresses, leading to unusual behavior. Now, raises new `AnymailInvalidAddress` exception.

Example: `from_email='Widgets, Inc. <widgets@example.com>'` is invalid: it needs double-quotes around the "Widgets, Inc." display-name portion. In earlier versions, this probably would have sent the message from something like "From: Widgets <@localhost>". Now, it will raise an exception.

**Potentially-breaking change:** If your code is using an unquoted display-name containing a comma in an email address, it will now raise an error. In earlier versions, this may have appeared to succeed, but was almost certainly not doing what you intended.

Fixes #44.
2016-12-15 13:57:49 -08:00
medmunds
4ca39a976f Postmark: handle Reply-To in EmailMessage headers
Move 'Reply-To' header into dedicated Postmark API param

Fixes #39
2016-11-01 12:12:21 -07:00
Seb Bacon
f0589e3338 Compatibility with earlier Python 2.7 versions
Compatibility with Python 2.7 versions older than 2.7.7

* Use Django's constant_time_compare method
* Include sparkpost in test requirements
* Don't use non-public `EnvironmentVarGuard` in tests

Fixes #41
2016-11-01 11:24:51 -07:00
medmunds
7248c3441e Postmark: add track_clicks support
Closes #38
2016-10-23 10:54:59 -07:00
medmunds
a1380b82f3 SendGrid: force empty text and html to " " with template_id
Work around an unexpected limitation in SendGrid template
rendering, where template text or html bodies are omitted
if the supplied message text or html is "". Changing empty
string to " " works around the issue.

https://sendgrid.com/docs/API_Reference/Web_API_v3/Transactional_Templates/smtpapi.html#-Text-or-HTML-Templates

Closes #32
2016-10-13 15:15:37 -07:00
medmunds
771ed513b2 Tests: switch to explicit u"unicode" literals
Drop `from __future__ import unicode_literals`;
it was there for Python 3.2 compatibility (which
Anymail doesn't support). Ensures tests use normal
strs in Python 2.x.
2016-10-13 14:37:08 -07:00
medmunds
fb21c0d25b Mailgun: Add MAILGUN_SENDER_DOMAIN setting
Allow custom MAILGUN_SENDER_DOMAIN in Anymail
settings. (Replaces need to use global esp_extra.)

Improve docs to cover cases where this is needed.

(esp_extra sender_domain is still supported for
overriding individual messages.)

Fixes #26.
2016-08-03 14:19:35 -07:00
medmunds
f95be248ec SparkPost: remove empty content params with template_id
When using a stored template, SparkPost disallows
subject, text, and html. Django's EmailMessage default
empty strings are enough to provoke "Both content
object and template_id are specified" from SparkPost,
so remove them (if empty) when using stored templates.

Update docs and tests; add integration test for template_id.

Fixes #24
2016-06-24 12:13:32 -07:00