Anymail's webhook validation was vulnerable to a timing attack.
An attacker could have used this to recover your WEBHOOK_AUTHORIZATION
shared secret, potentially allowing them to post fabricated or malicious
email tracking events to your app.
There have not been any reports of attempted exploit in the wild. (The
vulnerability was discovered through code review.) Attempts would be
visible in http logs as a very large number of 400 responses on
Anymail's webhook urls, or in Python error monitoring as a very large
number of AnymailWebhookValidationFailure exceptions.
If you are using Anymail's webhooks, you should upgrade to this release.
In addition, you may want to rotate to a new WEBHOOK_AUTHORIZATION
secret ([docs](http://anymail.readthedocs.io/en/stable/tips/securing_webhooks/#use-a-shared-authorization-secret)),
particularly if your logs indicate attempted exploit.
* 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
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.
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
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.)
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.
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.)
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
* Remove "pre-1.0" warnings in docs, readme
* Jump trove classifer from pre-alpha all the way up to stable
(arguably this should have been "beta" for the past several months)
* 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.