diff --git a/src/documents/mail.py b/src/documents/mail.py new file mode 100644 index 000000000..5183b1bae --- /dev/null +++ b/src/documents/mail.py @@ -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() diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index fd17bbf74..1d21b962b 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -12,7 +12,6 @@ from celery.signals import task_postrun from celery.signals import task_prerun from django.conf import settings from django.contrib.auth.models import User -from django.core.mail import EmailMessage from django.db import DatabaseError from django.db import close_old_connections from django.db import models @@ -30,6 +29,7 @@ from documents.data_models import DocumentMetadataOverrides from documents.file_handling import create_source_path_directory from documents.file_handling import delete_empty_directories from documents.file_handling import generate_unique_filename +from documents.mail import send_email from documents.models import Correspondent from documents.models import CustomField from documents.models import CustomFieldInstance @@ -972,17 +972,13 @@ def run_workflows( doc_url, ) try: - email = EmailMessage( + n_messages = send_email( subject=subject, body=body, 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): - document.refresh_from_db() - email.attach_file(original_file) - n_messages = email.send() logger.debug( f"Sent {n_messages} notification email(s) to {action.email.to}", extra={"group": logging_group},