diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 7d42085c3..60f9f474b 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -2077,8 +2077,8 @@ 19 - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 37 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 69 src/app/components/document-detail/document-detail.component.html @@ -5082,8 +5082,8 @@ 58 - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 64 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 96 src/app/components/manage/management-list/management-list.component.html @@ -5543,8 +5543,8 @@ 155 - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 29 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 61 src/app/components/common/system-status-dialog/system-status-dialog.component.html @@ -5585,8 +5585,8 @@ 162 - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 40 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 72 @@ -5765,103 +5765,159 @@ 320 - - Share Links + + Share - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html 4 - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 65 + + + + Email document + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 10 + + + + Email address(es) + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 12 + + + + Subject + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 16 + + + + Message + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 20 + + + + Use archive version + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 26 + + + + Send email + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html 32 + + Share links + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 38 + + No existing links - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 9,11 - - - - Share - - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 33 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 41,43 Share archive version - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 47 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 79 Expires - src/app/components/common/share-links-dropdown/share-links-dropdown.component.html - 51 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.html + 83 1 day - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 25 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 18 - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 111 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 129 7 days - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 26 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 19 30 days - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 27 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 20 Never - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 28 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 21 Error retrieving links - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 92 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 110 days - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 111 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 129 Error deleting link - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 140 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 158 Error creating link - src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts - 168 + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 186 + + + + Email sent + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 211 + + + + Error emailing document + + src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts + 215 diff --git a/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.html b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.html new file mode 100644 index 000000000..7f9d92577 --- /dev/null +++ b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.html @@ -0,0 +1,104 @@ +
+ + +
diff --git a/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.scss b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.scss new file mode 100644 index 000000000..48743fe78 --- /dev/null +++ b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.scss @@ -0,0 +1,17 @@ +.share-document-dropdown { + min-width: 360px; + + .col { + min-width: 350px; + } + + @media screen and (min-width: 1024px) { + &.x2 { + width: 720px; + } + } +} + +.copied-badge { + right: 7.5em; +} diff --git a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.spec.ts b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.spec.ts similarity index 81% rename from src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.spec.ts rename to src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.spec.ts index b7b0305be..e02bfcfa8 100644 --- a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.spec.ts @@ -14,23 +14,30 @@ import { By } from '@angular/platform-browser' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { of, throwError } from 'rxjs' import { FileVersion, ShareLink } from 'src/app/data/share-link' +import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { PermissionsService } from 'src/app/services/permissions.service' +import { DocumentService } from 'src/app/services/rest/document.service' import { ShareLinkService } from 'src/app/services/rest/share-link.service' import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' -import { ShareLinksDropdownComponent } from './share-links-dropdown.component' +import { ShareDocumentDropdownComponent } from './share-document-dropdown.component' -describe('ShareLinksDropdownComponent', () => { - let component: ShareLinksDropdownComponent - let fixture: ComponentFixture +describe('ShareDocumentDropdownComponent', () => { + let component: ShareDocumentDropdownComponent + let fixture: ComponentFixture let shareLinkService: ShareLinkService + let documentService: DocumentService + let permissionsService: PermissionsService let toastService: ToastService let httpController: HttpTestingController let clipboard: Clipboard beforeEach(() => { TestBed.configureTestingModule({ + declarations: [], imports: [ - ShareLinksDropdownComponent, + ShareDocumentDropdownComponent, + IfPermissionsDirective, NgxBootstrapIconsModule.pick(allIcons), ], providers: [ @@ -39,12 +46,15 @@ describe('ShareLinksDropdownComponent', () => { ], }) - fixture = TestBed.createComponent(ShareLinksDropdownComponent) + fixture = TestBed.createComponent(ShareDocumentDropdownComponent) shareLinkService = TestBed.inject(ShareLinkService) + documentService = TestBed.inject(DocumentService) + permissionsService = TestBed.inject(PermissionsService) toastService = TestBed.inject(ToastService) httpController = TestBed.inject(HttpTestingController) clipboard = TestBed.inject(Clipboard) + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) component = fixture.componentInstance fixture.detectChanges() }) @@ -232,4 +242,21 @@ describe('ShareLinksDropdownComponent', () => { ] ).toBeTruthy() }) + + it('should support sending document via email, showing error if needed', () => { + const toastErrorSpy = jest.spyOn(toastService, 'showError') + const toastSuccessSpy = jest.spyOn(toastService, 'showInfo') + component.emailAddress = 'hello@paperless-ngx.com' + component.emailSubject = 'Hello' + component.emailMessage = 'World' + jest + .spyOn(documentService, 'emailDocument') + .mockReturnValue(throwError(() => new Error('Unable to email document'))) + component.emailDocument() + expect(toastErrorSpy).toHaveBeenCalled() + + jest.spyOn(documentService, 'emailDocument').mockReturnValue(of(true)) + component.emailDocument() + expect(toastSuccessSpy).toHaveBeenCalled() + }) }) diff --git a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts similarity index 65% rename from src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts rename to src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts index 5e65eed73..3f66436e3 100644 --- a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.ts +++ b/src-ui/src/app/components/common/share-document-dropdown/share-document-dropdown.component.ts @@ -5,33 +5,40 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { first } from 'rxjs' import { FileVersion, ShareLink } from 'src/app/data/share-link' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' +import { DocumentService } from 'src/app/services/rest/document.service' import { ShareLinkService } from 'src/app/services/rest/share-link.service' +import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { environment } from 'src/environments/environment' +import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' + +const EXPIRATION_OPTIONS = [ + { label: $localize`1 day`, value: 1 }, + { label: $localize`7 days`, value: 7 }, + { label: $localize`30 days`, value: 30 }, + { label: $localize`Never`, value: null }, +] @Component({ - selector: 'pngx-share-links-dropdown', - templateUrl: './share-links-dropdown.component.html', - styleUrls: ['./share-links-dropdown.component.scss'], + selector: 'pngx-share-document-dropdown', + templateUrl: './share-document-dropdown.component.html', + styleUrls: ['./share-document-dropdown.component.scss'], imports: [ + IfPermissionsDirective, FormsModule, ReactiveFormsModule, NgbDropdownModule, NgxBootstrapIconsModule, ], }) -export class ShareLinksDropdownComponent implements OnInit { - EXPIRATION_OPTIONS = [ - { label: $localize`1 day`, value: 1 }, - { label: $localize`7 days`, value: 7 }, - { label: $localize`30 days`, value: 30 }, - { label: $localize`Never`, value: null }, - ] - - @Input() - title = $localize`Share Links` - - _documentId: number +export class ShareDocumentDropdownComponent + extends ComponentWithPermissions + implements OnInit +{ + public EXPIRATION_OPTIONS = EXPIRATION_OPTIONS + private _documentId: number @Input() set documentId(id: number) { @@ -50,6 +57,7 @@ export class ShareLinksDropdownComponent implements OnInit { set hasArchiveVersion(value: boolean) { this._hasArchiveVersion = value this.useArchiveVersion = value + this.emailUseArchiveVersion = value } get hasArchiveVersion(): boolean { @@ -66,11 +74,21 @@ export class ShareLinksDropdownComponent implements OnInit { useArchiveVersion: boolean = true + emailLoading: boolean = false + emailAddress: string = '' + emailSubject: string = '' + emailMessage: string = '' + emailUseArchiveVersion: boolean = true + constructor( private shareLinkService: ShareLinkService, + private documentService: DocumentService, + private settingsService: SettingsService, private toastService: ToastService, private clipboard: Clipboard - ) {} + ) { + super() + } ngOnInit(): void { if (this._documentId !== undefined) this.refresh() @@ -169,4 +187,33 @@ export class ShareLinksDropdownComponent implements OnInit { }, }) } + + get emailEnabled(): boolean { + return this.settingsService.get(SETTINGS_KEYS.EMAIL_ENABLED) + } + + public emailDocument() { + this.emailLoading = true + this.documentService + .emailDocument( + this._documentId, + this.emailAddress, + this.emailSubject, + this.emailMessage, + this.emailUseArchiveVersion + ) + .subscribe({ + next: () => { + this.emailLoading = false + this.emailAddress = '' + this.emailSubject = '' + this.emailMessage = '' + this.toastService.showInfo($localize`Email sent`) + }, + error: (e) => { + this.emailLoading = false + this.toastService.showError($localize`Error emailing document`, e) + }, + }) + } } diff --git a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.html b/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.html deleted file mode 100644 index 08298abc7..000000000 --- a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.html +++ /dev/null @@ -1,70 +0,0 @@ -
- - -
diff --git a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.scss b/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.scss deleted file mode 100644 index 47e19d871..000000000 --- a/src-ui/src/app/components/common/share-links-dropdown/share-links-dropdown.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -.share-links-dropdown { - min-width: 350px; - - // correct position on mobile - @media (max-width: 575.98px) { - &.show { - margin-left: -175px !important; - } - } -} - -.copied-badge { - right: 7.5em; -} diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index fc35bdb43..556874f9b 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -81,7 +81,7 @@ (added)="addField($event)"> - +
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 30e34d9cf..9fe0d7294 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -99,7 +99,7 @@ import { TagsComponent } from '../common/input/tags/tags.component' import { TextComponent } from '../common/input/text/text.component' import { UrlComponent } from '../common/input/url/url.component' import { PageHeaderComponent } from '../common/page-header/page-header.component' -import { ShareLinksDropdownComponent } from '../common/share-links-dropdown/share-links-dropdown.component' +import { ShareDocumentDropdownComponent } from '../common/share-document-dropdown/share-document-dropdown.component' import { DocumentHistoryComponent } from '../document-history/document-history.component' import { DocumentNotesComponent } from '../document-notes/document-notes.component' import { ComponentWithPermissions } from '../with-permissions/with-permissions.component' @@ -145,7 +145,7 @@ export enum ZoomSetting { CustomFieldsDropdownComponent, DocumentNotesComponent, DocumentHistoryComponent, - ShareLinksDropdownComponent, + ShareDocumentDropdownComponent, CheckComponent, DateComponent, DocumentLinkComponent, diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts index 4d7d7cef7..84f7f6f8a 100644 --- a/src-ui/src/app/services/rest/document.service.spec.ts +++ b/src-ui/src/app/services/rest/document.service.spec.ts @@ -355,6 +355,21 @@ it('should include custom fields in sort fields if user has permission', () => { ]) }) +it('should call appropriate api endpoint for email document', () => { + subscription = service + .emailDocument( + documents[0].id, + 'hello@paperless-ngx.com', + 'hello', + 'world', + true + ) + .subscribe() + httpTestingController.expectOne( + `${environment.apiBaseUrl}${endpoint}/${documents[0].id}/email/` + ) +}) + afterEach(() => { subscription?.unsubscribe() httpTestingController.verify() diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index bbb611adf..0c6c8cfa6 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -258,4 +258,19 @@ export class DocumentService extends AbstractPaperlessService { public get searchQuery(): string { return this._searchQuery } + + emailDocument( + documentId: number, + addresses: string, + subject: string, + message: string, + useArchiveVersion: boolean + ): Observable { + return this.http.post(this.getResourceUrl(documentId, 'email'), { + addresses: addresses, + subject: subject, + message: message, + use_archive_version: useArchiveVersion, + }) + } } diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 6247b0a6e..630aeaee2 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -15,6 +15,7 @@ from dateutil import parser from django.conf import settings from django.contrib.auth.models import Permission from django.contrib.auth.models import User +from django.core import mail from django.core.cache import cache from django.db import DataError from django.test import override_settings @@ -2651,6 +2652,153 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(resp.status_code, status.HTTP_200_OK) self.assertEqual(doc1.tags.count(), 2) + @override_settings( + EMAIL_ENABLED=True, + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + ) + def test_email_document(self): + """ + GIVEN: + - Existing document + WHEN: + - API request is made to email document action + THEN: + - Email is sent, with document (original or archive) attached + """ + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document 1", + checksum="1", + filename="test.pdf", + archive_checksum="A", + archive_filename="archive.pdf", + ) + doc2 = Document.objects.create( + title="test2", + mime_type="application/pdf", + content="this is a document 2", + checksum="2", + filename="test2.pdf", + ) + + archive_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") + source_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") + + shutil.copy(archive_file, doc.archive_path) + shutil.copy(source_file, doc2.source_path) + + self.client.post( + f"/api/documents/{doc.pk}/email/", + { + "addresses": "hello@paperless-ngx.com", + "subject": "test", + "message": "hello", + }, + ) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].attachments[0][0], "archive.pdf") + + self.client.post( + f"/api/documents/{doc2.pk}/email/", + { + "addresses": "hello@paperless-ngx.com", + "subject": "test", + "message": "hello", + "use_archive_version": False, + }, + ) + + self.assertEqual(len(mail.outbox), 2) + self.assertEqual(mail.outbox[1].attachments[0][0], "test2.pdf") + + @mock.patch("django.core.mail.message.EmailMessage.send", side_effect=Exception) + def test_email_document_errors(self, mocked_send): + """ + GIVEN: + - Existing document + WHEN: + - API request is made to email document action with insufficient permissions + - API request is made to email document action with invalid document id + - API request is made to email document action with missing data + - API request is made to email document action with invalid email address + - API request is made to email document action and error occurs during email send + THEN: + - Error response is returned + """ + user1 = User.objects.create_user(username="test1") + user1.user_permissions.add(*Permission.objects.all()) + user1.save() + + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document 1", + checksum="1", + filename="test.pdf", + archive_checksum="A", + archive_filename="archive.pdf", + ) + + doc2 = Document.objects.create( + title="test2", + mime_type="application/pdf", + content="this is a document 2", + checksum="2", + owner=self.user, + ) + + self.client.force_authenticate(user1) + + resp = self.client.post( + f"/api/documents/{doc2.pk}/email/", + { + "addresses": "hello@paperless-ngx.com", + "subject": "test", + "message": "hello", + }, + ) + self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN) + + resp = self.client.post( + "/api/documents/999/email/", + { + "addresses": "hello@paperless-ngx.com", + "subject": "test", + "message": "hello", + }, + ) + self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) + + resp = self.client.post( + f"/api/documents/{doc.pk}/email/", + { + "addresses": "hello@paperless-ngx.com", + }, + ) + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + + resp = self.client.post( + f"/api/documents/{doc.pk}/email/", + { + "addresses": "hello@paperless-ngx.com,hello", + "subject": "test", + "message": "hello", + }, + ) + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) + + resp = self.client.post( + f"/api/documents/{doc.pk}/email/", + { + "addresses": "hello@paperless-ngx.com", + "subject": "test", + "message": "hello", + }, + ) + self.assertEqual(resp.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + @mock.patch("django_softdelete.models.SoftDeleteModel.delete") def test_warn_on_delete_with_old_uuid_field(self, mocked_delete): """ diff --git a/src/documents/views.py b/src/documents/views.py index aceea6699..8e61f74f0 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -18,6 +18,7 @@ import pathvalidate from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User +from django.core.mail import EmailMessage from django.db import connections from django.db.migrations.loader import MigrationLoader from django.db.migrations.recorder import MigrationRecorder @@ -37,6 +38,7 @@ from django.http import HttpResponse from django.http import HttpResponseBadRequest from django.http import HttpResponseForbidden from django.http import HttpResponseRedirect +from django.http import HttpResponseServerError from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.decorators import method_decorator @@ -1023,6 +1025,58 @@ class DocumentViewSet( 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) + if request.user is not None and not has_perms_owner_aware( + request.user, + "view_document", + doc, + ): + 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") + + email = EmailMessage( + subject=request.data.get("subject"), + body=request.data.get("message"), + to=addresses, + ) + attachment = ( + doc.archive_path + if use_archive_version and doc.has_archive_version + else doc.source_path + ) + email.attach_file(attachment) + email.send() + logger.debug( + f"Sent document {doc.id} via email to {addresses}", + ) + return Response({"message": "Email sent"}) + except Exception as e: + logger.warning(f"An error occurred emailing document: {e!s}") + return HttpResponseServerError( + "Error emailing document, check logs for more detail.", + ) + @extend_schema_view( list=extend_schema(