Allows filtering email by the TO value(s) as well

This commit is contained in:
Trenton H 2023-01-25 14:06:36 -08:00
parent 70ac696f17
commit 3e467c517d
12 changed files with 80 additions and 9 deletions

View File

@ -9,6 +9,7 @@
"account": 2, "account": 2,
"folder": "INBOX", "folder": "INBOX",
"filter_from": null, "filter_from": null,
"filter_to": null,
"filter_subject": "[paperless]", "filter_subject": "[paperless]",
"filter_body": null, "filter_body": null,
"filter_attachment_filename": null, "filter_attachment_filename": null,

View File

@ -18,6 +18,7 @@
<div class="col"> <div class="col">
<p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p> <p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p>
<app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text> <app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text>
<app-input-text i18n-title title="Filter to" formControlName="filter_to" [error]="error?.filter_to"></app-input-text>
<app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text> <app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text>
<app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text> <app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text>
<app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text> <app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text>

View File

@ -149,6 +149,7 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa
account: new FormControl(null), account: new FormControl(null),
folder: new FormControl('INBOX'), folder: new FormControl('INBOX'),
filter_from: new FormControl(null), filter_from: new FormControl(null),
filter_to: new FormControl(null),
filter_subject: new FormControl(null), filter_subject: new FormControl(null),
filter_body: new FormControl(null), filter_body: new FormControl(null),
filter_attachment_filename: new FormControl(null), filter_attachment_filename: new FormControl(null),

View File

@ -386,6 +386,7 @@ export class SettingsComponent
account: rule.account, account: rule.account,
folder: rule.folder, folder: rule.folder,
filter_from: rule.filter_from, filter_from: rule.filter_from,
filter_to: rule.filter_to,
filter_subject: rule.filter_subject, filter_subject: rule.filter_subject,
filter_body: rule.filter_body, filter_body: rule.filter_body,
filter_attachment_filename: rule.filter_attachment_filename, filter_attachment_filename: rule.filter_attachment_filename,
@ -406,6 +407,7 @@ export class SettingsComponent
account: new FormControl(null), account: new FormControl(null),
folder: new FormControl(null), folder: new FormControl(null),
filter_from: new FormControl(null), filter_from: new FormControl(null),
filter_to: new FormControl(null),
filter_subject: new FormControl(null), filter_subject: new FormControl(null),
filter_body: new FormControl(null), filter_body: new FormControl(null),
filter_attachment_filename: new FormControl(null), filter_attachment_filename: new FormControl(null),

View File

