diff --git a/README.md b/README.md index 5fe9678..7008b9d 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ legacy Python 3.6 web stack: malformed `From`/`Subject` headers, nested gettext catalogs. It also normalizes non-ASCII sender addresses and backports the byte-oriented delivery used by newer `mailman-hyperkitty` releases, so surrogate-escaped message bytes do not pass through Requests as Unicode text. +Named MIME parts whose content type is missing or empty are extracted as binary +attachments instead of being appended to the plain-text message body. As an +additional safeguard, the NUL filtering introduced by django-mailman3 1.3.6 is +backported for scrubbed text fields only; binary attachments remain unchanged. Enable it only for the known package versions: ```yaml diff --git a/templates/mailman-legacy-hyperkitty-compatibility.py.j2 b/templates/mailman-legacy-hyperkitty-compatibility.py.j2 index a355932..de351ca 100644 --- a/templates/mailman-legacy-hyperkitty-compatibility.py.j2 +++ b/templates/mailman-legacy-hyperkitty-compatibility.py.j2 @@ -113,6 +113,42 @@ SCRUB_NEW = ''' if ctype == 'message/rfc822': decodedpayload = str(payload) ''' +SCRUB_NAMED_TEXT_ATTACHMENT_OLD = ''' if ctype == 'text/plain': + if part.is_attachment(): + attachments.append(self._parse_attachment(part, part_num)) + part.set_content('\\n') +''' +SCRUB_NAMED_TEXT_ATTACHMENT_NEW = ''' if ctype == 'text/plain': + filename = part.get_filename() + if part.is_attachment() or filename: + # A missing or empty MIME type defaults to text/plain. If + # a filename is present, keep binary data out of the body. + raw_content_type = part.get('Content-Type', '') + declared_type = str(raw_content_type).split(';', 1)[0].strip() + if filename and not declared_type: + part.set_type('application/octet-stream') + attachments.append(self._parse_attachment(part, part_num)) + part.set_content('\\n') +''' + +SCRUB_NUL_ONE_PART_OLD = ''' if next_part_match: + result = result[0:next_part_match.start(0)] + return result +''' +SCRUB_NUL_ONE_PART_NEW = ''' if next_part_match: + result = result[0:next_part_match.start(0)] + # Backport from django-mailman3 1.3.6: PostgreSQL text fields cannot + # contain NUL characters. + return re.sub('\\x00', '', result) +''' + +SCRUB_NUL_MULTIPART_OLD = ''' return '\\n'.join(text) +''' +SCRUB_NUL_MULTIPART_NEW = ''' # Backport from django-mailman3 1.3.6: PostgreSQL text fields + # cannot contain NUL characters. + return re.sub('\\x00', '', '\\n'.join(text)) +''' + MAILMAN_HYPERKITTY_SEND_OLD = ''' message_text = msg.as_string() except (MessageError, KeyError) as error: ''' @@ -185,13 +221,26 @@ def patch_hyperkitty(source): def patch_scrubber(source): - if SCRUB_NEW in source: - if SCRUB_OLD in source: - raise RuntimeError('django-mailman3 compatibility patch is ambiguous') - return source, False - return replace_once( - source, SCRUB_OLD, SCRUB_NEW, - 'django-mailman3 message/rfc822 handling'), True + updated = source + changed = False + replacements = ( + (SCRUB_OLD, SCRUB_NEW, 'django-mailman3 message/rfc822 handling'), + (SCRUB_NAMED_TEXT_ATTACHMENT_OLD, SCRUB_NAMED_TEXT_ATTACHMENT_NEW, + 'django-mailman3 named text attachment handling'), + (SCRUB_NUL_ONE_PART_OLD, SCRUB_NUL_ONE_PART_NEW, + 'django-mailman3 single-part NUL handling'), + (SCRUB_NUL_MULTIPART_OLD, SCRUB_NUL_MULTIPART_NEW, + 'django-mailman3 multipart NUL handling'), + ) + for old, new, description in replacements: + if new in updated: + if old in updated: + raise RuntimeError( + '{} compatibility patch is ambiguous'.format(description)) + continue + updated = replace_once(updated, old, new, description) + changed = True + return updated, changed def patch_mailman_hyperkitty(source):