Skip to content

Commit 1cbe9d8

Browse files
committed
Inbound: sanitize CRNL in ESP-parsed attachment filenames
Fixes #465.
1 parent 4626462 commit 1cbe9d8

3 files changed

Lines changed: 26 additions & 0 deletions

File tree

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ Features
4141

4242
* **Resend:** Add support for inbound email. (Thanks to `@btimby`_.)
4343

44+
Fixes
45+
~~~~~
46+
47+
* **Inbound:** Convert carriage return and line feed characters in attachment
48+
filenames to spaces for ESPs that parse them from invalid headers (e.g.,
49+
Postmark). (Thanks to `@BHSPitMonkey`_ for reporting the issue and providing
50+
an example.)
51+
4452

4553
v14.0
4654
-----
@@ -1959,6 +1967,7 @@ Features
19591967
.. _@b0d0nne11: https://github.com/b0d0nne11
19601968
.. _@blag: https://github.com/blag
19611969
.. _@btimby: https://github.com/btimby
1970+
.. _@BHSPitMonkey: https://github.com/BHSPitMonkey
19621971
.. _@cahna: https://github.com/cahna
19631972
.. _@calvin: https://github.com/calvin
19641973
.. _@carrerasrodrigo: https://github.com/carrerasrodrigo

anymail/inbound.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import warnings
23
from base64 import b64decode
34
from email.message import EmailMessage
@@ -393,6 +394,8 @@ def construct_attachment(
393394
)
394395

395396
if filename is not None:
397+
# Replace CR/NL sequences with a space. See issue #465.
398+
filename = re.sub(r"[\r\n]+", " ", filename)
396399
part.set_param("name", filename, header="Content-Type")
397400
part.set_param("filename", filename, header="Content-Disposition")
398401

tests/test_inbound.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,20 @@ def test_construct_attachment_unicode_filename(self):
203203
self.assertTrue(att.is_inline())
204204
self.assertEqual(att.get_content_text(), "Unicode ✓")
205205

206+
def test_construct_attachment_crnl_in_filename(self):
207+
# Issue #465: Postmark (at least) decodes newlines out of this header:
208+
# Content-Disposition: inline;
209+
# filename="=?iso-8859-1?Q?Outlook-Icon=0A=0ADesc.png?="
210+
# leading to the ValueError "Header values may not contain linefeed or
211+
# carriage return characters" when Anymail tries to use that filename
212+
# in Python's EmailMessage.
213+
att = AnymailInboundMessage.construct_attachment(
214+
content_type="text/plain",
215+
content="content",
216+
filename="Outlook-Icon\n\nDec.png",
217+
)
218+
self.assertEqual(att.get_filename(), "Outlook-Icon Dec.png")
219+
206220
def test_parse_raw_mime(self):
207221
# (we're not trying to exhaustively test email.parser MIME handling here;
208222
# just that AnymailInboundMessage.parse_raw_mime calls it correctly)

0 commit comments

Comments
 (0)