@ -42,6 +42,8 @@ export interface PaperlessMailRule extends ObjectWithId {
filter_from: string filter_from: string
filter_to: string
filter_subject: string filter_subject: string
filter_body: string filter_body: string

View File

@ -53,6 +53,7 @@ class MailRuleAdmin(admin.ModelAdmin):
), ),
"fields": ( "fields": (
"filter_from", "filter_from",
"filter_to",
"filter_subject", "filter_subject",
"filter_body", "filter_body",
"filter_attachment_filename", "filter_attachment_filename",

View File

@ -82,7 +82,12 @@ class BaseMailAction:
""" """
return {} return {}
def post_consume(self, M: MailBox, message_uid: str, parameter: str): def post_consume(
self,
M: MailBox,
message_uid: str,
parameter: str,
): # pragma: nocover
""" """
Perform mail action on the given mail uid in the mailbox. Perform mail action on the given mail uid in the mailbox.
""" """
@ -160,7 +165,7 @@ class TagMailAction(BaseMailAction):
return {"flagged": False} return {"flagged": False}
elif self.keyword: elif self.keyword:
return AND(NOT(gmail_label=self.keyword), no_keyword=self.keyword) return AND(NOT(gmail_label=self.keyword), no_keyword=self.keyword)
else: else: # pragma: nocover
raise ValueError("This should never happen.") raise ValueError("This should never happen.")
def post_consume(self, M: MailBox, message_uid: str, parameter: str): def post_consume(self, M: MailBox, message_uid: str, parameter: str):
@ -363,6 +368,8 @@ def make_criterias(rule):
criterias["date_gte"] = maximum_age criterias["date_gte"] = maximum_age
if rule.filter_from: if rule.filter_from:
criterias["from_"] = rule.filter_from criterias["from_"] = rule.filter_from
if rule.filter_to:
criterias["to"] = rule.filter_to
if rule.filter_subject: if rule.filter_subject:
criterias["subject"] = rule.filter_subject criterias["subject"] = rule.filter_subject
if rule.filter_body: if rule.filter_body:

View File

@ -0,0 +1,19 @@
# Generated by Django 4.1.7 on 2023-03-11 21:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("paperless_mail", "0018_processedmail"),
]
operations = [
migrations.AddField(
model_name="mailrule",
name="filter_to",
field=models.CharField(
blank=True, max_length=256, null=True, verbose_name="filter to"
),
),
]

View File

@ -113,12 +113,21 @@ class MailRule(document_models.ModelWithOwner):
null=True, null=True,
blank=True, blank=True,
) )
filter_to = models.CharField(
_("filter to"),
max_length=256,
null=True,
blank=True,
)
filter_subject = models.CharField( filter_subject = models.CharField(
_("filter subject"), _("filter subject"),
max_length=256, max_length=256,
null=True, null=True,
blank=True, blank=True,
) )
filter_body = models.CharField( filter_body = models.CharField(
_("filter body"), _("filter body"),
max_length=256, max_length=256,

View File

@ -70,6 +70,7 @@ class MailRuleSerializer(OwnedObjectSerializer):
"account", "account",
"folder", "folder",
"filter_from", "filter_from",
"filter_to",
"filter_subject", "filter_subject",
"filter_body", "filter_body",
"filter_attachment_filename", "filter_attachment_filename",

View File

@ -201,6 +201,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
account=account1, account=account1,
folder="INBOX", folder="INBOX",
filter_from="from@example.com", filter_from="from@example.com",
filter_to="someone@somewhere.com",
filter_subject="subject", filter_subject="subject",
filter_body="body", filter_body="body",
filter_attachment_filename="file.pdf", filter_attachment_filename="file.pdf",
@ -222,6 +223,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
self.assertEqual(returned_rule1["account"], account1.pk) self.assertEqual(returned_rule1["account"], account1.pk)
self.assertEqual(returned_rule1["folder"], rule1.folder) self.assertEqual(returned_rule1["folder"], rule1.folder)
self.assertEqual(returned_rule1["filter_from"], rule1.filter_from) self.assertEqual(returned_rule1["filter_from"], rule1.filter_from)
self.assertEqual(returned_rule1["filter_to"], rule1.filter_to)
self.assertEqual(returned_rule1["filter_subject"], rule1.filter_subject) self.assertEqual(returned_rule1["filter_subject"], rule1.filter_subject)
self.assertEqual(returned_rule1["filter_body"], rule1.filter_body) self.assertEqual(returned_rule1["filter_body"], rule1.filter_body)
self.assertEqual( self.assertEqual(
@ -275,6 +277,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
"account": account1.pk, "account": account1.pk,
"folder": "INBOX", "folder": "INBOX",
"filter_from": "from@example.com", "filter_from": "from@example.com",
"filter_to": "aperson@aplace.com",
"filter_subject": "subject", "filter_subject": "subject",
"filter_body": "body", "filter_body": "body",
"filter_attachment_filename": "file.pdf", "filter_attachment_filename": "file.pdf",
@ -307,6 +310,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase):
self.assertEqual(returned_rule1["account"], account1.pk) self.assertEqual(returned_rule1["account"], account1.pk)
self.assertEqual(returned_rule1["folder"], rule1["folder"]) self.assertEqual(returned_rule1["folder"], rule1["folder"])
self.assertEqual(returned_rule1["filter_from"], rule1["filter_from"]) self.assertEqual(returned_rule1["filter_from"], rule1["filter_from"])
self.assertEqual(returned_rule1["filter_to"], rule1["filter_to"])
self.assertEqual(returned_rule1["filter_subject"], rule1["filter_subject"]) self.assertEqual(returned_rule1["filter_subject"], rule1["filter_subject"])
self.assertEqual(returned_rule1["filter_body"], rule1["filter_body"]) self.assertEqual(returned_rule1["filter_body"], rule1["filter_body"])
self.assertEqual( self.assertEqual(

View File

@ -5,6 +5,7 @@ import uuid
from collections import namedtuple from collections import namedtuple
from typing import ContextManager from typing import ContextManager
from typing import List from typing import List
from typing import Optional
from typing import Union from typing import Union
from unittest import mock from unittest import mock
@ -131,12 +132,20 @@ class BogusMailBox(ContextManager):
from_ = criteria[criteria.index("FROM") + 1].strip('"') from_ = criteria[criteria.index("FROM") + 1].strip('"')
msg = filter(lambda m: from_ in m.from_, msg) msg = filter(lambda m: from_ in m.from_, msg)
if "TO" in criteria:
to_ = criteria[criteria.index("TO") + 1].strip('"')
msg = []
for m in self.messages:
for to_addrs in m.to:
if to_ in to_addrs:
msg.append(m)
if "UNFLAGGED" in criteria: if "UNFLAGGED" in criteria:
msg = filter(lambda m: not m.flagged, msg) msg = filter(lambda m: not m.flagged, msg)
if "UNKEYWORD" in criteria: if "UNKEYWORD" in criteria:
tag = criteria[criteria.index("UNKEYWORD") + 1].strip("'") tag = criteria[criteria.index("UNKEYWORD") + 1].strip("'")
msg = filter(lambda m: "processed" not in m.flags, msg) msg = filter(lambda m: tag not in m.flags, msg)
if "(X-GM-LABELS" in criteria: # ['NOT', '(X-GM-LABELS', '"processed"'] if "(X-GM-LABELS" in criteria: # ['NOT', '(X-GM-LABELS', '"processed"']
msg = filter(lambda m: "processed" not in m.flags, msg) msg = filter(lambda m: "processed" not in m.flags, msg)
@ -205,15 +214,21 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
body: str = "", body: str = "",
subject: str = "the suject", subject: str = "the suject",
from_: str = "noone@mail.com", from_: str = "noone@mail.com",
to: Optional[List[str]] = None,
seen: bool = False, seen: bool = False,
flagged: bool = False, flagged: bool = False,
processed: bool = False, processed: bool = False,
) -> MailMessage: ) -> MailMessage:
if to is None:
to = ["tosomeone@somewhere.com"]
email_msg = email.message.EmailMessage() email_msg = email.message.EmailMessage()
# TODO: This does NOT set the UID # TODO: This does NOT set the UID
email_msg["Message-ID"] = str(uuid.uuid4()) email_msg["Message-ID"] = str(uuid.uuid4())
email_msg["Subject"] = subject email_msg["Subject"] = subject
email_msg["From"] = from_ email_msg["From"] = from_
email_msg["To"] = str(" ,".join(to))
email_msg.set_content(body) email_msg.set_content(body)
# Either add some default number of attachments # Either add some default number of attachments
@ -266,6 +281,7 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.create_message( self.create_message(
subject="Invoice 1", subject="Invoice 1",
from_="amazon@amazon.de", from_="amazon@amazon.de",
to=["me@myselfandi.com", "helpdesk@mydomain.com"],
body="cables", body="cables",
seen=True, seen=True,
flagged=False, flagged=False,
@ -276,6 +292,7 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.create_message( self.create_message(
subject="Invoice 2", subject="Invoice 2",
body="from my favorite electronic store", body="from my favorite electronic store",
to=["invoices@mycompany.com"],
seen=False, seen=False,
flagged=True, flagged=True,
processed=True, processed=True,
@ -285,6 +302,7 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
self.create_message( self.create_message(
subject="Claim your $10M price now!", subject="Claim your $10M price now!",
from_="amazon@amazon-some-indian-site.org", from_="amazon@amazon-some-indian-site.org",
to="special@me.me",
seen=False, seen=False,
), ),
) )
@ -946,21 +964,26 @@ class TestMail(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
password="secret", password="secret",
) )
for (f_body, f_from, f_subject, expected_mail_count) in [ for (f_body, f_from, f_to, f_subject, expected_mail_count) in [
(None, None, "Claim", 1), (None, None, None, "Claim", 1),
("electronic", None, None, 1), ("electronic", None, None, None, 1),
(None, "amazon", None, 2), (None, "amazon", None, None, 2),
("cables", "amazon", "Invoice", 1), ("cables", "amazon", None, "Invoice", 1),
(None, None, "test@email.com", None, 0),
(None, None, "invoices@mycompany.com", None, 1),
("electronic", None, "invoices@mycompany.com", None, 1),
(None, "amazon", "me@myselfandi.com", None, 1),
]: ]:
with self.subTest(f_body=f_body, f_from=f_from, f_subject=f_subject): with self.subTest(f_body=f_body, f_from=f_from, f_subject=f_subject):
MailRule.objects.all().delete() MailRule.objects.all().delete()
rule = MailRule.objects.create( _ = MailRule.objects.create(
name="testrule3", name="testrule3",
account=account, account=account,
action=MailRule.MailAction.DELETE, action=MailRule.MailAction.DELETE,
filter_subject=f_subject, filter_subject=f_subject,
filter_body=f_body, filter_body=f_body,
filter_from=f_from, filter_from=f_from,
filter_to=f_to,
) )
self.reset_bogus_mailbox() self.reset_bogus_mailbox()
self._queue_consumption_tasks_mock.reset_mock() self._queue_consumption_tasks_mock.reset_mock()