<div class="modal-header">
- <h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
+ <h4 class="modal-title" id="modal-basic-title" i18n>{
+ documentIds.length,
+ plural,
+ =1 {Email Document} other {Email {{documentIds.length}} Documents}
+ }</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
</div>
<div class="modal-body">
<input class="form-check-input mt-0 me-2" type="checkbox" role="switch" id="useArchiveVersion" [disabled]="!hasArchiveVersion" [(ngModel)]="useArchiveVersion">
<label class="form-check-label w-100 text-start" for="useArchiveVersion" i18n>Use archive version</label>
</div>
- <button type="submit" class="btn btn-outline-primary" (click)="emailDocument()" [disabled]="loading || emailAddress.length === 0 || emailMessage.length === 0 || emailSubject.length === 0">
+ <button type="submit" class="btn btn-outline-primary" (click)="emailDocuments()" [disabled]="loading || emailAddress.length === 0 || emailMessage.length === 0 || emailSubject.length === 0">
@if (loading) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
}
<ng-container i18n>Send email</ng-container>
</button>
</div>
+ <div class="text-light fst-italic small mt-2">
+ <ng-container i18n>Some email servers may reject messages with large attachments.</ng-container>
+ </div>
</div>
documentService = TestBed.inject(DocumentService)
toastService = TestBed.inject(ToastService)
component = fixture.componentInstance
+ component.documentIds = [1]
fixture.detectChanges()
})
it('should set hasArchiveVersion and useArchiveVersion', () => {
expect(component.hasArchiveVersion).toBeTruthy()
+ expect(component.useArchiveVersion).toBeTruthy()
+
component.hasArchiveVersion = false
expect(component.hasArchiveVersion).toBeFalsy()
expect(component.useArchiveVersion).toBeFalsy()
})
- it('should support sending document via email, showing error if needed', () => {
+ it('should support sending single document via email, showing error if needed', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastSuccessSpy = jest.spyOn(toastService, 'showInfo')
+ component.documentIds = [1]
component.emailAddress = 'hello@paperless-ngx.com'
component.emailSubject = 'Hello'
component.emailMessage = 'World'
jest
- .spyOn(documentService, 'emailDocument')
+ .spyOn(documentService, 'emailDocuments')
.mockReturnValue(throwError(() => new Error('Unable to email document')))
- component.emailDocument()
- expect(toastErrorSpy).toHaveBeenCalled()
+ component.emailDocuments()
+ expect(toastErrorSpy).toHaveBeenCalledWith(
+ 'Error emailing document',
+ expect.any(Error)
+ )
+
+ jest.spyOn(documentService, 'emailDocuments').mockReturnValue(of(true))
+ component.emailDocuments()
+ expect(toastSuccessSpy).toHaveBeenCalledWith('Email sent')
+ })
+
+ it('should support sending multiple documents via email, showing appropriate messages', () => {
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastSuccessSpy = jest.spyOn(toastService, 'showInfo')
+ component.documentIds = [1, 2, 3]
+ component.emailAddress = 'hello@paperless-ngx.com'
+ component.emailSubject = 'Hello'
+ component.emailMessage = 'World'
+ jest
+ .spyOn(documentService, 'emailDocuments')
+ .mockReturnValue(throwError(() => new Error('Unable to email documents')))
+ component.emailDocuments()
+ expect(toastErrorSpy).toHaveBeenCalledWith(
+ 'Error emailing documents',
+ expect.any(Error)
+ )
- jest.spyOn(documentService, 'emailDocument').mockReturnValue(of(true))
- component.emailDocument()
- expect(toastSuccessSpy).toHaveBeenCalled()
+ jest.spyOn(documentService, 'emailDocuments').mockReturnValue(of(true))
+ component.emailDocuments()
+ expect(toastSuccessSpy).toHaveBeenCalledWith('Email sent')
})
it('should close the dialog', () => {
private toastService = inject(ToastService)
@Input()
- title = $localize`Email Document`
-
- @Input()
- documentId: number
+ documentIds: number[]
private _hasArchiveVersion: boolean = true
this.loading = false
}
- public emailDocument() {
+ public emailDocuments() {
this.loading = true
this.documentService
- .emailDocument(
- this.documentId,
+ .emailDocuments(
+ this.documentIds,
this.emailAddress,
this.emailSubject,
this.emailMessage,
},
error: (e) => {
this.loading = false
- this.toastService.showError($localize`Error emailing document`, e)
+ const errorMessage =
+ this.documentIds.length > 1
+ ? $localize`Error emailing documents`
+ : $localize`Error emailing document`
+ this.toastService.showError(errorMessage, e)
},
})
}
const modal = this.modalService.open(EmailDocumentDialogComponent, {
backdrop: 'static',
})
- modal.componentInstance.documentId = this.document.id
+ modal.componentInstance.documentIds = [this.document.id]
modal.componentInstance.hasArchiveVersion =
!!this.document?.archived_file_name
}
<button ngbDropdownItem (click)="mergeSelected()" [disabled]="!userCanAdd || list.selected.size < 2">
<i-bs name="journals"></i-bs> <ng-container i18n>Merge</ng-container>
</button>
+ <button ngbDropdownItem (click)="emailSelected()" [disabled]="!userCanEdit">
+ <i-bs name="envelope"></i-bs> <ng-container i18n>Email</ng-container>
+ </button>
</div>
</div>
</div>
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
+import { EmailDocumentDialogComponent } from '../../common/email-document-dialog/email-document-dialog.component'
import {
ChangedItems,
FilterableDropdownComponent,
)
})
}
+
+ emailSelected() {
+ const allHaveArchiveVersion = this.list.documents
+ .filter((d) => this.list.selected.has(d.id))
+ .every((doc) => !!doc.archived_file_name)
+
+ const modal = this.modalService.open(EmailDocumentDialogComponent, {
+ backdrop: 'static',
+ })
+ modal.componentInstance.documentIds = Array.from(this.list.selected)
+ modal.componentInstance.hasArchiveVersion = allHaveArchiveVersion
+ }
}
it('should call appropriate api endpoint for email document', () => {
subscription = service
- .emailDocument(
- documents[0].id,
+ .emailDocuments(
+ [documents[0].id],
'hello@paperless-ngx.com',
'hello',
'world',
true
)
.subscribe()
- httpTestingController.expectOne(
- `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/email/`
- )
+ httpTestingController.expectOne(`${environment.apiBaseUrl}${endpoint}/email/`)
})
afterEach(() => {
return this._searchQuery
}
- emailDocument(
- documentId: number,
+ emailDocuments(
+ documentIds: number[],
addresses: string,
subject: string,
message: string,
useArchiveVersion: boolean
): Observable<any> {
- return this.http.post(this.getResourceUrl(documentId, 'email'), {
+ return this.http.post(this.getResourceUrl(null, 'email'), {
+ documents: documentIds,
addresses: addresses,
subject: subject,
message: message,
subject: str,
body: str,
to: list[str],
- attachment: Path | None = None,
- attachment_mime_type: str | None = None,
+ attachments: list[tuple[Path, str]],
) -> int:
"""
- Send an email with an optional attachment.
+ Send an email with attachments.
+
+ Args:
+ subject: Email subject
+ body: Email body text
+ to: List of recipient email addresses
+ attachments: List of (path, mime_type) tuples for attachments (the list may be empty)
+
+ Returns:
+ Number of emails sent
+
TODO: re-evaluate this pending https://code.djangoproject.com/ticket/35581 / https://github.com/django/django/pull/18966
"""
email = EmailMessage(
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:
- content = f.read()
- if attachment_mime_type == "message/rfc822":
- # See https://forum.djangoproject.com/t/using-emailmessage-with-an-attached-email-file-crashes-due-to-non-ascii/37981
- content = message_from_bytes(f.read())
-
- email.attach(
- filename=attachment.name,
- content=content,
- mimetype=attachment_mime_type,
- )
+
+ # Something could be renaming the file concurrently so it can't be attached
+ with FileLock(settings.MEDIA_LOCK):
+ for attachment_path, mime_type in attachments:
+ with attachment_path.open("rb") as f:
+ content = f.read()
+ if mime_type == "message/rfc822":
+ # See https://forum.djangoproject.com/t/using-emailmessage-with-an-attached-email-file-crashes-due-to-non-ascii/37981
+ content = message_from_bytes(content)
+
+ email.attach(
+ filename=attachment_path.name,
+ content=content,
+ mimetype=mime_type,
+ )
+
return email.send()
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import DecimalValidator
+from django.core.validators import EmailValidator
from django.core.validators import MaxLengthValidator
from django.core.validators import RegexValidator
from django.core.validators import integer_validator
}[compression]
+class EmailSerializer(DocumentListSerializer):
+ addresses = serializers.CharField(
+ required=True,
+ label="Email addresses",
+ help_text="Comma-separated email addresses",
+ )
+
+ subject = serializers.CharField(
+ required=True,
+ label="Email subject",
+ )
+
+ message = serializers.CharField(
+ required=True,
+ label="Email message",
+ )
+
+ use_archive_version = serializers.BooleanField(
+ default=True,
+ label="Use archive version",
+ help_text="Use archive version of documents if available",
+ )
+
+ def validate_addresses(self, addresses):
+ address_list = [addr.strip() for addr in addresses.split(",")]
+ if not address_list:
+ raise serializers.ValidationError("At least one email address is required")
+
+ email_validator = EmailValidator()
+ try:
+ for address in address_list:
+ email_validator(address)
+ except ValidationError:
+ raise serializers.ValidationError(f"Invalid email address: {address}")
+
+ return ",".join(address_list)
+
+ def validate_documents(self, documents):
+ super().validate_documents(documents)
+ if not documents:
+ raise serializers.ValidationError("At least one document is required")
+
+ return documents
+
+
class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer):
class Meta:
model = StoragePath
else ""
)
try:
+ attachments = []
+ if action.email.include_document and original_file:
+ attachments = [(original_file, document.mime_type)]
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,
+ attachments=attachments,
)
logger.debug(
f"Sent {n_messages} notification email(s) to {action.email.to}",
"message": "hello",
},
)
- self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
+ self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
resp = self.client.post(
f"/api/documents/{doc.pk}/email/",
--- /dev/null
+import json
+import shutil
+from unittest import mock
+
+from django.contrib.auth.models import Permission
+from django.contrib.auth.models import User
+from django.core import mail
+from django.test import override_settings
+from rest_framework import status
+from rest_framework.test import APITestCase
+
+from documents.models import Document
+from documents.tests.utils import DirectoriesMixin
+from documents.tests.utils import SampleDirMixin
+
+
+class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
+ ENDPOINT = "/api/documents/email/"
+
+ def setUp(self):
+ super().setUp()
+
+ self.user = User.objects.create_superuser(username="temp_admin")
+ self.client.force_authenticate(user=self.user)
+
+ self.doc1 = Document.objects.create(
+ title="test1",
+ mime_type="application/pdf",
+ content="this is document 1",
+ checksum="1",
+ filename="test1.pdf",
+ archive_checksum="A1",
+ archive_filename="archive1.pdf",
+ )
+ self.doc2 = Document.objects.create(
+ title="test2",
+ mime_type="application/pdf",
+ content="this is document 2",
+ checksum="2",
+ filename="test2.pdf",
+ )
+
+ # Copy sample files to document paths
+ shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc1.archive_path)
+ shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc1.source_path)
+ shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc2.source_path)
+
+ @override_settings(
+ EMAIL_ENABLED=True,
+ EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
+ )
+ def test_email_success(self):
+ """
+ GIVEN:
+ - Multiple existing documents
+ WHEN:
+ - API request is made to bulk email documents
+ THEN:
+ - Email is sent with all documents attached
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk, self.doc2.pk],
+ "addresses": "hello@paperless-ngx.com,test@example.com",
+ "subject": "Bulk email test",
+ "message": "Here are your documents",
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["message"], "Email sent")
+ self.assertEqual(len(mail.outbox), 1)
+
+ email = mail.outbox[0]
+ self.assertEqual(email.to, ["hello@paperless-ngx.com", "test@example.com"])
+ self.assertEqual(email.subject, "Bulk email test")
+ self.assertEqual(email.body, "Here are your documents")
+ self.assertEqual(len(email.attachments), 2)
+
+ # Check attachment names (should default to archive version for doc1, original for doc2)
+ attachment_names = [att[0] for att in email.attachments]
+ self.assertIn("archive1.pdf", attachment_names)
+ self.assertIn("test2.pdf", attachment_names)
+
+ @override_settings(
+ EMAIL_ENABLED=True,
+ EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
+ )
+ def test_email_use_original_version(self):
+ """
+ GIVEN:
+ - Documents with archive versions
+ WHEN:
+ - API request is made to bulk email with use_archive_version=False
+ THEN:
+ - Original files are attached instead of archive versions
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk],
+ "addresses": "test@example.com",
+ "subject": "Test",
+ "message": "Test message",
+ "use_archive_version": False,
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].attachments[0][0], "test1.pdf")
+
+ def test_email_missing_required_fields(self):
+ """
+ GIVEN:
+ - Request with missing required fields
+ WHEN:
+ - API request is made to bulk email endpoint
+ THEN:
+ - Bad request response is returned
+ """
+ # Missing addresses
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk],
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # Missing subject
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk],
+ "addresses": "test@example.com",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # Missing message
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk],
+ "addresses": "test@example.com",
+ "subject": "Test",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # Missing documents
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "addresses": "test@example.com",
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_email_empty_document_list(self):
+ """
+ GIVEN:
+ - Request with empty document list
+ WHEN:
+ - API request is made to bulk email endpoint
+ THEN:
+ - Bad request response is returned
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [],
+ "addresses": "test@example.com",
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_email_invalid_document_id(self):
+ """
+ GIVEN:
+ - Request with non-existent document ID
+ WHEN:
+ - API request is made to bulk email endpoint
+ THEN:
+ - Bad request response is returned
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [999],
+ "addresses": "test@example.com",
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_email_invalid_email_address(self):
+ """
+ GIVEN:
+ - Request with invalid email address
+ WHEN:
+ - API request is made to bulk email endpoint
+ THEN:
+ - Bad request response is returned
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk],
+ "addresses": "invalid-email",
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ # Test multiple addresses with one invalid
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk],
+ "addresses": "valid@example.com,invalid-email",
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_email_insufficient_permissions(self):
+ """
+ GIVEN:
+ - User without permissions to view document
+ WHEN:
+ - API request is made to bulk email documents
+ THEN:
+ - Forbidden response is returned
+ """
+ user1 = User.objects.create_user(username="test1")
+ user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))
+
+ doc_owned = Document.objects.create(
+ title="owned_doc",
+ mime_type="application/pdf",
+ checksum="owned",
+ owner=self.user,
+ )
+
+ self.client.force_authenticate(user1)
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk, doc_owned.pk],
+ "addresses": "test@example.com",
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ @mock.patch(
+ "django.core.mail.message.EmailMessage.send",
+ side_effect=Exception("Email error"),
+ )
+ def test_email_send_error(self, mocked_send):
+ """
+ GIVEN:
+ - Existing documents
+ WHEN:
+ - API request is made to bulk email and error occurs during email send
+ THEN:
+ - Server error response is returned
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "documents": [self.doc1.pk],
+ "addresses": "test@example.com",
+ "subject": "Test",
+ "message": "Test message",
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
+ self.assertIn("Error emailing documents", response.content.decode())
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from drf_spectacular.utils import extend_schema
+from drf_spectacular.utils import extend_schema_serializer
from drf_spectacular.utils import extend_schema_view
from drf_spectacular.utils import inline_serializer
from guardian.utils import get_group_obj_perms_model
from documents.serialisers import DocumentListSerializer
from documents.serialisers import DocumentSerializer
from documents.serialisers import DocumentTypeSerializer
+from documents.serialisers import EmailSerializer
from documents.serialisers import NotesSerializer
from documents.serialisers import PostDocumentSerializer
from documents.serialisers import RunTaskViewSerializer
ordering_fields = ("name", "matching_algorithm", "match", "document_count")
+@extend_schema_serializer(
+ component_name="EmailDocumentRequest",
+ exclude_fields=("documents",),
+)
+class EmailDocumentDetailSchema(EmailSerializer):
+ pass
+
+
@extend_schema_view(
retrieve=extend_schema(
description="Retrieve a single document",
404: None,
},
),
- email=extend_schema(
+ email_document=extend_schema(
description="Email the document to one or more recipients as an attachment.",
- request=inline_serializer(
- name="EmailRequest",
- fields={
- "addresses": serializers.CharField(),
- "subject": serializers.CharField(),
- "message": serializers.CharField(),
- "use_archive_version": serializers.BooleanField(default=True),
- },
- ),
+ request=EmailDocumentDetailSchema,
responses={
200: inline_serializer(
- name="EmailResponse",
+ name="EmailDocumentResponse",
+ fields={"message": serializers.CharField()},
+ ),
+ 400: None,
+ 403: None,
+ 404: None,
+ 500: None,
+ },
+ deprecated=True,
+ ),
+ email_documents=extend_schema(
+ operation_id="email_documents",
+ description="Email one or more documents as attachments to one or more recipients.",
+ request=EmailSerializer,
+ responses={
+ 200: inline_serializer(
+ name="EmailDocumentsResponse",
fields={"message": serializers.CharField()},
),
400: None,
return Response(sorted(entries, key=lambda x: x["timestamp"], reverse=True))
- @action(methods=["post"], detail=True)
- def email(self, request, pk=None):
- try:
- doc = Document.objects.select_related("owner").get(pk=pk)
+ @action(methods=["post"], detail=True, url_path="email")
+ # TODO: deprecated as of 2.19, remove in future release
+ def email_document(self, request, pk=None):
+ request_data = request.data.copy()
+ request_data.setlist("documents", [pk])
+ return self.email_documents(request, data=request_data)
+
+ @action(
+ methods=["post"],
+ detail=False,
+ url_path="email",
+ serializer_class=EmailSerializer,
+ )
+ def email_documents(self, request, data=None):
+ serializer = EmailSerializer(data=data or request.data)
+ serializer.is_valid(raise_exception=True)
+
+ validated_data = serializer.validated_data
+ document_ids = validated_data.get("documents")
+ addresses = validated_data.get("addresses").split(",")
+ addresses = [addr.strip() for addr in addresses]
+ subject = validated_data.get("subject")
+ message = validated_data.get("message")
+ use_archive_version = validated_data.get("use_archive_version", True)
+
+ documents = Document.objects.select_related("owner").filter(pk__in=document_ids)
+ for document in documents:
if request.user is not None and not has_perms_owner_aware(
request.user,
"view_document",
- doc,
+ document,
):
return HttpResponseForbidden("Insufficient permissions")
- except Document.DoesNotExist:
- raise Http404
- try:
- if (
- "addresses" not in request.data
- or "subject" not in request.data
- or "message" not in request.data
- ):
- return HttpResponseBadRequest("Missing required fields")
-
- use_archive_version = request.data.get("use_archive_version", True)
-
- addresses = request.data.get("addresses").split(",")
- if not all(
- re.match(r"[^@]+@[^@]+\.[^@]+", address.strip())
- for address in addresses
- ):
- return HttpResponseBadRequest("Invalid email address found")
+ attachments = []
+ for doc in documents:
+ attachment_path = (
+ doc.archive_path
+ if use_archive_version and doc.has_archive_version
+ else doc.source_path
+ )
+ attachments.append((attachment_path, doc.mime_type))
+ try:
send_email(
- subject=request.data.get("subject"),
- body=request.data.get("message"),
+ subject=subject,
+ body=message,
to=addresses,
- attachment=(
- doc.archive_path
- if use_archive_version and doc.has_archive_version
- else doc.source_path
- ),
- attachment_mime_type=doc.mime_type,
+ attachments=attachments,
)
+
logger.debug(
- f"Sent document {doc.id} via email to {addresses}",
+ f"Sent documents {[doc.id for doc in documents]} via email to {addresses}",
)
return Response({"message": "Email sent"})
except Exception as e:
- logger.warning(f"An error occurred emailing document: {e!s}")
+ logger.warning(f"An error occurred emailing documents: {e!s}")
return HttpResponseServerError(
- "Error emailing document, check logs for more detail.",
+ "Error emailing documents, check logs for more detail.",
)