Feature: email document button (#8950)

This commit is contained in:
shamoon
2025-02-21 08:44:03 -08:00
committed by GitHub
parent 4f08b5fa20
commit c122c60d3f
19 changed files with 714 additions and 189 deletions

View File

@@ -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 = Path(__file__).parent / "samples" / "simple.pdf"
source_file = Path(__file__).parent / "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):
"""

View File

@@ -37,6 +37,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
@@ -106,6 +107,7 @@ from documents.filters import ObjectOwnedPermissionsFilter
from documents.filters import ShareLinkFilterSet
from documents.filters import StoragePathFilterSet
from documents.filters import TagFilterSet
from documents.mail import send_email
from documents.matching import match_correspondents
from documents.matching import match_document_types
from documents.matching import match_storage_paths
@@ -1023,6 +1025,57 @@ 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")
send_email(
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
),
attachment_mime_type=doc.mime_type,
)
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(