<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.",
             )