Compare commits

..
Author SHA1 Message Date
stumpylog 83051c33f3 Bump dateparser to 1.4.1 2026-07-15 15:02:13 -07:00
stumpylog 754c5458a4 Bump nltk to 3.10.0 2026-07-15 12:50:57 -07:00
stumpylog 804d179d13 Bump transformers to 5.13.1 2026-07-15 09:36:38 -07:00
stumpylog d74adf1fef Bump daphne to 4.2.2 2026-07-15 08:59:51 -07:00
stumpylog 0d4ea4c03b Bump django to 5.2.16 2026-07-15 08:58:38 -07:00
stumpylog 171b5a6a34 Few more before 2026-07-15 08:49:41 -07:00
stumpylog 95c133d8fc Even more vulns 2026-07-15 08:49:39 -07:00
14 changed files with 523 additions and 776 deletions
+1 -5
View File
@@ -30,7 +30,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs
# Comments:
# - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.11.28-python3.12-trixie-slim AS s6-overlay-base
FROM ghcr.io/astral-sh/uv:0.11.19-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6
@@ -238,10 +238,6 @@ RUN set -eux \
&& chown --from root:root --changes --recursive paperless:paperless /usr/src/paperless \
&& echo "Making fontconfig cache writable for arbitrary container UIDs" \
&& chmod 1777 /var/cache/fontconfig \
&& echo "Making /run world-writable for rootless operation" \
&& chmod 1777 /run \
&& echo "Removing setuid from s6-overlay-suexec for rootless compat" \
&& chmod u-s /command/s6-overlay-suexec \
&& echo "Collecting static files" \
&& PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py collectstatic --clear --no-input --link \
&& PAPERLESS_SECRET_KEY=build-time-dummy s6-setuidgid paperless python3 manage.py compilemessages \
+3 -3
View File
@@ -4,7 +4,7 @@
# correct networking for the tests
services:
gotenberg:
image: docker.io/gotenberg/gotenberg:8.34
image: docker.io/gotenberg/gotenberg:8.33
hostname: gotenberg
container_name: gotenberg
network_mode: host
@@ -24,7 +24,7 @@ services:
network_mode: host
restart: unless-stopped
greenmail:
image: docker.io/greenmail/standalone:2.1.9
image: docker.io/greenmail/standalone:2.1.8
hostname: greenmail
container_name: greenmail
environment:
@@ -35,7 +35,7 @@ services:
- "3143:3143" # IMAP
restart: unless-stopped
nginx:
image: docker.io/nginx:1.31.2-alpine
image: docker.io/nginx:1.31.1-alpine
hostname: nginx
container_name: nginx
ports:
@@ -72,7 +72,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.34
image: docker.io/gotenberg/gotenberg:8.33
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
@@ -67,7 +67,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.34
image: docker.io/gotenberg/gotenberg:8.33
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
@@ -56,7 +56,7 @@ services:
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: docker.io/gotenberg/gotenberg:8.34
image: docker.io/gotenberg/gotenberg:8.33
restart: unless-stopped
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
+1 -1
View File
@@ -142,7 +142,7 @@ a [superuser](usage.md#superusers) account.
It is not possible to run the container rootless if additional languages are specified via `PAPERLESS_OCR_LANGUAGES`.
If you want to run Paperless as a rootless container, set `user:` in `docker-compose.yml` to the UID and GID of your host user (use `id -u` and `id -g` to find these values). The container process starts directly as that user with no internal privilege remapping. Any UID and GID is supported:
If you want to run Paperless as a rootless container, set `user:` in `docker-compose.yml` to the UID and GID of your host user (use `id -u` and `id -g` to find these values). The container process starts directly as that user with no internal privilege remapping:
```yaml
webserver:
+9 -9
View File
@@ -16,12 +16,12 @@ classifiers = [
dependencies = [
"azure-ai-documentintelligence>=1.0.2",
"babel>=2.17",
"bleach~=6.3.0",
"bleach~=6.4.0",
"celery[redis]~=5.6.2",
"channels~=4.2",
"channels-redis~=4.2",
"concurrent-log-handler~=0.9.25",
"dateparser~=1.4",
"dateparser~=1.2",
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.13",
@@ -32,7 +32,7 @@ dependencies = [
"django-cors-headers~=4.9.0",
"django-extensions~=4.1",
"django-filter~=25.1",
"django-guardian~=3.3.2",
"django-guardian~=3.3.0",
"django-multiselectfield~=1.0.1",
"django-rich~=2.2.0",
"django-soft-delete~=1.0.18",
@@ -42,21 +42,21 @@ dependencies = [
"drf-spectacular~=0.28",
"drf-spectacular-sidecar~=2026.5.1",
"drf-writable-nested~=0.7.1",
"filelock~=3.29.7",
"filelock~=3.29.0",
"flower~=2.0.1",
"gotenberg-client~=0.14.0",
"httpx-oauth~=0.16",
"ijson>=3.5.1",
"ijson>=3.2",
"imap-tools~=1.13.0",
"jinja2~=3.1.5",
"langdetect~=1.0.9",
"llama-index-core>=0.14.23",
"llama-index-core>=0.14.22",
"llama-index-embeddings-huggingface>=0.6.1",
"llama-index-embeddings-ollama>=0.9",
"llama-index-embeddings-openai-like>=0.2.2",
"llama-index-llms-ollama>=0.9.1",
"llama-index-llms-openai-like>=0.7.1",
"nltk~=3.9.1",
"nltk~=3.10.0",
"ocrmypdf~=17.4.2",
"openai>=2.32",
"pathvalidate~=3.3.1",
@@ -101,11 +101,11 @@ dev = [
{ include-group = "testing" },
]
docs = [
"zensical>=0.0.47",
"zensical>=0.0.43",
]
lint = [
"prek~=0.3.10",
"ruff~=0.15.20",
"ruff~=0.15.15",
]
testing = [
"daphne",
+37 -37
View File
@@ -379,11 +379,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">273</context>
<context context-type="linenumber">272</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">275</context>
<context context-type="linenumber">274</context>
</context-group>
</trans-unit>
<trans-unit id="5890330709052835856" datatype="html">
@@ -724,11 +724,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">308</context>
<context context-type="linenumber">307</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">311</context>
<context context-type="linenumber">310</context>
</context-group>
</trans-unit>
<trans-unit id="2272120016352772836" datatype="html">
@@ -1135,11 +1135,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">233</context>
<context context-type="linenumber">232</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">235</context>
<context context-type="linenumber">234</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
@@ -1696,7 +1696,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">204</context>
<context context-type="linenumber">203</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context>
@@ -1801,11 +1801,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">296</context>
<context context-type="linenumber">295</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">298</context>
<context context-type="linenumber">297</context>
</context-group>
</trans-unit>
<trans-unit id="8492095365580052820" datatype="html">
@@ -2513,11 +2513,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">256</context>
<context context-type="linenumber">255</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">259</context>
<context context-type="linenumber">258</context>
</context-group>
</trans-unit>
<trans-unit id="3818027200170621545" datatype="html">
@@ -2870,11 +2870,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">287</context>
<context context-type="linenumber">286</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">289</context>
<context context-type="linenumber">288</context>
</context-group>
</trans-unit>
<trans-unit id="4569276013106377105" datatype="html">
@@ -3211,11 +3211,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">317</context>
<context context-type="linenumber">316</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">320</context>
<context context-type="linenumber">319</context>
</context-group>
</trans-unit>
<trans-unit id="472206565520537964" datatype="html">
@@ -3240,36 +3240,36 @@
<source>Close all</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">163</context>
<context context-type="linenumber">162</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">165</context>
<context context-type="linenumber">164</context>
</context-group>
</trans-unit>
<trans-unit id="3897348120591552265" datatype="html">
<source>Manage</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">174</context>
<context context-type="linenumber">173</context>
</context-group>
</trans-unit>
<trans-unit id="8008131619909556709" datatype="html">
<source>Attributes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">181</context>
<context context-type="linenumber">180</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">183</context>
<context context-type="linenumber">182</context>
</context-group>
</trans-unit>
<trans-unit id="7437910965833684826" datatype="html">
<source>Correspondents</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">209</context>
<context context-type="linenumber">208</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/suggestions-dropdown/suggestions-dropdown.component.html</context>
@@ -3288,7 +3288,7 @@
<source>Document types</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">214</context>
<context context-type="linenumber">213</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
@@ -3299,7 +3299,7 @@
<source>Storage paths</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">219</context>
<context context-type="linenumber">218</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
@@ -3310,7 +3310,7 @@
<source>Custom fields</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">224</context>
<context context-type="linenumber">223</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
@@ -3329,11 +3329,11 @@
<source>Workflows</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">242</context>
<context context-type="linenumber">241</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">244</context>
<context context-type="linenumber">243</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
@@ -3344,71 +3344,71 @@
<source>Mail</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">249</context>
<context context-type="linenumber">248</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">252</context>
<context context-type="linenumber">251</context>
</context-group>
</trans-unit>
<trans-unit id="7844706011418789951" datatype="html">
<source>Administration</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">267</context>
<context context-type="linenumber">266</context>
</context-group>
</trans-unit>
<trans-unit id="3008420115644088420" datatype="html">
<source>Configuration</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">280</context>
<context context-type="linenumber">279</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">282</context>
<context context-type="linenumber">281</context>
</context-group>
</trans-unit>
<trans-unit id="1534029177398918729" datatype="html">
<source>GitHub</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">327</context>
<context context-type="linenumber">326</context>
</context-group>
</trans-unit>
<trans-unit id="4112664765954374539" datatype="html">
<source>is available.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">336,337</context>
<context context-type="linenumber">335,336</context>
</context-group>
</trans-unit>
<trans-unit id="1175891574282637937" datatype="html">
<source>Click to view.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">337</context>
<context context-type="linenumber">336</context>
</context-group>
</trans-unit>
<trans-unit id="9811291095862612" datatype="html">
<source>Paperless-ngx can automatically check for updates</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">341</context>
<context context-type="linenumber">340</context>
</context-group>
</trans-unit>
<trans-unit id="894819944961861800" datatype="html">
<source> How does this work? </source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">348,350</context>
<context context-type="linenumber">347,349</context>
</context-group>
</trans-unit>
<trans-unit id="509090351011426949" datatype="html">
<source>Update available</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
<context context-type="linenumber">361</context>
<context context-type="linenumber">360</context>
</context-group>
</trans-unit>
<trans-unit id="1542489069631984294" datatype="html">
+5 -9
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 20:44+0000\n"
"POT-Creation-Date: 2026-07-11 23:04+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -2433,25 +2433,21 @@ msgid "uid"
msgstr ""
#: paperless_mail/models.py:342
msgid "uid validity"
msgstr ""
#: paperless_mail/models.py:350
msgid "subject"
msgstr ""
#: paperless_mail/models.py:358
#: paperless_mail/models.py:350
msgid "received"
msgstr ""
#: paperless_mail/models.py:365
#: paperless_mail/models.py:357
msgid "processed"
msgstr ""
#: paperless_mail/models.py:371
#: paperless_mail/models.py:363
msgid "status"
msgstr ""
#: paperless_mail/models.py:379
#: paperless_mail/models.py:371
msgid "error"
msgstr ""
+2 -39
View File
@@ -18,7 +18,6 @@ from celery import shared_task
from celery.canvas import Signature
from django.conf import settings
from django.db import DatabaseError
from django.db.models import Q
from django.utils import timezone
from django.utils.timezone import is_naive
from django.utils.timezone import make_aware
@@ -246,7 +245,6 @@ def apply_mail_action(
message_uid: str,
message_subject: str,
message_date: datetime.datetime,
uid_validity: str | None = None,
) -> None:
"""
This shared task applies the mail action of a particular mail rule to the
@@ -288,7 +286,6 @@ def apply_mail_action(
rule=rule,
folder=rule.folder,
uid=message_uid,
uid_validity=uid_validity,
subject=message_subject,
received=message_date,
status="SUCCESS",
@@ -300,7 +297,6 @@ def apply_mail_action(
rule=rule,
folder=rule.folder,
uid=message_uid,
uid_validity=uid_validity,
subject=message_subject,
received=message_date,
status="FAILED",
@@ -318,7 +314,6 @@ def error_callback(
message_uid: str,
message_subject: str,
message_date: datetime.datetime,
uid_validity: str | None = None,
) -> None:
"""
A shared task that is called whenever something goes wrong during
@@ -330,7 +325,6 @@ def error_callback(
rule=rule,
folder=rule.folder,
uid=message_uid,
uid_validity=uid_validity,
subject=message_subject,
received=make_aware(message_date) if is_naive(message_date) else message_date,
status="FAILED",
@@ -343,7 +337,6 @@ def queue_consumption_tasks(
consume_tasks: list[Signature],
rule: MailRule,
message: MailMessage,
uid_validity: str | None,
) -> None:
"""
Queue a list of consumption tasks (Signatures for the consume_file shared
@@ -355,7 +348,6 @@ def queue_consumption_tasks(
message_uid=message.uid,
message_subject=message.subject,
message_date=message.date,
uid_validity=uid_validity,
)
chord(header=consume_tasks, body=mail_action_task).on_error(
error_callback.s(
@@ -363,7 +355,6 @@ def queue_consumption_tasks(
message_uid=message.uid,
message_subject=message.subject,
message_date=message.date,
uid_validity=uid_validity,
),
).delay()
@@ -469,7 +460,6 @@ class MailAccountHandler(LoggingMixin):
super().__init__()
self.renew_logging_group()
self._init_preprocessors()
self._current_uid_validity: str | None = None
def _init_preprocessors(self) -> None:
self._message_preprocessors: list[MailMessagePreprocessor] = []
@@ -520,21 +510,6 @@ class MailAccountHandler(LoggingMixin):
"Unknown title selector.",
) # pragma: no cover
def _get_uid_validity(self, M: MailBox, folder: str) -> str | None:
try:
uid_validity = M.folder.status(folder, ["UIDVALIDITY"]).get("UIDVALIDITY")
if uid_validity is not None:
return str(uid_validity)
except errors.MailboxFolderStatusError as e:
self.log.warning(
f"Server does not support retrieving UIDVALIDITY for folder {folder}: {e}",
)
except Exception as e:
self.log.warning(
f"Unable to retrieve UIDVALIDITY for folder {folder}: {e}",
)
return None
def _get_correspondent(
self,
message: MailMessage,
@@ -672,8 +647,6 @@ class MailAccountHandler(LoggingMixin):
f"does not exist in account {rule.account}",
) from err
self._current_uid_validity = self._get_uid_validity(M, rule.folder)
criterias = make_criterias(rule, supports_gmail_labels=supports_gmail_labels)
self.log.debug(
@@ -714,17 +687,11 @@ class MailAccountHandler(LoggingMixin):
)
continue
already_processed = ProcessedMail.objects.filter(
if ProcessedMail.objects.filter(
rule=rule,
uid=message.uid,
folder=rule.folder,
)
if self._current_uid_validity is not None:
already_processed = already_processed.filter(
Q(uid_validity=self._current_uid_validity)
| Q(uid_validity__isnull=True),
)
if already_processed.exists():
).exists():
self.log.debug(
f"Skipping mail '{message.uid}' subject '{message.subject}' from '{message.from_}', already processed.",
)
@@ -954,7 +921,6 @@ class MailAccountHandler(LoggingMixin):
consume_tasks=consume_tasks,
rule=rule,
message=message,
uid_validity=self._current_uid_validity,
)
else:
# No files to consume, just mark as processed if it wasn't by .eml processing
@@ -962,13 +928,11 @@ class MailAccountHandler(LoggingMixin):
rule=rule,
uid=message.uid,
folder=rule.folder,
uid_validity=self._current_uid_validity,
).exists():
ProcessedMail.objects.create(
rule=rule,
folder=rule.folder,
uid=message.uid,
uid_validity=self._current_uid_validity,
subject=message.subject,
received=make_aware(message.date)
if is_naive(message.date)
@@ -1045,7 +1009,6 @@ class MailAccountHandler(LoggingMixin):
consume_tasks=[consume_task],
rule=rule,
message=message,
uid_validity=self._current_uid_validity,
)
processed_elements = 1
@@ -1,22 +0,0 @@
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("paperless_mail", "0003_mailrule_stop_processing"),
]
operations = [
migrations.AddField(
model_name="processedmail",
name="uid_validity",
field=models.CharField(
blank=True,
editable=False,
max_length=64,
null=True,
verbose_name="uid validity",
),
),
]
-8
View File
@@ -338,14 +338,6 @@ class ProcessedMail(document_models.ModelWithOwner):
editable=False,
)
uid_validity = models.CharField(
_("uid validity"),
null=True,
blank=True,
max_length=64,
editable=False,
)
subject = models.CharField(
_("subject"),
null=False,
-221
View File
@@ -54,16 +54,12 @@ class _AttachmentDef:
class BogusFolderManager:
current_folder = "INBOX"
uidvalidity = "1"
def set(self, new_folder) -> None:
if new_folder not in ["INBOX", "spam"]:
raise MailboxFolderSelectError(None, "uhm")
self.current_folder = new_folder
def status(self, folder, options):
return {"UIDVALIDITY": self.uidvalidity}
class BogusClient:
def __init__(self, messages) -> None:
@@ -948,223 +944,6 @@ class TestMail(
self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 1)
def test_handle_mail_account_skips_mail_already_processed_in_same_uidvalidity(
self,
) -> None:
"""
GIVEN:
- A ProcessedMail row recorded under the mailbox's current UIDVALIDITY
WHEN:
- A mail with the same UID is fetched from the same UIDVALIDITY epoch
THEN:
- The mail is skipped as a duplicate.
"""
account = MailAccount.objects.create(
name="test",
imap_server="",
username="admin",
password="secret",
)
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.MailAction.DELETE,
)
message = self.mailMocker.messageBuilder.create_message()
self.mailMocker.bogus_mailbox.messages = [message]
self.mailMocker.bogus_mailbox.updateClient()
self.mailMocker.bogus_mailbox.folder.uidvalidity = "SAME"
ProcessedMail.objects.create(
rule=rule,
folder=rule.folder,
uid=message.uid,
uid_validity="SAME",
subject="Previously processed mail",
status="SUCCESS",
received=timezone.make_aware(timezone.datetime(2023, 1, 1, 12, 0, 0)),
)
self.mail_account_handler.handle_mail_account(account)
self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 0)
def test_handle_mail_account_processes_mail_after_uidvalidity_change(
self,
) -> None:
"""
GIVEN:
- A ProcessedMail row recorded under a previous UIDVALIDITY epoch
WHEN:
- A mail with the same UID is fetched after UIDVALIDITY has changed
THEN:
- The mail is processed, not skipped as a duplicate.
"""
account = MailAccount.objects.create(
name="test",
imap_server="",
username="admin",
password="secret",
)
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.MailAction.DELETE,
)
message = self.mailMocker.messageBuilder.create_message()
self.mailMocker.bogus_mailbox.messages = [message]
self.mailMocker.bogus_mailbox.updateClient()
self.mailMocker.bogus_mailbox.folder.uidvalidity = "NEW"
ProcessedMail.objects.create(
rule=rule,
folder=rule.folder,
uid=message.uid,
uid_validity="OLD",
subject="Previously processed mail",
status="SUCCESS",
received=timezone.make_aware(timezone.datetime(2023, 1, 1, 12, 0, 0)),
)
self.mail_account_handler.handle_mail_account(account)
self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 1)
def test_handle_mail_account_skips_mail_processed_before_uidvalidity_tracking(
self,
) -> None:
"""
GIVEN:
- A ProcessedMail row recorded before UIDVALIDITY tracking existed
(uid_validity is NULL)
WHEN:
- A mail with the same UID is fetched
THEN:
- The mail is skipped as a duplicate, to avoid re-ingesting all
previously processed mail after upgrading.
"""
account = MailAccount.objects.create(
name="test",
imap_server="",
username="admin",
password="secret",
)
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.MailAction.DELETE,
)
message = self.mailMocker.messageBuilder.create_message()
self.mailMocker.bogus_mailbox.messages = [message]
self.mailMocker.bogus_mailbox.updateClient()
ProcessedMail.objects.create(
rule=rule,
folder=rule.folder,
uid=message.uid,
uid_validity=None,
subject="Previously processed mail",
status="SUCCESS",
received=timezone.make_aware(timezone.datetime(2023, 1, 1, 12, 0, 0)),
)
self.mail_account_handler.handle_mail_account(account)
self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 0)
def test_handle_mail_account_processes_mail_when_uidvalidity_unavailable(
self,
) -> None:
"""
GIVEN:
- The mail server fails to report a UIDVALIDITY for the folder
WHEN:
- A mail account is processed
THEN:
- The failure is logged and the rule still processes the mail,
instead of the whole rule being disabled.
"""
account = MailAccount.objects.create(
name="test",
imap_server="",
username="admin",
password="secret",
)
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.MailAction.DELETE,
)
message = self.mailMocker.messageBuilder.create_message()
self.mailMocker.bogus_mailbox.messages = [message]
self.mailMocker.bogus_mailbox.updateClient()
self.mailMocker.bogus_mailbox.folder.status = mock.MagicMock(
side_effect=errors.MailboxFolderStatusError(("NO", [b"unsupported"]), "OK"),
)
with self.assertLogs("paperless_mail", level="WARNING") as cm:
self.mail_account_handler.handle_mail_account(account)
self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 1)
self.assertEqual(len(cm.output), 1)
self.assertIn(
"Server does not support retrieving UIDVALIDITY",
cm.output[0],
)
def test_handle_mail_account_skips_mail_when_uidvalidity_unavailable_but_prior_record_exists(
self,
) -> None:
"""
GIVEN:
- A ProcessedMail row recorded with a real uid_validity value
- The mail server fails to report UIDVALIDITY (MailboxFolderStatusError),
so _get_uid_validity returns None
WHEN:
- A mail with the same UID is fetched
THEN:
- The mail is skipped as already-processed rather than re-ingested,
falling back to (rule, uid, folder) matching.
"""
account = MailAccount.objects.create(
name="test",
imap_server="",
username="admin",
password="secret",
)
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.MailAction.DELETE,
)
message = self.mailMocker.messageBuilder.create_message()
self.mailMocker.bogus_mailbox.messages = [message]
self.mailMocker.bogus_mailbox.updateClient()
ProcessedMail.objects.create(
rule=rule,
folder=rule.folder,
uid=message.uid,
uid_validity="REAL_VALIDITY",
subject="Previously processed mail",
status="SUCCESS",
received=timezone.make_aware(timezone.datetime(2023, 1, 1, 12, 0, 0)),
)
self.mailMocker.bogus_mailbox.folder.status = mock.MagicMock(
side_effect=errors.MailboxFolderStatusError(("NO", [b"unsupported"]), "OK"),
)
self.mail_account_handler.handle_mail_account(account)
self.assertEqual(self.mailMocker._queue_consumption_tasks_mock.call_count, 0)
@pytest.mark.flaky(reruns=4)
def test_handle_mail_account_flag(self) -> None:
account = MailAccount.objects.create(
Generated
+462 -419
View File
File diff suppressed because it is too large Load Diff