diff --git a/src-ui/src/app/components/manage/mail/mail.component.spec.ts b/src-ui/src/app/components/manage/mail/mail.component.spec.ts
index 4a134b880..14cd10944 100644
--- a/src-ui/src/app/components/manage/mail/mail.component.spec.ts
+++ b/src-ui/src/app/components/manage/mail/mail.component.spec.ts
@@ -43,14 +43,15 @@ import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { SwitchComponent } from '../../common/input/switch/switch.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
+import { By } from '@angular/platform-browser'
const mailAccounts = [
{ id: 1, name: 'account1' },
{ id: 2, name: 'account2' },
]
const mailRules = [
- { id: 1, name: 'rule1', owner: 1, account: 1 },
- { id: 2, name: 'rule2', owner: 2, account: 2 },
+ { id: 1, name: 'rule1', owner: 1, account: 1, enabled: true },
+ { id: 2, name: 'rule2', owner: 2, account: 2, enabled: true },
]
describe('MailComponent', () => {
@@ -321,4 +322,30 @@ describe('MailComponent', () => {
dialog.confirmClicked.emit({ permissions: perms, merge: true })
expect(accountPatchSpy).toHaveBeenCalled()
})
+
+ it('should update mail rule when enable is toggled', () => {
+ completeSetup()
+ const patchSpy = jest.spyOn(mailRuleService, '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(mailRules[0] as MailRule))
+ toggleInput.nativeElement.click()
+ patchSpy.mockReturnValueOnce(
+ of({ ...mailRules[0], enabled: false } as MailRule)
+ )
+ toggleInput.nativeElement.click()
+ expect(patchSpy).toHaveBeenCalled()
+ expect(toastInfoSpy).toHaveBeenCalled()
+ })
})
diff --git a/src-ui/src/app/components/manage/mail/mail.component.ts b/src-ui/src/app/components/manage/mail/mail.component.ts
index 5d00b6c13..288e8e121 100644
--- a/src-ui/src/app/components/manage/mail/mail.component.ts
+++ b/src-ui/src/app/components/manage/mail/mail.component.ts
@@ -170,6 +170,21 @@ export class MailComponent
this.editMailRule(clone, true)
}
+ onMailRuleEnableToggled(rule: MailRule) {
+ this.mailRuleService.patch(rule).subscribe({
+ next: () => {
+ this.toastService.showInfo(
+ rule.enabled
+ ? $localize`Rule "${rule.name}" enabled.`
+ : $localize`Rule "${rule.name}" disabled.`
+ )
+ },
+ error: (e) => {
+ this.toastService.showError($localize`Error toggling rule.`, e)
+ },
+ })
+ }
+
deleteMailRule(rule: MailRule) {
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
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 b52a2ebe4..84f97b742 100644
--- a/src/paperless_mail/mail.py
+++ b/src/paperless_mail/mail.py
@@ -536,6 +536,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(