mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-15 10:13:15 -05:00
adjust mail workflow, execute mail actions only after consumption is successful
This commit is contained in:
parent
edfd3bbe91
commit
d7cb7c78af
@ -9,6 +9,7 @@ from typing import Dict
|
|||||||
|
|
||||||
import magic
|
import magic
|
||||||
import pathvalidate
|
import pathvalidate
|
||||||
|
from celery import chord
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import DatabaseError
|
from django.db import DatabaseError
|
||||||
from documents.loggers import LoggingMixin
|
from documents.loggers import LoggingMixin
|
||||||
@ -25,6 +26,7 @@ from imap_tools import NOT
|
|||||||
from imap_tools.mailbox import MailBoxTls
|
from imap_tools.mailbox import MailBoxTls
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
|
from paperless_mail.tasks import apply_mail_action
|
||||||
|
|
||||||
# Apple Mail sets multiple IMAP KEYWORD and the general "\Flagged" FLAG
|
# Apple Mail sets multiple IMAP KEYWORD and the general "\Flagged" FLAG
|
||||||
# imaplib => conn.fetch(b"<message_id>", "FLAGS")
|
# imaplib => conn.fetch(b"<message_id>", "FLAGS")
|
||||||
@ -57,34 +59,34 @@ class BaseMailAction:
|
|||||||
def get_criteria(self) -> Dict:
|
def get_criteria(self) -> Dict:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def post_consume(self, M, message_uids, parameter):
|
def post_consume(self, M, message_uid, parameter):
|
||||||
pass # pragma: nocover
|
pass # pragma: nocover
|
||||||
|
|
||||||
|
|
||||||
class DeleteMailAction(BaseMailAction):
|
class DeleteMailAction(BaseMailAction):
|
||||||
def post_consume(self, M, message_uids, parameter):
|
def post_consume(self, M, message_uid, parameter):
|
||||||
M.delete(message_uids)
|
M.delete(message_uid)
|
||||||
|
|
||||||
|
|
||||||
class MarkReadMailAction(BaseMailAction):
|
class MarkReadMailAction(BaseMailAction):
|
||||||
def get_criteria(self):
|
def get_criteria(self):
|
||||||
return {"seen": False}
|
return {"seen": False}
|
||||||
|
|
||||||
def post_consume(self, M, message_uids, parameter):
|
def post_consume(self, M, message_uid, parameter):
|
||||||
M.flag(message_uids, [MailMessageFlags.SEEN], True)
|
M.flag(message_uid, [MailMessageFlags.SEEN], True)
|
||||||
|
|
||||||
|
|
||||||
class MoveMailAction(BaseMailAction):
|
class MoveMailAction(BaseMailAction):
|
||||||
def post_consume(self, M, message_uids, parameter):
|
def post_consume(self, M, message_uid, parameter):
|
||||||
M.move(message_uids, parameter)
|
M.move(message_uid, parameter)
|
||||||
|
|
||||||
|
|
||||||
class FlagMailAction(BaseMailAction):
|
class FlagMailAction(BaseMailAction):
|
||||||
def get_criteria(self):
|
def get_criteria(self):
|
||||||
return {"flagged": False}
|
return {"flagged": False}
|
||||||
|
|
||||||
def post_consume(self, M, message_uids, parameter):
|
def post_consume(self, M, message_uid, parameter):
|
||||||
M.flag(message_uids, [MailMessageFlags.FLAGGED], True)
|
M.flag(message_uid, [MailMessageFlags.FLAGGED], True)
|
||||||
|
|
||||||
|
|
||||||
class TagMailAction(BaseMailAction):
|
class TagMailAction(BaseMailAction):
|
||||||
@ -113,9 +115,9 @@ class TagMailAction(BaseMailAction):
|
|||||||
|
|
||||||
return {"no_keyword": self.keyword, "gmail_label": self.keyword}
|
return {"no_keyword": self.keyword, "gmail_label": self.keyword}
|
||||||
|
|
||||||
def post_consume(self, M: MailBox, message_uids, parameter):
|
def post_consume(self, M: MailBox, message_uid, parameter):
|
||||||
if re.search(r"gmail\.com$|googlemail\.com$", M._host):
|
if re.search(r"gmail\.com$|googlemail\.com$", M._host):
|
||||||
for uid in message_uids:
|
for uid in message_uid:
|
||||||
M.client.uid("STORE", uid, "X-GM-LABELS", self.keyword)
|
M.client.uid("STORE", uid, "X-GM-LABELS", self.keyword)
|
||||||
|
|
||||||
# AppleMail
|
# AppleMail
|
||||||
@ -123,21 +125,21 @@ class TagMailAction(BaseMailAction):
|
|||||||
|
|
||||||
# Remove all existing $MailFlagBits
|
# Remove all existing $MailFlagBits
|
||||||
M.flag(
|
M.flag(
|
||||||
message_uids,
|
message_uid,
|
||||||
set(itertools.chain(*APPLE_MAIL_TAG_COLORS.values())),
|
set(itertools.chain(*APPLE_MAIL_TAG_COLORS.values())),
|
||||||
False,
|
False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Set new $MailFlagBits
|
# Set new $MailFlagBits
|
||||||
M.flag(message_uids, APPLE_MAIL_TAG_COLORS.get(self.color), True)
|
M.flag(message_uid, APPLE_MAIL_TAG_COLORS.get(self.color), True)
|
||||||
|
|
||||||
# Set the general \Flagged
|
# Set the general \Flagged
|
||||||
# This defaults to the "red" flag in AppleMail and
|
# This defaults to the "red" flag in AppleMail and
|
||||||
# "stars" in Thunderbird or GMail
|
# "stars" in Thunderbird or GMail
|
||||||
M.flag(message_uids, [MailMessageFlags.FLAGGED], True)
|
M.flag(message_uid, [MailMessageFlags.FLAGGED], True)
|
||||||
|
|
||||||
elif self.keyword:
|
elif self.keyword:
|
||||||
M.flag(message_uids, [self.keyword], True)
|
M.flag(message_uid, [self.keyword], True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise MailError("No keyword specified.")
|
raise MailError("No keyword specified.")
|
||||||
@ -372,16 +374,12 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
f"Rule {rule}: Error while fetching folder {rule.folder}",
|
f"Rule {rule}: Error while fetching folder {rule.folder}",
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
post_consume_messages = []
|
|
||||||
|
|
||||||
mails_processed = 0
|
mails_processed = 0
|
||||||
total_processed_files = 0
|
total_processed_files = 0
|
||||||
|
|
||||||
for message in messages:
|
for message in messages:
|
||||||
try:
|
try:
|
||||||
processed_files = self.handle_message(message, rule)
|
processed_files = self.handle_message(M, message, rule)
|
||||||
if processed_files > 0:
|
|
||||||
post_consume_messages.append(message.uid)
|
|
||||||
|
|
||||||
total_processed_files += processed_files
|
total_processed_files += processed_files
|
||||||
mails_processed += 1
|
mails_processed += 1
|
||||||
@ -394,27 +392,9 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
|
|
||||||
self.log("debug", f"Rule {rule}: Processed {mails_processed} matching mail(s)")
|
self.log("debug", f"Rule {rule}: Processed {mails_processed} matching mail(s)")
|
||||||
|
|
||||||
self.log(
|
|
||||||
"debug",
|
|
||||||
f"Rule {rule}: Running mail actions on "
|
|
||||||
f"{len(post_consume_messages)} mails",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
get_rule_action(rule).post_consume(
|
|
||||||
M,
|
|
||||||
post_consume_messages,
|
|
||||||
rule.action_parameter,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise MailError(
|
|
||||||
f"Rule {rule}: Error while processing post-consume actions: " f"{e}",
|
|
||||||
) from e
|
|
||||||
|
|
||||||
return total_processed_files
|
return total_processed_files
|
||||||
|
|
||||||
def handle_message(self, message, rule: MailRule) -> int:
|
def handle_message(self, M: MailBox, message, rule: MailRule) -> int:
|
||||||
processed_elements = 0
|
processed_elements = 0
|
||||||
|
|
||||||
# Skip Message handling when only attachments are to be processed but
|
# Skip Message handling when only attachments are to be processed but
|
||||||
@ -441,6 +421,7 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING
|
or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING
|
||||||
):
|
):
|
||||||
processed_elements += self.process_eml(
|
processed_elements += self.process_eml(
|
||||||
|
M,
|
||||||
message,
|
message,
|
||||||
rule,
|
rule,
|
||||||
correspondent,
|
correspondent,
|
||||||
@ -453,6 +434,7 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING
|
or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING
|
||||||
):
|
):
|
||||||
processed_elements += self.process_attachments(
|
processed_elements += self.process_attachments(
|
||||||
|
M,
|
||||||
message,
|
message,
|
||||||
rule,
|
rule,
|
||||||
correspondent,
|
correspondent,
|
||||||
@ -464,6 +446,7 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
|
|
||||||
def process_attachments(
|
def process_attachments(
|
||||||
self,
|
self,
|
||||||
|
M: MailBox,
|
||||||
message: MailMessage,
|
message: MailMessage,
|
||||||
rule: MailRule,
|
rule: MailRule,
|
||||||
correspondent,
|
correspondent,
|
||||||
@ -471,6 +454,9 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
doc_type,
|
doc_type,
|
||||||
):
|
):
|
||||||
processed_attachments = 0
|
processed_attachments = 0
|
||||||
|
|
||||||
|
consume_tasks = list()
|
||||||
|
|
||||||
for att in message.attachments:
|
for att in message.attachments:
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -518,7 +504,7 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
f"{message.subject} from {message.from_}",
|
f"{message.subject} from {message.from_}",
|
||||||
)
|
)
|
||||||
|
|
||||||
consume_file.delay(
|
consume_task = consume_file.s(
|
||||||
path=temp_filename,
|
path=temp_filename,
|
||||||
override_filename=pathvalidate.sanitize_filename(
|
override_filename=pathvalidate.sanitize_filename(
|
||||||
att.filename,
|
att.filename,
|
||||||
@ -531,6 +517,8 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
override_tag_ids=tag_ids,
|
override_tag_ids=tag_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
consume_tasks.append(consume_task)
|
||||||
|
|
||||||
processed_attachments += 1
|
processed_attachments += 1
|
||||||
else:
|
else:
|
||||||
self.log(
|
self.log(
|
||||||
@ -540,10 +528,21 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
f"since guessed mime type {mime_type} is not supported "
|
f"since guessed mime type {mime_type} is not supported "
|
||||||
f"by paperless",
|
f"by paperless",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mail_action_task = apply_mail_action.s(
|
||||||
|
M=M,
|
||||||
|
action=get_rule_action(rule),
|
||||||
|
message_uid=message.uid,
|
||||||
|
parameter=rule.action_parameter,
|
||||||
|
)
|
||||||
|
|
||||||
|
chord(header=consume_tasks, body=mail_action_task).delay()
|
||||||
|
|
||||||
return processed_attachments
|
return processed_attachments
|
||||||
|
|
||||||
def process_eml(
|
def process_eml(
|
||||||
self,
|
self,
|
||||||
|
M: MailBox,
|
||||||
message: MailMessage,
|
message: MailMessage,
|
||||||
rule: MailRule,
|
rule: MailRule,
|
||||||
correspondent,
|
correspondent,
|
||||||
@ -584,7 +583,7 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
f"{message.subject} from {message.from_}",
|
f"{message.subject} from {message.from_}",
|
||||||
)
|
)
|
||||||
|
|
||||||
consume_file.delay(
|
consume_task = consume_file.s(
|
||||||
path=temp_filename,
|
path=temp_filename,
|
||||||
override_filename=pathvalidate.sanitize_filename(
|
override_filename=pathvalidate.sanitize_filename(
|
||||||
message.subject + ".eml",
|
message.subject + ".eml",
|
||||||
@ -594,5 +593,15 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
override_document_type_id=doc_type.id if doc_type else None,
|
override_document_type_id=doc_type.id if doc_type else None,
|
||||||
override_tag_ids=tag_ids,
|
override_tag_ids=tag_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
mail_action_task = apply_mail_action.s(
|
||||||
|
M=M,
|
||||||
|
action=get_rule_action(rule),
|
||||||
|
message_uid=message.uid,
|
||||||
|
parameter=rule.action_parameter,
|
||||||
|
)
|
||||||
|
|
||||||
|
(consume_task | mail_action_task).delay()
|
||||||
|
|
||||||
processed_elements = 1
|
processed_elements = 1
|
||||||
return processed_elements
|
return processed_elements
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
|
from imap_tools import MailBox
|
||||||
|
from paperless_mail.mail import BaseMailAction
|
||||||
from paperless_mail.mail import MailAccountHandler
|
from paperless_mail.mail import MailAccountHandler
|
||||||
from paperless_mail.mail import MailError
|
from paperless_mail.mail import MailError
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
@ -8,12 +10,23 @@ from paperless_mail.models import MailAccount
|
|||||||
logger = logging.getLogger("paperless.mail.tasks")
|
logger = logging.getLogger("paperless.mail.tasks")
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def apply_mail_action(
|
||||||
|
result: str,
|
||||||
|
M: MailBox,
|
||||||
|
action: BaseMailAction,
|
||||||
|
message_uid: str,
|
||||||
|
parameter: str,
|
||||||
|
):
|
||||||
|
action.post_consume(M, message_uid, parameter)
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def process_mail_accounts():
|
def process_mail_accounts():
|
||||||
total_new_documents = 0
|
total_new_documents = 0
|
||||||
for account in MailAccount.objects.all():
|
for account in MailAccount.objects.all():
|
||||||
try:
|
try:
|
||||||
total_new_documents += MailAccountHandler().handle_mail_account(account)
|
total_new_documents += MailAccountHandler().handl2e_mail_account(account)
|
||||||
except MailError:
|
except MailError:
|
||||||
logger.exception(f"Error while processing mail account {account}")
|
logger.exception(f"Error while processing mail account {account}")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user