We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[BUG] Mail rule filter attachment filename is case sensitive
### Description

The "Mail rules" interface says the Filter attachment filename should be case insensitive:

> Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or \*invoice\* are allowed. Case insensitive.

The latest revision of mail.py uses fnmatch, which follows the operating system's rules for case sensitivity:

https://github.com/paperless-ngx/paperless-ngx/blob/a728502988fe26da9aa2844916dcfcc2455d0be2/src/paperless_mail/mail.py#L286-L288

Issue ported from https://github.com/jonaswinkler/paperless-ng/issues/1394

### Expected behavior

Mail attachments should be matched case insensitive to the Filter attachment filename.

### Steps to reproduce

1. Set up paperless to consume from a mailbox
2. Set up a mail rule with a filter attachment filename of *.pdf
3. Send an attachment ending in *.PDF

### Webserver logs

_No response_

### Screenshots

_No response_

### Paperless-ngx version

1.6.0

### Host OS

Oracle Linux 8

### Installation method

Docker

### Browser

_No response_

### Configuration changes

_No response_

### Other

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/paperless_mail/mail.py`
Content:
```
1 import os
2 import tempfile
3 from datetime import date
4 from datetime import timedelta
5 from fnmatch import fnmatch
6 
7 import magic
8 import pathvalidate
9 from django.conf import settings
10 from django.db import DatabaseError
11 from django_q.tasks import async_task
12 from documents.loggers import LoggingMixin
13 from documents.models import Correspondent
14 from documents.parsers import is_mime_type_supported
15 from imap_tools import AND
16 from imap_tools import MailBox
17 from imap_tools import MailboxFolderSelectError
18 from imap_tools import MailBoxUnencrypted
19 from imap_tools import MailMessage
20 from imap_tools import MailMessageFlags
21 from paperless_mail.models import MailAccount
22 from paperless_mail.models import MailRule
23 
24 
25 class MailError(Exception):
26     pass
27 
28 
29 class BaseMailAction:
30     def get_criteria(self):
31         return {}
32 
33     def post_consume(self, M, message_uids, parameter):
34         pass  # pragma: nocover
35 
36 
37 class DeleteMailAction(BaseMailAction):
38     def post_consume(self, M, message_uids, parameter):
39         M.delete(message_uids)
40 
41 
42 class MarkReadMailAction(BaseMailAction):
43     def get_criteria(self):
44         return {"seen": False}
45 
46     def post_consume(self, M, message_uids, parameter):
47         M.flag(message_uids, [MailMessageFlags.SEEN], True)
48 
49 
50 class MoveMailAction(BaseMailAction):
51     def post_consume(self, M, message_uids, parameter):
52         M.move(message_uids, parameter)
53 
54 
55 class FlagMailAction(BaseMailAction):
56     def get_criteria(self):
57         return {"flagged": False}
58 
59     def post_consume(self, M, message_uids, parameter):
60         M.flag(message_uids, [MailMessageFlags.FLAGGED], True)
61 
62 
63 def get_rule_action(rule):
64     if rule.action == MailRule.ACTION_FLAG:
65         return FlagMailAction()
66     elif rule.action == MailRule.ACTION_DELETE:
67         return DeleteMailAction()
68     elif rule.action == MailRule.ACTION_MOVE:
69         return MoveMailAction()
70     elif rule.action == MailRule.ACTION_MARK_READ:
71         return MarkReadMailAction()
72     else:
73         raise NotImplementedError("Unknown action.")  # pragma: nocover
74 
75 
76 def make_criterias(rule):
77     maximum_age = date.today() - timedelta(days=rule.maximum_age)
78     criterias = {}
79     if rule.maximum_age > 0:
80         criterias["date_gte"] = maximum_age
81     if rule.filter_from:
82         criterias["from_"] = rule.filter_from
83     if rule.filter_subject:
84         criterias["subject"] = rule.filter_subject
85     if rule.filter_body:
86         criterias["body"] = rule.filter_body
87 
88     return {**criterias, **get_rule_action(rule).get_criteria()}
89 
90 
91 def get_mailbox(server, port, security):
92     if security == MailAccount.IMAP_SECURITY_NONE:
93         mailbox = MailBoxUnencrypted(server, port)
94     elif security == MailAccount.IMAP_SECURITY_STARTTLS:
95         mailbox = MailBox(server, port, starttls=True)
96     elif security == MailAccount.IMAP_SECURITY_SSL:
97         mailbox = MailBox(server, port)
98     else:
99         raise NotImplementedError("Unknown IMAP security")  # pragma: nocover
100     return mailbox
101 
102 
103 class MailAccountHandler(LoggingMixin):
104 
105     logging_name = "paperless_mail"
106 
107     def _correspondent_from_name(self, name):
108         try:
109             return Correspondent.objects.get_or_create(name=name)[0]
110         except DatabaseError as e:
111             self.log("error", f"Error while retrieving correspondent {name}: {e}")
112             return None
113 
114     def get_title(self, message, att, rule):
115         if rule.assign_title_from == MailRule.TITLE_FROM_SUBJECT:
116             return message.subject
117 
118         elif rule.assign_title_from == MailRule.TITLE_FROM_FILENAME:
119             return os.path.splitext(os.path.basename(att.filename))[0]
120 
121         else:
122             raise NotImplementedError(
123                 "Unknown title selector.",
124             )  # pragma: nocover
125 
126     def get_correspondent(self, message: MailMessage, rule):
127         c_from = rule.assign_correspondent_from
128 
129         if c_from == MailRule.CORRESPONDENT_FROM_NOTHING:
130             return None
131 
132         elif c_from == MailRule.CORRESPONDENT_FROM_EMAIL:
133             return self._correspondent_from_name(message.from_)
134 
135         elif c_from == MailRule.CORRESPONDENT_FROM_NAME:
136             from_values = message.from_values
137             if from_values is not None and len(from_values.name) > 0:
138                 return self._correspondent_from_name(from_values.name)
139             else:
140                 return self._correspondent_from_name(message.from_)
141 
142         elif c_from == MailRule.CORRESPONDENT_FROM_CUSTOM:
143             return rule.assign_correspondent
144 
145         else:
146             raise NotImplementedError(
147                 "Unknwown correspondent selector",
148             )  # pragma: nocover
149 
150     def handle_mail_account(self, account):
151 
152         self.renew_logging_group()
153 
154         self.log("debug", f"Processing mail account {account}")
155 
156         total_processed_files = 0
157 
158         with get_mailbox(
159             account.imap_server,
160             account.imap_port,
161             account.imap_security,
162         ) as M:
163 
164             try:
165                 M.login(account.username, account.password)
166             except Exception:
167                 raise MailError(f"Error while authenticating account {account}")
168 
169             self.log(
170                 "debug",
171                 f"Account {account}: Processing " f"{account.rules.count()} rule(s)",
172             )
173 
174             for rule in account.rules.order_by("order"):
175                 try:
176                     total_processed_files += self.handle_mail_rule(M, rule)
177                 except Exception as e:
178                     self.log(
179                         "error",
180                         f"Rule {rule}: Error while processing rule: {e}",
181                         exc_info=True,
182                     )
183 
184         return total_processed_files
185 
186     def handle_mail_rule(self, M, rule):
187 
188         self.log("debug", f"Rule {rule}: Selecting folder {rule.folder}")
189 
190         try:
191             M.folder.set(rule.folder)
192         except MailboxFolderSelectError:
193             raise MailError(
194                 f"Rule {rule}: Folder {rule.folder} "
195                 f"does not exist in account {rule.account}",
196             )
197 
198         criterias = make_criterias(rule)
199 
200         self.log(
201             "debug",
202             f"Rule {rule}: Searching folder with criteria " f"{str(AND(**criterias))}",
203         )
204 
205         try:
206             messages = M.fetch(
207                 criteria=AND(**criterias),
208                 mark_seen=False,
209                 charset=rule.account.character_set,
210             )
211         except Exception:
212             raise MailError(f"Rule {rule}: Error while fetching folder {rule.folder}")
213 
214         post_consume_messages = []
215 
216         mails_processed = 0
217         total_processed_files = 0
218 
219         for message in messages:
220             try:
221                 processed_files = self.handle_message(message, rule)
222                 if processed_files > 0:
223                     post_consume_messages.append(message.uid)
224 
225                 total_processed_files += processed_files
226                 mails_processed += 1
227             except Exception as e:
228                 self.log(
229                     "error",
230                     f"Rule {rule}: Error while processing mail " f"{message.uid}: {e}",
231                     exc_info=True,
232                 )
233 
234         self.log("debug", f"Rule {rule}: Processed {mails_processed} matching mail(s)")
235 
236         self.log(
237             "debug",
238             f"Rule {rule}: Running mail actions on "
239             f"{len(post_consume_messages)} mails",
240         )
241 
242         try:
243             get_rule_action(rule).post_consume(
244                 M,
245                 post_consume_messages,
246                 rule.action_parameter,
247             )
248 
249         except Exception as e:
250             raise MailError(
251                 f"Rule {rule}: Error while processing post-consume actions: " f"{e}",
252             )
253 
254         return total_processed_files
255 
256     def handle_message(self, message, rule):
257         if not message.attachments:
258             return 0
259 
260         self.log(
261             "debug",
262             f"Rule {rule}: "
263             f"Processing mail {message.subject} from {message.from_} with "
264             f"{len(message.attachments)} attachment(s)",
265         )
266 
267         correspondent = self.get_correspondent(message, rule)
268         tag = rule.assign_tag
269         doc_type = rule.assign_document_type
270 
271         processed_attachments = 0
272 
273         for att in message.attachments:
274 
275             if (
276                 not att.content_disposition == "attachment"
277                 and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY
278             ):
279                 self.log(
280                     "debug",
281                     f"Rule {rule}: "
282                     f"Skipping attachment {att.filename} "
283                     f"with content disposition {att.content_disposition}",
284                 )
285                 continue
286 
287             if rule.filter_attachment_filename:
288                 if not fnmatch(att.filename, rule.filter_attachment_filename):
289                     continue
290 
291             title = self.get_title(message, att, rule)
292 
293             # don't trust the content type of the attachment. Could be
294             # generic application/octet-stream.
295             mime_type = magic.from_buffer(att.payload, mime=True)
296 
297             if is_mime_type_supported(mime_type):
298 
299                 os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
300                 _, temp_filename = tempfile.mkstemp(
301                     prefix="paperless-mail-",
302                     dir=settings.SCRATCH_DIR,
303                 )
304                 with open(temp_filename, "wb") as f:
305                     f.write(att.payload)
306 
307                 self.log(
308                     "info",
309                     f"Rule {rule}: "
310                     f"Consuming attachment {att.filename} from mail "
311                     f"{message.subject} from {message.from_}",
312                 )
313 
314                 async_task(
315                     "documents.tasks.consume_file",
316                     path=temp_filename,
317                     override_filename=pathvalidate.sanitize_filename(
318                         att.filename,
319                     ),
320                     override_title=title,
321                     override_correspondent_id=correspondent.id
322                     if correspondent
323                     else None,
324                     override_document_type_id=doc_type.id if doc_type else None,
325                     override_tag_ids=[tag.id] if tag else None,
326                     task_name=att.filename[:100],
327                 )
328 
329                 processed_attachments += 1
330             else:
331                 self.log(
332                     "debug",
333                     f"Rule {rule}: "
334                     f"Skipping attachment {att.filename} "
335                     f"since guessed mime type {mime_type} is not supported "
336                     f"by paperless",
337                 )
338 
339         return processed_attachments
340
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py
--- a/src/paperless_mail/mail.py
+++ b/src/paperless_mail/mail.py
@@ -253,7 +253,7 @@
 
         return total_processed_files
 
