mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Enhancement: Allow excluding mail attachments by name (#4691)
* Adds new filtering to exclude attachments from processing * Frontend use include / exclude mail rule filename filters --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
		| @@ -1315,7 +1315,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">42</context> |           <context context-type="linenumber">43</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context> | ||||||
| @@ -2520,7 +2520,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">32</context> |           <context context-type="linenumber">33</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4875491778188965469" datatype="html"> |       <trans-unit id="4875491778188965469" datatype="html"> | ||||||
| @@ -2531,7 +2531,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">34</context> |           <context context-type="linenumber">35</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7198346314713788799" datatype="html"> |       <trans-unit id="7198346314713788799" datatype="html"> | ||||||
| @@ -2570,7 +2570,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">40</context> |           <context context-type="linenumber">41</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context> |           <context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context> | ||||||
| @@ -2605,7 +2605,7 @@ | |||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">41</context> |           <context context-type="linenumber">42</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context> | ||||||
| @@ -3016,8 +3016,8 @@ | |||||||
|           <context context-type="linenumber">23</context> |           <context context-type="linenumber">23</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5031687746498952417" datatype="html"> |       <trans-unit id="4603548543464136402" datatype="html"> | ||||||
|         <source>Filter attachment filename</source> |         <source>Filter attachment filename includes</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">24</context> |           <context context-type="linenumber">24</context> | ||||||
| @@ -3030,53 +3030,67 @@ | |||||||
|           <context context-type="linenumber">24</context> |           <context context-type="linenumber">24</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|  |       <trans-unit id="6869675473865305593" datatype="html"> | ||||||
|  |         <source>Filter attachment filename excluding</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">25</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|  |       <trans-unit id="6774472763442688477" datatype="html"> | ||||||
|  |         <source>Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.</source> | ||||||
|  |         <context-group purpose="location"> | ||||||
|  |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|  |           <context context-type="linenumber">25</context> | ||||||
|  |         </context-group> | ||||||
|  |       </trans-unit> | ||||||
|       <trans-unit id="9216117865911519658" datatype="html"> |       <trans-unit id="9216117865911519658" datatype="html"> | ||||||
|         <source>Action</source> |         <source>Action</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">27</context> |           <context context-type="linenumber">28</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4274038999388817994" datatype="html"> |       <trans-unit id="4274038999388817994" datatype="html"> | ||||||
|         <source>Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched.</source> |         <source>Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">27</context> |           <context context-type="linenumber">28</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1261794314435932203" datatype="html"> |       <trans-unit id="1261794314435932203" datatype="html"> | ||||||
|         <source>Action parameter</source> |         <source>Action parameter</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">28</context> |           <context context-type="linenumber">29</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5512171567357420308" datatype="html"> |       <trans-unit id="5512171567357420308" datatype="html"> | ||||||
|         <source>Assignments specified here will supersede any consumption templates.</source> |         <source>Assignments specified here will supersede any consumption templates.</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">29</context> |           <context context-type="linenumber">30</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6093797930511670257" datatype="html"> |       <trans-unit id="6093797930511670257" datatype="html"> | ||||||
|         <source>Assign title from</source> |         <source>Assign title from</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">30</context> |           <context context-type="linenumber">31</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4754802869258527587" datatype="html"> |       <trans-unit id="4754802869258527587" datatype="html"> | ||||||
|         <source>Assign correspondent from</source> |         <source>Assign correspondent from</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">33</context> |           <context context-type="linenumber">34</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5232720756589450549" datatype="html"> |       <trans-unit id="5232720756589450549" datatype="html"> | ||||||
|         <source>Assign owner from rule</source> |         <source>Assign owner from rule</source> | ||||||
|         <context-group purpose="location"> |         <context-group purpose="location"> | ||||||
|           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> |           <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">35</context> |           <context context-type="linenumber">36</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6886003843406464884" datatype="html"> |       <trans-unit id="6886003843406464884" datatype="html"> | ||||||
|   | |||||||
| @@ -21,7 +21,8 @@ | |||||||
|         <pngx-input-text i18n-title title="Filter to" formControlName="filter_to" [error]="error?.filter_to"></pngx-input-text> |         <pngx-input-text i18n-title title="Filter to" formControlName="filter_to" [error]="error?.filter_to"></pngx-input-text> | ||||||
|         <pngx-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></pngx-input-text> |         <pngx-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></pngx-input-text> | ||||||
|         <pngx-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></pngx-input-text> |         <pngx-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></pngx-input-text> | ||||||
|         <pngx-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"></pngx-input-text> |         <pngx-input-text i18n-title title="Filter attachment filename includes" formControlName="filter_attachment_filename_include" 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_include"></pngx-input-text> | ||||||
|  |         <pngx-input-text i18n-title title="Filter attachment filename excluding" formControlName="filter_attachment_filename_exclude" i18n-hint hint="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename_exclude"></pngx-input-text> | ||||||
|       </div> |       </div> | ||||||
|       <div class="col-md-4"> |       <div class="col-md-4"> | ||||||
|         <pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select> |         <pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select> | ||||||
|   | |||||||
| @@ -158,7 +158,8 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa | |||||||
|       filter_to: 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_include: new FormControl(null), | ||||||
|  |       filter_attachment_filename_exclude: new FormControl(null), | ||||||
|       maximum_age: new FormControl(null), |       maximum_age: new FormControl(null), | ||||||
|       attachment_type: new FormControl(MailFilterAttachmentType.Attachments), |       attachment_type: new FormControl(MailFilterAttachmentType.Attachments), | ||||||
|       consumption_scope: new FormControl(MailRuleConsumptionScope.Attachments), |       consumption_scope: new FormControl(MailRuleConsumptionScope.Attachments), | ||||||
|   | |||||||
| @@ -49,7 +49,9 @@ export interface PaperlessMailRule extends ObjectWithPermissions { | |||||||
|  |  | ||||||
|   filter_body: string |   filter_body: string | ||||||
|  |  | ||||||
|   filter_attachment_filename: string |   filter_attachment_filename_include: string | ||||||
|  |  | ||||||
|  |   filter_attachment_filename_exclude: string | ||||||
|  |  | ||||||
|   maximum_age: number |   maximum_age: number | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,7 +23,8 @@ const mail_rules = [ | |||||||
|     filter_to: null, |     filter_to: null, | ||||||
|     filter_subject: null, |     filter_subject: null, | ||||||
|     filter_body: null, |     filter_body: null, | ||||||
|     filter_attachment_filename: null, |     filter_attachment_filename_include: null, | ||||||
|  |     filter_attachment_filename_exclude: null, | ||||||
|     maximum_age: 30, |     maximum_age: 30, | ||||||
|     attachment_type: MailFilterAttachmentType.Everything, |     attachment_type: MailFilterAttachmentType.Everything, | ||||||
|     action: MailAction.MarkRead, |     action: MailAction.MarkRead, | ||||||
| @@ -40,7 +41,8 @@ const mail_rules = [ | |||||||
|     filter_to: null, |     filter_to: null, | ||||||
|     filter_subject: null, |     filter_subject: null, | ||||||
|     filter_body: null, |     filter_body: null, | ||||||
|     filter_attachment_filename: null, |     filter_attachment_filename_include: null, | ||||||
|  |     filter_attachment_filename_exclude: null, | ||||||
|     maximum_age: 30, |     maximum_age: 30, | ||||||
|     attachment_type: MailFilterAttachmentType.Everything, |     attachment_type: MailFilterAttachmentType.Everything, | ||||||
|     action: MailAction.Delete, |     action: MailAction.Delete, | ||||||
| @@ -57,7 +59,8 @@ const mail_rules = [ | |||||||
|     filter_to: null, |     filter_to: null, | ||||||
|     filter_subject: null, |     filter_subject: null, | ||||||
|     filter_body: null, |     filter_body: null, | ||||||
|     filter_attachment_filename: null, |     filter_attachment_filename_include: null, | ||||||
|  |     filter_attachment_filename_exclude: null, | ||||||
|     maximum_age: 30, |     maximum_age: 30, | ||||||
|     attachment_type: MailFilterAttachmentType.Everything, |     attachment_type: MailFilterAttachmentType.Everything, | ||||||
|     action: MailAction.Flag, |     action: MailAction.Flag, | ||||||
|   | |||||||
| @@ -5769,7 +5769,7 @@ class TestApiConsumptionTemplates(DirectoriesMixin, APITestCase): | |||||||
|             filter_to="someone@somewhere.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_include="file.pdf", | ||||||
|             maximum_age=30, |             maximum_age=30, | ||||||
|             action=MailRule.MailAction.MARK_READ, |             action=MailRule.MailAction.MARK_READ, | ||||||
|             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, |             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ class TestConsumptionTemplates(DirectoriesMixin, FileSystemAssertsMixin, TestCas | |||||||
|             filter_to="someone@somewhere.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_include="file.pdf", | ||||||
|             maximum_age=30, |             maximum_age=30, | ||||||
|             action=MailRule.MailAction.MARK_READ, |             action=MailRule.MailAction.MARK_READ, | ||||||
|             assign_title_from=MailRule.TitleSource.NONE, |             assign_title_from=MailRule.TitleSource.NONE, | ||||||
|   | |||||||
| @@ -68,7 +68,8 @@ class MailRuleAdmin(GuardedModelAdmin): | |||||||
|                     "filter_to", |                     "filter_to", | ||||||
|                     "filter_subject", |                     "filter_subject", | ||||||
|                     "filter_body", |                     "filter_body", | ||||||
|                     "filter_attachment_filename", |                     "filter_attachment_filename_include", | ||||||
|  |                     "filter_attachment_filename_exclude", | ||||||
|                     "maximum_age", |                     "maximum_age", | ||||||
|                     "consumption_scope", |                     "consumption_scope", | ||||||
|                     "attachment_type", |                     "attachment_type", | ||||||
|   | |||||||
| @@ -668,12 +668,29 @@ class MailAccountHandler(LoggingMixin): | |||||||
|                 ) |                 ) | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             if rule.filter_attachment_filename and not fnmatch( |             if rule.filter_attachment_filename_include and not fnmatch( | ||||||
|                 att.filename.lower(), |                 att.filename.lower(), | ||||||
|                 rule.filter_attachment_filename.lower(), |                 rule.filter_attachment_filename_include.lower(), | ||||||
|             ): |             ): | ||||||
|                 # Force the filename and pattern to the lowercase |                 # Force the filename and pattern to the lowercase | ||||||
|                 # as this is system dependent otherwise |                 # as this is system dependent otherwise | ||||||
|  |                 self.log.debug( | ||||||
|  |                     f"Rule {rule}: " | ||||||
|  |                     f"Skipping attachment {att.filename} " | ||||||
|  |                     f"does not match pattern {rule.filter_attachment_filename_include}", | ||||||
|  |                 ) | ||||||
|  |                 continue | ||||||
|  |             elif rule.filter_attachment_filename_exclude and fnmatch( | ||||||
|  |                 att.filename.lower(), | ||||||
|  |                 rule.filter_attachment_filename_exclude.lower(), | ||||||
|  |             ): | ||||||
|  |                 # Force the filename and pattern to the lowercase | ||||||
|  |                 # as this is system dependent otherwise | ||||||
|  |                 self.log.debug( | ||||||
|  |                     f"Rule {rule}: " | ||||||
|  |                     f"Skipping attachment {att.filename} " | ||||||
|  |                     f"does match pattern {rule.filter_attachment_filename_exclude}", | ||||||
|  |                 ) | ||||||
|                 continue |                 continue | ||||||
|  |  | ||||||
|             correspondent = self._get_correspondent(message, rule) |             correspondent = self._get_correspondent(message, rule) | ||||||
|   | |||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | # Generated by Django 4.2.7 on 2023-11-28 17:47 | ||||||
|  |  | ||||||
|  | from django.db import migrations | ||||||
|  | from django.db import models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |     dependencies = [ | ||||||
|  |         ("paperless_mail", "0022_mailrule_assign_owner_from_rule_and_more"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.RenameField( | ||||||
|  |             model_name="mailrule", | ||||||
|  |             old_name="filter_attachment_filename", | ||||||
|  |             new_name="filter_attachment_filename_include", | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="mailrule", | ||||||
|  |             name="filter_attachment_filename_exclude", | ||||||
|  |             field=models.CharField( | ||||||
|  |                 blank=True, | ||||||
|  |                 help_text="Do not consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.", | ||||||
|  |                 max_length=256, | ||||||
|  |                 null=True, | ||||||
|  |                 verbose_name="filter attachment filename exclusive", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -139,8 +139,8 @@ class MailRule(document_models.ModelWithOwner): | |||||||
|         blank=True, |         blank=True, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     filter_attachment_filename = models.CharField( |     filter_attachment_filename_include = models.CharField( | ||||||
|         _("filter attachment filename"), |         _("filter attachment filename inclusive"), | ||||||
|         max_length=256, |         max_length=256, | ||||||
|         null=True, |         null=True, | ||||||
|         blank=True, |         blank=True, | ||||||
| @@ -151,6 +151,18 @@ class MailRule(document_models.ModelWithOwner): | |||||||
|         ), |         ), | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     filter_attachment_filename_exclude = models.CharField( | ||||||
|  |         _("filter attachment filename exclusive"), | ||||||
|  |         max_length=256, | ||||||
|  |         null=True, | ||||||
|  |         blank=True, | ||||||
|  |         help_text=_( | ||||||
|  |             "Do not consume documents which entirely match this " | ||||||
|  |             "filename if specified. Wildcards such as *.pdf or " | ||||||
|  |             "*invoice* are allowed. Case insensitive.", | ||||||
|  |         ), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     maximum_age = models.PositiveIntegerField( |     maximum_age = models.PositiveIntegerField( | ||||||
|         _("maximum age"), |         _("maximum age"), | ||||||
|         default=30, |         default=30, | ||||||
|   | |||||||
| @@ -79,7 +79,8 @@ class MailRuleSerializer(OwnedObjectSerializer): | |||||||
|             "filter_to", |             "filter_to", | ||||||
|             "filter_subject", |             "filter_subject", | ||||||
|             "filter_body", |             "filter_body", | ||||||
|             "filter_attachment_filename", |             "filter_attachment_filename_include", | ||||||
|  |             "filter_attachment_filename_exclude", | ||||||
|             "maximum_age", |             "maximum_age", | ||||||
|             "action", |             "action", | ||||||
|             "action_parameter", |             "action_parameter", | ||||||
|   | |||||||
| @@ -377,7 +377,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): | |||||||
|             filter_to="someone@somewhere.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_include="file.pdf", | ||||||
|             maximum_age=30, |             maximum_age=30, | ||||||
|             action=MailRule.MailAction.MARK_READ, |             action=MailRule.MailAction.MARK_READ, | ||||||
|             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, |             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, | ||||||
| @@ -400,8 +400,8 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): | |||||||
|         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( | ||||||
|             returned_rule1["filter_attachment_filename"], |             returned_rule1["filter_attachment_filename_include"], | ||||||
|             rule1.filter_attachment_filename, |             rule1.filter_attachment_filename_include, | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(returned_rule1["maximum_age"], rule1.maximum_age) |         self.assertEqual(returned_rule1["maximum_age"], rule1.maximum_age) | ||||||
|         self.assertEqual(returned_rule1["action"], rule1.action) |         self.assertEqual(returned_rule1["action"], rule1.action) | ||||||
| @@ -453,7 +453,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): | |||||||
|             "filter_to": "aperson@aplace.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_include": "file.pdf", | ||||||
|             "maximum_age": 30, |             "maximum_age": 30, | ||||||
|             "action": MailRule.MailAction.MARK_READ, |             "action": MailRule.MailAction.MARK_READ, | ||||||
|             "assign_title_from": MailRule.TitleSource.FROM_SUBJECT, |             "assign_title_from": MailRule.TitleSource.FROM_SUBJECT, | ||||||
| @@ -488,8 +488,8 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): | |||||||
|         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( | ||||||
|             returned_rule1["filter_attachment_filename"], |             returned_rule1["filter_attachment_filename_include"], | ||||||
|             rule1["filter_attachment_filename"], |             rule1["filter_attachment_filename_include"], | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(returned_rule1["maximum_age"], rule1["maximum_age"]) |         self.assertEqual(returned_rule1["maximum_age"], rule1["maximum_age"]) | ||||||
|         self.assertEqual(returned_rule1["action"], rule1["action"]) |         self.assertEqual(returned_rule1["action"], rule1["action"]) | ||||||
| @@ -545,7 +545,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): | |||||||
|             filter_from="from@example.com", |             filter_from="from@example.com", | ||||||
|             filter_subject="subject", |             filter_subject="subject", | ||||||
|             filter_body="body", |             filter_body="body", | ||||||
|             filter_attachment_filename="file.pdf", |             filter_attachment_filename_include="file.pdf", | ||||||
|             maximum_age=30, |             maximum_age=30, | ||||||
|             action=MailRule.MailAction.MARK_READ, |             action=MailRule.MailAction.MARK_READ, | ||||||
|             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, |             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, | ||||||
| @@ -589,7 +589,7 @@ class TestAPIMailRules(DirectoriesMixin, APITestCase): | |||||||
|             filter_from="from@example.com", |             filter_from="from@example.com", | ||||||
|             filter_subject="subject", |             filter_subject="subject", | ||||||
|             filter_body="body", |             filter_body="body", | ||||||
|             filter_attachment_filename="file.pdf", |             filter_attachment_filename_include="file.pdf", | ||||||
|             maximum_age=30, |             maximum_age=30, | ||||||
|             action=MailRule.MailAction.MARK_READ, |             action=MailRule.MailAction.MARK_READ, | ||||||
|             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, |             assign_title_from=MailRule.TitleSource.FROM_SUBJECT, | ||||||
|   | |||||||
| @@ -526,6 +526,16 @@ class TestMail( | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def test_filename_filter(self): |     def test_filename_filter(self): | ||||||
|  |         """ | ||||||
|  |         GIVEN: | ||||||
|  |             - Email with multiple similar named attachments | ||||||
|  |             - Rule with inclusive and exclusive filters | ||||||
|  |         WHEN: | ||||||
|  |             - Mail action filtering is checked | ||||||
|  |         THEN: | ||||||
|  |             - Mail action should not be performed for files excluded | ||||||
|  |             - Mail action should be performed for files included | ||||||
|  |         """ | ||||||
|         message = self.create_message( |         message = self.create_message( | ||||||
|             attachments=[ |             attachments=[ | ||||||
|                 _AttachmentDef(filename="f1.pdf"), |                 _AttachmentDef(filename="f1.pdf"), | ||||||
| @@ -537,15 +547,67 @@ class TestMail( | |||||||
|             ], |             ], | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         @dataclasses.dataclass(frozen=True) | ||||||
|  |         class FilterTestCase: | ||||||
|  |             name: str | ||||||
|  |             include_pattern: Optional[str] | ||||||
|  |             exclude_pattern: Optional[str] | ||||||
|  |             expected_matches: list[str] | ||||||
|  |  | ||||||
|         tests = [ |         tests = [ | ||||||
|             ("*.pdf", ["f1.pdf", "f2.pdf", "f3.pdf", "file.PDf", "f1.Pdf"]), |             FilterTestCase( | ||||||
|             ("f1.pdf", ["f1.pdf", "f1.Pdf"]), |                 "PDF Wildcard", | ||||||
|             ("*", ["f1.pdf", "f2.pdf", "f3.pdf", "f2.png", "file.PDf", "f1.Pdf"]), |                 include_pattern="*.pdf", | ||||||
|             ("*.png", ["f2.png"]), |                 exclude_pattern=None, | ||||||
|  |                 expected_matches=["f1.pdf", "f2.pdf", "f3.pdf", "file.PDf", "f1.Pdf"], | ||||||
|  |             ), | ||||||
|  |             FilterTestCase( | ||||||
|  |                 "F1 PDF Only", | ||||||
|  |                 include_pattern="f1.pdf", | ||||||
|  |                 exclude_pattern=None, | ||||||
|  |                 expected_matches=["f1.pdf", "f1.Pdf"], | ||||||
|  |             ), | ||||||
|  |             FilterTestCase( | ||||||
|  |                 "All Files", | ||||||
|  |                 include_pattern="*", | ||||||
|  |                 exclude_pattern=None, | ||||||
|  |                 expected_matches=[ | ||||||
|  |                     "f1.pdf", | ||||||
|  |                     "f2.pdf", | ||||||
|  |                     "f3.pdf", | ||||||
|  |                     "f2.png", | ||||||
|  |                     "file.PDf", | ||||||
|  |                     "f1.Pdf", | ||||||
|  |                 ], | ||||||
|  |             ), | ||||||
|  |             FilterTestCase( | ||||||
|  |                 "PNG Only", | ||||||
|  |                 include_pattern="*.png", | ||||||
|  |                 exclude_pattern=None, | ||||||
|  |                 expected_matches=["f2.png"], | ||||||
|  |             ), | ||||||
|  |             FilterTestCase( | ||||||
|  |                 "PDF Files without f1", | ||||||
|  |                 include_pattern="*.pdf", | ||||||
|  |                 exclude_pattern="f1*", | ||||||
|  |                 expected_matches=["f2.pdf", "f3.pdf", "file.PDf"], | ||||||
|  |             ), | ||||||
|  |             FilterTestCase( | ||||||
|  |                 "All Files, no PNG", | ||||||
|  |                 include_pattern="*", | ||||||
|  |                 exclude_pattern="*.png", | ||||||
|  |                 expected_matches=[ | ||||||
|  |                     "f1.pdf", | ||||||
|  |                     "f2.pdf", | ||||||
|  |                     "f3.pdf", | ||||||
|  |                     "file.PDf", | ||||||
|  |                     "f1.Pdf", | ||||||
|  |                 ], | ||||||
|  |             ), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         for pattern, matches in tests: |         for test_case in tests: | ||||||
|             with self.subTest(msg=pattern): |             with self.subTest(msg=test_case.name): | ||||||
|                 self._queue_consumption_tasks_mock.reset_mock() |                 self._queue_consumption_tasks_mock.reset_mock() | ||||||
|                 account = MailAccount(name=str(uuid.uuid4())) |                 account = MailAccount(name=str(uuid.uuid4())) | ||||||
|                 account.save() |                 account.save() | ||||||
| @@ -553,14 +615,15 @@ class TestMail( | |||||||
|                     name=str(uuid.uuid4()), |                     name=str(uuid.uuid4()), | ||||||
|                     assign_title_from=MailRule.TitleSource.FROM_FILENAME, |                     assign_title_from=MailRule.TitleSource.FROM_FILENAME, | ||||||
|                     account=account, |                     account=account, | ||||||
|                     filter_attachment_filename=pattern, |                     filter_attachment_filename_include=test_case.include_pattern, | ||||||
|  |                     filter_attachment_filename_exclude=test_case.exclude_pattern, | ||||||
|                 ) |                 ) | ||||||
|                 rule.save() |                 rule.save() | ||||||
|  |  | ||||||
|                 self.mail_account_handler._handle_message(message, rule) |                 self.mail_account_handler._handle_message(message, rule) | ||||||
|                 self.assert_queue_consumption_tasks_call_args( |                 self.assert_queue_consumption_tasks_call_args( | ||||||
|                     [ |                     [ | ||||||
|                         [{"override_filename": m} for m in matches], |                         [{"override_filename": m} for m in test_case.expected_matches], | ||||||
|                     ], |                     ], | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
| @@ -593,7 +656,7 @@ class TestMail( | |||||||
|             name=str(uuid.uuid4()), |             name=str(uuid.uuid4()), | ||||||
|             assign_title_from=MailRule.TitleSource.FROM_FILENAME, |             assign_title_from=MailRule.TitleSource.FROM_FILENAME, | ||||||
|             account=account, |             account=account, | ||||||
|             filter_attachment_filename="*.pdf", |             filter_attachment_filename_include="*.pdf", | ||||||
|             attachment_type=MailRule.AttachmentProcessing.EVERYTHING, |             attachment_type=MailRule.AttachmentProcessing.EVERYTHING, | ||||||
|             action=MailRule.MailAction.DELETE, |             action=MailRule.MailAction.DELETE, | ||||||
|         ) |         ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Trenton H
					Trenton H