Commit Graph

274 Commits

Author SHA1 Message Date
medmunds
b72e0b0274 Internal: Hoist RequestsPayload.serialize_json to BasePayload
(Non-Requests payloads sometimes want to serialize json, too.)
2018-03-24 10:09:46 -07:00
medmunds
3d27e3fe6b Inbound: decode Unicode and other non-ASCII email headers on Python 2
In AnymailInboundMessage, work around Python 2 email.parser.Parser's
lack of handling for RFC2047-encoded email headers. (The Python 3 email
package already decodes these automatically.)

Improves inbound handling on Python 2 for all ESPs that provide raw
MIME email or raw headers with inbound events. (Mailgun, Mandrill,
SendGrid, SparkPost.)
2018-03-24 10:03:18 -07:00
medmunds
70094cf3bc Inbound: correctly parse long (folded) headers in raw MIME messages
Work around Python 2 email.parser.Parser bug handling RFC5322 folded
headers. Fixes problems where long headers in inbound mail (e.g.,
Subject) get truncated or have unexpected spaces.

This change also updates AnymailInboundMessage.parse_raw_mime to use
the improved "default" email.policy on Python 3 (rather than the
default "compat32" policy). This likely fixes several other parsing
bugs that will still affect code running on Python 2.

Improves inbound parsing for all ESPs that provide raw MIME email.
(Mailgun, Mandrill, SendGrid, SparkPost)
2018-03-23 19:00:42 -07:00
medmunds
70f5e3e84c Release 2.0 2018-03-08 14:59:18 -08:00
Mike Edmunds
d93a66326c SendinBlue: implement tracking webhooks
Completes #84
2018-03-08 13:47:46 -08:00
medmunds
e3f986df8f SendinBlue: fix template from_email checking
And add some tests for template unsupported feature warnings,
recipients in non-template sends.
2018-03-01 18:14:02 -08:00
medmunds
9478bf5958 [Breaking] Webhooks: disallow deprecated WEBHOOK_AUTHORIZATION setting
Drop support for the WEBHOOK_AUTHORIZATION setting deprecated in v1.4.
Only the WEBHOOK_SECRET replacement is allowed now.

Most Django management commands will now issue a system check error
if the old name is still used in settings.py
2018-03-01 14:11:15 -08:00
medmunds
deea8c5d5b Setup: add universal wheel; update metadata; clean up setup.py
* Follow current setup.py recommendations from the pypa sample project
  (utf-8 encoding on file reads, ensure files are read relative to
  own location)
* Add/update some missing classifiers and other metadata
* Read _version.py constants into an isolated dict (rather than
  the global setup.py context)
* Add setup.cfg specifying universal bdist_wheel
2018-03-01 13:14:05 -08:00
medmunds
06c7077e37 Fix: flag extra_headers["To"] as unsupported
Django's SMTP EmailBackend allows spoofing the To header by setting
`message.extra_headers["To"]`` different from `message.to`.

No current Anymail ESP supports this. Treat extra_headers["To"] as
an unsupported ESP feature, to flag attempts to use it.

Also document Anymail's special header handling that replicates
Django's SMTP EmailBackend behavior.
2018-02-27 13:43:58 -08:00
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
fffd762f56 Release 1.4 2018-02-08 11:44:23 -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
medmunds
4d34a181b6 Add Django AppConfig 2018-02-07 12:47:06 -08:00
medmunds
3468b12cc0 Release 1.3 2018-02-02 11:51:25 -08:00
medmunds
db586ede1f Security: prevent timing attack on WEBHOOK_AUTHORIZATION secret
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.
2018-02-02 11:41:14 -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
7029298b93 Release 1.2 2017-11-02 14:08:18 -07: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
930753e4b6 Release 1.1 2017-10-28 11:45:31 -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
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
c657d883a8 Release 1.0 2017-09-17 17:02:51 -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
168d46a254 Release 1.0.rc0 2017-09-08 17:29: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
872b67ee57 Open development for 0.12 2017-07-13 16:43:14 -07:00
medmunds
7c6072afc9 Release 0.11 2017-07-13 16:28:21 -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
1cc357836f Open development for 0.11 2017-05-22 12:09:19 -07:00
medmunds
6cc7ba38b0 Release 0.10 2017-05-22 11:49:30 -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
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
Luke Plant
7d7448011b Fixed crasher when sending rfc822 messages as attachments. (#59) 2017-04-15 11:45:32 -07:00
medmunds
7b010ab224 Open development for 0.10 2017-04-04 12:18:24 -07:00
medmunds
ac94aa5b27 Release 0.9 2017-04-04 12:11:07 -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
23bbc3284f **Mandrill, Postmark:** Normalize soft-bounce webhook
events to event_type 'bounced' (rather than 'deferred').

Fixes #56
2017-03-16 15:36:06 -07:00
medmunds
68c7f6ccf3 Open development for 0.9 2017-02-01 16:09:10 -08:00
medmunds
eb3ce34d26 Release 0.8 2017-02-01 15:55:55 -08:00