-    def handle_message(self, message, rule):
+    def handle_message(self, message, rule) -> int:
         if not message.attachments:
             return 0
 
@@ -285,7 +285,12 @@
                 continue
 
             if rule.filter_attachment_filename:
-                if not fnmatch(att.filename, rule.filter_attachment_filename):
+                # Force the filename and pattern to the lowercase
+                # as this is system dependent otherwise
+                if not fnmatch(
+                    att.filename.lower(),
+                    rule.filter_attachment_filename.lower(),
+                ):
                     continue
 
             title = self.get_title(message, att, rule)

{"golden_diff": "diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py\n--- a/src/paperless_mail/mail.py\n+++ b/src/paperless_mail/mail.py\n@@ -253,7 +253,7 @@\n \n         return total_processed_files\n \n-    def handle_message(self, message, rule):\n+    def handle_message(self, message, rule) -> int:\n         if not message.attachments:\n             return 0\n \n@@ -285,7 +285,12 @@\n                 continue\n \n             if rule.filter_attachment_filename:\n-                if not fnmatch(att.filename, rule.filter_attachment_filename):\n+                # Force the filename and pattern to the lowercase\n+                # as this is system dependent otherwise\n+                if not fnmatch(\n+                    att.filename.lower(),\n+                    rule.filter_attachment_filename.lower(),\n+                ):\n                     continue\n \n             title = self.get_title(message, att, rule)\n", "issue": "[BUG] Mail rule filter attachment filename is case sensitive\n### Description\r\n\r\nThe \"Mail rules\" interface says the Filter attachment filename should be case insensitive:\r\n\r\n> Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or \\*invoice\\* are allowed. Case insensitive.\r\n\r\nThe latest revision of mail.py uses fnmatch, which follows the operating system's rules for case sensitivity:\r\n\r\nhttps://github.com/paperless-ngx/paperless-ngx/blob/a728502988fe26da9aa2844916dcfcc2455d0be2/src/paperless_mail/mail.py#L286-L288\r\n\r\nIssue ported from https://github.com/jonaswinkler/paperless-ng/issues/1394\r\n\r\n### Expected behavior\r\n\r\nMail attachments should be matched case insensitive to the Filter attachment filename.\r\n\r\n### Steps to reproduce\r\n\r\n1. Set up paperless to consume from a mailbox\r\n2. Set up a mail rule with a filter attachment filename of *.pdf\r\n3. Send an attachment ending in *.PDF\r\n\r\n### Webserver logs\r\n\r\n_No response_\r\n\r\n### Screenshots\r\n\r\n_No response_\r\n\r\n### Paperless-ngx version\r\n\r\n1.6.0\r\n\r\n### Host OS\r\n\r\nOracle Linux 8\r\n\r\n### Installation method\r\n\r\nDocker\r\n\r\n### Browser\r\n\r\n_No response_\r\n\r\n### Configuration changes\r\n\r\n_No response_\r\n\r\n### Other\r\n\r\n_No response_\n", "before_files": [{"content": "import os\nimport tempfile\nfrom datetime import date\nfrom datetime import timedelta\nfrom fnmatch import fnmatch\n\nimport magic\nimport pathvalidate\nfrom django.conf import settings\nfrom django.db import DatabaseError\nfrom django_q.tasks import async_task\nfrom documents.loggers import LoggingMixin\nfrom documents.models import Correspondent\nfrom documents.parsers import is_mime_type_supported\nfrom imap_tools import AND\nfrom imap_tools import MailBox\nfrom imap_tools import MailboxFolderSelectError\nfrom imap_tools import MailBoxUnencrypted\nfrom imap_tools import MailMessage\nfrom imap_tools import MailMessageFlags\nfrom paperless_mail.models import MailAccount\nfrom paperless_mail.models import MailRule\n\n\nclass MailError(Exception):\n    pass\n\n\nclass BaseMailAction:\n    def get_criteria(self):\n        return {}\n\n    def post_consume(self, M, message_uids, parameter):\n        pass  # pragma: nocover\n\n\nclass DeleteMailAction(BaseMailAction):\n    def post_consume(self, M, message_uids, parameter):\n        M.delete(message_uids)\n\n\nclass MarkReadMailAction(BaseMailAction):\n    def get_criteria(self):\n        return {\"seen\": False}\n\n    def post_consume(self, M, message_uids, parameter):\n        M.flag(message_uids, [MailMessageFlags.SEEN], True)\n\n\nclass MoveMailAction(BaseMailAction):\n    def post_consume(self, M, message_uids, parameter):\n        M.move(message_uids, parameter)\n\n\nclass FlagMailAction(BaseMailAction):\n    def get_criteria(self):\n        return {\"flagged\": False}\n\n    def post_consume(self, M, message_uids, parameter):\n        M.flag(message_uids, [MailMessageFlags.FLAGGED], True)\n\n\ndef get_rule_action(rule):\n    if rule.action == MailRule.ACTION_FLAG:\n        return FlagMailAction()\n    elif rule.action == MailRule.ACTION_DELETE:\n        return DeleteMailAction()\n    elif rule.action == MailRule.ACTION_MOVE:\n        return MoveMailAction()\n    elif rule.action == MailRule.ACTION_MARK_READ:\n        return MarkReadMailAction()\n    else:\n        raise NotImplementedError(\"Unknown action.\")  # pragma: nocover\n\n\ndef make_criterias(rule):\n    maximum_age = date.today() - timedelta(days=rule.maximum_age)\n    criterias = {}\n    if rule.maximum_age > 0:\n        criterias[\"date_gte\"] = maximum_age\n    if rule.filter_from:\n        criterias[\"from_\"] = rule.filter_from\n    if rule.filter_subject:\n        criterias[\"subject\"] = rule.filter_subject\n    if rule.filter_body:\n        criterias[\"body\"] = rule.filter_body\n\n    return {**criterias, **get_rule_action(rule).get_criteria()}\n\n\ndef get_mailbox(server, port, security):\n    if security == MailAccount.IMAP_SECURITY_NONE:\n        mailbox = MailBoxUnencrypted(server, port)\n    elif security == MailAccount.IMAP_SECURITY_STARTTLS:\n        mailbox = MailBox(server, port, starttls=True)\n    elif security == MailAccount.IMAP_SECURITY_SSL:\n        mailbox = MailBox(server, port)\n    else:\n        raise NotImplementedError(\"Unknown IMAP security\")  # pragma: nocover\n    return mailbox\n\n\nclass MailAccountHandler(LoggingMixin):\n\n    logging_name = \"paperless_mail\"\n\n    def _correspondent_from_name(self, name):\n        try:\n            return Correspondent.objects.get_or_create(name=name)[0]\n        except DatabaseError as e:\n            self.log(\"error\", f\"Error while retrieving correspondent {name}: {e}\")\n            return None\n\n    def get_title(self, message, att, rule):\n        if rule.assign_title_from == MailRule.TITLE_FROM_SUBJECT:\n            return message.subject\n\n        elif rule.assign_title_from == MailRule.TITLE_FROM_FILENAME:\n            return os.path.splitext(os.path.basename(att.filename))[0]\n\n        else:\n            raise NotImplementedError(\n                \"Unknown title selector.\",\n            )  # pragma: nocover\n\n    def get_correspondent(self, message: MailMessage, rule):\n        c_from = rule.assign_correspondent_from\n\n        if c_from == MailRule.CORRESPONDENT_FROM_NOTHING:\n            return None\n\n        elif c_from == MailRule.CORRESPONDENT_FROM_EMAIL:\n            return self._correspondent_from_name(message.from_)\n\n        elif c_from == MailRule.CORRESPONDENT_FROM_NAME:\n            from_values = message.from_values\n            if from_values is not None and len(from_values.name) > 0:\n                return self._correspondent_from_name(from_values.name)\n            else:\n                return self._correspondent_from_name(message.from_)\n\n        elif c_from == MailRule.CORRESPONDENT_FROM_CUSTOM:\n            return rule.assign_correspondent\n\n        else:\n            raise NotImplementedError(\n                \"Unknwown correspondent selector\",\n            )  # pragma: nocover\n\n    def handle_mail_account(self, account):\n\n        self.renew_logging_group()\n\n        self.log(\"debug\", f\"Processing mail account {account}\")\n\n        total_processed_files = 0\n\n        with get_mailbox(\n            account.imap_server,\n            account.imap_port,\n            account.imap_security,\n        ) as M:\n\n            try:\n                M.login(account.username, account.password)\n            except Exception:\n                raise MailError(f\"Error while authenticating account {account}\")\n\n            self.log(\n                \"debug\",\n                f\"Account {account}: Processing \" f\"{account.rules.count()} rule(s)\",\n            )\n\n            for rule in account.rules.order_by(\"order\"):\n                try:\n                    total_processed_files += self.handle_mail_rule(M, rule)\n                except Exception as e:\n                    self.log(\n                        \"error\",\n                        f\"Rule {rule}: Error while processing rule: {e}\",\n                        exc_info=True,\n                    )\n\n        return total_processed_files\n\n    def handle_mail_rule(self, M, rule):\n\n        self.log(\"debug\", f\"Rule {rule}: Selecting folder {rule.folder}\")\n\n        try:\n            M.folder.set(rule.folder)\n        except MailboxFolderSelectError:\n            raise MailError(\n                f\"Rule {rule}: Folder {rule.folder} \"\n                f\"does not exist in account {rule.account}\",\n            )\n\n        criterias = make_criterias(rule)\n\n        self.log(\n            \"debug\",\n            f\"Rule {rule}: Searching folder with criteria \" f\"{str(AND(**criterias))}\",\n        )\n\n        try:\n            messages = M.fetch(\n                criteria=AND(**criterias),\n                mark_seen=False,\n                charset=rule.account.character_set,\n            )\n        except Exception:\n            raise MailError(f\"Rule {rule}: Error while fetching folder {rule.folder}\")\n\n        post_consume_messages = []\n\n        mails_processed = 0\n        total_processed_files = 0\n\n        for message in messages:\n            try:\n                processed_files = self.handle_message(message, rule)\n                if processed_files > 0:\n                    post_consume_messages.append(message.uid)\n\n                total_processed_files += processed_files\n                mails_processed += 1\n            except Exception as e:\n                self.log(\n                    \"error\",\n                    f\"Rule {rule}: Error while processing mail \" f\"{message.uid}: {e}\",\n                    exc_info=True,\n                )\n\n        self.log(\"debug\", f\"Rule {rule}: Processed {mails_processed} matching mail(s)\")\n\n        self.log(\n            \"debug\",\n            f\"Rule {rule}: Running mail actions on \"\n            f\"{len(post_consume_messages)} mails\",\n        )\n\n        try:\n            get_rule_action(rule).post_consume(\n                M,\n                post_consume_messages,\n                rule.action_parameter,\n            )\n\n        except Exception as e:\n            raise MailError(\n                f\"Rule {rule}: Error while processing post-consume actions: \" f\"{e}\",\n            )\n\n        return total_processed_files\n\n    def handle_message(self, message, rule):\n        if not message.attachments:\n            return 0\n\n        self.log(\n            \"debug\",\n            f\"Rule {rule}: \"\n            f\"Processing mail {message.subject} from {message.from_} with \"\n            f\"{len(message.attachments)} attachment(s)\",\n        )\n\n        correspondent = self.get_correspondent(message, rule)\n        tag = rule.assign_tag\n        doc_type = rule.assign_document_type\n\n        processed_attachments = 0\n\n        for att in message.attachments:\n\n            if (\n                not att.content_disposition == \"attachment\"\n                and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY\n            ):\n                self.log(\n                    \"debug\",\n                    f\"Rule {rule}: \"\n                    f\"Skipping attachment {att.filename} \"\n                    f\"with content disposition {att.content_disposition}\",\n                )\n                continue\n\n            if rule.filter_attachment_filename:\n                if not fnmatch(att.filename, rule.filter_attachment_filename):\n                    continue\n\n            title = self.get_title(message, att, rule)\n\n            # don't trust the content type of the attachment. Could be\n            # generic application/octet-stream.\n            mime_type = magic.from_buffer(att.payload, mime=True)\n\n            if is_mime_type_supported(mime_type):\n\n                os.makedirs(settings.SCRATCH_DIR, exist_ok=True)\n                _, temp_filename = tempfile.mkstemp(\n                    prefix=\"paperless-mail-\",\n                    dir=settings.SCRATCH_DIR,\n                )\n                with open(temp_filename, \"wb\") as f:\n                    f.write(att.payload)\n\n                self.log(\n                    \"info\",\n                    f\"Rule {rule}: \"\n                    f\"Consuming attachment {att.filename} from mail \"\n                    f\"{message.subject} from {message.from_}\",\n                )\n\n                async_task(\n                    \"documents.tasks.consume_file\",\n                    path=temp_filename,\n                    override_filename=pathvalidate.sanitize_filename(\n                        att.filename,\n                    ),\n                    override_title=title,\n                    override_correspondent_id=correspondent.id\n                    if correspondent\n                    else None,\n                    override_document_type_id=doc_type.id if doc_type else None,\n                    override_tag_ids=[tag.id] if tag else None,\n                    task_name=att.filename[:100],\n                )\n\n                processed_attachments += 1\n            else:\n                self.log(\n                    \"debug\",\n                    f\"Rule {rule}: \"\n                    f\"Skipping attachment {att.filename} \"\n                    f\"since guessed mime type {mime_type} is not supported \"\n                    f\"by paperless\",\n                )\n\n        return processed_attachments\n", "path": "src/paperless_mail/mail.py"}], "after_files": [{"content": "import os\nimport tempfile\nfrom datetime import date\nfrom datetime import timedelta\nfrom fnmatch import fnmatch\n\nimport magic\nimport pathvalidate\nfrom django.conf import settings\nfrom django.db import DatabaseError\nfrom django_q.tasks import async_task\nfrom documents.loggers import LoggingMixin\nfrom documents.models import Correspondent\nfrom documents.parsers import is_mime_type_supported\nfrom imap_tools import AND\nfrom imap_tools import MailBox\nfrom imap_tools import MailboxFolderSelectError\nfrom imap_tools import MailBoxUnencrypted\nfrom imap_tools import MailMessage\nfrom imap_tools import MailMessageFlags\nfrom paperless_mail.models import MailAccount\nfrom paperless_mail.models import MailRule\n\n\nclass MailError(Exception):\n    pass\n\n\nclass BaseMailAction:\n    def get_criteria(self):\n        return {}\n\n    def post_consume(self, M, message_uids, parameter):\n        pass  # pragma: nocover\n\n\nclass DeleteMailAction(BaseMailAction):\n    def post_consume(self, M, message_uids, parameter):\n        M.delete(message_uids)\n\n\nclass MarkReadMailAction(BaseMailAction):\n    def get_criteria(self):\n        return {\"seen\": False}\n\n    def post_consume(self, M, message_uids, parameter):\n        M.flag(message_uids, [MailMessageFlags.SEEN], True)\n\n\nclass MoveMailAction(BaseMailAction):\n    def post_consume(self, M, message_uids, parameter):\n        M.move(message_uids, parameter)\n\n\nclass FlagMailAction(BaseMailAction):\n    def get_criteria(self):\n        return {\"flagged\": False}\n\n    def post_consume(self, M, message_uids, parameter):\n        M.flag(message_uids, [MailMessageFlags.FLAGGED], True)\n\n\ndef get_rule_action(rule):\n    if rule.action == MailRule.ACTION_FLAG:\n        return FlagMailAction()\n    elif rule.action == MailRule.ACTION_DELETE:\n        return DeleteMailAction()\n    elif rule.action == MailRule.ACTION_MOVE:\n        return MoveMailAction()\n    elif rule.action == MailRule.ACTION_MARK_READ:\n        return MarkReadMailAction()\n    else:\n        raise NotImplementedError(\"Unknown action.\")  # pragma: nocover\n\n\ndef make_criterias(rule):\n    maximum_age = date.today() - timedelta(days=rule.maximum_age)\n    criterias = {}\n    if rule.maximum_age > 0:\n        criterias[\"date_gte\"] = maximum_age\n    if rule.filter_from:\n        criterias[\"from_\"] = rule.filter_from\n    if rule.filter_subject:\n        criterias[\"subject\"] = rule.filter_subject\n    if rule.filter_body:\n        criterias[\"body\"] = rule.filter_body\n\n    return {**criterias, **get_rule_action(rule).get_criteria()}\n\n\ndef get_mailbox(server, port, security):\n    if security == MailAccount.IMAP_SECURITY_NONE:\n        mailbox = MailBoxUnencrypted(server, port)\n    elif security == MailAccount.IMAP_SECURITY_STARTTLS:\n        mailbox = MailBox(server, port, starttls=True)\n    elif security == MailAccount.IMAP_SECURITY_SSL:\n        mailbox = MailBox(server, port)\n    else:\n        raise NotImplementedError(\"Unknown IMAP security\")  # pragma: nocover\n    return mailbox\n\n\nclass MailAccountHandler(LoggingMixin):\n\n    logging_name = \"paperless_mail\"\n\n    def _correspondent_from_name(self, name):\n        try:\n            return Correspondent.objects.get_or_create(name=name)[0]\n        except DatabaseError as e:\n            self.log(\"error\", f\"Error while retrieving correspondent {name}: {e}\")\n            return None\n\n    def get_title(self, message, att, rule):\n        if rule.assign_title_from == MailRule.TITLE_FROM_SUBJECT:\n            return message.subject\n\n        elif rule.assign_title_from == MailRule.TITLE_FROM_FILENAME:\n            return os.path.splitext(os.path.basename(att.filename))[0]\n\n        else:\n            raise NotImplementedError(\n                \"Unknown title selector.\",\n            )  # pragma: nocover\n\n    def get_correspondent(self, message: MailMessage, rule):\n        c_from = rule.assign_correspondent_from\n\n        if c_from == MailRule.CORRESPONDENT_FROM_NOTHING:\n            return None\n\n        elif c_from == MailRule.CORRESPONDENT_FROM_EMAIL:\n            return self._correspondent_from_name(message.from_)\n\n        elif c_from == MailRule.CORRESPONDENT_FROM_NAME:\n            from_values = message.from_values\n            if from_values is not None and len(from_values.name) > 0:\n                return self._correspondent_from_name(from_values.name)\n            else:\n                return self._correspondent_from_name(message.from_)\n\n        elif c_from == MailRule.CORRESPONDENT_FROM_CUSTOM:\n            return rule.assign_correspondent\n\n        else:\n            raise NotImplementedError(\n                \"Unknwown correspondent selector\",\n            )  # pragma: nocover\n\n    def handle_mail_account(self, account):\n\n        self.renew_logging_group()\n\n        self.log(\"debug\", f\"Processing mail account {account}\")\n\n        total_processed_files = 0\n\n        with get_mailbox(\n            account.imap_server,\n            account.imap_port,\n            account.imap_security,\n        ) as M:\n\n            try:\n                M.login(account.username, account.password)\n            except Exception:\n                raise MailError(f\"Error while authenticating account {account}\")\n\n            self.log(\n                \"debug\",\n                f\"Account {account}: Processing \" f\"{account.rules.count()} rule(s)\",\n            )\n\n            for rule in account.rules.order_by(\"order\"):\n                try:\n                    total_processed_files += self.handle_mail_rule(M, rule)\n                except Exception as e:\n                    self.log(\n                        \"error\",\n                        f\"Rule {rule}: Error while processing rule: {e}\",\n                        exc_info=True,\n                    )\n\n        return total_processed_files\n\n    def handle_mail_rule(self, M, rule):\n\n        self.log(\"debug\", f\"Rule {rule}: Selecting folder {rule.folder}\")\n\n        try:\n            M.folder.set(rule.folder)\n        except MailboxFolderSelectError:\n            raise MailError(\n                f\"Rule {rule}: Folder {rule.folder} \"\n                f\"does not exist in account {rule.account}\",\n            )\n\n        criterias = make_criterias(rule)\n\n        self.log(\n            \"debug\",\n            f\"Rule {rule}: Searching folder with criteria \" f\"{str(AND(**criterias))}\",\n        )\n\n        try:\n            messages = M.fetch(\n                criteria=AND(**criterias),\n                mark_seen=False,\n                charset=rule.account.character_set,\n            )\n        except Exception:\n            raise MailError(f\"Rule {rule}: Error while fetching folder {rule.folder}\")\n\n        post_consume_messages = []\n\n        mails_processed = 0\n        total_processed_files = 0\n\n        for message in messages:\n            try:\n                processed_files = self.handle_message(message, rule)\n                if processed_files > 0:\n                    post_consume_messages.append(message.uid)\n\n                total_processed_files += processed_files\n                mails_processed += 1\n            except Exception as e:\n                self.log(\n                    \"error\",\n                    f\"Rule {rule}: Error while processing mail \" f\"{message.uid}: {e}\",\n                    exc_info=True,\n                )\n\n        self.log(\"debug\", f\"Rule {rule}: Processed {mails_processed} matching mail(s)\")\n\n        self.log(\n            \"debug\",\n            f\"Rule {rule}: Running mail actions on \"\n            f\"{len(post_consume_messages)} mails\",\n        )\n\n        try:\n            get_rule_action(rule).post_consume(\n                M,\n                post_consume_messages,\n                rule.action_parameter,\n            )\n\n        except Exception as e:\n            raise MailError(\n                f\"Rule {rule}: Error while processing post-consume actions: \" f\"{e}\",\n            )\n\n        return total_processed_files\n\n    def handle_message(self, message, rule) -> int:\n        if not message.attachments:\n            return 0\n\n        self.log(\n            \"debug\",\n            f\"Rule {rule}: \"\n            f\"Processing mail {message.subject} from {message.from_} with \"\n            f\"{len(message.attachments)} attachment(s)\",\n        )\n\n        correspondent = self.get_correspondent(message, rule)\n        tag = rule.assign_tag\n        doc_type = rule.assign_document_type\n\n        processed_attachments = 0\n\n        for att in message.attachments:\n\n            if (\n                not att.content_disposition == \"attachment\"\n                and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY\n            ):\n                self.log(\n                    \"debug\",\n                    f\"Rule {rule}: \"\n                    f\"Skipping attachment {att.filename} \"\n                    f\"with content disposition {att.content_disposition}\",\n                )\n                continue\n\n            if rule.filter_attachment_filename:\n                # Force the filename and pattern to the lowercase\n                # as this is system dependent otherwise\n                if not fnmatch(\n                    att.filename.lower(),\n                    rule.filter_attachment_filename.lower(),\n                ):\n                    continue\n\n            title = self.get_title(message, att, rule)\n\n            # don't trust the content type of the attachment. Could be\n            # generic application/octet-stream.\n            mime_type = magic.from_buffer(att.payload, mime=True)\n\n            if is_mime_type_supported(mime_type):\n\n                os.makedirs(settings.SCRATCH_DIR, exist_ok=True)\n                _, temp_filename = tempfile.mkstemp(\n                    prefix=\"paperless-mail-\",\n                    dir=settings.SCRATCH_DIR,\n                )\n                with open(temp_filename, \"wb\") as f:\n                    f.write(att.payload)\n\n                self.log(\n                    \"info\",\n                    f\"Rule {rule}: \"\n                    f\"Consuming attachment {att.filename} from mail \"\n                    f\"{message.subject} from {message.from_}\",\n                )\n\n                async_task(\n                    \"documents.tasks.consume_file\",\n                    path=temp_filename,\n                    override_filename=pathvalidate.sanitize_filename(\n                        att.filename,\n                    ),\n                    override_title=title,\n                    override_correspondent_id=correspondent.id\n                    if correspondent\n                    else None,\n                    override_document_type_id=doc_type.id if doc_type else None,\n                    override_tag_ids=[tag.id] if tag else None,\n                    task_name=att.filename[:100],\n                )\n\n                processed_attachments += 1\n            else:\n                self.log(\n                    \"debug\",\n                    f\"Rule {rule}: \"\n                    f\"Skipping attachment {att.filename} \"\n                    f\"since guessed mime type {mime_type} is not supported \"\n                    f\"by paperless\",\n                )\n\n        return processed_attachments\n", "path": "src/paperless_mail/mail.py"}]}