diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts
index 0bccbad2d..9d92d9ba7 100644
--- a/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts
+++ b/src-ui/src/app/components/manage/workflows/workflows.component.spec.ts
@@ -211,4 +211,27 @@ describe('WorkflowsComponent', () => {
editDialog.confirmClicked.emit()
expect(reloadSpy).toHaveBeenCalled()
})
+
+ it('should update workflow when enable is toggled', () => {
+ const patchSpy = jest.spyOn(workflowService, 'patch')
+ const toggleInput = fixture.debugElement.query(
+ By.css('input[type="checkbox"]')
+ )
+ const toastErrorSpy = jest.spyOn(toastService, 'showError')
+ const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
+ // fail first
+ patchSpy.mockReturnValueOnce(
+ throwError(() => new Error('Error getting config'))
+ )
+ toggleInput.nativeElement.click()
+ expect(patchSpy).toHaveBeenCalled()
+ expect(toastErrorSpy).toHaveBeenCalled()
+ // succeed second
+ patchSpy.mockReturnValueOnce(of(workflows[0]))
+ toggleInput.nativeElement.click()
+ patchSpy.mockReturnValueOnce(of({ ...workflows[0], enabled: false }))
+ toggleInput.nativeElement.click()
+ expect(patchSpy).toHaveBeenCalled()
+ expect(toastInfoSpy).toHaveBeenCalled()
+ })
})
diff --git a/src-ui/src/app/components/manage/workflows/workflows.component.ts b/src-ui/src/app/components/manage/workflows/workflows.component.ts
index 92b421e9f..592dd3efe 100644
--- a/src-ui/src/app/components/manage/workflows/workflows.component.ts
+++ b/src-ui/src/app/components/manage/workflows/workflows.component.ts
@@ -130,4 +130,21 @@ export class WorkflowsComponent
})
})
}
+
+ onWorkflowEnableToggled(workflow: Workflow) {
+ this.workflowService.patch(workflow).subscribe({
+ next: () => {
+ this.toastService.showInfo(
+ workflow.enabled
+ ? $localize`Enabled workflow`
+ : $localize`Disabled workflow`
+ )
+ this.workflowService.clearCache()
+ this.reload()
+ },
+ error: (e) => {
+ this.toastService.showError($localize`Error toggling workflow.`, e)
+ },
+ })
+ }
}
diff --git a/src-ui/src/app/data/mail-rule.ts b/src-ui/src/app/data/mail-rule.ts
index 2611fa3ba..7888b19e6 100644
--- a/src-ui/src/app/data/mail-rule.ts
+++ b/src-ui/src/app/data/mail-rule.ts
@@ -39,6 +39,8 @@ export interface MailRule extends ObjectWithPermissions {
order: number
+ enabled: boolean
+
folder: string
filter_from: string
diff --git a/src-ui/src/app/services/rest/mail-rule.service.spec.ts b/src-ui/src/app/services/rest/mail-rule.service.spec.ts
index ea84e8b86..87e21172c 100644
--- a/src-ui/src/app/services/rest/mail-rule.service.spec.ts
+++ b/src-ui/src/app/services/rest/mail-rule.service.spec.ts
@@ -18,6 +18,7 @@ const mail_rules = [
id: 1,
account: 1,
order: 1,
+ enabled: true,
folder: 'INBOX',
filter_from: null,
filter_to: null,
@@ -36,6 +37,7 @@ const mail_rules = [
id: 2,
account: 1,
order: 1,
+ enabled: true,
folder: 'INBOX',
filter_from: null,
filter_to: null,
@@ -54,6 +56,7 @@ const mail_rules = [
id: 3,
account: 1,
order: 1,
+ enabled: true,
folder: 'INBOX',
filter_from: null,
filter_to: null,
diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss
index c83ebd493..ef856fbc7 100644
--- a/src-ui/src/styles.scss
+++ b/src-ui/src/styles.scss
@@ -369,6 +369,10 @@ textarea,
cursor: not-allowed;
}
+.cursor-pointer {
+ cursor: pointer;
+}
+
ul.pagination {
margin-bottom: 0;
}
diff --git a/src/documents/tests/test_migration_workflows.py b/src/documents/tests/test_migration_workflows.py
index 403067ca6..81bb577b2 100644
--- a/src/documents/tests/test_migration_workflows.py
+++ b/src/documents/tests/test_migration_workflows.py
@@ -8,7 +8,7 @@ class TestMigrateWorkflow(TestMigrations):
dependencies = (
(
"paperless_mail",
- "0025_alter_mailaccount_owner_alter_mailrule_owner_and_more",
+ "0026_mailrule_enabled",
),
)
diff --git a/src/paperless_mail/admin.py b/src/paperless_mail/admin.py
index adec5e17c..2ff313584 100644
--- a/src/paperless_mail/admin.py
+++ b/src/paperless_mail/admin.py
@@ -53,7 +53,7 @@ class MailRuleAdmin(GuardedModelAdmin):
}
fieldsets = (
- (None, {"fields": ("name", "order", "account", "folder")}),
+ (None, {"fields": ("name", "order", "account", "enabled", "folder")}),
(
_("Filter"),
{
diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py
index 45a73a61c..77d293ea0 100644
--- a/src/paperless_mail/mail.py
+++ b/src/paperless_mail/mail.py
@@ -544,6 +544,9 @@ class MailAccountHandler(LoggingMixin):
)
for rule in account.rules.order_by("order"):
+ if not rule.enabled:
+ self.log.debug(f"Rule {rule}: Skipping disabled rule")
+ continue
try:
total_processed_files += self._handle_mail_rule(
M,
diff --git a/src/paperless_mail/migrations/0026_mailrule_enabled.py b/src/paperless_mail/migrations/0026_mailrule_enabled.py
new file mode 100644
index 000000000..c10ee698c
--- /dev/null
+++ b/src/paperless_mail/migrations/0026_mailrule_enabled.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.1.1 on 2024-09-30 15:17
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ (
+ "paperless_mail",
+ "0025_alter_mailaccount_owner_alter_mailrule_owner_and_more",
+ ),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="mailrule",
+ name="enabled",
+ field=models.BooleanField(default=True, verbose_name="enabled"),
+ ),
+ ]
diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py
index c53b16f1f..c23ea48c7 100644
--- a/src/paperless_mail/models.py
+++ b/src/paperless_mail/models.py
@@ -115,6 +115,8 @@ class MailRule(document_models.ModelWithOwner):
verbose_name=_("account"),
)
+ enabled = models.BooleanField(_("enabled"), default=True)
+
folder = models.CharField(
_("folder"),
default="INBOX",
diff --git a/src/paperless_mail/serialisers.py b/src/paperless_mail/serialisers.py
index 38ee9661e..9237b47de 100644
--- a/src/paperless_mail/serialisers.py
+++ b/src/paperless_mail/serialisers.py
@@ -74,6 +74,7 @@ class MailRuleSerializer(OwnedObjectSerializer):
"id",
"name",
"account",
+ "enabled",
"folder",
"filter_from",
"filter_to",
diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py
index c12b54ffe..9078335a6 100644
--- a/src/paperless_mail/tests/test_mail.py
+++ b/src/paperless_mail/tests/test_mail.py
@@ -1388,6 +1388,41 @@ class TestMail(
self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 0)
self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3)
+ def test_disabled_rule(self):
+ """
+ GIVEN:
+ - Mail rule is disabled
+ WHEN:
+ - Mail account is handled
+ THEN:
+ - Should not process any messages
+ """
+ account = MailAccount.objects.create(
+ name="test",
+ imap_server="",
+ username="admin",
+ password="secret",
+ )
+ MailRule.objects.create(
+ name="testrule",
+ account=account,
+ action=MailRule.MailAction.MARK_READ,
+ enabled=False,
+ )
+
+ self.mail_account_handler.handle_mail_account(account)
+ self.mailMocker.apply_mail_actions()
+
+ self.assertEqual(len(self.mailMocker.bogus_mailbox.messages), 3)
+ self.assertEqual(len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)), 2)
+
+ self.mail_account_handler.handle_mail_account(account)
+ self.mailMocker.apply_mail_actions()
+ self.assertEqual(
+ len(self.mailMocker.bogus_mailbox.fetch("UNSEEN", False)),
+ 2,
+ ) # still 2
+
class TestManagementCommand(TestCase):
@mock.patch(
diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py
index a0baa4821..e8186ea0f 100644
--- a/src/paperless_mail/tests/test_parsers.py
+++ b/src/paperless_mail/tests/test_parsers.py
@@ -497,6 +497,7 @@ class TestParser:
assert mail_parser.archive_path is not None
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_generate_pdf_html_email(
self,
httpx_mock: HTTPXMock,
@@ -575,6 +576,7 @@ class TestParser:
with pytest.raises(ParseError):
mail_parser.parse(html_email_file, "message/rfc822")
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
def test_generate_pdf_html_email_merge_failure(
self,
httpx_mock: HTTPXMock,
diff --git a/src/paperless_tika/tests/test_tika_parser.py b/src/paperless_tika/tests/test_tika_parser.py
index 6b048f252..cebae2486 100644
--- a/src/paperless_tika/tests/test_tika_parser.py
+++ b/src/paperless_tika/tests/test_tika_parser.py
@@ -5,7 +5,6 @@ from pathlib import Path
import pytest
from httpx import codes
-from httpx._multipart import DataField
from pytest_django.fixtures import SettingsWrapper
from pytest_httpx import HTTPXMock
@@ -128,11 +127,22 @@ class TestTikaParser:
tika_parser.convert_to_pdf(sample_odt_file, None)
request = httpx_mock.get_request()
- found = False
- for field in request.stream.fields:
- if isinstance(field, DataField) and field.name == "pdfa":
- assert field.value == expected_form_value
- found = True
- assert found, "pdfFormat was not found"
- httpx_mock.reset(assert_all_responses_were_requested=False)
+ expected_field_name = "pdfa"
+
+ content_type = request.headers["Content-Type"]
+ assert "multipart/form-data" in content_type
+
+ boundary = content_type.split("boundary=")[1]
+
+ parts = request.content.split(f"--{boundary}".encode())
+
+ form_field_found = any(
+ f'name="{expected_field_name}"'.encode() in part
+ and expected_form_value.encode() in part
+ for part in parts
+ )
+
+ assert form_field_found
+
+ httpx_mock.reset()