Move send email to its own module, handle non-ascii in file name

This commit is contained in:
shamoon 2025-01-16 08:38:05 -08:00
parent eb60f3917f
commit 17ea6a458e
2 changed files with 65 additions and 27 deletions

61
src/documents/mail.py Normal file
View File

@ -0,0 +1,61 @@
from email.encoders import encode_base64
from email.mime.base import MIMEBase
from pathlib import Path
from urllib.parse import quote
from django.conf import settings
from django.core.mail import EmailMessage
from filelock import FileLock
def send_email(
subject: str,
body: str,
to: list[str],
attachment: Path | None = None,
attachment_mime_type: str | None = None,
) -> int:
"""
Send an email with an optional attachment.
TODO: re-evaluate this pending https://code.djangoproject.com/ticket/35581 / https://github.com/django/django/pull/18966
"""
email = EmailMessage(
subject=subject,
body=body,
to=to,
)
if attachment:
# Something could be renaming the file concurrently so it can't be attached
with FileLock(settings.MEDIA_LOCK), attachment.open("rb") as f:
file_content = f.read()
main_type, sub_type = (
attachment_mime_type.split("/", 1)
if attachment_mime_type
else ("application", "octet-stream")
)
mime_part = MIMEBase(main_type, sub_type)
mime_part.set_payload(file_content)
encode_base64(mime_part)
# see https://github.com/stumpylog/tika-client/blob/f65a2b792fc3cf15b9b119501bba9bddfac15fcc/src/tika_client/_base.py#L46-L57
try:
attachment.name.encode("ascii")
except UnicodeEncodeError:
filename_safed = attachment.name.encode("ascii", "ignore").decode(
"ascii",
)
filepath_quoted = quote(attachment.name, encoding="utf-8")
mime_part.add_header(
"Content-Disposition",
f"attachment; filename={filename_safed}; filename*=UTF-8''{filepath_quoted}",
)
else:
mime_part.add_header(
"Content-Disposition",
f"attachment; filename={attachment.name}",
)
email.attach(mime_part)
return email.send()

View File

@ -1,9 +1,6 @@
import logging import logging
import os import os
import shutil import shutil
from email.encoders import encode_base64
from email.mime.base import MIMEBase
from email.utils import encode_rfc2231
from pathlib import Path from pathlib import Path
import httpx import httpx
@ -15,7 +12,6 @@ from celery.signals import task_postrun
from celery.signals import task_prerun from celery.signals import task_prerun
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.mail import EmailMessage
from django.db import DatabaseError from django.db import DatabaseError
from django.db import close_old_connections from django.db import close_old_connections
from django.db import models from django.db import models
@ -33,6 +29,7 @@ from documents.data_models import DocumentMetadataOverrides
from documents.file_handling import create_source_path_directory from documents.file_handling import create_source_path_directory
from documents.file_handling import delete_empty_directories from documents.file_handling import delete_empty_directories
from documents.file_handling import generate_unique_filename from documents.file_handling import generate_unique_filename
from documents.mail import send_email
from documents.models import Correspondent from documents.models import Correspondent
from documents.models import CustomField from documents.models import CustomField
from documents.models import CustomFieldInstance from documents.models import CustomFieldInstance
@ -975,33 +972,13 @@ def run_workflows(
doc_url, doc_url,
) )
try: try:
email = EmailMessage( n_messages = send_email(
subject=subject, subject=subject,
body=body, body=body,
to=action.email.to.split(","), to=action.email.to.split(","),
attachment=original_file if action.email.include_document else None,
attachment_mime_type=document.mime_type,
) )
if action.email.include_document:
# Something could be renaming the file concurrently so it can't be attached
with FileLock(settings.MEDIA_LOCK), open(original_file, "rb") as f:
file_content = f.read()
main_type, sub_type = (
document.mime_type.split("/", 1)
if document.mime_type
else ("application", "octet-stream")
)
mime_part = MIMEBase(main_type, sub_type)
mime_part.set_payload(file_content)
encode_base64(mime_part)
mime_part.add_header(
"Content-Disposition",
f'attachment; filename="{encode_rfc2231(str(original_file.name))}"',
)
email.attach(mime_part)
n_messages = email.send()
logger.debug( logger.debug(
f"Sent {n_messages} notification email(s) to {action.email.to}", f"Sent {n_messages} notification email(s) to {action.email.to}",
extra={"group": logging_group}, extra={"group": logging_group},