diff --git a/docs/usage.md b/docs/usage.md
index 2ea7115cf..2dcfcda66 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -392,6 +392,8 @@ still have "object-level" permissions.
| View | Confers the ability to view (not edit) a document, tag, etc.
Users without 'view' (or higher) permissions will be shown _'Private'_ in place of the object name for example when viewing a document with a tag for which the user doesn't have permissions. |
| Edit | Confers the ability to edit (and view) a document, tag, etc. |
+For related metadata such as tags, correspondents, document types, and storage paths, object visibility and document assignment are intentionally distinct. A user may still retain or submit a known object ID when editing a document even if that related object is displayed as _Private_ or omitted from search and selection results. This allows documents to preserve existing assignments that the current user cannot necessarily inspect in detail.
+
### Password reset
In order to enable the password reset feature you will need to setup an SMTP backend, see
diff --git a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts
index fa845f369..1dbd54edf 100644
--- a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts
+++ b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.spec.ts
@@ -26,6 +26,7 @@ import {
} from 'src/app/data/matching-model'
import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
+import { PermissionsService } from 'src/app/services/permissions.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -87,6 +88,7 @@ describe('EditDialogComponent', () => {
let component: TestComponent
let fixture: ComponentFixture
let tagService: TagService
+ let permissionsService: PermissionsService
let settingsService: SettingsService
let activeModal: NgbActiveModal
let httpTestingController: HttpTestingController
@@ -118,8 +120,10 @@ describe('EditDialogComponent', () => {
}).compileComponents()
tagService = TestBed.inject(TagService)
+ permissionsService = TestBed.inject(PermissionsService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = currentUser
+ permissionsService.initialize([], currentUser as any)
activeModal = TestBed.inject(NgbActiveModal)
httpTestingController = TestBed.inject(HttpTestingController)
@@ -226,6 +230,25 @@ describe('EditDialogComponent', () => {
expect(updateSpy).toHaveBeenCalled()
})
+ it('should not submit owner or permissions for non-owner edits', () => {
+ component.object = tag
+ component.dialogMode = EditDialogMode.EDIT
+ component.ngOnInit()
+
+ component.objectForm.get('name').setValue('Updated tag')
+ component.save()
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}tags/${tag.id}/`
+ )
+ expect(req.request.method).toEqual('PUT')
+ expect(req.request.body.name).toEqual('Updated tag')
+ expect(req.request.body.owner).toEqual(tag.owner)
+ expect(req.request.body.set_permissions).toBeUndefined()
+
+ req.flush({})
+ })
+
it('should create an object on save in edit mode', () => {
const createSpy = jest.spyOn(tagService, 'create')
component.dialogMode = EditDialogMode.CREATE
diff --git a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts
index 75534a777..3110f3090 100644
--- a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts
+++ b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts
@@ -18,6 +18,7 @@ import { ObjectWithId } from 'src/app/data/object-with-id'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { User } from 'src/app/data/user'
+import { PermissionsService } from 'src/app/services/permissions.service'
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -42,6 +43,7 @@ export abstract class EditDialogComponent<
protected activeModal = inject(NgbActiveModal)
protected userService = inject(UserService)
protected settingsService = inject(SettingsService)
+ protected permissionsService = inject(PermissionsService)
users: User[]
@@ -69,10 +71,6 @@ export abstract class EditDialogComponent<
ngOnInit(): void {
if (this.object != null && this.dialogMode !== EditDialogMode.CREATE) {
- if ((this.object as ObjectWithPermissions).permissions) {
- this.object['set_permissions'] = this.object['permissions']
- }
-
this.object['permissions_form'] = {
owner: (this.object as ObjectWithPermissions).owner,
set_permissions: (this.object as ObjectWithPermissions).permissions,
@@ -151,18 +149,28 @@ export abstract class EditDialogComponent<
return Object.assign({}, this.objectForm.value)
}
+ protected shouldSubmitPermissions(): boolean {
+ return (
+ this.dialogMode === EditDialogMode.CREATE ||
+ this.permissionsService.currentUserOwnsObject(this.object)
+ )
+ }
+
save() {
this.error = null
const formValues = this.getFormValues()
const permissionsObject: PermissionsFormObject =
this.objectForm.get('permissions_form')?.value
- if (permissionsObject) {
+ if (permissionsObject && this.shouldSubmitPermissions()) {
formValues.owner = permissionsObject.owner
formValues.set_permissions = permissionsObject.set_permissions
- delete formValues.permissions_form
}
+ delete formValues.permissions_form
var newObject = Object.assign(Object.assign({}, this.object), formValues)
+ if (!this.shouldSubmitPermissions()) {
+ delete newObject['set_permissions']
+ }
var serverResponse: Observable
switch (this.dialogMode) {
case EditDialogMode.CREATE:
diff --git a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts
index 1c87a4308..215b2c137 100644
--- a/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts
+++ b/src-ui/src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.ts
@@ -9,7 +9,6 @@ import { first } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
-import { PermissionsService } from 'src/app/services/permissions.service'
import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -39,7 +38,6 @@ export class UserEditDialogComponent
implements OnInit
{
private toastService = inject(ToastService)
- private permissionsService = inject(PermissionsService)
private groupsService: GroupService
groups: Group[]
diff --git a/src-ui/src/app/components/common/input/tags/tags.component.spec.ts b/src-ui/src/app/components/common/input/tags/tags.component.spec.ts
index ae543075e..21ca3eeff 100644
--- a/src-ui/src/app/components/common/input/tags/tags.component.spec.ts
+++ b/src-ui/src/app/components/common/input/tags/tags.component.spec.ts
@@ -205,6 +205,20 @@ describe('TagsComponent', () => {
expect(component.value).toEqual([2, 1])
})
+ it('should not duplicate parents when adding sibling nested tags', () => {
+ const root: Tag = { id: 1, name: 'root' }
+ const parent: Tag = { id: 2, name: 'parent', parent: 1 }
+ const leafA: Tag = { id: 3, name: 'leaf-a', parent: 2 }
+ const leafB: Tag = { id: 4, name: 'leaf-b', parent: 2 }
+ component.tags = [root, parent, leafA, leafB]
+
+ component.value = []
+ component.addTag(3)
+ component.addTag(4)
+
+ expect(component.value).toEqual([3, 2, 1, 4])
+ })
+
it('should return ancestors from root to parent using getParentChain', () => {
const root: Tag = { id: 1, name: 'root' }
const mid: Tag = { id: 2, name: 'mid', parent: 1 }
diff --git a/src-ui/src/app/components/common/input/tags/tags.component.ts b/src-ui/src/app/components/common/input/tags/tags.component.ts
index 4546dabcb..c540ff9d7 100644
--- a/src-ui/src/app/components/common/input/tags/tags.component.ts
+++ b/src-ui/src/app/components/common/input/tags/tags.component.ts
@@ -153,11 +153,13 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
}
public onAdd(tag: Tag) {
- if (tag.parent) {
+ if (tag?.parent) {
// add all parents recursively
const parent = this.getTag(tag.parent)
- this.value = [...this.value, parent.id]
- this.onAdd(parent)
+ if (parent && !this.value.includes(parent.id)) {
+ this.value = [...this.value, parent.id]
+ this.onAdd(parent)
+ }
}
}
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index ea8cc70b6..011045ea8 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -967,6 +967,8 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
getattr(request, "user", None) if request is not None else None,
doc_ids,
)
+ elif field.data_type == CustomField.FieldDataType.DATE:
+ data["value"] = serializers.DateField().to_internal_value(data["value"])
return data
diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py
index f8d670e30..4ff5a2b33 100644
--- a/src/documents/signals/handlers.py
+++ b/src/documents/signals/handlers.py
@@ -818,7 +818,6 @@ def run_workflows(
# Refresh this so the matching data is fresh and instance fields are re-freshed
# Otherwise, this instance might be behind and overwrite the work another process did
document.refresh_from_db()
- doc_tag_ids = list(document.tags.values_list("pk", flat=True))
if matching.document_matches_workflow(document, workflow, trigger_type):
action: WorkflowAction
@@ -836,14 +835,13 @@ def run_workflows(
apply_assignment_to_document(
action,
document,
- doc_tag_ids,
logging_group,
)
elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
if use_overrides and overrides:
apply_removal_to_overrides(action, overrides)
else:
- apply_removal_to_document(action, document, doc_tag_ids)
+ apply_removal_to_document(action, document)
elif action.type == WorkflowAction.WorkflowActionType.EMAIL:
context = build_workflow_action_context(document, overrides)
execute_email_action(
@@ -886,7 +884,6 @@ def run_workflows(
"modified",
],
)
- document.tags.set(doc_tag_ids)
WorkflowRun.objects.create(
workflow=workflow,
diff --git a/src/documents/tests/test_api_custom_fields.py b/src/documents/tests/test_api_custom_fields.py
index 998bc445a..9a775dc9d 100644
--- a/src/documents/tests/test_api_custom_fields.py
+++ b/src/documents/tests/test_api_custom_fields.py
@@ -1425,3 +1425,41 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data["results"]
self.assertEqual(results[0]["document_count"], 0)
+
+ def test_patch_document_invalid_date_custom_field_returns_validation_error(self):
+ """
+ GIVEN:
+ - A date custom field
+ - A document
+ WHEN:
+ - Patching the document with a date string in the wrong format
+ THEN:
+ - HTTP 400 is returned instead of an internal server error
+ - No custom field instance is created
+ """
+ cf_date = CustomField.objects.create(
+ name="datefield",
+ data_type=CustomField.FieldDataType.DATE,
+ )
+ doc = Document.objects.create(
+ title="Doc",
+ checksum="123",
+ mime_type="application/pdf",
+ )
+
+ response = self.client.patch(
+ f"/api/documents/{doc.pk}/",
+ {
+ "custom_fields": [
+ {
+ "field": cf_date.pk,
+ "value": "10.03.2026",
+ },
+ ],
+ },
+ format="json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn("custom_fields", response.data)
+ self.assertEqual(CustomFieldInstance.objects.count(), 0)
diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py
index baa0ffc56..352ce7810 100644
--- a/src/documents/tests/test_api_documents.py
+++ b/src/documents/tests/test_api_documents.py
@@ -1087,6 +1087,43 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(len(response.data["all"]), 50)
self.assertCountEqual(response.data["all"], [d.id for d in docs])
+ def test_default_ordering_uses_id_as_tiebreaker(self):
+ """
+ GIVEN:
+ - Documents sharing the same created date
+ WHEN:
+ - API request for documents without an explicit ordering
+ THEN:
+ - Results are correctly ordered by created > id
+ """
+ older_doc = Document.objects.create(
+ checksum="older",
+ content="older",
+ created=date(2024, 1, 1),
+ )
+ first_same_date_doc = Document.objects.create(
+ checksum="same-date-1",
+ content="same-date-1",
+ created=date(2024, 1, 2),
+ )
+ second_same_date_doc = Document.objects.create(
+ checksum="same-date-2",
+ content="same-date-2",
+ created=date(2024, 1, 2),
+ )
+
+ response = self.client.get("/api/documents/")
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(
+ [result["id"] for result in response.data["results"]],
+ [
+ second_same_date_doc.id,
+ first_same_date_doc.id,
+ older_doc.id,
+ ],
+ )
+
def test_statistics(self):
doc1 = Document.objects.create(
title="none1",
@@ -2953,6 +2990,58 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertEqual(create_resp.status_code, status.HTTP_201_CREATED)
self.assertEqual(create_resp.data["document"], doc.pk)
+ def test_share_link_update_methods_not_allowed(self):
+ """
+ GIVEN:
+ - An existing share link
+ WHEN:
+ - PUT and PATCH requests are made to its detail endpoint
+ THEN:
+ - The API rejects them with 405 and the link is unchanged
+ """
+ doc = Document.objects.create(
+ title="test",
+ mime_type="application/pdf",
+ content="share link content",
+ )
+ expiration = timezone.now() + timedelta(days=7)
+ create_resp = self.client.post(
+ "/api/share_links/",
+ data={
+ "document": doc.pk,
+ "expiration": expiration.isoformat(),
+ "file_version": ShareLink.FileVersion.ORIGINAL,
+ },
+ format="json",
+ )
+ self.assertEqual(create_resp.status_code, status.HTTP_201_CREATED)
+ share_link_id = create_resp.data["id"]
+
+ patch_resp = self.client.patch(
+ f"/api/share_links/{share_link_id}/",
+ data={
+ "expiration": None,
+ "file_version": ShareLink.FileVersion.ARCHIVE,
+ },
+ format="json",
+ )
+ self.assertEqual(patch_resp.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
+
+ put_resp = self.client.put(
+ f"/api/share_links/{share_link_id}/",
+ data={
+ "document": doc.pk,
+ "expiration": None,
+ "file_version": ShareLink.FileVersion.ARCHIVE,
+ },
+ format="json",
+ )
+ self.assertEqual(put_resp.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
+
+ share_link = ShareLink.objects.get(pk=share_link_id)
+ self.assertEqual(share_link.file_version, ShareLink.FileVersion.ORIGINAL)
+ self.assertIsNotNone(share_link.expiration)
+
def test_next_asn(self):
"""
GIVEN:
diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py
index 924533698..e4cc85087 100644
--- a/src/documents/tests/test_workflows.py
+++ b/src/documents/tests/test_workflows.py
@@ -3512,6 +3512,124 @@ class TestWorkflows(
as_json=False,
)
+ @mock.patch("documents.signals.handlers.execute_webhook_action")
+ def test_workflow_webhook_action_does_not_overwrite_concurrent_tags(
+ self,
+ mock_execute_webhook_action,
+ ):
+ """
+ GIVEN:
+ - A document updated workflow with only a webhook action
+ - A tag update that happens after run_workflows
+ WHEN:
+ - The workflow runs
+ THEN:
+ - The concurrent tag update is preserved
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ webhook_action = WorkflowActionWebhook.objects.create(
+ use_params=False,
+ body="Test message: {{doc_url}}",
+ url="http://paperless-ngx.com",
+ include_document=False,
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.WEBHOOK,
+ webhook=webhook_action,
+ )
+ w = Workflow.objects.create(
+ name="Webhook workflow",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ inbox_tag = Tag.objects.create(name="inbox")
+ error_tag = Tag.objects.create(name="error")
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ )
+ doc.tags.add(inbox_tag)
+
+ def add_error_tag(*args, **kwargs):
+ Document.objects.get(pk=doc.pk).tags.add(error_tag)
+
+ mock_execute_webhook_action.side_effect = add_error_tag
+
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ doc.refresh_from_db()
+ self.assertCountEqual(doc.tags.all(), [inbox_tag, error_tag])
+
+ @mock.patch("documents.signals.handlers.execute_webhook_action")
+ def test_workflow_tag_actions_do_not_overwrite_concurrent_tags(
+ self,
+ mock_execute_webhook_action,
+ ):
+ """
+ GIVEN:
+ - A document updated workflow that clears tags and assigns an inbox tag
+ - A later tag update that happens before the workflow finishes
+ WHEN:
+ - The workflow runs
+ THEN:
+ - The later tag update is preserved
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ removal_action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.REMOVAL,
+ remove_all_tags=True,
+ )
+ assign_action = WorkflowAction.objects.create(
+ assign_owner=self.user2,
+ )
+ assign_action.assign_tags.add(self.t1)
+ webhook_action = WorkflowActionWebhook.objects.create(
+ use_params=False,
+ body="Test message: {{doc_url}}",
+ url="http://paperless-ngx.com",
+ include_document=False,
+ )
+ notify_action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.WEBHOOK,
+ webhook=webhook_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow tag race",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(removal_action)
+ w.actions.add(assign_action)
+ w.actions.add(notify_action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ owner=self.user3,
+ )
+ doc.tags.add(self.t2, self.t3)
+
+ def add_error_tag(*args, **kwargs):
+ Document.objects.get(pk=doc.pk).tags.add(self.t2)
+
+ mock_execute_webhook_action.side_effect = add_error_tag
+
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ doc.refresh_from_db()
+ self.assertEqual(doc.owner, self.user2)
+ self.assertCountEqual(doc.tags.all(), [self.t1, self.t2])
+
@override_settings(
PAPERLESS_URL="http://localhost:8000",
)
diff --git a/src/documents/views.py b/src/documents/views.py
index 5985c17a8..732fe2232 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -76,6 +76,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.filters import SearchFilter
from rest_framework.generics import GenericAPIView
+from rest_framework.mixins import CreateModelMixin
from rest_framework.mixins import DestroyModelMixin
from rest_framework.mixins import ListModelMixin
from rest_framework.mixins import RetrieveModelMixin
@@ -785,7 +786,7 @@ class DocumentViewSet(
def get_queryset(self):
return (
Document.objects.distinct()
- .order_by("-created")
+ .order_by("-created", "-id")
.annotate(num_notes=Count("notes"))
.select_related("correspondent", "storage_path", "document_type", "owner")
.prefetch_related("tags", "custom_fields", "notes")
@@ -2702,7 +2703,14 @@ class TasksViewSet(ReadOnlyModelViewSet):
)
-class ShareLinkViewSet(ModelViewSet, PassUserMixin):
+class ShareLinkViewSet(
+ PassUserMixin,
+ CreateModelMixin,
+ RetrieveModelMixin,
+ DestroyModelMixin,
+ ListModelMixin,
+ GenericViewSet,
+):
model = ShareLink
queryset = ShareLink.objects.all()
diff --git a/src/documents/workflows/mutations.py b/src/documents/workflows/mutations.py
index ef85dba0f..2612202e6 100644
--- a/src/documents/workflows/mutations.py
+++ b/src/documents/workflows/mutations.py
@@ -16,7 +16,6 @@ logger = logging.getLogger("paperless.workflows.mutations")
def apply_assignment_to_document(
action: WorkflowAction,
document: Document,
- doc_tag_ids: list[int],
logging_group,
):
"""
@@ -25,12 +24,7 @@ def apply_assignment_to_document(
action: WorkflowAction, annotated with 'has_assign_*' boolean fields
"""
if action.has_assign_tags:
- tag_ids_to_add: set[int] = set()
- for tag in action.assign_tags.all():
- tag_ids_to_add.add(tag.pk)
- tag_ids_to_add.update(int(pk) for pk in tag.get_ancestors_pks())
-
- doc_tag_ids[:] = list(set(doc_tag_ids) | tag_ids_to_add)
+ document.add_nested_tags(action.assign_tags.all())
if action.assign_correspondent:
document.correspondent = action.assign_correspondent
@@ -197,7 +191,6 @@ def apply_assignment_to_overrides(
def apply_removal_to_document(
action: WorkflowAction,
document: Document,
- doc_tag_ids: list[int],
):
"""
Apply removal actions to a Document instance.
@@ -206,14 +199,15 @@ def apply_removal_to_document(
"""
if action.remove_all_tags:
- doc_tag_ids.clear()
+ document.tags.clear()
else:
tag_ids_to_remove: set[int] = set()
for tag in action.remove_tags.all():
tag_ids_to_remove.add(tag.pk)
tag_ids_to_remove.update(int(pk) for pk in tag.get_descendants_pks())
- doc_tag_ids[:] = [t for t in doc_tag_ids if t not in tag_ids_to_remove]
+ if tag_ids_to_remove:
+ document.tags.remove(*tag_ids_to_remove)
if action.remove_all_correspondents or (
document.correspondent
diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py
index edb266c51..ef13a01e0 100644
--- a/src/paperless_mail/mail.py
+++ b/src/paperless_mail/mail.py
@@ -472,6 +472,7 @@ class MailAccountHandler(LoggingMixin):
name=name,
defaults={
"match": name,
+ "matching_algorithm": Correspondent.MATCH_LITERAL,
},
)[0]
except DatabaseError as e:
diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py
index f8ab14bdd..68582b5f7 100644
--- a/src/paperless_mail/tests/test_mail.py
+++ b/src/paperless_mail/tests/test_mail.py
@@ -448,7 +448,7 @@ class TestMail(
c = handler._get_correspondent(message, rule)
self.assertIsNotNone(c)
self.assertEqual(c.name, "someone@somewhere.com")
- self.assertEqual(c.matching_algorithm, MatchingModel.MATCH_ANY)
+ self.assertEqual(c.matching_algorithm, MatchingModel.MATCH_LITERAL)
self.assertEqual(c.match, "someone@somewhere.com")
c = handler._get_correspondent(message2, rule)
self.assertIsNotNone(c)