diff --git a/docs/usage_overview.rst b/docs/usage_overview.rst index d7cfb8250..a321afb93 100644 --- a/docs/usage_overview.rst +++ b/docs/usage_overview.rst @@ -161,6 +161,9 @@ These are as follows: will not consume flagged mails. * **Move to folder:** Moves consumed mails out of the way so that paperless wont consume them again. +* **Add custom Tag:** Adds a custom tag to mails with consumed documents (the IMAP + standard calls these "keywords"). Paperless will not consume mails already tagged. + Not all mail servers support this feature! .. caution:: diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 67cc22130..fefcbda5e 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -62,6 +62,17 @@ class FlagMailAction(BaseMailAction): M.flag(message_uids, [MailMessageFlags.FLAGGED], True) +class TagMailAction(BaseMailAction): + def __init__(self, parameter): + self.keyword = parameter + + def get_criteria(self): + return {"no_keyword": self.keyword} + + def post_consume(self, M: MailBox, message_uids, parameter): + M.flag(message_uids, [self.keyword], True) + + def get_rule_action(rule): if rule.action == MailRule.MailAction.FLAG: return FlagMailAction() @@ -71,6 +82,8 @@ def get_rule_action(rule): return MoveMailAction() elif rule.action == MailRule.MailAction.MARK_READ: return MarkReadMailAction() + elif rule.action == MailRule.MailAction.TAG: + return TagMailAction(rule.action_parameter) else: raise NotImplementedError("Unknown action.") # pragma: nocover diff --git a/src/paperless_mail/migrations/0015_alter_mailrule_action.py b/src/paperless_mail/migrations/0015_alter_mailrule_action.py new file mode 100644 index 000000000..62a44e6aa --- /dev/null +++ b/src/paperless_mail/migrations/0015_alter_mailrule_action.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.4 on 2022-05-29 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("paperless_mail", "0014_alter_mailrule_action"), + ] + + operations = [ + migrations.AlterField( + model_name="mailrule", + name="action", + field=models.PositiveIntegerField( + choices=[ + (1, "Delete"), + (2, "Move to specified folder"), + (3, "Mark as read, don't process read mails"), + (4, "Flag the mail, don't process flagged mails"), + (5, "Tag the mail with specified tag, don't process tagged mails"), + ], + default=3, + verbose_name="action", + ), + ), + ] diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py index 4e90197b7..4c0a1a557 100644 --- a/src/paperless_mail/models.py +++ b/src/paperless_mail/models.py @@ -65,6 +65,7 @@ class MailRule(models.Model): MOVE = 2, _("Move to specified folder") MARK_READ = 3, _("Mark as read, don't process read mails") FLAG = 4, _("Flag the mail, don't process flagged mails") + TAG = 5, _("Tag the mail with specified tag, don't process tagged mails") class TitleSource(models.IntegerChoices): FROM_SUBJECT = 1, _("Use subject as title") diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 24014b4dc..2aaa588d6 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -96,6 +96,10 @@ class BogusMailBox(ContextManager): if "UNFLAGGED" in criteria: msg = filter(lambda m: not m.flagged, msg) + if "UNKEYWORD" in criteria: + tag = criteria[criteria.index("UNKEYWORD") + 1].strip("'") + msg = filter(lambda m: "processed" not in m.flags, msg) + return list(msg) def delete(self, uid_list): @@ -109,6 +113,9 @@ class BogusMailBox(ContextManager): message.flagged = value if flag == MailMessageFlags.SEEN: message.seen = value + if flag == "processed": + message._raw_flag_data.append(f"+FLAGS (processed)".encode()) + MailMessage.flags.fget.cache_clear() def move(self, uid_list, folder): if folder == "spam": @@ -130,6 +137,7 @@ def create_message( from_: str = "noone@mail.com", seen: bool = False, flagged: bool = False, + processed: bool = False, ) -> MailMessage: email_msg = email.message.EmailMessage() # TODO: This does NOT set the UID @@ -175,6 +183,9 @@ def create_message( imap_msg.seen = seen imap_msg.flagged = flagged + if processed: + imap_msg._raw_flag_data.append(f"+FLAGS (processed)".encode()) + MailMessage.flags.fget.cache_clear() return imap_msg @@ -217,6 +228,7 @@ class TestMail(DirectoriesMixin, TestCase): body="cables", seen=True, flagged=False, + processed=False, ), ) self.bogus_mailbox.messages.append( @@ -225,6 +237,7 @@ class TestMail(DirectoriesMixin, TestCase): body="from my favorite electronic store", seen=False, flagged=True, + processed=True, ), ) self.bogus_mailbox.messages.append( @@ -571,6 +584,29 @@ class TestMail(DirectoriesMixin, TestCase): self.assertEqual(len(self.bogus_mailbox.messages), 2) self.assertEqual(len(self.bogus_mailbox.messages_spam), 1) + def test_handle_mail_account_tag(self): + account = MailAccount.objects.create( + name="test", + imap_server="", + username="admin", + password="secret", + ) + + _ = MailRule.objects.create( + name="testrule", + account=account, + action=MailRule.MailAction.TAG, + action_parameter="processed", + ) + + self.assertEqual(len(self.bogus_mailbox.messages), 3) + self.assertEqual(self.async_task.call_count, 0) + self.assertEqual(len(self.bogus_mailbox.fetch("UNKEYWORD processed", False)), 2) + self.mail_account_handler.handle_mail_account(account) + self.assertEqual(self.async_task.call_count, 2) + self.assertEqual(len(self.bogus_mailbox.fetch("UNKEYWORD processed", False)), 0) + self.assertEqual(len(self.bogus_mailbox.messages), 3) + def test_error_login(self): account = MailAccount.objects.create( name="test",