From 3c8196527f73fdcbdd8da9fdc5d199184b78a254 Mon Sep 17 00:00:00 2001 From: phail Date: Sat, 9 Apr 2022 13:07:14 +0200 Subject: [PATCH 01/60] adapt to starttls interface change in imap_tools pin imap-tools version to avoid breaking changes improve mail log --- Pipfile | 2 +- src/paperless_mail/mail.py | 76 +++++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/Pipfile b/Pipfile index 7cbd65b55..f66f08428 100644 --- a/Pipfile +++ b/Pipfile @@ -19,7 +19,7 @@ djangorestframework = "~=3.13" filelock = "*" fuzzywuzzy = {extras = ["speedup"], version = "*"} gunicorn = "*" -imap-tools = "*" +imap-tools = "~=0.53.0" langdetect = "*" pathvalidate = "*" pillow = "~=9.0" diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index a7e455829..f6d2cccc7 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -18,6 +18,7 @@ from imap_tools import MailboxFolderSelectError from imap_tools import MailBoxUnencrypted from imap_tools import MailMessage from imap_tools import MailMessageFlags +from imap_tools.mailbox import MailBoxTls from paperless_mail.models import MailAccount from paperless_mail.models import MailRule @@ -89,14 +90,18 @@ def make_criterias(rule): def get_mailbox(server, port, security): - if security == MailAccount.IMAP_SECURITY_NONE: - mailbox = MailBoxUnencrypted(server, port) - elif security == MailAccount.IMAP_SECURITY_STARTTLS: - mailbox = MailBox(server, port, starttls=True) - elif security == MailAccount.IMAP_SECURITY_SSL: - mailbox = MailBox(server, port) - else: - raise NotImplementedError("Unknown IMAP security") # pragma: nocover + try: + if security == MailAccount.IMAP_SECURITY_NONE: + mailbox = MailBoxUnencrypted(server, port) + elif security == MailAccount.IMAP_SECURITY_STARTTLS: + mailbox = MailBoxTls(server, port) + elif security == MailAccount.IMAP_SECURITY_SSL: + mailbox = MailBox(server, port) + else: + raise NotImplementedError("Unknown IMAP security") # pragma: nocover + except Exception as e: + print(f"Error while retrieving mailbox from {server}: {e}") + raise return mailbox @@ -154,32 +159,45 @@ class MailAccountHandler(LoggingMixin): self.log("debug", f"Processing mail account {account}") total_processed_files = 0 - - with get_mailbox( - account.imap_server, - account.imap_port, - account.imap_security, - ) as M: - - try: - M.login(account.username, account.password) - except Exception: - raise MailError(f"Error while authenticating account {account}") - - self.log( - "debug", - f"Account {account}: Processing " f"{account.rules.count()} rule(s)", - ) - - for rule in account.rules.order_by("order"): + try: + with get_mailbox( + account.imap_server, + account.imap_port, + account.imap_security, + ) as M: try: - total_processed_files += self.handle_mail_rule(M, rule) + M.login(account.username, account.password) except Exception as e: self.log( "error", - f"Rule {rule}: Error while processing rule: {e}", - exc_info=True, + f"Error while authenticating account {account}: {e}", + exc_info=False, ) + return total_processed_files + + self.log( + "debug", + f"Account {account}: Processing " + f"{account.rules.count()} rule(s)", + ) + + for rule in account.rules.order_by("order"): + try: + total_processed_files += self.handle_mail_rule(M, rule) + except Exception as e: + self.log( + "error", + f"Rule {rule}: Error while processing rule: {e}", + exc_info=True, + ) + except MailError: + raise + except Exception as e: + self.log( + "error", + f"Error while retrieving mailbox {account}: {e}", + exc_info=False, + ) return total_processed_files From c05b39a05640fa00d43de9a8c1f96f737c3a2f45 Mon Sep 17 00:00:00 2001 From: phail Date: Wed, 13 Apr 2022 23:37:21 +0200 Subject: [PATCH 02/60] fix unittest --- src/paperless_mail/mail.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index f6d2cccc7..9553e2b1a 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -173,7 +173,9 @@ class MailAccountHandler(LoggingMixin): f"Error while authenticating account {account}: {e}", exc_info=False, ) - return total_processed_files + raise MailError( + f"Error while authenticating account {account}", + ) from e self.log( "debug", From 5fcf1b5434f42a55ebe4dd083d3021f27234d0cf Mon Sep 17 00:00:00 2001 From: phail Date: Thu, 14 Apr 2022 00:19:30 +0200 Subject: [PATCH 03/60] remove uneeded print and fix merge fail --- src/paperless_mail/mail.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index c80517c6e..1e868ceaa 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -90,18 +90,14 @@ def make_criterias(rule): def get_mailbox(server, port, security): - try: - if security == MailAccount.IMAP_SECURITY_NONE: - mailbox = MailBoxUnencrypted(server, port) - elif security == MailAccount.IMAP_SECURITY_STARTTLS: - mailbox = MailBoxTls(server, port) - elif security == MailAccount.IMAP_SECURITY_SSL: - mailbox = MailBox(server, port) - else: - raise NotImplementedError("Unknown IMAP security") # pragma: nocover - except Exception as e: - print(f"Error while retrieving mailbox from {server}: {e}") - raise + if security == MailAccount.ImapSecurity.NONE: + mailbox = MailBoxUnencrypted(server, port) + elif security == MailAccount.ImapSecurity.STARTTLS: + mailbox = MailBoxTls(server, port) + elif security == MailAccount.ImapSecurity.SSL: + mailbox = MailBox(server, port) + else: + raise NotImplementedError("Unknown IMAP security") # pragma: nocover return mailbox From cca576f5182fddcdeb132832fdf3fae0edfe4bfd Mon Sep 17 00:00:00 2001 From: phail Date: Fri, 15 Apr 2022 14:40:02 +0200 Subject: [PATCH 04/60] add feature to consume imap mail als .eml --- src/paperless_mail/admin.py | 1 + src/paperless_mail/mail.py | 164 +++++++++++------- .../0010_mailrule_consumption_scope.py | 32 ++++ src/paperless_mail/models.py | 14 ++ 4 files changed, 149 insertions(+), 62 deletions(-) create mode 100644 src/paperless_mail/migrations/0010_mailrule_consumption_scope.py diff --git a/src/paperless_mail/admin.py b/src/paperless_mail/admin.py index b56bc0727..1e22e6ebd 100644 --- a/src/paperless_mail/admin.py +++ b/src/paperless_mail/admin.py @@ -56,6 +56,7 @@ class MailRuleAdmin(admin.ModelAdmin): "filter_body", "filter_attachment_filename", "maximum_age", + "consumption_scope", "attachment_type", ), }, diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 1e868ceaa..72a74639c 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -269,8 +269,11 @@ class MailAccountHandler(LoggingMixin): return total_processed_files - def handle_message(self, message, rule) -> int: - if not message.attachments: + def handle_message(self, message, rule: MailRule) -> int: + if ( + not message.attachments + and rule.consumption_scope == MailRule.ConsumptionScope.ATTACHMENTS_ONLY + ): return 0 self.log( @@ -286,76 +289,113 @@ class MailAccountHandler(LoggingMixin): processed_attachments = 0 - for att in message.attachments: + if ( + rule.consumption_scope == MailRule.ConsumptionScope.EML_ONLY + or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING + ): + os.makedirs(settings.SCRATCH_DIR, exist_ok=True) + _, temp_filename = tempfile.mkstemp( + prefix="paperless-mail-", + dir=settings.SCRATCH_DIR, + ) + with open(temp_filename, "wb") as f: + f.write(message.obj.as_bytes()) - if ( - not att.content_disposition == "attachment" - and rule.attachment_type - == MailRule.AttachmentProcessing.ATTACHMENTS_ONLY - ): - self.log( - "debug", - f"Rule {rule}: " - f"Skipping attachment {att.filename} " - f"with content disposition {att.content_disposition}", - ) - continue + self.log( + "info", + f"Rule {rule}: " + f"Consuming eml from mail " + f"{message.subject} from {message.from_}", + ) - if 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(), + async_task( + "documents.tasks.consume_file", + path=temp_filename, + override_filename=pathvalidate.sanitize_filename( + message.subject + ".eml", + ), + override_title=message.subject, + override_correspondent_id=correspondent.id if correspondent else None, + override_document_type_id=doc_type.id if doc_type else None, + override_tag_ids=[tag.id] if tag else None, + task_name=message.subject[:100], + ) + processed_attachments += 1 + + if ( + rule.consumption_scope == MailRule.ConsumptionScope.ATTACHMENTS_ONLY + or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING + ): + for att in message.attachments: + + if ( + not att.content_disposition == "attachment" + and rule.attachment_type + == MailRule.AttachmentProcessing.ATTACHMENTS_ONLY ): + self.log( + "debug", + f"Rule {rule}: " + f"Skipping attachment {att.filename} " + f"with content disposition {att.content_disposition}", + ) continue - title = self.get_title(message, att, rule) + if 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 - # don't trust the content type of the attachment. Could be - # generic application/octet-stream. - mime_type = magic.from_buffer(att.payload, mime=True) + title = self.get_title(message, att, rule) - if is_mime_type_supported(mime_type): + # don't trust the content type of the attachment. Could be + # generic application/octet-stream. + mime_type = magic.from_buffer(att.payload, mime=True) - os.makedirs(settings.SCRATCH_DIR, exist_ok=True) - _, temp_filename = tempfile.mkstemp( - prefix="paperless-mail-", - dir=settings.SCRATCH_DIR, - ) - with open(temp_filename, "wb") as f: - f.write(att.payload) + if is_mime_type_supported(mime_type): - self.log( - "info", - f"Rule {rule}: " - f"Consuming attachment {att.filename} from mail " - f"{message.subject} from {message.from_}", - ) + os.makedirs(settings.SCRATCH_DIR, exist_ok=True) + _, temp_filename = tempfile.mkstemp( + prefix="paperless-mail-", + dir=settings.SCRATCH_DIR, + ) + with open(temp_filename, "wb") as f: + f.write(att.payload) - async_task( - "documents.tasks.consume_file", - path=temp_filename, - override_filename=pathvalidate.sanitize_filename( - att.filename, - ), - override_title=title, - override_correspondent_id=correspondent.id - if correspondent - else None, - override_document_type_id=doc_type.id if doc_type else None, - override_tag_ids=[tag.id] if tag else None, - task_name=att.filename[:100], - ) + self.log( + "info", + f"Rule {rule}: " + f"Consuming attachment {att.filename} from mail " + f"{message.subject} from {message.from_}", + ) - processed_attachments += 1 - else: - self.log( - "debug", - f"Rule {rule}: " - f"Skipping attachment {att.filename} " - f"since guessed mime type {mime_type} is not supported " - f"by paperless", - ) + async_task( + "documents.tasks.consume_file", + path=temp_filename, + override_filename=pathvalidate.sanitize_filename( + att.filename, + ), + override_title=title, + override_correspondent_id=correspondent.id + if correspondent + else None, + override_document_type_id=doc_type.id if doc_type else None, + override_tag_ids=[tag.id] if tag else None, + task_name=att.filename[:100], + ) + + processed_attachments += 1 + else: + self.log( + "debug", + f"Rule {rule}: " + f"Skipping attachment {att.filename} " + f"since guessed mime type {mime_type} is not supported " + f"by paperless", + ) return processed_attachments diff --git a/src/paperless_mail/migrations/0010_mailrule_consumption_scope.py b/src/paperless_mail/migrations/0010_mailrule_consumption_scope.py new file mode 100644 index 000000000..8569cd378 --- /dev/null +++ b/src/paperless_mail/migrations/0010_mailrule_consumption_scope.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.4 on 2022-04-14 22:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("paperless_mail", "0009_alter_mailrule_action_alter_mailrule_folder"), + ] + + operations = [ + migrations.AddField( + model_name="mailrule", + name="consumption_scope", + field=models.PositiveIntegerField( + choices=[ + (1, "Only process attachments."), + ( + 2, + "Process full Mail (with embedded attachments in file) as .eml", + ), + ( + 3, + "Process full Mail (with embedded attachments in file) as .eml + process attachments as separate documents", + ), + ], + default=1, + verbose_name="consumption scope", + ), + ), + ] diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py index 2c7b9fb6d..e4809a790 100644 --- a/src/paperless_mail/models.py +++ b/src/paperless_mail/models.py @@ -56,6 +56,14 @@ class MailRule(models.Model): verbose_name = _("mail rule") verbose_name_plural = _("mail rules") + class ConsumptionScope(models.IntegerChoices): + ATTACHMENTS_ONLY = 1, _("Only process attachments.") + EML_ONLY = 2, _("Process full Mail (with embedded attachments in file) as .eml") + EVERYTHING = 3, _( + "Process full Mail (with embedded attachments in file) as .eml " + "+ process attachments as separate documents", + ) + class AttachmentProcessing(models.IntegerChoices): ATTACHMENTS_ONLY = 1, _("Only process attachments.") EVERYTHING = 2, _("Process all files, including 'inline' " "attachments.") @@ -144,6 +152,12 @@ class MailRule(models.Model): ), ) + consumption_scope = models.PositiveIntegerField( + _("consumption scope"), + choices=ConsumptionScope.choices, + default=ConsumptionScope.ATTACHMENTS_ONLY, + ) + action = models.PositiveIntegerField( _("action"), choices=AttachmentAction.choices, From 027897ff0309423f524626b894981298a3606c8b Mon Sep 17 00:00:00 2001 From: phail Date: Tue, 19 Apr 2022 00:39:00 +0200 Subject: [PATCH 05/60] work in progress Mail parsing --- Pipfile | 1 + src/paperless_mail/mail.py | 2 +- src/paperless_tika/apps.py | 2 + src/paperless_tika/parsers.py | 148 ++++++++++++++++++++++++++++++++++ src/paperless_tika/signals.py | 16 ++++ 5 files changed, 168 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 0feabe237..da2644251 100644 --- a/Pipfile +++ b/Pipfile @@ -53,6 +53,7 @@ concurrent-log-handler = "*" zipp = {version = "*", markers = "python_version < '3.9'"} pyzbar = "*" pdf2image = "*" +click = "==8.0.4" [dev-packages] coveralls = "*" diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 72a74639c..865581446 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -199,7 +199,7 @@ class MailAccountHandler(LoggingMixin): return total_processed_files - def handle_mail_rule(self, M, rule): + def handle_mail_rule(self, M, rule: MailRule): self.log("debug", f"Rule {rule}: Selecting folder {rule.folder}") diff --git a/src/paperless_tika/apps.py b/src/paperless_tika/apps.py index 5cab21427..791d234a0 100644 --- a/src/paperless_tika/apps.py +++ b/src/paperless_tika/apps.py @@ -1,6 +1,7 @@ from django.apps import AppConfig from django.conf import settings from paperless_tika.signals import tika_consumer_declaration +from paperless_tika.signals import tika_consumer_declaration_eml class PaperlessTikaConfig(AppConfig): @@ -11,4 +12,5 @@ class PaperlessTikaConfig(AppConfig): if settings.PAPERLESS_TIKA_ENABLED: document_consumer_declaration.connect(tika_consumer_declaration) + document_consumer_declaration.connect(tika_consumer_declaration_eml) AppConfig.ready(self) diff --git a/src/paperless_tika/parsers.py b/src/paperless_tika/parsers.py index 22218dfe7..294f637ef 100644 --- a/src/paperless_tika/parsers.py +++ b/src/paperless_tika/parsers.py @@ -1,4 +1,6 @@ import os +import re +from io import StringIO import dateutil.parser import requests @@ -6,6 +8,9 @@ from django.conf import settings from documents.parsers import DocumentParser from documents.parsers import make_thumbnail_from_pdf from documents.parsers import ParseError +from PIL import Image +from PIL import ImageDraw +from PIL import ImageFont from tika import parser @@ -97,3 +102,146 @@ class TikaDocumentParser(DocumentParser): file.close() return pdf_path + + +class TikaDocumentParserEml(DocumentParser): + """ + This parser sends documents to a local tika server + """ + + logging_name = "paperless.parsing.tikaeml" + + def get_thumbnail(self, document_path, mime_type, file_name=None): + + img = Image.new("RGB", (500, 700), color="white") + draw = ImageDraw.Draw(img) + font = ImageFont.truetype( + font=settings.THUMBNAIL_FONT_NAME, + size=20, + layout_engine=ImageFont.LAYOUT_BASIC, + ) + draw.text((5, 5), self.text, font=font, fill="black") + + out_path = os.path.join(self.tempdir, "thumb.png") + img.save(out_path) + + return out_path + + def extract_metadata(self, document_path, mime_type): + tika_server = settings.PAPERLESS_TIKA_ENDPOINT + try: + parsed = parser.from_file(document_path, tika_server) + except Exception as e: + self.log( + "warning", + f"Error while fetching document metadata for " f"{document_path}: {e}", + ) + return [] + + return [ + { + "namespace": "", + "prefix": "", + "key": key, + "value": parsed["metadata"][key], + } + for key in parsed["metadata"] + ] + + def parse(self, document_path, mime_type, file_name=None): + self.log("info", f"Sending {document_path} to Tika server") + tika_server = settings.PAPERLESS_TIKA_ENDPOINT + + try: + parsed = parser.from_file(document_path, tika_server) + except Exception as err: + raise ParseError( + f"Could not parse {document_path} with tika server at " + f"{tika_server}: {err}", + ) + + text = re.sub(" +", " ", str(parsed)) + text = re.sub("\n+", "\n", text) + self.text = text + + print(text) + + try: + self.date = dateutil.parser.isoparse(parsed["metadata"]["Creation-Date"]) + except Exception as e: + self.log( + "warning", + f"Unable to extract date for document " f"{document_path}: {e}", + ) + + md_path = self.convert_to_md(document_path, file_name) + self.archive_path = self.convert_md_to_pdf(md_path) + + def convert_md_to_pdf(self, md_path): + pdf_path = os.path.join(self.tempdir, "convert.pdf") + gotenberg_server = settings.PAPERLESS_TIKA_GOTENBERG_ENDPOINT + url = gotenberg_server + "/forms/chromium/convert/markdown" + + self.log("info", f"Converting {md_path} to PDF as {pdf_path}") + html = StringIO( + """ + + + + + My PDF + + + {{ toHTML "convert.md" }} + + + """, + ) + md = StringIO( + """ +# Subject + +blub \nblah +blib + """, + ) + + files = { + "md": ( + os.path.basename(md_path), + md, + ), + "html": ( + "index.html", + html, + ), + } + headers = {} + + try: + response = requests.post(url, files=files, headers=headers) + response.raise_for_status() # ensure we notice bad responses + except Exception as err: + raise ParseError(f"Error while converting document to PDF: {err}") + + with open(pdf_path, "wb") as file: + file.write(response.content) + file.close() + + return pdf_path + + def convert_to_md(self, document_path, file_name): + md_path = os.path.join(self.tempdir, "convert.md") + + self.log("info", f"Converting {document_path} to markdown as {md_path}") + + with open(md_path, "w") as file: + md = [ + "# Subject", + "\n\n", + "blah", + ] + file.writelines(md) + file.close() + + return md_path diff --git a/src/paperless_tika/signals.py b/src/paperless_tika/signals.py index 39838f076..a852cfdb2 100644 --- a/src/paperless_tika/signals.py +++ b/src/paperless_tika/signals.py @@ -22,3 +22,19 @@ def tika_consumer_declaration(sender, **kwargs): "text/rtf": ".rtf", }, } + + +def get_parser_eml(*args, **kwargs): + from .parsers import TikaDocumentParserEml + + return TikaDocumentParserEml(*args, **kwargs) + + +def tika_consumer_declaration_eml(sender, **kwargs): + return { + "parser": get_parser_eml, + "weight": 10, + "mime_types": { + "message/rfc822": ".eml", + }, + } From d8d2d53c59c4902f94b9ac76f16a370662b9086e Mon Sep 17 00:00:00 2001 From: phail Date: Tue, 19 Apr 2022 20:14:31 +0200 Subject: [PATCH 06/60] fix Mail actions mixup --- src/paperless_mail/mail.py | 8 +++--- .../migrations/0011_alter_mailrule_action.py | 27 +++++++++++++++++++ src/paperless_mail/models.py | 14 +++++----- src/paperless_mail/tests/test_mail.py | 20 +++++++------- 4 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 src/paperless_mail/migrations/0011_alter_mailrule_action.py diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 865581446..8beb17382 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -62,13 +62,13 @@ class FlagMailAction(BaseMailAction): def get_rule_action(rule): - if rule.action == MailRule.AttachmentAction.FLAG: + if rule.action == MailRule.MailAction.FLAG: return FlagMailAction() - elif rule.action == MailRule.AttachmentAction.DELETE: + elif rule.action == MailRule.MailAction.DELETE: return DeleteMailAction() - elif rule.action == MailRule.AttachmentAction.MOVE: + elif rule.action == MailRule.MailAction.MOVE: return MoveMailAction() - elif rule.action == MailRule.AttachmentAction.MARK_READ: + elif rule.action == MailRule.MailAction.MARK_READ: return MarkReadMailAction() else: raise NotImplementedError("Unknown action.") # pragma: nocover diff --git a/src/paperless_mail/migrations/0011_alter_mailrule_action.py b/src/paperless_mail/migrations/0011_alter_mailrule_action.py new file mode 100644 index 000000000..4dbff1386 --- /dev/null +++ b/src/paperless_mail/migrations/0011_alter_mailrule_action.py @@ -0,0 +1,27 @@ +# Generated by Django 4.0.4 on 2022-04-19 18:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("paperless_mail", "0010_mailrule_consumption_scope"), + ] + + operations = [ + migrations.AlterField( + model_name="mailrule", + name="action", + field=models.PositiveIntegerField( + choices=[ + (1, "Delete"), + (2, "Move to specified folder"), + (3, "Mark as read, don't process read mails"), + (4, "Flag the mail, don't process flagged mails"), + ], + default=3, + verbose_name="action", + ), + ), + ] diff --git a/src/paperless_mail/models.py b/src/paperless_mail/models.py index e4809a790..dbf9b17f4 100644 --- a/src/paperless_mail/models.py +++ b/src/paperless_mail/models.py @@ -68,11 +68,11 @@ class MailRule(models.Model): ATTACHMENTS_ONLY = 1, _("Only process attachments.") EVERYTHING = 2, _("Process all files, including 'inline' " "attachments.") - class AttachmentAction(models.IntegerChoices): - DELETE = 1, _("Mark as read, don't process read mails") - MOVE = 2, _("Flag the mail, don't process flagged mails") - MARK_READ = 3, _("Move to specified folder") - FLAG = 4, _("Delete") + class MailAction(models.IntegerChoices): + DELETE = 1, _("Delete") + MOVE = 2, _("Move to specified folder") + MARK_READ = 3, _("Mark as read, don't process read mails") + FLAG = 4, _("Flag the mail, don't process flagged mails") class TitleSource(models.IntegerChoices): FROM_SUBJECT = 1, _("Use subject as title") @@ -160,8 +160,8 @@ class MailRule(models.Model): action = models.PositiveIntegerField( _("action"), - choices=AttachmentAction.choices, - default=AttachmentAction.MARK_READ, + choices=MailAction.choices, + default=MailAction.MARK_READ, ) action_parameter = models.CharField( diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 9335bcd75..3f3b69871 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -467,7 +467,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule", account=account, - action=MailRule.AttachmentAction.MARK_READ, + action=MailRule.MailAction.MARK_READ, ) self.assertEqual(len(self.bogus_mailbox.messages), 3) @@ -490,7 +490,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule", account=account, - action=MailRule.AttachmentAction.DELETE, + action=MailRule.MailAction.DELETE, filter_subject="Invoice", ) @@ -511,7 +511,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule", account=account, - action=MailRule.AttachmentAction.FLAG, + action=MailRule.MailAction.FLAG, filter_subject="Invoice", ) @@ -534,7 +534,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule", account=account, - action=MailRule.AttachmentAction.MOVE, + action=MailRule.MailAction.MOVE, action_parameter="spam", filter_subject="Claim", ) @@ -580,7 +580,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule", account=account, - action=MailRule.AttachmentAction.MOVE, + action=MailRule.MailAction.MOVE, action_parameter="spam", filter_subject="Claim", ) @@ -601,7 +601,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule", account=account, - action=MailRule.AttachmentAction.MOVE, + action=MailRule.MailAction.MOVE, action_parameter="spam", filter_subject="Claim", order=1, @@ -610,7 +610,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule2", account=account, - action=MailRule.AttachmentAction.MOVE, + action=MailRule.MailAction.MOVE, action_parameter="spam", filter_subject="Claim", order=2, @@ -640,7 +640,7 @@ class TestMail(DirectoriesMixin, TestCase): _ = MailRule.objects.create( name="testrule", account=account, - action=MailRule.AttachmentAction.MOVE, + action=MailRule.MailAction.MOVE, action_parameter="spam", ) @@ -665,7 +665,7 @@ class TestMail(DirectoriesMixin, TestCase): name="testrule", filter_from="amazon@amazon.de", account=account, - action=MailRule.AttachmentAction.MOVE, + action=MailRule.MailAction.MOVE, action_parameter="spam", assign_correspondent_from=MailRule.CorrespondentSource.FROM_EMAIL, ) @@ -702,7 +702,7 @@ class TestMail(DirectoriesMixin, TestCase): rule = MailRule.objects.create( name="testrule3", account=account, - action=MailRule.AttachmentAction.DELETE, + action=MailRule.MailAction.DELETE, filter_subject="Claim", ) From 790bcf05ed7f478dd497a8f4fcb47c10063a7859 Mon Sep 17 00:00:00 2001 From: phail Date: Mon, 25 Apr 2022 20:55:00 +0200 Subject: [PATCH 07/60] add prototype archive pdf --- src/paperless_mail/mail_template/index.html | 46 +++++ src/paperless_mail/mail_template/output.css | 0 src/paperless_tika/parsers.py | 186 ++++++++++++-------- 3 files changed, 157 insertions(+), 75 deletions(-) create mode 100644 src/paperless_mail/mail_template/index.html create mode 100644 src/paperless_mail/mail_template/output.css diff --git a/src/paperless_mail/mail_template/index.html b/src/paperless_mail/mail_template/index.html new file mode 100644 index 000000000..b1f332f75 --- /dev/null +++ b/src/paperless_mail/mail_template/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + +
+ +
+ +
23.04.2022 18:18
+ +
From
+
{{ from }}
+ +
Subject
+
{{ Subject }} +
+ +
To
+
{{ To }} +
+ +
CC
+
{{ CC }} +
+ +
BCC
+
{{ BCC }} +
+
+ + +
+ + +
{{ content }} +
+ + + + diff --git a/src/paperless_mail/mail_template/output.css b/src/paperless_mail/mail_template/output.css new file mode 100644 index 000000000..e69de29bb diff --git a/src/paperless_tika/parsers.py b/src/paperless_tika/parsers.py index 294f637ef..9eed095b3 100644 --- a/src/paperless_tika/parsers.py +++ b/src/paperless_tika/parsers.py @@ -8,9 +8,6 @@ from django.conf import settings from documents.parsers import DocumentParser from documents.parsers import make_thumbnail_from_pdf from documents.parsers import ParseError -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont from tika import parser @@ -112,22 +109,19 @@ class TikaDocumentParserEml(DocumentParser): logging_name = "paperless.parsing.tikaeml" def get_thumbnail(self, document_path, mime_type, file_name=None): + if not self.archive_path: + self.archive_path = self.generate_pdf(document_path) - img = Image.new("RGB", (500, 700), color="white") - draw = ImageDraw.Draw(img) - font = ImageFont.truetype( - font=settings.THUMBNAIL_FONT_NAME, - size=20, - layout_engine=ImageFont.LAYOUT_BASIC, + return make_thumbnail_from_pdf( + self.archive_path, + self.tempdir, + self.logging_group, ) - draw.text((5, 5), self.text, font=font, fill="black") - - out_path = os.path.join(self.tempdir, "thumb.png") - img.save(out_path) - - return out_path def extract_metadata(self, document_path, mime_type): + result = [] + prefix_pattern = re.compile(r"(.*):(.*)") + tika_server = settings.PAPERLESS_TIKA_ENDPOINT try: parsed = parser.from_file(document_path, tika_server) @@ -136,17 +130,38 @@ class TikaDocumentParserEml(DocumentParser): "warning", f"Error while fetching document metadata for " f"{document_path}: {e}", ) - return [] + return result - return [ - { - "namespace": "", - "prefix": "", - "key": key, - "value": parsed["metadata"][key], - } - for key in parsed["metadata"] - ] + for key, value in parsed["metadata"].items(): + if isinstance(value, list): + value = ", ".join([str(e) for e in value]) + value = str(value) + try: + m = prefix_pattern.match(key) + result.append( + { + "namespace": "", + "prefix": m.group(1), + "key": m.group(2), + "value": value, + }, + ) + except AttributeError: + result.append( + { + "namespace": "", + "prefix": "", + "key": key, + "value": value, + }, + ) + except Exception as e: + self.log( + "warning", + f"Error while reading metadata {key}: {value}. Error: " f"{e}", + ) + result.sort(key=lambda item: (item["prefix"], item["key"])) + return result def parse(self, document_path, mime_type, file_name=None): self.log("info", f"Sending {document_path} to Tika server") @@ -160,57 +175,94 @@ class TikaDocumentParserEml(DocumentParser): f"{tika_server}: {err}", ) - text = re.sub(" +", " ", str(parsed)) - text = re.sub("\n+", "\n", text) - self.text = text + metadata = parsed["metadata"].copy() - print(text) + subject = metadata.pop("dc:subject", "") + content = parsed["content"].strip() + + if content.startswith(subject): + content = content[len(subject) :].strip() + + content = re.sub(" +", " ", content) + content = re.sub("\n+", "\n", content) + + self.text = ( + f"{content}\n" + f"______________________\n" + f"From: {metadata.pop('Message-From', '')}\n" + f"To: {metadata.pop('Message-To', '')}\n" + f"CC: {metadata.pop('Message-CC', '')}" + ) try: - self.date = dateutil.parser.isoparse(parsed["metadata"]["Creation-Date"]) + self.date = dateutil.parser.isoparse(parsed["metadata"]["dcterms:created"]) except Exception as e: self.log( "warning", f"Unable to extract date for document " f"{document_path}: {e}", ) - md_path = self.convert_to_md(document_path, file_name) - self.archive_path = self.convert_md_to_pdf(md_path) + self.archive_path = self.generate_pdf(document_path, parsed) + + def generate_pdf(self, document_path, parsed=None): + if not parsed: + self.log("info", f"Sending {document_path} to Tika server") + tika_server = settings.PAPERLESS_TIKA_ENDPOINT + + try: + parsed = parser.from_file(document_path, tika_server) + except Exception as err: + raise ParseError( + f"Could not parse {document_path} with tika server at " + f"{tika_server}: {err}", + ) + + def clean_html(text: str): + if isinstance(text, list): + text = ", ".join([str(e) for e in text]) + if type(text) != str: + text = str(text) + text = text.replace("&", "&") + text = text.replace("<", "<") + text = text.replace(">", ">") + text = text.replace(" ", " ") + text = text.replace("'", "'") + text = text.replace('"', """) + return text - def convert_md_to_pdf(self, md_path): pdf_path = os.path.join(self.tempdir, "convert.pdf") gotenberg_server = settings.PAPERLESS_TIKA_GOTENBERG_ENDPOINT - url = gotenberg_server + "/forms/chromium/convert/markdown" + url = gotenberg_server + "/forms/chromium/convert/html" + + self.log("info", f"Converting {document_path} to PDF as {pdf_path}") + + subject = parsed["metadata"].pop("dc:subject", "") + content = parsed.pop("content", "").strip() + + if content.startswith(subject): + content = content[len(subject) :].strip() - self.log("info", f"Converting {md_path} to PDF as {pdf_path}") html = StringIO( - """ - - - - - My PDF - - - {{ toHTML "convert.md" }} - - - """, - ) - md = StringIO( - """ -# Subject - -blub \nblah -blib - """, + f""" + + + + + My PDF + + +

{clean_html(subject)}

+

From: {clean_html(parsed['metadata'].pop('Message-From', ''))} +

To: {clean_html(parsed['metadata'].pop('Message-To', ''))} +

CC: {clean_html(parsed['metadata'].pop('Message-CC', ''))} +

Date: {clean_html(parsed['metadata'].pop('dcterms:created', ''))} +

{clean_html(content)}
+ + + """, ) files = { - "md": ( - os.path.basename(md_path), - md, - ), "html": ( "index.html", html, @@ -229,19 +281,3 @@ blib file.close() return pdf_path - - def convert_to_md(self, document_path, file_name): - md_path = os.path.join(self.tempdir, "convert.md") - - self.log("info", f"Converting {document_path} to markdown as {md_path}") - - with open(md_path, "w") as file: - md = [ - "# Subject", - "\n\n", - "blah", - ] - file.writelines(md) - file.close() - - return md_path From a2b5b3b2530daf7e88600d88803eb4ee43ab8dad Mon Sep 17 00:00:00 2001 From: phail Date: Tue, 26 Apr 2022 23:12:36 +0200 Subject: [PATCH 08/60] moved files --- src/{paperless_mail => paperless_tika}/mail_template/index.html | 0 src/{paperless_mail => paperless_tika}/mail_template/output.css | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{paperless_mail => paperless_tika}/mail_template/index.html (100%) rename src/{paperless_mail => paperless_tika}/mail_template/output.css (100%) diff --git a/src/paperless_mail/mail_template/index.html b/src/paperless_tika/mail_template/index.html similarity index 100% rename from src/paperless_mail/mail_template/index.html rename to src/paperless_tika/mail_template/index.html diff --git a/src/paperless_mail/mail_template/output.css b/src/paperless_tika/mail_template/output.css similarity index 100% rename from src/paperless_mail/mail_template/output.css rename to src/paperless_tika/mail_template/output.css From c8081595c4450780eade4921a81d0b1bd08105cc Mon Sep 17 00:00:00 2001 From: phail Date: Tue, 26 Apr 2022 23:25:48 +0200 Subject: [PATCH 09/60] improve pdf generation --- src/paperless_tika/mail_template/index.html | 10 +- src/paperless_tika/parsers.py | 146 ++++++++++---------- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/src/paperless_tika/mail_template/index.html b/src/paperless_tika/mail_template/index.html index b1f332f75..f7d7fbf9d 100644 --- a/src/paperless_tika/mail_template/index.html +++ b/src/paperless_tika/mail_template/index.html @@ -12,25 +12,25 @@
-
23.04.2022 18:18
+
{{ date }}
From
{{ from }}
Subject
-
{{ Subject }} +
{{ subject }}
To
-
{{ To }} +
{{ to }}
CC
-
{{ CC }} +
{{ cc }}
BCC
-
{{ BCC }} +
{{ bcc }}
diff --git a/src/paperless_tika/parsers.py b/src/paperless_tika/parsers.py index 9eed095b3..9cea35cf9 100644 --- a/src/paperless_tika/parsers.py +++ b/src/paperless_tika/parsers.py @@ -107,6 +107,25 @@ class TikaDocumentParserEml(DocumentParser): """ logging_name = "paperless.parsing.tikaeml" + _tika_parsed = None + + def get_tika_result(self, document_path): + if not self._tika_parsed: + self.log("info", f"Sending {document_path} to Tika server") + tika_server = settings.PAPERLESS_TIKA_ENDPOINT + + try: + self._tika_parsed = parser.from_file( + document_path, + tika_server, + ) + except Exception as err: + raise ParseError( + f"Could not parse {document_path} with tika server at " + f"{tika_server}: {err}", + ) + + return self._tika_parsed def get_thumbnail(self, document_path, mime_type, file_name=None): if not self.archive_path: @@ -122,10 +141,9 @@ class TikaDocumentParserEml(DocumentParser): result = [] prefix_pattern = re.compile(r"(.*):(.*)") - tika_server = settings.PAPERLESS_TIKA_ENDPOINT try: - parsed = parser.from_file(document_path, tika_server) - except Exception as e: + parsed = self.get_tika_result(document_path) + except ParseError as e: self.log( "warning", f"Error while fetching document metadata for " f"{document_path}: {e}", @@ -164,20 +182,9 @@ class TikaDocumentParserEml(DocumentParser): return result def parse(self, document_path, mime_type, file_name=None): - self.log("info", f"Sending {document_path} to Tika server") - tika_server = settings.PAPERLESS_TIKA_ENDPOINT + parsed = self.get_tika_result(document_path) - try: - parsed = parser.from_file(document_path, tika_server) - except Exception as err: - raise ParseError( - f"Could not parse {document_path} with tika server at " - f"{tika_server}: {err}", - ) - - metadata = parsed["metadata"].copy() - - subject = metadata.pop("dc:subject", "") + subject = parsed["metadata"].get("dc:subject", "") content = parsed["content"].strip() if content.startswith(subject): @@ -187,36 +194,25 @@ class TikaDocumentParserEml(DocumentParser): content = re.sub("\n+", "\n", content) self.text = ( - f"{content}\n" - f"______________________\n" - f"From: {metadata.pop('Message-From', '')}\n" - f"To: {metadata.pop('Message-To', '')}\n" - f"CC: {metadata.pop('Message-CC', '')}" + f"{content}\n\n" + f"From: {parsed['metadata'].get('Message-From', '')}\n" + f"To: {parsed['metadata'].get('Message-To', '')}\n" + f"CC: {parsed['metadata'].get('Message-CC', '')}" ) try: - self.date = dateutil.parser.isoparse(parsed["metadata"]["dcterms:created"]) + self.date = dateutil.parser.isoparse( + parsed["metadata"]["dcterms:created"], + ) except Exception as e: self.log( "warning", f"Unable to extract date for document " f"{document_path}: {e}", ) - self.archive_path = self.generate_pdf(document_path, parsed) - - def generate_pdf(self, document_path, parsed=None): - if not parsed: - self.log("info", f"Sending {document_path} to Tika server") - tika_server = settings.PAPERLESS_TIKA_ENDPOINT - - try: - parsed = parser.from_file(document_path, tika_server) - except Exception as err: - raise ParseError( - f"Could not parse {document_path} with tika server at " - f"{tika_server}: {err}", - ) + self.archive_path = self.generate_pdf(document_path) + def generate_pdf(self, document_path): def clean_html(text: str): if isinstance(text, list): text = ", ".join([str(e) for e in text]) @@ -230,51 +226,59 @@ class TikaDocumentParserEml(DocumentParser): text = text.replace('"', """) return text + parsed = self.get_tika_result(document_path) + pdf_path = os.path.join(self.tempdir, "convert.pdf") gotenberg_server = settings.PAPERLESS_TIKA_GOTENBERG_ENDPOINT url = gotenberg_server + "/forms/chromium/convert/html" self.log("info", f"Converting {document_path} to PDF as {pdf_path}") - subject = parsed["metadata"].pop("dc:subject", "") - content = parsed.pop("content", "").strip() + data = {} + data["subject"] = clean_html(parsed["metadata"].get("dc:subject", "")) + data["from"] = clean_html(parsed["metadata"].get("Message-From", "")) + data["to"] = clean_html(parsed["metadata"].get("Message-To", "")) + data["cc"] = clean_html(parsed["metadata"].get("Message-CC", "")) + data["date"] = clean_html(parsed["metadata"].get("dcterms:created", "")) - if content.startswith(subject): - content = content[len(subject) :].strip() + content = parsed.get("content", "").strip() + if content.startswith(data["subject"]): + content = content[len(data["subject"]) :].strip() + data["content"] = clean_html(content) - html = StringIO( - f""" - - - - - My PDF - - -

{clean_html(subject)}

-

From: {clean_html(parsed['metadata'].pop('Message-From', ''))} -

To: {clean_html(parsed['metadata'].pop('Message-To', ''))} -

CC: {clean_html(parsed['metadata'].pop('Message-CC', ''))} -

Date: {clean_html(parsed['metadata'].pop('dcterms:created', ''))} -

{clean_html(content)}
- - - """, - ) + html_file = os.path.join(os.path.dirname(__file__), "mail_template/index.html") + css_file = os.path.join(os.path.dirname(__file__), "mail_template/output.css") + placeholder_pattern = re.compile(r"{{(.+)}}") + html = StringIO() - files = { - "html": ( - "index.html", - html, - ), - } - headers = {} + with open(html_file, "r") as html_template_handle: + with open(css_file, "rb") as css_handle: + for line in html_template_handle.readlines(): + for placeholder in placeholder_pattern.findall(line): + line = re.sub( + "{{" + placeholder + "}}", + data.get(placeholder.strip(), ""), + line, + ) + html.write(line) + html.seek(0) + files = { + "html": ( + "index.html", + html, + ), + "css": ( + "output.css", + css_handle, + ), + } + headers = {} - try: - response = requests.post(url, files=files, headers=headers) - response.raise_for_status() # ensure we notice bad responses - except Exception as err: - raise ParseError(f"Error while converting document to PDF: {err}") + try: + response = requests.post(url, files=files, headers=headers) + response.raise_for_status() # ensure we notice bad responses + except Exception as err: + raise ParseError(f"Error while converting document to PDF: {err}") with open(pdf_path, "wb") as file: file.write(response.content) From 0e40ef5f352ca3e6ffe5b76374481f2a888e24dd Mon Sep 17 00:00:00 2001 From: phail Date: Wed, 27 Apr 2022 19:52:59 +0200 Subject: [PATCH 10/60] add css for pdf generation --- src/paperless_tika/mail_template/output.css | 860 ++++++++++++++++++++ 1 file changed, 860 insertions(+) diff --git a/src/paperless_tika/mail_template/output.css b/src/paperless_tika/mail_template/output.css index e69de29bb..ea34ab3e6 100644 --- a/src/paperless_tika/mail_template/output.css +++ b/src/paperless_tika/mail_template/output.css @@ -0,0 +1,860 @@ +/* +! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* +Ensure the default browser behavior of the `hidden` attribute. +*/ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.col-span-12 { + grid-column: span 12 / span 12; +} + +.col-span-10 { + grid-column: span 10 / span 10; +} + +.col-span-2 { + grid-column: span 2 / span 2; +} + +.col-span-1 { + grid-column: span 1 / span 1; +} + +.col-span-9 { + grid-column: span 9 / span 9; +} + +.col-span-8 { + grid-column: span 8 / span 8; +} + +.col-span-3 { + grid-column: span 3 / span 3; +} + +.col-start-3 { + grid-column-start: 3; +} + +.col-start-1 { + grid-column-start: 1; +} + +.col-start-2 { + grid-column-start: 2; +} + +.col-start-12 { + grid-column-start: 12; +} + +.col-start-11 { + grid-column-start: 11; +} + +.col-start-10 { + grid-column-start: 10; +} + +.row-start-1 { + grid-row-start: 1; +} + +.row-start-2 { + grid-row-start: 2; +} + +.row-start-3 { + grid-row-start: 3; +} + +.row-start-4 { + grid-row-start: 4; +} + +.row-start-5 { + grid-row-start: 5; +} + +.mt-5 { + margin-top: 1.25rem; +} + +.mb-5 { + margin-bottom: 1.25rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.mt-16 { + margin-top: 4rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mt-12 { + margin-top: 3rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-11 { + margin-top: 2.75rem; +} + +.mb-11 { + margin-bottom: 2.75rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.box-border { + box-sizing: border-box; +} + +.box-content { + box-sizing: content-box; +} + +.flex { + display: flex; +} + +.grid { + display: grid; +} + +.h-1 { + height: 0.25rem; +} + +.h-\[4px\] { + height: 4px; +} + +.h-\[8px\] { + height: 8px; +} + +.h-\[30px\] { + height: 30px; +} + +.h-\[2px\] { + height: 2px; +} + +.h-\[1px\] { + height: 1px; +} + +.w-screen { + width: 100vw; +} + +.w-full { + width: 100%; +} + +.max-w-lg { + max-width: 32rem; +} + +.max-w-3xl { + max-width: 48rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.max-w-7xl { + max-width: 80rem; +} + +.auto-cols-min { + grid-auto-columns: -webkit-min-content; + grid-auto-columns: min-content; +} + +.auto-cols-fr { + grid-auto-columns: minmax(0, 1fr); +} + +.auto-cols-max { + grid-auto-columns: -webkit-max-content; + grid-auto-columns: max-content; +} + +.grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.grid-cols-7 { + grid-template-columns: repeat(7, minmax(0, 1fr)); +} + +.grid-cols-12 { + grid-template-columns: repeat(12, minmax(0, 1fr)); +} + +.grid-rows-4 { + grid-template-rows: repeat(4, minmax(0, 1fr)); +} + +.grid-rows-5 { + grid-template-rows: repeat(5, minmax(0, 1fr)); +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-center { + justify-content: center; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-y-3 { + row-gap: 0.75rem; +} + +.gap-x-3 { + -moz-column-gap: 0.75rem; + column-gap: 0.75rem; +} + +.gap-x-2 { + -moz-column-gap: 0.5rem; + column-gap: 0.5rem; +} + +.whitespace-pre-line { + white-space: pre-line; +} + +.border { + border-width: 1px; +} + +.border-t { + border-top-width: 1px; +} + +.border-t-2 { + border-top-width: 2px; +} + +.border-b-2 { + border-bottom-width: 2px; +} + +.border-t-4 { + border-top-width: 4px; +} + +.border-b-4 { + border-bottom-width: 4px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-solid { + border-style: solid; +} + +.border-black { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); +} + +.bg-red-600 { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-slate-300 { + --tw-bg-opacity: 1; + background-color: rgb(203 213 225 / var(--tw-bg-opacity)); +} + +.bg-slate-200 { + --tw-bg-opacity: 1; + background-color: rgb(226 232 240 / var(--tw-bg-opacity)); +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.text-right { + text-align: right; +} + +.align-middle { + vertical-align: middle; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.font-bold { + font-weight: 700; +} + +.text-slate-400 { + --tw-text-opacity: 1; + color: rgb(148 163 184 / var(--tw-text-opacity)); +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.underline { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} From c1efe11cf3908aa6920e795a785451fd11c2eb71 Mon Sep 17 00:00:00 2001 From: phail Date: Wed, 27 Apr 2022 23:32:10 +0200 Subject: [PATCH 11/60] improve pdf generation --- src/paperless_tika/mail_template/index.html | 22 +++++++-------- src/paperless_tika/mail_template/output.css | 12 ++++----- src/paperless_tika/parsers.py | 30 ++++++++++++++++++--- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/paperless_tika/mail_template/index.html b/src/paperless_tika/mail_template/index.html index f7d7fbf9d..7a8740dd8 100644 --- a/src/paperless_tika/mail_template/index.html +++ b/src/paperless_tika/mail_template/index.html @@ -14,24 +14,20 @@
{{ date }}
-
From
+
{{ from_label }}
{{ from }}
-
Subject
-
{{ subject }} -
+
{{ subject_label }}
+
{{ subject }}
-
To
-
{{ to }} -
+
{{ to_label }}
+
{{ to }}
-
CC
-
{{ cc }} -
+
{{ cc_label }}
+
{{ cc }}
-
BCC
-
{{ bcc }} -
+
{{ bcc_label }}
+
{{ bcc }}
diff --git a/src/paperless_tika/mail_template/output.css b/src/paperless_tika/mail_template/output.css index ea34ab3e6..8b05e953b 100644 --- a/src/paperless_tika/mail_template/output.css +++ b/src/paperless_tika/mail_template/output.css @@ -696,7 +696,7 @@ Ensure the default browser behavior of the `hidden` attribute. } .auto-cols-fr { - grid-auto-columns: minmax(0, 1fr); + grid-auto-columns: minmax(); } .auto-cols-max { @@ -705,23 +705,23 @@ Ensure the default browser behavior of the `hidden` attribute. } .grid-cols-5 { - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax()); } .grid-cols-7 { - grid-template-columns: repeat(7, minmax(0, 1fr)); + grid-template-columns: repeat(7, minmax()); } .grid-cols-12 { - grid-template-columns: repeat(12, minmax(0, 1fr)); + grid-template-columns: repeat(12, minmax()); } .grid-rows-4 { - grid-template-rows: repeat(4, minmax(0, 1fr)); + grid-template-rows: repeat(4, minmax()); } .grid-rows-5 { - grid-template-rows: repeat(5, minmax(0, 1fr)); + grid-template-rows: repeat(5, minmax()); } .flex-col { diff --git a/src/paperless_tika/parsers.py b/src/paperless_tika/parsers.py index 9cea35cf9..bc6c7aef9 100644 --- a/src/paperless_tika/parsers.py +++ b/src/paperless_tika/parsers.py @@ -215,7 +215,7 @@ class TikaDocumentParserEml(DocumentParser): def generate_pdf(self, document_path): def clean_html(text: str): if isinstance(text, list): - text = ", ".join([str(e) for e in text]) + text = "\n".join([str(e) for e in text]) if type(text) != str: text = str(text) text = text.replace("&", "&") @@ -236,9 +236,20 @@ class TikaDocumentParserEml(DocumentParser): data = {} data["subject"] = clean_html(parsed["metadata"].get("dc:subject", "")) + if data["subject"] != "": + data["subject_label"] = "Subject" data["from"] = clean_html(parsed["metadata"].get("Message-From", "")) + if data["from"] != "": + data["from_label"] = "From" data["to"] = clean_html(parsed["metadata"].get("Message-To", "")) + if data["to"] != "": + data["to_label"] = "To" data["cc"] = clean_html(parsed["metadata"].get("Message-CC", "")) + if data["cc"] != "": + data["cc_label"] = "CC" + data["bcc"] = clean_html(parsed["metadata"].get("Message-BCC", "")) + if data["bcc"] != "": + data["bcc_label"] = "BCC" data["date"] = clean_html(parsed["metadata"].get("dcterms:created", "")) content = parsed.get("content", "").strip() @@ -273,9 +284,22 @@ class TikaDocumentParserEml(DocumentParser): ), } headers = {} - + data = { + "marginTop": "0", + "marginBottom": "0", + "marginLeft": "0", + "marginRight": "0", + "paperWidth": "8.27", + "paperHeight": "11.7", + "scale": "1.0", + } try: - response = requests.post(url, files=files, headers=headers) + response = requests.post( + url, + files=files, + headers=headers, + data=data, + ) response.raise_for_status() # ensure we notice bad responses except Exception as err: raise ParseError(f"Error while converting document to PDF: {err}") From 47189643ff07059e9c9c256f725bdf837c481811 Mon Sep 17 00:00:00 2001 From: phail Date: Fri, 29 Apr 2022 22:58:11 +0200 Subject: [PATCH 12/60] add eml parser to paperless_mail --- Pipfile | 1 + .../compose/docker-compose.postgres-tika.yml | 2 +- .../docker-compose.sqlite-tika.arm.yml | 7 +- docker/compose/docker-compose.sqlite-tika.yml | 2 +- docs/configuration.rst | 2 +- docs/troubleshooting.rst | 2 +- scripts/start_services.sh | 2 +- src/paperless_mail/apps.py | 7 + src/paperless_mail/mail_template/index.html | 56 + src/paperless_mail/mail_template/input.css | 3 + src/paperless_mail/mail_template/output.css | 706 +++++++++ .../mail_template/package-lock.json | 1260 +++++++++++++++++ src/paperless_mail/mail_template/package.json | 5 + .../mail_template/tailwind.config.js | 7 + src/paperless_mail/parsers.py | 225 +++ src/paperless_mail/signals.py | 14 + 16 files changed, 2293 insertions(+), 8 deletions(-) create mode 100644 src/paperless_mail/mail_template/index.html create mode 100644 src/paperless_mail/mail_template/input.css create mode 100644 src/paperless_mail/mail_template/output.css create mode 100644 src/paperless_mail/mail_template/package-lock.json create mode 100644 src/paperless_mail/mail_template/package.json create mode 100644 src/paperless_mail/mail_template/tailwind.config.js create mode 100644 src/paperless_mail/parsers.py create mode 100644 src/paperless_mail/signals.py diff --git a/Pipfile b/Pipfile index da2644251..debccb323 100644 --- a/Pipfile +++ b/Pipfile @@ -54,6 +54,7 @@ zipp = {version = "*", markers = "python_version < '3.9'"} pyzbar = "*" pdf2image = "*" click = "==8.0.4" +bleach = "*" [dev-packages] coveralls = "*" diff --git a/docker/compose/docker-compose.postgres-tika.yml b/docker/compose/docker-compose.postgres-tika.yml index c6a72e903..ad856b0fe 100644 --- a/docker/compose/docker-compose.postgres-tika.yml +++ b/docker/compose/docker-compose.postgres-tika.yml @@ -82,7 +82,7 @@ services: restart: unless-stopped command: - "gotenberg" - - "--chromium-disable-routes=true" + - "--chromium-disable-web-security" tika: image: apache/tika diff --git a/docker/compose/docker-compose.sqlite-tika.arm.yml b/docker/compose/docker-compose.sqlite-tika.arm.yml index d6ac848ec..930c66884 100644 --- a/docker/compose/docker-compose.sqlite-tika.arm.yml +++ b/docker/compose/docker-compose.sqlite-tika.arm.yml @@ -69,10 +69,11 @@ services: PAPERLESS_TIKA_ENDPOINT: http://tika:9998 gotenberg: - image: thecodingmachine/gotenberg + image: gotenberg/gotenberg:7 restart: unless-stopped - environment: - DISABLE_GOOGLE_CHROME: 1 + command: + - "gotenberg" + - "--chromium-disable-web-security" tika: image: iwishiwasaneagle/apache-tika-arm@sha256:a78c25ffe57ecb1a194b2859d42a61af46e9e845191512b8f1a4bf90578ffdfd diff --git a/docker/compose/docker-compose.sqlite-tika.yml b/docker/compose/docker-compose.sqlite-tika.yml index d9327533e..147f803a1 100644 --- a/docker/compose/docker-compose.sqlite-tika.yml +++ b/docker/compose/docker-compose.sqlite-tika.yml @@ -71,7 +71,7 @@ services: restart: unless-stopped command: - "gotenberg" - - "--chromium-disable-routes=true" + - "--chromium-disable-web-security" tika: image: apache/tika diff --git a/docs/configuration.rst b/docs/configuration.rst index 3541f2e07..d4bde7bc8 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -494,7 +494,7 @@ requires are as follows: restart: unless-stopped command: - "gotenberg" - - "--chromium-disable-routes=true" + - "--chromium-disable-web-security" tika: image: apache/tika diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 3ae4909de..4e3cada82 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -129,7 +129,7 @@ If using docker-compose, this is achieved by the following configuration change restart: unless-stopped command: - "gotenberg" - - "--chromium-disable-routes=true" + - "--chromium-disable-web-security" - "--api-timeout=60" Permission denied errors in the consumption directory diff --git a/scripts/start_services.sh b/scripts/start_services.sh index 24e3233cd..bebb5c913 100755 --- a/scripts/start_services.sh +++ b/scripts/start_services.sh @@ -2,5 +2,5 @@ docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13 docker run -d -p 6379:6379 redis:latest -docker run -p 3000:3000 -d gotenberg/gotenberg:7 +docker run -p 3000:3000 -d gotenberg/gotenberg:7 gotenberg --chromium-disable-web-security docker run -p 9998:9998 -d apache/tika diff --git a/src/paperless_mail/apps.py b/src/paperless_mail/apps.py index 1c5d656e0..fa6b1a267 100644 --- a/src/paperless_mail/apps.py +++ b/src/paperless_mail/apps.py @@ -1,8 +1,15 @@ from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ +from paperless_mail.signals import mail_consumer_declaration class PaperlessMailConfig(AppConfig): name = "paperless_mail" verbose_name = _("Paperless mail") + + def ready(self): + from documents.signals import document_consumer_declaration + + document_consumer_declaration.connect(mail_consumer_declaration) + AppConfig.ready(self) diff --git a/src/paperless_mail/mail_template/index.html b/src/paperless_mail/mail_template/index.html new file mode 100644 index 000000000..a3a01cb3e --- /dev/null +++ b/src/paperless_mail/mail_template/index.html @@ -0,0 +1,56 @@ + + + + + + + + + + +
+ +
+ +
{{ date }}
+ +
{{ from_label }}
+
{{ from }}
+ +
{{ subject_label }}
+
{{ subject }}
+ +
{{ to_label }}
+
{{ to }}
+ +
{{ cc_label }}
+
{{ cc }}
+ +
{{ bcc_label }}
+
{{ bcc }}
+ +
{{ attachments_label }}
+
{{ attachments }}
+
+ + +
+ + +
{{ content }}
+ +
+ +
+ + + + diff --git a/src/paperless_mail/mail_template/input.css b/src/paperless_mail/mail_template/input.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/src/paperless_mail/mail_template/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/paperless_mail/mail_template/output.css b/src/paperless_mail/mail_template/output.css new file mode 100644 index 000000000..fa51c7539 --- /dev/null +++ b/src/paperless_mail/mail_template/output.css @@ -0,0 +1,706 @@ +/* +! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* +Ensure the default browser behavior of the `hidden` attribute. +*/ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.col-span-2 { + grid-column: span 2 / span 2; +} + +.col-span-8 { + grid-column: span 8 / span 8; +} + +.col-span-10 { + grid-column: span 10 / span 10; +} + +.col-span-3 { + grid-column: span 3 / span 3; +} + +.col-span-4 { + grid-column: span 4 / span 4; +} + +.col-span-7 { + grid-column: span 7 / span 7; +} + +.col-start-11 { + grid-column-start: 11; +} + +.col-start-1 { + grid-column-start: 1; +} + +.col-start-2 { + grid-column-start: 2; +} + +.col-start-10 { + grid-column-start: 10; +} + +.col-start-9 { + grid-column-start: 9; +} + +.row-start-1 { + grid-row-start: 1; +} + +.row-start-2 { + grid-row-start: 2; +} + +.row-start-3 { + grid-row-start: 3; +} + +.row-start-4 { + grid-row-start: 4; +} + +.row-start-5 { + grid-row-start: 5; +} + +.row-start-6 { + grid-row-start: 6; +} + +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +.my-0\.5 { + margin-top: 0.125rem; + margin-bottom: 0.125rem; +} + +.my-0 { + margin-top: 0px; + margin-bottom: 0px; +} + +.mb-5 { + margin-bottom: 1.25rem; +} + +.box-content { + box-sizing: content-box; +} + +.flex { + display: flex; +} + +.grid { + display: grid; +} + +.h-\[1px\] { + height: 1px; +} + +.w-screen { + width: 100vw; +} + +.w-full { + width: 100%; +} + +.max-w-4xl { + max-width: 56rem; +} + +.grid-cols-12 { + grid-template-columns: repeat(12, minmax(0, 1fr)); +} + +.grid-rows-5 { + grid-template-rows: repeat(5, minmax(0, 1fr)); +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.gap-x-2 { + -moz-column-gap: 0.5rem; + column-gap: 0.5rem; +} + +.whitespace-pre-line { + white-space: pre-line; +} + +.break-words { + overflow-wrap: break-word; +} + +.border-t { + border-top-width: 1px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-solid { + border-style: solid; +} + +.border-black { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-slate-200 { + --tw-bg-opacity: 1; + background-color: rgb(226 232 240 / var(--tw-bg-opacity)); +} + +.p-4 { + padding: 1rem; +} + +.text-right { + text-align: right; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.font-bold { + font-weight: 700; +} + +.text-slate-400 { + --tw-text-opacity: 1; + color: rgb(148 163 184 / var(--tw-text-opacity)); +} + +.text-blue-600 { + --tw-text-opacity: 1; + color: rgb(37 99 235 / var(--tw-text-opacity)); +} + +.underline { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} diff --git a/src/paperless_mail/mail_template/package-lock.json b/src/paperless_mail/mail_template/package-lock.json new file mode 100644 index 000000000..9d7a08b10 --- /dev/null +++ b/src/paperless_mail/mail_template/package-lock.json @@ -0,0 +1,1260 @@ +{ + "name": "phail-html", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "tailwindcss": "^3.0.24" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "node_modules/detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "dependencies": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", + "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.1", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.24.tgz", + "integrity": "sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==", + "dev": true, + "dependencies": { + "arg": "^5.0.1", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.12", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + } + }, + "dependencies": { + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz", + "integrity": "sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "dev": true, + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "lilconfig": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz", + "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "nanoid": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "postcss": { + "version": "8.4.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.12.tgz", + "integrity": "sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg==", + "dev": true, + "requires": { + "nanoid": "^3.3.1", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postcss-js": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", + "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", + "dev": true, + "requires": { + "camelcase-css": "^2.0.1" + } + }, + "postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "requires": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + } + }, + "postcss-nested": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", + "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "tailwindcss": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.24.tgz", + "integrity": "sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig==", + "dev": true, + "requires": { + "arg": "^5.0.1", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.12", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "5.0.6", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + } + } +} diff --git a/src/paperless_mail/mail_template/package.json b/src/paperless_mail/mail_template/package.json new file mode 100644 index 000000000..48785a003 --- /dev/null +++ b/src/paperless_mail/mail_template/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "tailwindcss": "^3.0.24" + } +} diff --git a/src/paperless_mail/mail_template/tailwind.config.js b/src/paperless_mail/mail_template/tailwind.config.js new file mode 100644 index 000000000..1e01796ee --- /dev/null +++ b/src/paperless_mail/mail_template/tailwind.config.js @@ -0,0 +1,7 @@ +module.exports = { + content: ['./*.html'], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py new file mode 100644 index 000000000..c3ac323ae --- /dev/null +++ b/src/paperless_mail/parsers.py @@ -0,0 +1,225 @@ +import os +import re +from io import StringIO + +import requests +from bleach import clean +from bleach import linkify +from django.conf import settings +from documents.parsers import DocumentParser +from documents.parsers import make_thumbnail_from_pdf +from documents.parsers import ParseError +from imap_tools import MailMessage + + +class MailDocumentParser(DocumentParser): + """ + This parser sends documents to a local tika server + """ + + logging_name = "paperless.parsing.mail" + _parsed = None + + def get_parsed(self, document_path) -> MailMessage: + if not self._parsed: + try: + with open(document_path, "rb") as eml: + self._parsed = MailMessage.from_bytes(eml.read()) + except Exception as err: + raise ParseError( + f"Could not parse {document_path}: {err}", + ) + + return self._parsed + + def get_thumbnail(self, document_path, mime_type, file_name=None): + if not self.archive_path: + self.archive_path = self.generate_pdf(document_path) + + return make_thumbnail_from_pdf( + self.archive_path, + self.tempdir, + self.logging_group, + ) + + def extract_metadata(self, document_path, mime_type): + result = [] + + try: + mail = self.get_parsed(document_path) + except ParseError as e: + self.log( + "warning", + f"Error while fetching document metadata for " f"{document_path}: {e}", + ) + return result + + for key, value in mail.headers.items(): + value = ", ".join(i for i in value) + + result.append( + { + "namespace": "", + "prefix": "header", + "key": key, + "value": value, + }, + ) + + result.append( + { + "namespace": "", + "prefix": "", + "key": "attachments", + "value": ", ".join( + f"{attachment.filename}({(attachment.size / 1024):.2f} KiB)" + for attachment in mail.attachments + ), + }, + ) + + result.append( + { + "namespace": "", + "prefix": "", + "key": "date", + "value": mail.date.strftime("%Y-%m-%d %H:%M:%S %Z"), + }, + ) + + result.sort(key=lambda item: (item["prefix"], item["key"])) + return result + + def parse(self, document_path, mime_type, file_name=None): + mail = self.get_parsed(document_path) + + content = mail.text.strip() + + content = re.sub(" +", " ", content) + content = re.sub("\n+", "\n", content) + + self.text = f"{content}\n\n" + self.text += f"Subject: {mail.subject}\n" + self.text += f"From: {mail.from_values.full}\n" + self.text += f"To: {', '.join(address.full for address in mail.to_values)}\n" + if len(mail.cc_values) >= 1: + self.text += ( + f"CC: {', '.join(address.full for address in mail.cc_values)}\n" + ) + if len(mail.bcc_values) >= 1: + self.text += ( + f"BCC: {', '.join(address.full for address in mail.bcc_values)}\n" + ) + if len(mail.attachments) >= 1: + att = ", ".join(f"{a.filename} ({a.size})" for a in mail.attachments) + self.text += f"Attachments: {att}" + + self.date = mail.date + self.archive_path = self.generate_pdf(document_path) + + def generate_pdf(self, document_path): + def clean_html(text: str): + if isinstance(text, list): + text = "\n".join([str(e) for e in text]) + if type(text) != str: + text = str(text) + text = text.replace("&", "&") + text = text.replace("<", "<") + text = text.replace(">", ">") + text = text.replace(" ", "  ") + text = text.replace("'", "'") + text = text.replace('"', """) + text = clean(text) + text = linkify(text, parse_email=True) + text = text.replace("\n", "
") + return text + + def clean_html_script(text: str): + text = text.replace("
diff --git a/src/paperless_mail/tests/samples/simple_text.eml b/src/paperless_mail/tests/samples/simple_text.eml index 49080bde8..ae5cc579d 100644 --- a/src/paperless_mail/tests/samples/simple_text.eml +++ b/src/paperless_mail/tests/samples/simple_text.eml @@ -14,6 +14,8 @@ User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Thunderbird/102.3.1 Content-Language: en-US To: some@one.de +Cc: asdasd@æsdasd.de, asdadasdasdasda.asdasd@æsdasd.de +Bcc: fdf@fvf.de From: Some One Content-Type: text/plain; charset=UTF-8; format=flowed Content-Transfer-Encoding: 7bit diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index e545067a4..66b19d182 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -69,7 +69,7 @@ class TestParser(TestCase): # The created intermediary pdf is not reproducible. But the thumbnail image should always look the same. expected_hash = ( - "18a2513c80584e538c4a129e8a2b0ce19bf0276eab9c95b72fa93e941db38d12" + "eeb2cf861f4873d2e569d0dfbfd385c2ac11722accf0fd3a32a54e3b115317a9" ) self.assertEqual( thumb_hash, @@ -226,7 +226,7 @@ class TestParser(TestCase): # Validate parsing returns the expected results parser.parse(os.path.join(self.SAMPLE_FILES, "html.eml"), "message/rfc822") - text_expected = "Some Text\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB)\n\nHTML content: Some Text\nand an embedded image.\nParagraph unchanged." + text_expected = "Some Text\r\n\r\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nHTML content: Some Text\nand an embedded image.\nParagraph unchanged." self.assertEqual(text_expected, parser.text) self.assertEqual( datetime.datetime( @@ -241,6 +241,27 @@ class TestParser(TestCase): parser.date, ) + # Validate parsing returns the expected results + parser = MailDocumentParser(None) + parser.parse( + os.path.join(self.SAMPLE_FILES, "simple_text.eml"), + "message/rfc822", + ) + text_expected = "This is just a simple Text Mail.\n\nSubject: Simple Text Mail\n\nFrom: Some One \n\nTo: some@one.de\n\nCC: asdasd@æsdasd.de, asdadasdasdasda.asdasd@æsdasd.de\n\nBCC: fdf@fvf.de\n\n" + self.assertEqual(text_expected, parser.text) + self.assertEqual( + datetime.datetime( + 2022, + 10, + 12, + 21, + 40, + 43, + tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)), + ), + parser.date, + ) + # Just check if file exists, the unittest for generate_pdf() goes deeper. self.assertTrue(os.path.isfile(parser.archive_path)) @@ -267,6 +288,18 @@ class TestParser(TestCase): parsed = parser.tika_parse(html) self.assertEqual(expected_text, parsed) + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html") + def test_generate_pdf_parse_error(self, m: mock.MagicMock, n: mock.MagicMock): + m.return_value = b"" + n.return_value = b"" + parser = MailDocumentParser(None) + + # Check if exception is raised when the pdf can not be created. + parser.gotenberg_server = "" + with pytest.raises(ParseError): + parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output def test_generate_pdf(self, m): parser = MailDocumentParser(None) @@ -295,7 +328,7 @@ class TestParser(TestCase): # The created pdf is not reproducible. But the converted image should always look the same. expected_hash = ( - "23468c4597d63bbefd38825e27c7f05ac666573fc35447d9ddf1784c9c31c6ea" + "4f338619575a21c5227de003a14216b07ba00a372ca5f132745e974a1f990e09" ) self.assertEqual( thumb_hash, @@ -341,7 +374,7 @@ class TestParser(TestCase): # The created pdf is not reproducible. But the converted image should always look the same. expected_hash = ( - "635bda532707faf69f06b040660445b656abcc7d622cc29c24a5c7fd2c713c5f" + "8734a3f0a567979343824e468cd737bf29c02086bbfd8773e94feb986968ad32" ) self.assertEqual( thumb_hash, From 90cb0836bb5d5f06b205290c2431efa7527b7c72 Mon Sep 17 00:00:00 2001 From: phail Date: Thu, 27 Oct 2022 23:11:41 +0200 Subject: [PATCH 34/60] Downgrade pdf validation to text only --- src/paperless_mail/tests/test_parsers.py | 84 +++--------------------- 1 file changed, 10 insertions(+), 74 deletions(-) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 66b19d182..953263f78 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -8,8 +8,8 @@ from urllib.request import urlopen import pytest from django.test import TestCase from documents.parsers import ParseError -from documents.parsers import run_convert from paperless_mail.parsers import MailDocumentParser +from pdfminer.high_level import extract_text class TestParser(TestCase): @@ -311,30 +311,9 @@ class TestParser(TestCase): pdf_path = parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) self.assertTrue(os.path.isfile(pdf_path)) - converted = os.path.join(parser.tempdir, "test_generate_pdf.webp") - run_convert( - density=300, - scale="500x5000>", - alpha="remove", - strip=True, - trim=False, - auto_orient=True, - input_file=f"{pdf_path}", # Do net define an index to convert all pages. - output_file=converted, - logging_group=None, - ) - self.assertTrue(os.path.isfile(converted)) - thumb_hash = self.hashfile(converted) - - # The created pdf is not reproducible. But the converted image should always look the same. - expected_hash = ( - "4f338619575a21c5227de003a14216b07ba00a372ca5f132745e974a1f990e09" - ) - self.assertEqual( - thumb_hash, - expected_hash, - f"PDF looks different. Check if {converted} looks weird.", - ) + extracted = extract_text(pdf_path) + expected = "From Name \n\n2022-10-15 09:23\n\nSubject HTML Message\n\nTo someone@example.de\n\nAttachments IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nSome Text \n\nand an embedded image.\n\n\x0cSome Text\n\n This image should not be shown.\n\nand an embedded image.\n\nParagraph unchanged.\n\n\x0c" + self.assertEqual(expected, extracted) def test_mail_to_html(self): parser = MailDocumentParser(None) @@ -357,30 +336,9 @@ class TestParser(TestCase): file.write(parser.generate_pdf_from_mail(mail)) file.close() - converted = os.path.join(parser.tempdir, "test_generate_pdf_from_mail.webp") - run_convert( - density=300, - scale="500x5000>", - alpha="remove", - strip=True, - trim=False, - auto_orient=True, - input_file=f"{pdf_path}", # Do net define an index to convert all pages. - output_file=converted, - logging_group=None, - ) - self.assertTrue(os.path.isfile(converted)) - thumb_hash = self.hashfile(converted) - - # The created pdf is not reproducible. But the converted image should always look the same. - expected_hash = ( - "8734a3f0a567979343824e468cd737bf29c02086bbfd8773e94feb986968ad32" - ) - self.assertEqual( - thumb_hash, - expected_hash, - f"PDF looks different. Check if {converted} looks weird.", - ) + extracted = extract_text(pdf_path) + expected = "From Name \n\n2022-10-15 09:23\n\nSubject HTML Message\n\nTo someone@example.de\n\nAttachments IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nSome Text \n\nand an embedded image.\n\n\x0c" + self.assertEqual(expected, extracted) def test_transform_inline_html(self): class MailAttachmentMock: @@ -432,31 +390,9 @@ class TestParser(TestCase): file.write(result) file.close() - converted = os.path.join(parser.tempdir, "test_generate_pdf_from_html.webp") - run_convert( - density=300, - scale="500x5000>", - alpha="remove", - strip=True, - trim=False, - auto_orient=True, - input_file=f"{pdf_path}", # Do net define an index to convert all pages. - output_file=converted, - logging_group=None, - ) - self.assertTrue(os.path.isfile(converted)) - thumb_hash = self.hashfile(converted) - - # The created pdf is not reproducible. But the converted image should always look the same. - expected_hash = ( - "267d61f0ab8f128a037002a424b2cb4bfe18a81e17f0b70f15d241688ed47d1a" - ) - self.assertEqual( - thumb_hash, - expected_hash, - f"PDF looks different. Check if {converted} looks weird. " - f"If Rick Astley is shown, Gotenberg loads from web which is bad for Mail content.", - ) + extracted = extract_text(pdf_path) + expected = "Some Text\n\n This image should not be shown.\n\nand an embedded image.\n\nParagraph unchanged.\n\n\x0c" + self.assertEqual(expected, extracted) def test_is_online_image_still_available(self): """ From 3c81a7468b47cdf5a2df0826429df99ab21f769b Mon Sep 17 00:00:00 2001 From: phail Date: Thu, 27 Oct 2022 23:41:29 +0200 Subject: [PATCH 35/60] replace thumbnail creation with mock --- src/paperless_mail/tests/test_parsers.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 953263f78..f4985a6ee 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -57,24 +57,21 @@ class TestParser(TestCase): sha256.update(data) return sha256.hexdigest() + @mock.patch("paperless_mail.parsers.make_thumbnail_from_pdf") @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_get_thumbnail(self, m): + def test_get_thumbnail(self, m, mock_make_thumbnail_from_pdf: mock.MagicMock): parser = MailDocumentParser(None) thumb = parser.get_thumbnail( os.path.join(self.SAMPLE_FILES, "simple_text.eml"), "message/rfc822", ) - self.assertTrue(os.path.isfile(thumb)) - thumb_hash = self.hashfile(thumb) - - # The created intermediary pdf is not reproducible. But the thumbnail image should always look the same. - expected_hash = ( - "eeb2cf861f4873d2e569d0dfbfd385c2ac11722accf0fd3a32a54e3b115317a9" + self.assertEqual( + parser.archive_path, + mock_make_thumbnail_from_pdf.call_args_list[0].args[0], ) self.assertEqual( - thumb_hash, - expected_hash, - "Thumbnail file hash not as expected.", + parser.tempdir, + mock_make_thumbnail_from_pdf.call_args_list[0].args[1], ) @mock.patch("documents.loggers.LoggingMixin.log") From 2204090151dbb34f57af8482366db1b3a4153ad8 Mon Sep 17 00:00:00 2001 From: phail Date: Thu, 27 Oct 2022 23:53:47 +0200 Subject: [PATCH 36/60] fix string --- src/paperless_mail/tests/test_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index f4985a6ee..596e7d180 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -223,7 +223,7 @@ class TestParser(TestCase): # Validate parsing returns the expected results parser.parse(os.path.join(self.SAMPLE_FILES, "html.eml"), "message/rfc822") - text_expected = "Some Text\r\n\r\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nHTML content: Some Text\nand an embedded image.\nParagraph unchanged." + text_expected = "Some Text\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nHTML content: Some Text\nand an embedded image.\nParagraph unchanged." self.assertEqual(text_expected, parser.text) self.assertEqual( datetime.datetime( From 6df73ae940420eed2b1f5c300b90992d0becb013 Mon Sep 17 00:00:00 2001 From: phail Date: Sat, 29 Oct 2022 23:20:35 +0200 Subject: [PATCH 37/60] gotenberg with modified cmd --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afd3d1a73..c4965a348 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: ports: - "9998:9998/tcp" gotenberg: - image: docker.io/gotenberg/gotenberg:7.6 + image: ghcr.io/p-h-a-i-l/gotenberg:7.6 ports: - "3000:3000/tcp" env: From 3de6e0bcf1b6e0e7afe578e37fd67f5b455c1987 Mon Sep 17 00:00:00 2001 From: phail Date: Thu, 3 Nov 2022 00:58:36 +0100 Subject: [PATCH 38/60] put parser into setup make test using convert optional Gotenberg live testing --- .github/workflows/ci.yml | 4 + src/paperless_mail/tests/samples/first.pdf | Bin 0 -> 6714 bytes src/paperless_mail/tests/samples/second.pdf | Bin 0 -> 6723 bytes .../tests/samples/simple_text.eml.pdf | Bin 0 -> 22301 bytes .../tests/samples/simple_text.eml.pdf.webp | Bin 0 -> 5340 bytes src/paperless_mail/tests/test_parsers.py | 195 +++++++++------- src/paperless_mail/tests/test_parsers_live.py | 220 ++++++++++++++++++ 7 files changed, 329 insertions(+), 90 deletions(-) create mode 100644 src/paperless_mail/tests/samples/first.pdf create mode 100644 src/paperless_mail/tests/samples/second.pdf create mode 100644 src/paperless_mail/tests/samples/simple_text.eml.pdf create mode 100644 src/paperless_mail/tests/samples/simple_text.eml.pdf.webp create mode 100644 src/paperless_mail/tests/test_parsers_live.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4965a348..8e506c8aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,10 @@ jobs: PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }} PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }} PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }} + # Skip Tests which require convert + PAPERLESS_TEST_SKIP_CONVERT: 1 + # Enable Gotenberg end to end testing + GOTENBERG_LIVE: 1 steps: - name: Checkout diff --git a/src/paperless_mail/tests/samples/first.pdf b/src/paperless_mail/tests/samples/first.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4f74613f97a8cf297ce4c453e346b34c54a5364c GIT binary patch literal 6714 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 literal 0 HcmV?d00001 diff --git a/src/paperless_mail/tests/samples/second.pdf b/src/paperless_mail/tests/samples/second.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2955c8d5d133267d97e5279dc7a30886fdd95fb9 GIT binary patch literal 6723 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 literal 0 HcmV?d00001 diff --git a/src/paperless_mail/tests/samples/simple_text.eml.pdf b/src/paperless_mail/tests/samples/simple_text.eml.pdf new file mode 100644 index 0000000000000000000000000000000000000000..678e6df42eb3d69a13c6b209bef01494c5847240 GIT binary patch literal 22301 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 literal 0 HcmV?d00001 diff --git a/src/paperless_mail/tests/samples/simple_text.eml.pdf.webp b/src/paperless_mail/tests/samples/simple_text.eml.pdf.webp new file mode 100644 index 0000000000000000000000000000000000000000..614aeee9c2edaef9ac705e15de6c9234050b008a GIT binary patch literal 5340 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 literal 0 HcmV?d00001 diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 596e7d180..a0c19e6f5 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -1,9 +1,6 @@ import datetime -import hashlib import os from unittest import mock -from urllib.error import HTTPError -from urllib.request import urlopen import pytest from django.test import TestCase @@ -15,19 +12,25 @@ from pdfminer.high_level import extract_text class TestParser(TestCase): SAMPLE_FILES = os.path.join(os.path.dirname(__file__), "samples") - def test_get_parsed(self): - parser = MailDocumentParser(None) + def setUp(self) -> None: + self.parser = MailDocumentParser(logging_group=None) + def tearDown(self) -> None: + self.parser.cleanup() + + def test_get_parsed(self): # Check if exception is raised when parsing fails. with pytest.raises(ParseError): - parser.get_parsed(os.path.join(self.SAMPLE_FILES, "na")) + self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "na")) # Check if exception is raised when the mail is faulty. with pytest.raises(ParseError): - parser.get_parsed(os.path.join(self.SAMPLE_FILES, "broken.eml")) + self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "broken.eml")) # Parse Test file and check relevant content - parsed1 = parser.get_parsed(os.path.join(self.SAMPLE_FILES, "simple_text.eml")) + parsed1 = self.parser.get_parsed( + os.path.join(self.SAMPLE_FILES, "simple_text.eml"), + ) self.assertEqual(parsed1.date.year, 2022) self.assertEqual(parsed1.date.month, 10) @@ -42,48 +45,45 @@ class TestParser(TestCase): self.assertEqual(parsed1.to, ("some@one.de",)) # Check if same parsed object as before is returned, even if another file is given. - parsed2 = parser.get_parsed(os.path.join(os.path.join(self.SAMPLE_FILES, "na"))) + parsed2 = self.parser.get_parsed( + os.path.join(os.path.join(self.SAMPLE_FILES, "na")), + ) self.assertEqual(parsed1, parsed2) - @staticmethod - def hashfile(file): - buf_size = 65536 # An arbitrary (but fixed) buffer - sha256 = hashlib.sha256() - with open(file, "rb") as f: - while True: - data = f.read(buf_size) - if not data: - break - sha256.update(data) - return sha256.hexdigest() - + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") @mock.patch("paperless_mail.parsers.make_thumbnail_from_pdf") - @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_get_thumbnail(self, m, mock_make_thumbnail_from_pdf: mock.MagicMock): - parser = MailDocumentParser(None) - thumb = parser.get_thumbnail( + def test_get_thumbnail( + self, + mock_make_thumbnail_from_pdf: mock.MagicMock, + mock_generate_pdf: mock.MagicMock, + ): + mocked_return = "Passing the return value through.." + mock_make_thumbnail_from_pdf.return_value = mocked_return + + mock_generate_pdf.return_value = "Mocked return value.." + + thumb = self.parser.get_thumbnail( os.path.join(self.SAMPLE_FILES, "simple_text.eml"), "message/rfc822", ) self.assertEqual( - parser.archive_path, + self.parser.archive_path, mock_make_thumbnail_from_pdf.call_args_list[0].args[0], ) self.assertEqual( - parser.tempdir, + self.parser.tempdir, mock_make_thumbnail_from_pdf.call_args_list[0].args[1], ) + self.assertEqual(mocked_return, thumb) @mock.patch("documents.loggers.LoggingMixin.log") def test_extract_metadata(self, m: mock.MagicMock): - parser = MailDocumentParser(None) - # Validate if warning is logged when parsing fails - self.assertEqual([], parser.extract_metadata("na", "message/rfc822")) + self.assertEqual([], self.parser.extract_metadata("na", "message/rfc822")) self.assertEqual("warning", m.call_args[0][0]) # Validate Metadata parsing returns the expected results - metadata = parser.extract_metadata( + metadata = self.parser.extract_metadata( os.path.join(self.SAMPLE_FILES, "simple_text.eml"), "message/rfc822", ) @@ -209,22 +209,22 @@ class TestParser(TestCase): in metadata, ) - @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_parse(self, m): - parser = MailDocumentParser(None) - + def test_parse_na(self): # Check if exception is raised when parsing fails. with pytest.raises(ParseError): - parser.parse( + self.parser.parse( os.path.join(os.path.join(self.SAMPLE_FILES, "na")), "message/rfc822", ) + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output + def test_parse_html_eml(self, m, n): # Validate parsing returns the expected results - parser.parse(os.path.join(self.SAMPLE_FILES, "html.eml"), "message/rfc822") + self.parser.parse(os.path.join(self.SAMPLE_FILES, "html.eml"), "message/rfc822") text_expected = "Some Text\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nHTML content: Some Text\nand an embedded image.\nParagraph unchanged." - self.assertEqual(text_expected, parser.text) + self.assertEqual(text_expected, self.parser.text) self.assertEqual( datetime.datetime( 2022, @@ -235,17 +235,20 @@ class TestParser(TestCase): 19, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)), ), - parser.date, + self.parser.date, ) + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output + def test_parse_simple_eml(self, m, n): # Validate parsing returns the expected results - parser = MailDocumentParser(None) - parser.parse( + + self.parser.parse( os.path.join(self.SAMPLE_FILES, "simple_text.eml"), "message/rfc822", ) text_expected = "This is just a simple Text Mail.\n\nSubject: Simple Text Mail\n\nFrom: Some One \n\nTo: some@one.de\n\nCC: asdasd@æsdasd.de, asdadasdasdasda.asdasd@æsdasd.de\n\nBCC: fdf@fvf.de\n\n" - self.assertEqual(text_expected, parser.text) + self.assertEqual(text_expected, self.parser.text) self.assertEqual( datetime.datetime( 2022, @@ -256,33 +259,32 @@ class TestParser(TestCase): 43, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200)), ), - parser.date, + self.parser.date, ) # Just check if file exists, the unittest for generate_pdf() goes deeper. - self.assertTrue(os.path.isfile(parser.archive_path)) + self.assertTrue(os.path.isfile(self.parser.archive_path)) @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output def test_tika_parse(self, m): html = '

Some Text

' expected_text = "\n\n\n\n\n\n\n\n\nSome Text\n" - parser = MailDocumentParser(None) - tika_server_original = parser.tika_server + tika_server_original = self.parser.tika_server # Check if exception is raised when Tika cannot be reached. with pytest.raises(ParseError): - parser.tika_server = "" - parser.tika_parse(html) + self.parser.tika_server = "" + self.parser.tika_parse(html) # Check unsuccessful parsing - parser.tika_server = tika_server_original + self.parser.tika_server = tika_server_original - parsed = parser.tika_parse(None) + parsed = self.parser.tika_parse(None) self.assertEqual("", parsed) # Check successful parsing - parsed = parser.tika_parse(html) + parsed = self.parser.tika_parse(html) self.assertEqual(expected_text, parsed) @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") @@ -290,32 +292,63 @@ class TestParser(TestCase): def test_generate_pdf_parse_error(self, m: mock.MagicMock, n: mock.MagicMock): m.return_value = b"" n.return_value = b"" - parser = MailDocumentParser(None) # Check if exception is raised when the pdf can not be created. - parser.gotenberg_server = "" + self.parser.gotenberg_server = "" with pytest.raises(ParseError): - parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) - - @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_generate_pdf(self, m): - parser = MailDocumentParser(None) + self.parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) + @mock.patch("paperless_mail.parsers.requests.post") + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html") + def test_generate_pdf( + self, + mock_generate_pdf_from_html: mock.MagicMock, + mock_generate_pdf_from_mail: mock.MagicMock, + mock_post: mock.MagicMock, + ): # Check if exception is raised when the mail can not be parsed. with pytest.raises(ParseError): - parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "broken.eml")) + self.parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "broken.eml")) - pdf_path = parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) + mock_generate_pdf_from_mail.return_value = b"Mail Return" + mock_generate_pdf_from_html.return_value = b"HTML Return" + + mock_response = mock.MagicMock() + mock_response.content = b"Content" + mock_post.return_value = mock_response + pdf_path = self.parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) self.assertTrue(os.path.isfile(pdf_path)) - extracted = extract_text(pdf_path) - expected = "From Name \n\n2022-10-15 09:23\n\nSubject HTML Message\n\nTo someone@example.de\n\nAttachments IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nSome Text \n\nand an embedded image.\n\n\x0cSome Text\n\n This image should not be shown.\n\nand an embedded image.\n\nParagraph unchanged.\n\n\x0c" - self.assertEqual(expected, extracted) + mock_generate_pdf_from_mail.assert_called_once_with( + self.parser.get_parsed(None), + ) + mock_generate_pdf_from_html.assert_called_once_with( + self.parser.get_parsed(None).html, + self.parser.get_parsed(None).attachments, + ) + self.assertEqual( + self.parser.gotenberg_server + "/forms/pdfengines/merge", + mock_post.call_args.args[0], + ) + self.assertEqual({}, mock_post.call_args.kwargs["headers"]) + self.assertEqual( + b"Mail Return", + mock_post.call_args.kwargs["files"]["1_mail.pdf"][1].read(), + ) + self.assertEqual( + b"HTML Return", + mock_post.call_args.kwargs["files"]["2_html.pdf"][1].read(), + ) + + mock_response.raise_for_status.assert_called_once() + + with open(pdf_path, "rb") as file: + self.assertEqual(b"Content", file.read()) def test_mail_to_html(self): - parser = MailDocumentParser(None) - mail = parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) - html_handle = parser.mail_to_html(mail) + mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) + html_handle = self.parser.mail_to_html(mail) with open( os.path.join(self.SAMPLE_FILES, "html.eml.html"), @@ -324,13 +357,12 @@ class TestParser(TestCase): @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output def test_generate_pdf_from_mail(self, m): - parser = MailDocumentParser(None) - mail = parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) + mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) - pdf_path = os.path.join(parser.tempdir, "test_generate_pdf_from_mail.pdf") + pdf_path = os.path.join(self.parser.tempdir, "test_generate_pdf_from_mail.pdf") with open(pdf_path, "wb") as file: - file.write(parser.generate_pdf_from_mail(mail)) + file.write(self.parser.generate_pdf_from_mail(mail)) file.close() extracted = extract_text(pdf_path) @@ -343,8 +375,6 @@ class TestParser(TestCase): self.payload = payload self.content_id = content_id - parser = MailDocumentParser(None) - result = None with open(os.path.join(self.SAMPLE_FILES, "sample.html")) as html_file: @@ -354,7 +384,7 @@ class TestParser(TestCase): attachments = [ MailAttachmentMock(png, "part1.pNdUSz0s.D3NqVtPg@example.de"), ] - result = parser.transform_inline_html(html, attachments) + result = self.parser.transform_inline_html(html, attachments) resulting_html = result[-1][1].read() self.assertTrue(result[-1][0] == "index.html") @@ -368,8 +398,6 @@ class TestParser(TestCase): self.payload = payload self.content_id = content_id - parser = MailDocumentParser(None) - result = None with open(os.path.join(self.SAMPLE_FILES, "sample.html")) as html_file: @@ -379,9 +407,9 @@ class TestParser(TestCase): attachments = [ MailAttachmentMock(png, "part1.pNdUSz0s.D3NqVtPg@example.de"), ] - result = parser.generate_pdf_from_html(html, attachments) + result = self.parser.generate_pdf_from_html(html, attachments) - pdf_path = os.path.join(parser.tempdir, "test_generate_pdf_from_html.pdf") + pdf_path = os.path.join(self.parser.tempdir, "test_generate_pdf_from_html.pdf") with open(pdf_path, "wb") as file: file.write(result) @@ -390,16 +418,3 @@ class TestParser(TestCase): extracted = extract_text(pdf_path) expected = "Some Text\n\n This image should not be shown.\n\nand an embedded image.\n\nParagraph unchanged.\n\n\x0c" self.assertEqual(expected, extracted) - - def test_is_online_image_still_available(self): - """ - A public image is used in the html sample file. We have no control - whether this image stays online forever, so here we check if it is still there - """ - - # Start by Testing if nonexistent URL really throws an Exception - with pytest.raises(HTTPError): - urlopen("https://upload.wikimedia.org/wikipedia/en/f/f7/nonexistent.png") - - # Now check the URL used in samples/sample.html - urlopen("https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png") diff --git a/src/paperless_mail/tests/test_parsers_live.py b/src/paperless_mail/tests/test_parsers_live.py new file mode 100644 index 000000000..6676ebc1e --- /dev/null +++ b/src/paperless_mail/tests/test_parsers_live.py @@ -0,0 +1,220 @@ +import hashlib +import os +from unittest import mock +from urllib.error import HTTPError +from urllib.request import urlopen + +import pytest +from django.test import TestCase +from documents.parsers import ParseError +from documents.parsers import run_convert +from paperless_mail.parsers import MailDocumentParser +from pdfminer.high_level import extract_text + + +class TestParserLive(TestCase): + SAMPLE_FILES = os.path.join(os.path.dirname(__file__), "samples") + + def setUp(self) -> None: + self.parser = MailDocumentParser(logging_group=None) + + def tearDown(self) -> None: + self.parser.cleanup() + + @staticmethod + def hashfile(file): + buf_size = 65536 # An arbitrary (but fixed) buffer + sha256 = hashlib.sha256() + with open(file, "rb") as f: + while True: + data = f.read(buf_size) + if not data: + break + sha256.update(data) + return sha256.hexdigest() + + # Only run if convert is available + @pytest.mark.skipif( + "PAPERLESS_TEST_SKIP_CONVERT" in os.environ, + reason="PAPERLESS_TEST_SKIP_CONVERT set, skipping Test", + ) + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output + def test_get_thumbnail(self, m, mock_generate_pdf: mock.MagicMock): + mock_generate_pdf.return_value = os.path.join( + self.SAMPLE_FILES, + "simple_text.eml.pdf", + ) + thumb = self.parser.get_thumbnail( + os.path.join(self.SAMPLE_FILES, "simple_text.eml"), + "message/rfc822", + ) + self.assertTrue(os.path.isfile(thumb)) + + expected = os.path.join(self.SAMPLE_FILES, "simple_text.eml.pdf.webp") + + self.assertEqual( + self.hashfile(thumb), + self.hashfile(expected), + f"Created Thumbnail {thumb} differs from expected file {expected}", + ) + + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output + def test_tika_parse(self, m): + html = '

Some Text

' + expected_text = "\n\n\n\n\n\n\n\n\nSome Text\n" + + tika_server_original = self.parser.tika_server + + # Check if exception is raised when Tika cannot be reached. + with pytest.raises(ParseError): + self.parser.tika_server = "" + self.parser.tika_parse(html) + + # Check unsuccessful parsing + self.parser.tika_server = tika_server_original + + parsed = self.parser.tika_parse(None) + self.assertEqual("", parsed) + + # Check successful parsing + parsed = self.parser.tika_parse(html) + self.assertEqual(expected_text, parsed) + + @pytest.mark.skipif( + "GOTENBERG_LIVE" not in os.environ, + reason="No gotenberg server", + ) + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html") + def test_generate_pdf_gotenberg_merging( + self, + mock_generate_pdf_from_html: mock.MagicMock, + mock_generate_pdf_from_mail: mock.MagicMock, + ): + + with open(os.path.join(self.SAMPLE_FILES, "first.pdf"), "rb") as first: + mock_generate_pdf_from_mail.return_value = first.read() + + with open(os.path.join(self.SAMPLE_FILES, "second.pdf"), "rb") as second: + mock_generate_pdf_from_html.return_value = second.read() + + pdf_path = self.parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) + self.assertTrue(os.path.isfile(pdf_path)) + + extracted = extract_text(pdf_path) + expected = ( + "first\tPDF\tto\tbe\tmerged.\n\n\x0csecond\tPDF\tto\tbe\tmerged.\n\n\x0c" + ) + self.assertEqual(expected, extracted) + + # Only run if convert is available + @pytest.mark.skipif( + "PAPERLESS_TEST_SKIP_CONVERT" in os.environ, + reason="PAPERLESS_TEST_SKIP_CONVERT set, skipping Test", + ) + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output + def test_generate_pdf_from_mail(self, m): + # TODO + mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) + + pdf_path = os.path.join(self.parser.tempdir, "test_generate_pdf_from_mail.pdf") + + with open(pdf_path, "wb") as file: + file.write(self.parser.generate_pdf_from_mail(mail)) + file.close() + + converted = os.path.join(parser.tempdir, "test_generate_pdf_from_mail.webp") + run_convert( + density=300, + scale="500x5000>", + alpha="remove", + strip=True, + trim=False, + auto_orient=True, + input_file=f"{pdf_path}", # Do net define an index to convert all pages. + output_file=converted, + logging_group=None, + ) + self.assertTrue(os.path.isfile(converted)) + thumb_hash = self.hashfile(converted) + + # The created pdf is not reproducible. But the converted image should always look the same. + expected_hash = ( + "8734a3f0a567979343824e468cd737bf29c02086bbfd8773e94feb986968ad32" + ) + self.assertEqual( + thumb_hash, + expected_hash, + f"PDF looks different. Check if {converted} looks weird.", + ) + + # Only run if convert is available + @pytest.mark.skipif( + "PAPERLESS_TEST_SKIP_CONVERT" in os.environ, + reason="PAPERLESS_TEST_SKIP_CONVERT set, skipping Test", + ) + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output + def test_generate_pdf_from_html(self, m): + # TODO + class MailAttachmentMock: + def __init__(self, payload, content_id): + self.payload = payload + self.content_id = content_id + + result = None + + with open(os.path.join(self.SAMPLE_FILES, "sample.html")) as html_file: + with open(os.path.join(self.SAMPLE_FILES, "sample.png"), "rb") as png_file: + html = html_file.read() + png = png_file.read() + attachments = [ + MailAttachmentMock(png, "part1.pNdUSz0s.D3NqVtPg@example.de"), + ] + result = self.parser.generate_pdf_from_html(html, attachments) + + pdf_path = os.path.join(self.parser.tempdir, "test_generate_pdf_from_html.pdf") + + with open(pdf_path, "wb") as file: + file.write(result) + file.close() + + converted = os.path.join(parser.tempdir, "test_generate_pdf_from_html.webp") + run_convert( + density=300, + scale="500x5000>", + alpha="remove", + strip=True, + trim=False, + auto_orient=True, + input_file=f"{pdf_path}", # Do net define an index to convert all pages. + output_file=converted, + logging_group=None, + ) + self.assertTrue(os.path.isfile(converted)) + thumb_hash = self.hashfile(converted) + + # The created pdf is not reproducible. But the converted image should always look the same. + expected_hash = ( + "267d61f0ab8f128a037002a424b2cb4bfe18a81e17f0b70f15d241688ed47d1a" + ) + self.assertEqual( + thumb_hash, + expected_hash, + f"PDF looks different. Check if {converted} looks weird. " + f"If Rick Astley is shown, Gotenberg loads from web which is bad for Mail content.", + ) + + @staticmethod + def test_is_online_image_still_available(): + """ + A public image is used in the html sample file. We have no control + whether this image stays online forever, so here we check if it is still there + """ + + # Start by Testing if nonexistent URL really throws an Exception + with pytest.raises(HTTPError): + urlopen("https://upload.wikimedia.org/wikipedia/en/f/f7/nonexistent.png") + + # Now check the URL used in samples/sample.html + urlopen("https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png") From acd383241777e16e923ecceece9f2b669adb1ef3 Mon Sep 17 00:00:00 2001 From: phail Date: Thu, 3 Nov 2022 21:08:15 +0100 Subject: [PATCH 39/60] merge Pipfile.lock --- Pipfile.lock | 430 +++++++++++++++++++++++++++------------------------ 1 file changed, 227 insertions(+), 203 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 317a8cbda..18494f726 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f8bdb4da9007a887c66a7e0243c486419676aa1a053c9a78e3760abb1f60e0a0" + "sha256": "ebc09a2366e86169c02be7c5fa3dddcf2af3cc7c063daeeb68bfe7e6eac8aa7a" }, "pipfile-spec": 6, "requires": {}, @@ -109,6 +109,14 @@ ], "version": "==3.6.4.0" }, + "bleach": { + "hashes": [ + "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", + "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" + ], + "index": "pypi", + "version": "==5.0.1" + }, "celery": { "extras": [ "redis" @@ -218,7 +226,7 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.1.1" }, "click": { @@ -234,7 +242,7 @@ "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" ], - "markers": "python_version < '4' and python_full_version >= '3.6.2'", + "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'", "version": "==0.3.0" }, "click-plugins": { @@ -276,35 +284,35 @@ }, "cryptography": { "hashes": [ - "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", - "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", - "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", - "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", - "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", - "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", - "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", - "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", - "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", - "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", - "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", - "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", - "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", - "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", - "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", - "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", - "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", - "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", - "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", - "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", - "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", - "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", - "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", - "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", - "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", - "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" + "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d", + "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd", + "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146", + "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7", + "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436", + "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0", + "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828", + "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b", + "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55", + "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36", + "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50", + "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2", + "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a", + "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8", + "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0", + "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548", + "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320", + "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748", + "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249", + "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959", + "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f", + "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0", + "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd", + "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220", + "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", + "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" ], "markers": "python_version >= '3.6'", - "version": "==38.0.1" + "version": "==38.0.3" }, "daphne": { "hashes": [ @@ -316,11 +324,11 @@ }, "dateparser": { "hashes": [ - "sha256:3821bf191f95b2658c4abd91571c09821ce7a2bc179bf6cefd8b4515c3ccf9ef", - "sha256:d31659dc806a7d88e2b510b2c74f68b525ae531f145c62a57a99bd616b7f90cf" + "sha256:711f7eef6d431225bec56c00e386af3f6a47083276253375bdae1ae6c8d23d4a", + "sha256:ae7a7de30f26983d09fff802c1f9d35d54e1c11d7ab52ae904a1f3fc037ecba5" ], "index": "pypi", - "version": "==1.1.2" + "version": "==1.1.3" }, "deprecated": { "hashes": [ @@ -339,11 +347,11 @@ }, "django": { "hashes": [ - "sha256:26dc24f99c8956374a054bcbf58aab8dc0cad2e6ac82b0fe036b752c00eee793", - "sha256:b8d843714810ab88d59344507d4447be8b2cf12a49031363b6eed9f1b9b2280f" + "sha256:678bbfc8604eb246ed54e2063f0765f13b321a50526bdc8cb1f943eda7fa31f1", + "sha256:6b1de6886cae14c7c44d188f580f8ba8da05750f544c80ae5ad43375ab293cd5" ], "index": "pypi", - "version": "==4.1.2" + "version": "==4.1.3" }, "django-celery-results": { "hashes": [ @@ -990,28 +998,28 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:9696f386133df0fc8ca5af4895afe5d78f5fcfe5258111c2a79a1c3e41ffa96d", - "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148" + "sha256:24becda58d49ceac4dc26232eb179ef2b21f133fecda7eed6018d341766ed76e", + "sha256:e7f2129cba4ff3b3656bbdda0e74ee00d2f874a8bcdb9dd16f5fec7b3e173cae" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.31" + "version": "==3.0.32" }, "psycopg2": { "hashes": [ - "sha256:07b90a24d5056687781ddaef0ea172fd951f2f7293f6ffdd03d4f5077801f426", - "sha256:1da77c061bdaab450581458932ae5e469cc6e36e0d62f988376e9f513f11cb5c", - "sha256:46361c054df612c3cc813fdb343733d56543fb93565cff0f8ace422e4da06acb", - "sha256:839f9ea8f6098e39966d97fcb8d08548fbc57c523a1e27a1f0609addf40f777c", - "sha256:849bd868ae3369932127f0771c08d1109b254f08d48dc42493c3d1b87cb2d308", - "sha256:8de6a9fc5f42fa52f559e65120dcd7502394692490c98fed1221acf0819d7797", - "sha256:a11946bad3557ca254f17357d5a4ed63bdca45163e7a7d2bfb8e695df069cc3a", - "sha256:aa184d551a767ad25df3b8d22a0a62ef2962e0e374c04f6cbd1204947f540d61", - "sha256:aafa96f2da0071d6dd0cbb7633406d99f414b40ab0f918c9d9af7df928a1accb", - "sha256:c7fa041b4acb913f6968fce10169105af5200f296028251d817ab37847c30184", - "sha256:d529926254e093a1b669f692a3aa50069bc71faf5b0ecd91686a78f62767d52f" + "sha256:190d51e8c1b25a47484e52a79638a8182451d6f6dff99f26ad9bd81e5359a0fa", + "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e", + "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a", + "sha256:322fd5fca0b1113677089d4ebd5222c964b1760e361f151cbb2706c4912112c5", + "sha256:4cb9936316d88bfab614666eb9e32995e794ed0f8f6b3b718666c22819c1d7ee", + "sha256:922cc5f0b98a5f2b1ff481f5551b95cd04580fd6f0c72d9b22e6c0145a4840e0", + "sha256:a5246d2e683a972e2187a8714b5c2cf8156c064629f9a9b1a873c1730d9e245a", + "sha256:b9ac1b0d8ecc49e05e4e182694f418d27f3aedcfca854ebd6c05bb1cffa10d6d", + "sha256:d3ef67e630b0de0779c42912fe2cbae3805ebaba30cda27fea2a3de650a9414f", + "sha256:f5b6320dbc3cf6cfb9f25308286f9f7ab464e65cfb105b64cc9c52831748ced2", + "sha256:fc04dd5189b90d825509caa510f20d1d504761e78b8dfb95a0ede180f71d50e5" ], "index": "pypi", - "version": "==2.9.4" + "version": "==2.9.5" }, "pyasn1": { "hashes": [ @@ -1174,98 +1182,98 @@ }, "rapidfuzz": { "hashes": [ - "sha256:028aa9edfa044629a0a9e924a168a01f61c8f570c9ea919e2ed214826ba1cdfb", - "sha256:02a5c8b780af49a4e5f08033450d3f7c696f6c04e57c67ecbb19c9944ea3ce20", - "sha256:07c623741958dd49d8c2c51f7c2e62472f41b8d795cc9c734e441e30de3f8330", - "sha256:0ae8c0c2f51f618d54579341c44ba198849d9cd845bb0dc85d1711fd8de9a159", - "sha256:129c93a76c4fed176b4eaaf78fecd290932971bca10315dee9feaf94a7b443b1", - "sha256:13ad87a539b13794292fb661b8c4f4c19e6c066400d9db991e3a1441f55fc29b", - "sha256:16426b441d28efb3b1fe10f2b81aa469020655cef068a32de6ec24433590ee5b", - "sha256:1a600b037a56a61111c01809b5e4c4b5aac12edf2769c094fefab02d496a95a4", - "sha256:1e0f6f878c20454a7e7ea2ed30970ae0334852c5e422e7014757821fa33c1588", - "sha256:20f2f0f0746ffc165168ca160c5b1a1485076bdde5b656cf3dbe532ef2ac57ff", - "sha256:2207927f42ae7b7fbcc1a4ff86f202647776200f3d8723603e7acf96af924e9f", - "sha256:22487992f4811c8aef449736f483514f0294d5593f5f9c95cbfb2474dbc363b9", - "sha256:22646f54d44d98d6f462fb3f1ac997ea53aaebdd1344039e8f75090f43e47a89", - "sha256:22e8b127abcf0b10ebf8a9b3351c3c980e5c27cb60a865632d90d6a698060a9a", - "sha256:238fddfb90fab858ced88d064608bff9aed83cec11a7630a4e95b7e49734d8b1", - "sha256:241912c4f7f232c7518384f8cea719cf2ff290f80735355a217e9c690d274f62", - "sha256:25db060ba8082c38f75da482ff15d3b60a4bc59f158b6d29a2c5bccadd2b71b0", - "sha256:2676f7ccd22a67638baff054a8e13924f20d87efb3a873f6ea248a395a80e2c8", - "sha256:27fa0e7d5e5291dc3e48c6512524f2f8e7ba3d397fa712a85a97639e3d6597e9", - "sha256:2c2cd1e01e8aef2bd1b5152e66e0b865f31eb2d00a8d28cbbbb802f42e2dbe43", - "sha256:2e9709a163ec3b890f9a4173261e9ef586046feee74bbece62595bf103421178", - "sha256:2fda8c003d9ae4f3674c783887b31ecb76f4ab58670a8f01b93efd0917c1e503", - "sha256:33cfd01cb7f8a48c8e057198e3814a120323c0360017dd5c4eba07d097b43b39", - "sha256:35737fd5766ca212d98e0598fb4d302f509e1cbf7b6dc42e2eddefd956150815", - "sha256:35b34f33a9f0a86fdba39053b203d8d517da76f3553230a55867c51f0d802b67", - "sha256:3abe9c25f9ba260a6828d24002a15112c5f21c6877c5f8c294ffe4b9d197c6d2", - "sha256:3c7f3c61e2d1530cf7e1647bdbb466f0f83fa30d2c579b6d75e444f88ff47913", - "sha256:3e8707e98b645f80834e24103e7cd67f1b772999bb979da6d61ca1fcdc07672a", - "sha256:3eae4c6880dbabee9f363950510c09d7e12dea8dbc6ebcd2ff58e594a78d9370", - "sha256:3f7b798807ac9c5f632e8f359adf1393f81d9211e4961eedb5e2d4ce311e0078", - "sha256:43398361d54fed476ccfdb52dc34d88c64461f0ec35f8abf10dd0413a3f19d8c", - "sha256:4581600ded8f37e8387d0eef93520fb33dafab6ccb37d005e20d05cd3fbdd9db", - "sha256:45def44f1140c6f5c7a5389645d02e8011d27a6e64f529f33cee687e7c25af07", - "sha256:475a551c43ba23b4ca328c9bbcae39205729f4751280eb9763da08d97d328953", - "sha256:4787d8e9f4b184d383ad000cdd48330ae75ec927c5832067a6b3617c5f6fb677", - "sha256:48539221026b0a84b6d2c2665c3dde784e3c0fac28975658c03fed352f8e1d7e", - "sha256:4f3f0ddfe3176e19c0a3cf6ad29e9ff216ff5fdec035b001ebabad91ef155107", - "sha256:4fc958b21416825c599e228278c69efb480169cd99d1a21787a54f53fbff026c", - "sha256:589464c49a332c644b750f2ebc3737d444427669323ace623bd4948e414a641a", - "sha256:5c6a9ada752149e23051852867596b35afc79015760e23676ac287bcad58e0b6", - "sha256:5dcd7bd175d870338fc9ae43d0184ecd45958f5ca2ee7ea0a7953eedc4d9718e", - "sha256:5e2c0a5a0346ce95a965ed6fa941edcf129cac22bf63314b684a3fe64078c95b", - "sha256:5fa88543c5744d725fc989afd79926c226e1c5f5c00904834851997f367da2b5", - "sha256:626eaa1b52a9dafa9bf377bcdcfdf1ea867dd51b5bb5dab1a05938c3303f317f", - "sha256:651664023726f28f7447e40fa2b8a015514f9db4b58654e9bf7d3729e2606eab", - "sha256:74f821370ac01f677b5a26e0606084f2eb671f7bb4f3e2e82d94a100b1c28457", - "sha256:7a67d93fd2e6e5a4e278eade2bbef16ba88d4efcb4eed106f075b0b21427a92f", - "sha256:7e34c996cc245a21f376c3b8eede1296339845f039c8c270297a455d3a1ad71b", - "sha256:7f2db0c684d9999c81084aa370e2e6b266b694e76c7e356bbeb3b282ca524475", - "sha256:7febf074f7de7ebc374100be0036fc592659af911b6efbc1135cdebfe939c57d", - "sha256:800b1498989bfb64118b219eeb42957b3d93ec7d6955dfc742a3cbf3be738f2f", - "sha256:8eadfb5394ab5b9c6e3d4bb00ef49e19f60a4e431190c103e647d4e4bff3332e", - "sha256:917e3e2ffc0e078cce4a632f65d32a339f18cad22b5536a32c641bf1900e7f96", - "sha256:96fe7da85d8721c3a5c362f6b1e7fd26ad74a76bebc369fb7ae62907cf069940", - "sha256:9836bea98f25a67c296f57cf6de420c88f46e430ee85d25ae5f397968c7adcdf", - "sha256:9c8295dd49582dfb6a29e5f9dfa1691a0edd2e0512377ceb2c8dd11e7fabd38a", - "sha256:9ceb8d6f1bd18a058cb8472c6e8cc84802413a65b029a7832589ba7b76c0eb11", - "sha256:9e239e404dbb9fec308409e174710b5e53ff8bd9647e8875e2ca245b1f762f89", - "sha256:9f849d4889a0f1bc2260b981b1ae8393938e8a2c92666e1757e69f947c6ce868", - "sha256:a6d78f967b7b162013fc85821a74cc7cd021fbf045f166629c9bd523799d8e51", - "sha256:a8d5787f5c52c00991032b976b69f5c1e181a3bddce76fd43c91b2c4901c96ce", - "sha256:a9a90ab26b12218d10d5f148e84e8facd62f562bc25d32e2c3cf3c743f7e0e67", - "sha256:ac6ce417174a086a496aefc7caa614640dc33d418a922ee0a184b093d84f2f6c", - "sha256:ac95a2ca4add04f349f8f5c05269d8b194a72ebdfc4f86a725c15d79a420d429", - "sha256:ad279e4892652583671a7ece977dd9b1eb17ae9752fbc9013c950095b044a315", - "sha256:ad5935f4a0ec3a2c3d19021fcd27addce4892ae00f71cc4180009bc4bed930ac", - "sha256:ae0519d01a05c6204c2d27ae49b2231787d9a6efc801d5dbf131b20065fd21e3", - "sha256:b1114da71974c86e64a98afff8d88cf3a3351b289d07f0218e67d56b506cb9e2", - "sha256:b2b81c6cb59b955b82a4853e3fbef7231da87c5522a69daaf9b01bd81d137ec3", - "sha256:b63402c5af2ad744c2c1ab2e7265eb317e75257fd27eb6f087fea76464b065db", - "sha256:b75fe7abf55e7da6d32174b5ac207a465d1bc69d777049c277776472c0b7d82c", - "sha256:c0006c6450d02efdfef8d3f81e6a87790572486046676fe29f4c5da8708ea11b", - "sha256:c1191a1c9f24134c6048770aabaa2f7def8d6d4c919da857d5e7dabdf63308f2", - "sha256:c43579c7a64f21c9b4d4c3106ace46a8ebfb8e704372e6c8cc45807d1b86462f", - "sha256:c62472a70c7f22f1ae9864c02554dbc234a1dfbac24388542bf87437a4169379", - "sha256:c6c4064b2324b86f7a035379928fe1f3aca4ca5ba75ebedc9ea0d821b0e05606", - "sha256:c6fa81c8d3c901d9f174482185f23b02052e71da015da3a613be98f28fd2672b", - "sha256:c74b960a1b93ac22e6cbf268ce509fb2c2338a29180c3d844df4a57bfff73273", - "sha256:c9c2b3b00033afdb745cc77b8c2ed858b08bb9a773c9a81a1160ece59a361545", - "sha256:c9fdbe8b1b32a731ee48a21a161358e55067c9cabd36ba0b8d844e5106056920", - "sha256:d0fc1e32353afef426488d2e19cd295f1f504323215275ec0871bdae2b052a70", - "sha256:d5b65d9f2860210087739adadc075bd9215b363d00c3c8e68369560683a4c3df", - "sha256:d6e6972c5bd1ee4f532029616dfe0f5133f7cc688ebc05dbbc03e19b4ec12199", - "sha256:e333bb6a69c515a1fce149002aaf7d8902fddab54db14fe14c89c6da402410d2", - "sha256:ef812c73fff460678defaab3a95ec9b7551ef14d424eb6af7b75e376050119d2", - "sha256:f030223aa618a48d2f8339fd674c4c03db88649313e2c65107e9c04e09edc7f2", - "sha256:f371453d0c1109e93ef569741a27171e602ef1fbea5c27a8f190f403234fd36b", - "sha256:f74636ca4a3ce700f4fe2dbe10d224ee4fb52ecab12ea3007a2bc2fcd0d53888", - "sha256:f96973d42caf0e4566882b6e7acbba753199d7acb4db486f14ab553c7b395cd5" + "sha256:068dbbe59752f9eb8ec946df48af60669cd61bb722dfb89f48f142f9fbdc275d", + "sha256:0a9524a22db1d5cb386c0cfea8b533fb0c7f8bd196682579e559febdf34a8b8a", + "sha256:0e8db03317d4d61fc35a29a6fbcaab286d0f93f9430855ee690dee1f01c10d7d", + "sha256:0f2dd1fe2d1b614dc8ad65e76fd0cb58430a1029d371b1797fa353d9d1251e84", + "sha256:0fa9e82d73f64bdd1bcc85cdedd6aef3547c9edcffc55e413c28fbee128bbee7", + "sha256:1436fbaf695a05dd4d6b7a309605fdb738f14951a2d836cbc8e1b0d629891238", + "sha256:1548e3ca0b496286a21d7f0007e00931ba2cfd1bbce0a855237ecff80ff312d7", + "sha256:159fc6b4dd1a0e2b7873ca36307de61b385dfa0bb8fa8c8485b23bd40c5b99fe", + "sha256:17b733c64ea52ebae625459da348bb21788a6f74d63f6efc760c2d1fd98e6b25", + "sha256:1869e213378d187ced55ae45077c9613a37e59a6429d2431343f0b124f235e1e", + "sha256:187f97eb3b0b36447d20d80c5934a4a65cf0749a754029fedc1379c657f00bf5", + "sha256:1b8da9e0198ca4f16b84aaa0ef4227c1dfde17d1ffa6e33b01f2cd1a0becbfaf", + "sha256:1c82e9065add225968ac3b34820b0b91d26d88289102e2e34c817643ff1616c2", + "sha256:1f1ae125e6b89f83ba9fcfef9c9b9fc14ab81243522ee78009d302d138a695db", + "sha256:1ff41df445dd0903c2e2c5c943169deb616debc1ff26b19ddd4ab2974f6e2247", + "sha256:200363583476fb09e2db2809507e544809ec8b624ff5c0585d169e7497124f56", + "sha256:20634aa6571997391463ba07cd72294a296f22ffe8f6fccab2b78cba9efc796a", + "sha256:249b7d331f6940e11bdea0040c7251aa0ecb3a3f388f6b388efc9d7c951ff9d8", + "sha256:2d493cab218f9aeb177b043b0fca3cbcce60ed6116835dd50ef87eebb253e840", + "sha256:2e2af01a2d090f71eba16954536b2c3a713bf2f005f23b827ef62f0ffa83e310", + "sha256:317b1d986abe1261d6ffcc19345db64d13eb0b0975be4a45a5467a61f59c041a", + "sha256:3389a5113bf82fcb2a07ab73b3ff539a86a06df53c2bd979739e01a28521d2bb", + "sha256:33ae43add9871311aa0115db6e98577b10ee8250f8038ffadb24f3bae94ce217", + "sha256:38727c4b856c7b91a18abb9a58101886c34cf4f9098f0a76fc487582d32c5f93", + "sha256:3e65dbc81ecd8e799a79dc9a6d92c4dee5b8a81df5e0eccbbfa929e86b58c2c6", + "sha256:3f72c6e588f5e1617a7dcefc885efeb98ffae9c21d6703910a1d41cf7f86de27", + "sha256:4000ac171e904c3be46c20d2ff75bc087126d8994fab41891806a80c2cc077c0", + "sha256:43b09d7b1b91b6a438f8b28e6daa035d096867690d13601e79bdc91c42cfce78", + "sha256:45e532cb6851cac350898293e6f57ba41544a0aab956403427909139e8587fe9", + "sha256:470b79619f0bbb96308dd77f800297c02ee4af1d1f7a24f33dd54885492e5506", + "sha256:48d1404eb673275f90b4e725cf4ad33e7663b1f82fccf01fa51ebe11a93dfd20", + "sha256:4ad8d13dcacb2f31a61aa31d414f9101370d4ec9dd5b4a1d721e73ff047997be", + "sha256:4b3759b1f59118b9f54606da8fb23f775320172609f796a22badbe94cc5b9aba", + "sha256:4cbf24b94cbb4c40aacad8a542a9c016083a16262c79673599ed0c5a23700d25", + "sha256:4dc7baadba7478798a66e325789e6f7f19aba5e556caa9509a4f9cddf17ee5f8", + "sha256:51402ceb8747d13aca21ce76df70a4309e9e0fceaf9e594397748855ce5ab113", + "sha256:542b9e5c70a15cae3f182b7d2744dedde9d9f169b754e730790816f7dc51b730", + "sha256:599301048ef558a1de0f24e48be6b97d49b2eaecf66f6e8815ceaa18c78de45f", + "sha256:5c1c58fa810a9bf8cc27b5da720c6da6da08c3b736ab56c1fd0d53db724a3182", + "sha256:5d52b8c97f66a4a67ceee11f1e81959669c3fcd3008e65c6941ab326cda7d8a9", + "sha256:60687f534b46c2d216d51bc6bd886f3b7bb871d4c390012229169baf7f7e326c", + "sha256:60b9f30e24e73336644e5098f0b79fd164f54fedcdaf78c5c12ff30d668eb0cb", + "sha256:610db32a6ced2d2ffedc94d16cfc7762b1495727a5da6f31a84130a12c40921c", + "sha256:613982d09586087b24e0c850d7b3bb0dc1a3c7aed11dcf6fe5c4dcc9ac2d63c8", + "sha256:62180830ab02ba7163d59e723cdafee606908f001ad410694b41fe3d98acfd44", + "sha256:637cfcd7eb10eab0d02bf2ce3d478e403d9c86695100523300c6bfc67cd70452", + "sha256:682790babbe4234d06ff89116d523269c5e2f7c52af0efaca01959b2e33ff1ef", + "sha256:6d5c9690c717b884e031268238bfc2ef2afa32b033f4e3413a832802f427157d", + "sha256:6faa0d8c5d2506a665a6635823d13346ce91480802995eb2e306766b883ee307", + "sha256:717e1289cd6fbe182b287912e51b89a3841d07fd12e3ac58ea61a574efa82306", + "sha256:728790ed57682c4570e6637cacf3f1795ec786a184474703a53c431c5301b4d0", + "sha256:75cf7ed7370d2fd8a4151e9a5e5f1aa2315cc1910060e05ed03065e8f41e13dd", + "sha256:794858517ed8d3f17516ab85054bbfed5bdb9354fdba59d0643643b35a5ec4fb", + "sha256:7d474cb2b5e2b613c3c869ce9c0666d3a22551cfe1b49e4d3c344b83529454ec", + "sha256:86c9102e65a9b1e66ff15f42cd67ef7999b82c21ec402a41eb2216ec7046aeb2", + "sha256:89560a047cabd48703c38aa8108817c31cfd029109690474c7d52a6c1fa4cb4c", + "sha256:898f44a77d12e2d199096f5ed81805fd1b53d90ad1a47787c3108361600e6923", + "sha256:8db783fd4a874a45489ade1b78a7b8d2fbc45f01bf6d349d48d5511be36f592c", + "sha256:90fff8fc261a79698f5b4326db5e7a9442670ad84c83f421d985be54e2947074", + "sha256:9279eb1acbdb58a9864001a934d54976a6f7f3a2a1c1b5bcd10ef46557b9ade2", + "sha256:976a28d9651450086ea1666b4c5c6e4695623fae155b55b8da5bacc8edf1c7a8", + "sha256:99c685fce0a69e3c1b2238bb7a28c7ec746e16524d2225f64ad91825d87659e7", + "sha256:99f22fca5b0d9b3ce5a0cc6faad98caebd533356b5f786f46a2558369ea7bc58", + "sha256:9a2b974717613edbed2c74e45c1611a62711103555ab3ff6b4b18d16746713e8", + "sha256:9b92621ab63f0de61fa2bbe391cc38c1bb8a7998af65f58607120f8979e5266c", + "sha256:9fa33dc301eef34ef441a31327e027a14a18fd9d3b9f50369745d21e199a1f03", + "sha256:a52bb5781e4246579e01950c73b6c6c4963ca495063748b1b2641c756fdafb13", + "sha256:a603f05cb6a6f6590549300d807d35ce193db5a0054c9cba9e7725b89e0be2d5", + "sha256:a895173559fa33084a9a94ccecf9e1a6d455134bf680b84a26fb01aecdb4c89d", + "sha256:a8d124fe0b1942279fcea5b774c74ca48553da5407e904670fbd6c1e823bcf45", + "sha256:aba223a34d60f8a15701a38450dceabe646cda1caf64448d914087cdca8c0aa8", + "sha256:abec93183205a3e022eb40bd55b8d20f59b2bd26b46a5389ff9ebab375ca3570", + "sha256:aeb1a15984a6aaa3392a519dc1c9b1af98fbeb4b0a2a545806641ddb5f385631", + "sha256:b1a6165a3c1226760eb3c9f7605264ce24b5cb33907cab00025f6eca7c2b349f", + "sha256:c04921f442968e7c7760cb21bf4f897c1026840b97ad695c7c9c6caeddacea5f", + "sha256:c1947fa6ee4d4bd9c73091e0ef75079f8114c1564603bc42bb506575e0b48575", + "sha256:c2b1575d24f58d7337391dad32543a5dc2ebf3ee771df479e5d334b36ff30718", + "sha256:cb38bc3e49694bd568a48a81235e6e8e587b79e5f079fefbea4239dc609af713", + "sha256:cbfda5c6a10adc3ec18b0ed8b1aebcd8f1b63d4555ff352521d7be56a7fb19cc", + "sha256:ceb459c9df17b9b35e7302c4e0df992de46fab859a59b70eeb6f55e6c7e8221e", + "sha256:cee41d563a1fa0fbd0d9028cca10775189cdc69e47f845d2ee2d6343264f183d", + "sha256:d979e315e2bc5775a1e52d73e9a82e29844bf293be4f5491dae634c2c2d7a2f0", + "sha256:dffba251ed903b8741530fb296664a06ab32bae30d700359ec423dd546da785e", + "sha256:e4d371b057679000f00296caa827a885c3ed74fdf4b3eddb99e95385b73f120b", + "sha256:f4038e27f672983c7586a7d91d2c9039b8637490efc705bdc6b3432d0845cc82", + "sha256:f8d455f89ab9dfe95b516cdbfd5f946751f38309fe4f725c66620d7b1327e4da", + "sha256:faa9e3c531fa4e59c5297cbee178cae43201cdf1d150ab0e44b7d7d2620a2d8d", + "sha256:fae3e77478cabd3b5d772ffd50c29c871ee7aabdde0b8be4899a35a7d99fcd2a", + "sha256:fbdb56a12c6d218fb0c53b6e4327fcc1a7b3dea2923ec3fbd6d037a38b5222f0" ], "index": "pypi", - "version": "==2.12.0" + "version": "==2.13.1" }, "redis": { "extras": [ @@ -1642,7 +1650,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==4.4.0" }, "tzdata": { @@ -1784,6 +1792,13 @@ ], "version": "==0.2.5" }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, "websockets": { "hashes": [ "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41", @@ -1955,46 +1970,49 @@ }, "zope.interface": { "hashes": [ - "sha256:006f8dd81fae28027fc28ada214855166712bf4f0bfbc5a8788f9b70982b9437", - "sha256:03f5ae315db0d0de668125d983e2a819a554f3fdb2d53b7e934e3eb3c3c7375d", - "sha256:0eb2b3e84f48dd9cfc8621c80fba905d7e228615c67f76c7df7c716065669bb6", - "sha256:1e3495bb0cdcea212154e558082c256f11b18031f05193ae2fb85d048848db14", - "sha256:26c1456520fdcafecc5765bec4783eeafd2e893eabc636908f50ee31fe5c738c", - "sha256:2cb3003941f5f4fa577479ac6d5db2b940acb600096dd9ea9bf07007f5cab46f", - "sha256:37ec9ade9902f412cc7e7a32d71f79dec3035bad9bd0170226252eed88763c48", - "sha256:3eedf3d04179774d750e8bb4463e6da350956a50ed44d7b86098e452d7ec385e", - "sha256:3f68404edb1a4fb6aa8a94675521ca26c83ebbdbb90e894f749ae0dc4ca98418", - "sha256:423c074e404f13e6fa07f4454f47fdbb38d358be22945bc812b94289d9142374", - "sha256:43490ad65d4c64e45a30e51a2beb7a6b63e1ff395302ad22392224eb618476d6", - "sha256:47ff078734a1030c48103422a99e71a7662d20258c00306546441adf689416f7", - "sha256:58a66c2020a347973168a4a9d64317bac52f9fdfd3e6b80b252be30da881a64e", - "sha256:58a975f89e4584d0223ab813c5ba4787064c68feef4b30d600f5e01de90ae9ce", - "sha256:5c6023ae7defd052cf76986ce77922177b0c2f3913bea31b5b28fbdf6cb7099e", - "sha256:6566b3d2657e7609cd8751bcb1eab1202b1692a7af223035a5887d64bb3a2f3b", - "sha256:687cab7f9ae18d2c146f315d0ca81e5ffe89a139b88277afa70d52f632515854", - "sha256:700ebf9662cf8df70e2f0cb4988e078c53f65ee3eefd5c9d80cf988c4175c8e3", - "sha256:740f3c1b44380658777669bcc42f650f5348e53797f2cee0d93dc9b0f9d7cc69", - "sha256:7bdcec93f152e0e1942102537eed7b166d6661ae57835b20a52a2a3d6a3e1bf3", - "sha256:7d9ec1e6694af39b687045712a8ad14ddcb568670d5eb1b66b48b98b9312afba", - "sha256:85dd6dd9aaae7a176948d8bb62e20e2968588fd787c29c5d0d964ab475168d3d", - "sha256:8b9f153208d74ccfa25449a0c6cb756ab792ce0dc99d9d771d935f039b38740c", - "sha256:8c791f4c203ccdbcda588ea4c8a6e4353e10435ea48ddd3d8734a26fe9714cba", - "sha256:970661ece2029915b8f7f70892e88404340fbdefd64728380cad41c8dce14ff4", - "sha256:9cdc4e898d3b1547d018829fd4a9f403e52e51bba24be0fbfa37f3174e1ef797", - "sha256:9dc4493aa3d87591e3d2bf1453e25b98038c839ca8e499df3d7106631b66fe83", - "sha256:a69c28d85bb7cf557751a5214cb3f657b2b035c8c96d71080c1253b75b79b69b", - "sha256:aeac590cce44e68ee8ad0b8ecf4d7bf15801f102d564ca1b0eb1f12f584ee656", - "sha256:be11fce0e6af6c0e8d93c10ef17b25aa7c4acb7ec644bff2596c0d639c49e20f", - "sha256:cbbf83914b9a883ab324f728de869f4e406e0cbcd92df7e0a88decf6f9ab7d5a", - "sha256:cfa614d049667bed1c737435c609c0956c5dc0dbafdc1145ee7935e4658582cb", - "sha256:d18fb0f6c8169d26044128a2e7d3c39377a8a151c564e87b875d379dbafd3930", - "sha256:d80f6236b57a95eb19d5e47eb68d0296119e1eff6deaa2971ab8abe3af918420", - "sha256:da7912ae76e1df6a1fb841b619110b1be4c86dfb36699d7fd2f177105cdea885", - "sha256:df6593e150d13cfcce69b0aec5df7bc248cb91e4258a7374c129bb6d56b4e5ca", - "sha256:f70726b60009433111fe9928f5d89cbb18962411d33c45fb19eb81b9bbd26fcd" + "sha256:026e7da51147910435950a46c55159d68af319f6e909f14873d35d411f4961db", + "sha256:061a41a3f96f076686d7f1cb87f3deec6f0c9f0325dcc054ac7b504ae9bb0d82", + "sha256:0eda7f61da6606a28b5efa5d8ad79b4b5bb242488e53a58993b2ec46c924ffee", + "sha256:13a7c6e3df8aa453583412de5725bf761217d06f66ff4ed776d44fbcd13ec4e4", + "sha256:185f0faf6c3d8f2203e8755f7ca16b8964d97da0abde89c367177a04e36f2568", + "sha256:2204a9d545fdbe0d9b0bf4d5e2fc67e7977de59666f7131c1433fde292fc3b41", + "sha256:27c53aa2f46d42940ccdcb015fd525a42bf73f94acd886296794a41f229d5946", + "sha256:3c293c5c0e1cabe59c33e0d02fcee5c3eb365f79a20b8199a26ca784e406bd0d", + "sha256:3e42b1c3f4fd863323a8275c52c78681281a8f2e1790f0e869d911c1c7b25c46", + "sha256:3e5540b7d703774fd171b7a7dc2a3cb70e98fc273b8b260b1bf2f7d3928f125b", + "sha256:4477930451521ac7da97cc31d49f7b83086d5ae76e52baf16aac659053119f6d", + "sha256:475b6e371cdbeb024f2302e826222bdc202186531f6dc095e8986c034e4b7961", + "sha256:489c4c46fcbd9364f60ff0dcb93ec9026eca64b2f43dc3b05d0724092f205e27", + "sha256:509a8d39b64a5e8d473f3f3db981f3ca603d27d2bc023c482605c1b52ec15662", + "sha256:58331d2766e8e409360154d3178449d116220348d46386430097e63d02a1b6d2", + "sha256:59a96d499ff6faa9b85b1309f50bf3744eb786e24833f7b500cbb7052dc4ae29", + "sha256:6cb8f9a1db47017929634264b3fc7ea4c1a42a3e28d67a14f14aa7b71deaa0d2", + "sha256:6d678475fdeb11394dc9aaa5c564213a1567cc663082e0ee85d52f78d1fbaab2", + "sha256:72a93445937cc71f0b8372b0c9e7c185328e0db5e94d06383a1cb56705df1df4", + "sha256:76cf472c79d15dce5f438a4905a1309be57d2d01bc1de2de30bda61972a79ab4", + "sha256:7b4547a2f624a537e90fb99cec4d8b3b6be4af3f449c3477155aae65396724ad", + "sha256:7f2e4ebe0a000c5727ee04227cf0ff5ae612fe599f88d494216e695b1dac744d", + "sha256:8343536ea4ee15d6525e3e726bb49ffc3f2034f828a49237a36be96842c06e7c", + "sha256:8de7bde839d72d96e0c92e8d1fdb4862e89b8fc52514d14b101ca317d9bcf87c", + "sha256:90f611d4cdf82fb28837fe15c3940255755572a4edf4c72e2306dbce7dcb3092", + "sha256:9ad58724fabb429d1ebb6f334361f0a3b35f96be0e74bfca6f7de8530688b2df", + "sha256:a1393229c9c126dd1b4356338421e8882347347ab6fe3230cb7044edc813e424", + "sha256:a20fc9cccbda2a28e8db8cabf2f47fead7e9e49d317547af6bf86a7269e4b9a1", + "sha256:a69f6d8b639f2317ba54278b64fef51d8250ad2c87acac1408b9cc461e4d6bb6", + "sha256:a6f51ffbdcf865f140f55c484001415505f5e68eb0a9eab1d37d0743b503b423", + "sha256:c9552ee9e123b7997c7630fb95c466ee816d19e721c67e4da35351c5f4032726", + "sha256:cd423d49abcf0ebf02c29c3daffe246ff756addb891f8aab717b3a4e2e1fd675", + "sha256:d0587d238b7867544134f4dcca19328371b8fd03fc2c56d15786f410792d0a68", + "sha256:d1f2d91c9c6cd54d750fa34f18bd73c71b372d0e6d06843bc7a5f21f5fd66fe0", + "sha256:d2f2ec42fbc21e1af5f129ec295e29fee6f93563e6388656975caebc5f851561", + "sha256:d743b03a72fefed807a4512c079fb1aa5e7777036cc7a4b6ff79ae4650a14f73", + "sha256:dd4b9251e95020c3d5d104b528dbf53629d09c146ce9c8dfaaf8f619ae1cce35", + "sha256:e4988d94962f517f6da2d52337170b84856905b31b7dc504ed9c7b7e4bab2fc3", + "sha256:e6a923d2dec50f2b4d41ce198af3516517f2e458220942cf393839d2f9e22000", + "sha256:e8c8764226daad39004b7873c3880eb4860c594ff549ea47c045cdf313e1bad5" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.5.0" + "version": "==5.5.1" } }, "develop": { @@ -2069,7 +2087,7 @@ "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845", "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==2.1.1" }, "click": { @@ -2089,9 +2107,7 @@ "version": "==0.4.6" }, "coverage": { - "extras": [ - "toml" - ], + "extras": [], "hashes": [ "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", @@ -2202,11 +2218,11 @@ }, "faker": { "hashes": [ - "sha256:096c15e136adb365db24d8c3964fe26bfc68fe060c9385071a339f8c14e09c8a", - "sha256:a741b77f484215c3aab2604100669657189548f440fcb2ed0f8b7ee21c385629" + "sha256:1bfb1b447cd45169a74a09f821cee47f51548508b62a29f6dfdab1d001d448a4", + "sha256:bd922a6ad210bb96a5b31987e10918851131c9670429172d5bfb3a5ede238a79" ], "markers": "python_version >= '3.7'", - "version": "==15.1.1" + "version": "==15.1.3" }, "filelock": { "hashes": [ @@ -2240,6 +2256,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.1" }, + "importlib-metadata": { + "hashes": [ + "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", + "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" + ], + "markers": "python_version < '3.10'", + "version": "==5.0.0" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -2458,14 +2482,6 @@ "index": "pypi", "version": "==0.8.1" }, - "pytest-forked": { - "hashes": [ - "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e", - "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8" - ], - "markers": "python_version >= '3.6'", - "version": "==1.4.0" - }, "pytest-sugar": { "hashes": [ "sha256:3da42de32ce4e1e95b448d61c92804433f5d4058c0a765096991c2e93d5a289f", @@ -2476,11 +2492,11 @@ }, "pytest-xdist": { "hashes": [ - "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf", - "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65" + "sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291", + "sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b" ], "index": "pypi", - "version": "==2.5.0" + "version": "==3.0.2" }, "python-dateutil": { "hashes": [ @@ -2591,11 +2607,11 @@ }, "sphinx-rtd-theme": { "hashes": [ - "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", - "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c" + "sha256:36da4267c804b98197419df8aa415d245449b8945301fce8c961038e0ba79ec5", + "sha256:6e20f00f62b2c05434a33c5116bc3348a41ca94af03d3d7d1714c63457073bb3" ], "index": "pypi", - "version": "==1.0.0" + "version": "==1.1.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -2666,7 +2682,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version < '3.11'", + "markers": "python_full_version < '3.11.0a7'", "version": "==2.0.1" }, "tornado": { @@ -2699,7 +2715,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==4.4.0" }, "urllib3": { @@ -2717,6 +2733,14 @@ ], "markers": "python_version >= '3.6'", "version": "==20.16.6" + }, + "zipp": { + "hashes": [ + "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1", + "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8" + ], + "markers": "python_version < '3.9'", + "version": "==3.10.0" } } } From b01cbc9aa08a51a63b1719e71335d140ce73a432 Mon Sep 17 00:00:00 2001 From: phail Date: Sat, 12 Nov 2022 15:48:30 +0100 Subject: [PATCH 40/60] add conditions to unittests --- .../tests/samples/sample.html.pdf.webp | Bin 0 -> 2830 bytes src/paperless_mail/tests/test_parsers.py | 88 ++++++++++++++---- src/paperless_mail/tests/test_parsers_live.py | 65 ++++++++++--- 3 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 src/paperless_mail/tests/samples/sample.html.pdf.webp diff --git a/src/paperless_mail/tests/samples/sample.html.pdf.webp b/src/paperless_mail/tests/samples/sample.html.pdf.webp new file mode 100644 index 0000000000000000000000000000000000000000..534157660b25e9bf210b2ab898aff969aa8a655b GIT binary patch literal 2830 zcmWIYbaP|lW?%?+bqWXzuu%BL#lY~PlwmHT))&SjOm-7J7Wph)qO7Da(L-T_5NBrI z!}3J=OWT#&j^6gu_^SQS{mt`@>^J^hFFMwlo?5^0Q~KZYP5UM4e7`Y$b9>>}{*3jF z`E~nfoAgrd_wv8~yXJrYx3NC*gVHDa1pdeOFYG^9pY^BVSJJ=r-&UX7erf*C`g1i* z_16EMzf667`@8Us|6ecv{~^AJG1aPeXHBkK!=*R9j3ukTsW0JK^{cwq_Q`+uenx>7 z=iKf?hc=nL&poQqJTdgkcd6==|N7-@Dn8vbNV#qH@Wsc+$3fhWkB>JbFodvb-scPZ zq`s#u(gAFMoK3~Y2aL>nMdkL}t?Usv?=N09Zv(g1zIl)REA1~nKHe|gnX)700=g?+8EO=Y7vZ z!0^z4XApCCxrxp0^Gnv8|a^(i(_IGz{(KEHvJ1nlGT*EJvZ(-h zRg<%HbFJ;Md7tVR!~6~P{wOt^1B8Kr;orY2Qc=E9ty|Kk+ljyac{ILKU}mp!v-s3$ z`D)?D1(Dltw#_m2Y);wNAzpBo#mDdR1!k$5Q{j(8&K~!&K9iOw)1-CT-bHtB(3#6U z$A12OG%KrY*Wo31Tvj@(x?7!}diUpV#?6_*>!!pPcuKlTP5iI?(5qVP?*IMwHaL~r z6jtndx?|(sLuaN)+;6?RSnTx!y=}#Bf7bo>iPN}c*uH%Cd8_U3?gtwg#BNvQ{gU>7 zef*@0TTOR_n{jxU%|CpzV|C@a$<^<8(-Dd^v z&g;CDe0avb>$8g*tJ|ih8O%AZqIDtcrQOZeO)DM#FHr6&UB6oB)#X~V?FS9ZQ^HT@ zW`2&|cri5NjD_*KearKgvFUJz?|I$zYfoy?wK=}-1~2cQe-LW2X|t$&8P5TS2|`D; z<_MVSvbRS!7khQ>+w&Z* zrY@x?GNn!-A^xck_Mx&Uc?Qt-U7Q*LeH-yB58FKK3=N zxmRr5VrD#$Z(&*8EXtF@6!quHIhOAouYSE+;OBPcM0v}+FE;w_LLR2vk(L_Sy8AXH zvVL-1r?lzH`Exrqv--2D9bYE6WyzUHU8fko^vZ;`if1a{m-Swp{$|yF(H+eHh1bY> zidgM?q!gvse|N&=GJn6F?cTdpetfChy5!!Oly_Wt0vR7{Z`8<@O?`Vxdh_-7sZB;f ze^X!l`Om!Qc9YEH^@;DUr5luo`Pv3;2;AioJGHNQQR{z}v|iqQ4hBEoo5l$j&RBK( zru|icTHfxiEzvi`Z_J;h&t+=3-rYqbZSTcPs}E~er0=t`otk+6jQV-!pc}_|4oy;j zeCEzf(FwQbef=}_^0kHu-##hx=!sv^l1!6OdMzg;vE=Bi@@MNB1-`lPa@`eioZVHu zbZOH5d7g6#2|E{xy1Te3zHgN43izrdYWlBU@^!)2726fNeb(OcnNzU+ zUFqL_1~=m-XnwIh%sIiDv*xPo>03H>FFtiKlteVef81OwxzbX4$ID+wJ=C6QRu<0O zuVGwJ_>^n6WyhzG_W`(NDN9_6#$*J8gw!Z(cxUyo>|GlZD$IWdo%FS^u>e|cM+VRsBXWzwZh)4KTgd@B!L!(6tv#XzO_&aD15Zh;q` z|H@W+p_uTbvzVhPK56-%uEv+Y^BB_AJrXV@}!q@dp zIa9r|Cl_q2Onut=RrtNwvL~%Q_AiFfm65t?N(v>^7;4jV+r*M z3U;^d`mpZV==yo{QwAgRuDtx;jLN;NZ<&|nsQ8K{dl^`_%n*rPV|h2v>*XQqNjZl% zpOAR`R;e&CfN}bx4S&7g{tytHYjSSo+Y^z0{qH8GG~KwjrZ_2={X@Kc-B;awVROGF z3{U+SE={Ph`*ts++D19>a{2s;3;rhc|LO3qSnBX-;tNfI6D!3Jy-Qdr`By!Qf2n}O z7VXvfT_<+#+jsMdtkx30tWYKvSxcUK%vtyB%HDcDTQqs;&2TUK2hAyC(yxkuI6d8$#SJB52ZH^I`zB1vaH;>iwJE-H$K1ugCr;g{G*fGK zjCjnpruh?luV)KfZ$84z;y7*Bvd{NB3=5ZS=h_&;%FAxOu>Zg>xgz7v;49BBPPlyR zWBC0C4uS~_Dh@3)Ju>~wjJw&~F5j5m>^*YB)9}&3!%O_S|HWlXd4!9)Jhof1sCTZ% zq|Ik4FV67!+GSqY#go6+%;Ci|xdXlJ2b9lCX7wCe@u>RquY=ozg6HYI;&r+4pmptG z2F{19%b8|{ScEN0Wz}H*&8B=dV)hi5W$(=lI(VgL7@Pds`em-Sed2irtE4|^%jUk$ h{`TPh+ZQo%4+KJ_Bbmb^jB+(TFj!9b9LPWb002HtoPGcR literal 0 HcmV?d00001 diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index a0c19e6f5..2bc0fbdad 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -268,7 +268,7 @@ class TestParser(TestCase): @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output def test_tika_parse(self, m): html = '

Some Text

' - expected_text = "\n\n\n\n\n\n\n\n\nSome Text\n" + expected_text = "Some Text" tika_server_original = self.parser.tika_server @@ -285,7 +285,7 @@ class TestParser(TestCase): # Check successful parsing parsed = self.parser.tika_parse(html) - self.assertEqual(expected_text, parsed) + self.assertEqual(expected_text, parsed.strip()) @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html") @@ -355,19 +355,46 @@ class TestParser(TestCase): ) as html_expected_handle: self.assertHTMLEqual(html_expected_handle.read(), html_handle.read()) - @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_generate_pdf_from_mail(self, m): + @mock.patch("paperless_mail.parsers.requests.post") + @mock.patch("paperless_mail.parsers.MailDocumentParser.mail_to_html") + def test_generate_pdf_from_mail( + self, + mock_mail_to_html: mock.MagicMock, + mock_post: mock.MagicMock, + ): + mock_response = mock.MagicMock() + mock_response.content = b"Content" + mock_post.return_value = mock_response + + mock_mail_to_html.return_value = "Testresponse" + mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) - pdf_path = os.path.join(self.parser.tempdir, "test_generate_pdf_from_mail.pdf") + retval = self.parser.generate_pdf_from_mail(mail) + self.assertEqual(b"Content", retval) - with open(pdf_path, "wb") as file: - file.write(self.parser.generate_pdf_from_mail(mail)) - file.close() + mock_generate_pdf_from_mail.assert_called_once_with( + self.parser.get_parsed(None), + ) + mock_generate_pdf_from_html.assert_called_once_with( + self.parser.get_parsed(None).html, + self.parser.get_parsed(None).attachments, + ) + self.assertEqual( + self.parser.gotenberg_server + "/forms/pdfengines/merge", + mock_post.call_args.args[0], + ) + self.assertEqual({}, mock_post.call_args.kwargs["headers"]) + self.assertEqual( + b"Mail Return", + mock_post.call_args.kwargs["files"]["1_mail.pdf"][1].read(), + ) + self.assertEqual( + b"HTML Return", + mock_post.call_args.kwargs["files"]["2_html.pdf"][1].read(), + ) - extracted = extract_text(pdf_path) - expected = "From Name \n\n2022-10-15 09:23\n\nSubject HTML Message\n\nTo someone@example.de\n\nAttachments IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nSome Text \n\nand an embedded image.\n\n\x0c" - self.assertEqual(expected, extracted) + mock_response.raise_for_status.assert_called_once() def test_transform_inline_html(self): class MailAttachmentMock: @@ -391,13 +418,18 @@ class TestParser(TestCase): self.assertTrue(result[0][0] in resulting_html) self.assertFalse(" Date: Sun, 13 Nov 2022 22:33:26 +0100 Subject: [PATCH 41/60] fix live tests --- src/paperless_mail/tests/samples/html.eml.pdf | Bin 0 -> 22907 bytes .../tests/samples/html.eml.pdf.webp | Bin 0 -> 6012 bytes .../tests/samples/sample.html.pdf | Bin 0 -> 30170 bytes src/paperless_mail/tests/test_parsers.py | 61 +++++++++++------- src/paperless_mail/tests/test_parsers_live.py | 39 +++++++++-- 5 files changed, 68 insertions(+), 32 deletions(-) create mode 100644 src/paperless_mail/tests/samples/html.eml.pdf create mode 100644 src/paperless_mail/tests/samples/html.eml.pdf.webp create mode 100644 src/paperless_mail/tests/samples/sample.html.pdf diff --git a/src/paperless_mail/tests/samples/html.eml.pdf b/src/paperless_mail/tests/samples/html.eml.pdf new file mode 100644 index 0000000000000000000000000000000000000000..058988f66172a822ab2fcfd62daeb780a4332aab GIT binary patch literal 22907 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 literal 0 HcmV?d00001 diff --git a/src/paperless_mail/tests/samples/sample.html.pdf b/src/paperless_mail/tests/samples/sample.html.pdf new file mode 100644 index 0000000000000000000000000000000000000000..24c2dbf87373267b9c473712406a131a9d71dc82 GIT binary patch literal 30170 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 Date: Sun, 13 Nov 2022 22:49:52 +0100 Subject: [PATCH 42/60] merge pipfile --- Pipfile.lock | 180 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 105 insertions(+), 75 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b6d73906f..d40977c95 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9fefc737155e789ced61b41750b4273c7780ac7801c50cf36dc5925be3b85783" + "sha256": "a9410a1269e5129c9114fde990a0e4a5367931093c5b7290051c3602aab61557" }, "pipfile-spec": 6, "requires": {}, @@ -109,6 +109,14 @@ ], "version": "==3.6.4.0" }, + "bleach": { + "hashes": [ + "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a", + "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c" + ], + "index": "pypi", + "version": "==5.0.1" + }, "celery": { "extras": [ "redis" @@ -276,36 +284,35 @@ }, "cryptography": { "hashes": [ - "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", - "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", - "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", - "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", - "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", - "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", - "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", - "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", - "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", - "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", - "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", - "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", - "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", - "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", - "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", - "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", - "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", - "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", - "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", - "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", - "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", - "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", - "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", - "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", - "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", - "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" + "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d", + "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd", + "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146", + "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7", + "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436", + "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0", + "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828", + "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b", + "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55", + "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36", + "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50", + "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2", + "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a", + "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8", + "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0", + "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548", + "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320", + "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748", + "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249", + "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959", + "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f", + "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0", + "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd", + "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220", + "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", + "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" ], - "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==38.0.1" + "version": "==38.0.3" }, "daphne": { "hashes": [ @@ -839,43 +846,43 @@ }, "pikepdf": { "hashes": [ - "sha256:053911bc19fbc987ff32e8e2836693480bef4c400785f700e8645729aecc7dae", - "sha256:0b10914b14667b7de19c9b27437cd798afee82690e16763965c7f7a2ab4da90a", - "sha256:0bed47afb90c78e2262e5b2e0d4722245b50c28bdaa317bdd39c4ef125d59a2c", - "sha256:0e51db2d246d277e3b11f3341b42c737deea60a49c3d8666388bff466fe1c4d0", - "sha256:3b9037377d9ca2b91c47b50ef9edb2fd295b0bf963ba14ee732da52006600677", - "sha256:475ef831d47e0aacd1bbfc366090c645bdc881bfdcf682f2a789860f645a1bf1", - "sha256:5233f8f1dee1bed95379c1b67af926725fe6d8ba210c587406f51a6bae651132", - "sha256:52fda5f8b730489a10cc492249465d52b4f190f7eb9b6c0cf80e49d5be3d8652", - "sha256:6129a5277f850ae6cdfd2cfff7e2c4a0b24618aee942a99763c84791dfbbd067", - "sha256:73281beedc1c234d50fe11f8beb55cc5ea2d43625ad7aa88dcdea54ef019939c", - "sha256:7e7ef427346a8324c32e9e2dbd6d10b0c9acaeb30d646223418206ff33594d7e", - "sha256:8338786c9912703dee8a501c1dd3b5e1065c22628cc4f781a548e378ae0f9f0c", - "sha256:8ba102fe535677c54c2e6cca65c9e3742ff7abb03934590a06c77a17c9e86827", - "sha256:91faf319bae1f7a9a0665b1622680296e6ec351497e8b3da1f7b83c8c2fbbc9c", - "sha256:93450025948474b656a6bef0f64c91f5d2f18b4234d7470c4bb8b77e8eb36dc4", - "sha256:972c6a21c9e49fc53a36b9550616b9240fc3291e07990db0e93d2ea4bfd7f5cd", - "sha256:9b403d7b24a09a090ebeb7760890c5386c515b86a6cc6d4f9c7b36eefe94b52f", - "sha256:9b94b0bb3adf6be2407aaca8693bb1589fa5021c019ac05b1a87db299feefebe", - "sha256:a0645e34902fe51205048e982f30e78e2b1fe767203988dd4b1395f6326cc4d9", - "sha256:a7e6f4ac418092a48dbd1809905edd5c6640f271c49981558a1c3da3753c0524", - "sha256:ae5aa1fd18373fd96b8ea6610d78d1c12a43e105291e45ceaaa54637e76a6929", - "sha256:b6066f3bcdef27e255be8f3b6e299ec4c04b50688872f6cabe8e27908574563c", - "sha256:ba4db2e76fe4292c3ea47e39acb72f417e7b31f4fc594c91426b88e6e01d4940", - "sha256:bb7d56298d74e307f0ace8d8e02eb9cd2a1421993542566eb605367aa4886d58", - "sha256:bf5b505b4ac50332eb550ae61cf64854f774f5ddb10339f5dd8b20cfa44fa8e9", - "sha256:c892bfd06b69ab26732daa53c2d85f864bff0a9422b0c03004cfb58797406b2a", - "sha256:c9c46bcae66c8ea181aa3fd961e98b46bc656e40cc33df9794c63a0c84d719b2", - "sha256:cff08868ee479ffbaa64fb425260ddce14bffbc21bdd2a3b3de941589abab741", - "sha256:dba7ad05626a0768708c1dfa11b28b8461c6750994159a06f2ac58e0045a9685", - "sha256:e089f720703d5e8c419634b08fad466d540d081005e41b6382afd7728d327029", - "sha256:f02c4f10a645f43bcde681b31d90f6313d8edce3480729cc6d78c5411f3e9772", - "sha256:f0ff1e4bfbafbeea902a0bfc23e8017443a3be485d02c92cd7dab9c16d50543c", - "sha256:f4539bc4a586ec8dce7ceba474be726ca64135c48ad61c47dfba98139a7aebfb", - "sha256:fa397d5ee36f357f1ba60103004c32befc9aa7e3143ef3a9fbf6e3686b2fed99" + "sha256:0207442d9b943efec7eb07ea5b3f138d90cc61429a3c3243902ac909cb508fb2", + "sha256:0709832cc49ef51f004975e6e9bdc6daee8a8d68de621d428f13c95a83952e7f", + "sha256:07448689cc4c1e249e26ae694a2060210948e61035356cca3a5b8baf3a6a147d", + "sha256:0bbf45e702bb0556d705e1a4b5da391921e024cc1e6823675f2ea200acec1199", + "sha256:1376f3f4b1c34ac089a644af2e499ca713e4e4ec17035362350c0ec78ca6286e", + "sha256:15725f1bf572abb9a675f61874da66dc22144e74f374f7e8d023558e8c9c3f38", + "sha256:18383d8e6a620c52974b75034ad99c423f80468a434b52de456bb74d5ab51360", + "sha256:1dedbb95bb2c67d6923c91cdcd5a92703d10e4c4825d85cd7b8b474039978741", + "sha256:2035b39d2e5c97b6d9ede632f514403888e3f47d2b1e8b69b98420766ccb898b", + "sha256:2dd952e678dfc523f2c481c3d0a29b9823f07024f73dea7e9c03d2ac6592a61c", + "sha256:3d06dabf16592bb7975e1124000212c3c3bab1e97ed3f7c6534ea92efe9b621a", + "sha256:40999c3f48e5d0259662f6f708694d3ace43b01b4a2a197cfed5cf230557b116", + "sha256:46c7c9a7128d1751eedbe769dbc6c0a7983eccded74fd7d1e236d83a50c9cc58", + "sha256:529d4d099eecbdaa3e06490a032954ce96feae2596c1ea22f961dbf791444a3c", + "sha256:5887799a29510b53c7015b05d7276ee2e0f0ff1d782c75c3a3d1d9f68013665e", + "sha256:5c2de883986ef25e2e9b8ded8e5c285cb390950742164ce1bf116158009cd9c9", + "sha256:62a8c05876b9c7af4cad0ba9a8f22c77775bcceb118c35d682735955f5485297", + "sha256:6df4510606546c9c995afe3f799c506fe90798602b0628affffb7e1516fa1062", + "sha256:746897cbfc0c200de6be428a4e92dee72d0e03e1ff00d56006ee94fb59be199d", + "sha256:801100d8b4b885a203e76bc7266296f909944d621e6a0ac480fa2a0a0e0b1bb4", + "sha256:823f8b1cbad1182709d81afa32c23ac37b9c8ed33bdbc2b41f674be9420dc108", + "sha256:94057ca79525ba5eb5cc9c42337364f0e9e5f239887c0457dffb4ba3e6ac0187", + "sha256:9b1ba16cc5eb243c5e684c220752358a8e1e28a4e02ecdf2c3d24646f29c623f", + "sha256:9c9d75bb77dfe9b6f8915bc5339cfb0db427c3cb7cd75aa419b1da3c82f122ed", + "sha256:a0b78071d5fcd6b2288da469e89c030475095349dd57d82f5c40c37600d02e14", + "sha256:a1679c7d5b374895b6196784a75b8122ca0bb9248f5d97cd5ed77c569e264e88", + "sha256:ab8d610ca732a6369479605817cc55ee6f62d5b105ffe7e3749c3785c383631e", + "sha256:aff2ce52f0ab4ea8a1fbe57b06982b9fa9f997dd6bbec4b141091a1e71145a63", + "sha256:bc9b625f5ed454f445bf5012682b24d334adc9f853d41e44cfee7c52ddf92666", + "sha256:e0e66d49f8a85a4e0f915d42471643a5020bcdbef02586e49328ed417c13326a", + "sha256:e3dbcecc145d46d37738a407e0ddcce7cfb76d3e116ab3ba9c80f4dd14e71a3d", + "sha256:e99a90279a8254fa149d56cd307f94908c7844b2b8b42b61d241259804e40643", + "sha256:efc497cd01c55c5dbdd8a81766e317f44f728b3ceb65d7b6c6a064772c60e1c7", + "sha256:f35cecdab44cb01377e93a60a475bf4437854d98cb94379fcd65c6daa1c9a37e" ], "index": "pypi", - "version": "==6.2.2" + "version": "==6.2.4" }, "pillow": { "hashes": [ @@ -1624,7 +1631,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==4.4.0" }, "tzdata": { @@ -1766,6 +1773,13 @@ ], "version": "==0.2.5" }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, "websockets": { "hashes": [ "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41", @@ -2222,6 +2236,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.1" }, + "importlib-metadata": { + "hashes": [ + "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", + "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" + ], + "markers": "python_version < '3.10'", + "version": "==5.0.0" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -2346,19 +2368,19 @@ }, "pathspec": { "hashes": [ - "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", - "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" + "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5", + "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0" ], "markers": "python_version >= '3.7'", - "version": "==0.10.1" + "version": "==0.10.2" }, "platformdirs": { "hashes": [ - "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb", - "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0" + "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7", + "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10" ], "markers": "python_version >= '3.7'", - "version": "==2.5.3" + "version": "==2.5.4" }, "pluggy": { "hashes": [ @@ -2673,7 +2695,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.10'", "version": "==4.4.0" }, "urllib3": { @@ -2686,11 +2708,19 @@ }, "virtualenv": { "hashes": [ - "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108", - "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e" + "sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e", + "sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29" ], "markers": "python_version >= '3.6'", - "version": "==20.16.6" + "version": "==20.16.7" + }, + "zipp": { + "hashes": [ + "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1", + "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8" + ], + "markers": "python_version < '3.9'", + "version": "==3.10.0" } } } From e3c1bde79334940bb70d8ed5235de85bbafc15ca Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 12:06:35 +0100 Subject: [PATCH 43/60] remove log mocking, replace pytest raises, use humanfriendly --- src/paperless_mail/parsers.py | 14 ++--- .../tests/samples/html.eml.html | 2 +- src/paperless_mail/tests/samples/html.eml.pdf | Bin 22907 -> 23054 bytes .../tests/samples/html.eml.pdf.webp | Bin 6012 -> 6098 bytes src/paperless_mail/tests/test_parsers.py | 53 ++++++++++-------- src/paperless_mail/tests/test_parsers_live.py | 37 +++++------- 6 files changed, 51 insertions(+), 55 deletions(-) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index a845c3157..da0cf96b9 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -10,6 +10,7 @@ from django.conf import settings from documents.parsers import DocumentParser from documents.parsers import make_thumbnail_from_pdf from documents.parsers import ParseError +from humanfriendly import format_size from imap_tools import MailMessage from tika import parser @@ -125,10 +126,8 @@ class MailDocumentParser(DocumentParser): if len(mail.attachments) >= 1: att = [] for a in mail.attachments: - if a.size >= 1024 * 600: - att.append(f"{a.filename} ({(a.size / 1024 / 1024):.2f} MiB)") - else: - att.append(f"{a.filename} ({(a.size / 1024):.2f} KiB)") + att.append(f"{a.filename} ({format_size(a.size, binary=True)})") + self.text += f"Attachments: {', '.join(att)}\n\n" if mail.html != "": @@ -191,7 +190,7 @@ class MailDocumentParser(DocumentParser): return pdf_path @staticmethod - def mail_to_html(mail) -> StringIO: + def mail_to_html(mail: MailMessage) -> StringIO: data = {} def clean_html(text: str): @@ -228,10 +227,7 @@ class MailDocumentParser(DocumentParser): att = [] for a in mail.attachments: - if a.size >= 1024 * 600: - att.append(f"{a.filename} ({(a.size / 1024 / 1024):.2f} MiB)") - else: - att.append(f"{a.filename} ({(a.size / 1024):.2f} KiB)") + att.append(f"{a.filename} ({format_size(a.size, binary=True)})") data["attachments"] = clean_html(", ".join(att)) if data["attachments"] != "": data["attachments_label"] = "Attachments" diff --git a/src/paperless_mail/tests/samples/html.eml.html b/src/paperless_mail/tests/samples/html.eml.html index fbc4f9460..a73be6f95 100644 --- a/src/paperless_mail/tests/samples/html.eml.html +++ b/src/paperless_mail/tests/samples/html.eml.html @@ -30,7 +30,7 @@
Attachments
-
IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)
+
IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (600.24 KiB)
diff --git a/src/paperless_mail/tests/samples/html.eml.pdf b/src/paperless_mail/tests/samples/html.eml.pdf index 058988f66172a822ab2fcfd62daeb780a4332aab..de4aeb03840290f980bfac3e99d4b50c8264d785 100644 GIT binary patch delta 11640 zcmeypiLq}B!0|RwUE`8tp6qm%3R0RzeDd9(Px*qB&N;{4a? zWpst?n# zdv;}XiTgd~4U1}#TGv;9GIavi;v&udyW)=A*OsxWZ#^f-$33fURij*x4eJtrW~L>x zJ_^*Za(1dE-{9O^y{v5O;gywh4CR}7%#tM5Mqh8AuvT4i)0&C*PYP?l6TH;7K_~xC zs%k>2y@p(ChM)711*%tBrhc0t{6ju9A^p@fFO{&4lGoLjW{Pzk3#dq&U9W6x^L+l~ zO!FUGjAzQt$yy{*>$Tz1?m3xnK0o@DaO!N;{Cm?UJxb4VW$m>3@RH-g=1Ggc2Sq-X z_*KC%&)Iv$-dVw0c;fzVbZMR(<}Uk}+xyE>nPZn_vZiPWvzFLSRhj)nWJ<|7AO9)m zm6y&4z8hxf99e!|Q7Wyf^k@ldp7E|n^{1L=tnPij;pw*-ijFU@{aMQ?XL|a|(V3BH zR!h_8YDa9n$+zBdwzbXq*T%P;H>EY(glpNo)%_;={^7*k?;l<6KV8k0SNrwL>rd6? ze}8-K+4b|+tR@ker$QTKxeG0QE0Wfqeb2e(-=oH&mp_s}_;JMhl2Y=|SE)7rU-Xwz`<~dR&4sKff-L5i zMqHBvm}FTjP0URv{}NNF*Yc{`Y;r$v*2>gIrL3nNfwLAdzv7rBHLELig?YT>%!kU% z#+y~no>}RZdu+}6nNw^}%#l3p^zPa2b2}d_et+iAv+uvt?e^b)zvk$}H^t}b)@=P& zHLtocz^P;E6k&y3$UskiX$&vvPLro6u{{m8epH>O#Kt~5QWpEl93 zUZExT@N0gxGZlB7m$^I_P_pf=tqA=r8cdy!wj z=uflza$k)VHQQfhI~nN-PqL}ZIym|0L%I4)t8dp6>(9UETISayUH-bfg`>CPeagoz zlaAj$H(}!qzrcCo_GTM*Z2uFU^J+p|p^=%yMZs&=+U_itaVk6PG09C$Soq%diO)Ys z3dZP&oM-wLu<)(^sha-`1$%6!x+~}ZIi;>!9(_!sJg;TK-B+^T68^5={C?(_38vrG zg;od8uc`la_}8hv&r|)Q+M|xWun+EV(Yp0qOsB+BaN;wakCuCSIOjb)zof&mz-uwf z0$~Z^Df{+GUpu4yD&C{!NbyuJzjsTrpRW8;W4ACj{q~gQ>rYS8U|76l^YO6bLhPc~ ze!i1?Tbb_@GyUBDonLOG_{*MLZG8UImF-)f_KGb!VpzIpH($L_X{pnq?|1C0E7o~G zSFBuYTiW=qe9QS;_W$|zm?xQ-P1WAh`~CO*g?7PWYlVeBrU%r`O+S=!K6u;mJ$vVE z@_lo)bw#!7mDmEda_;j?D~}heKRP&#v3~iabxW*Yx)yoLU+U*kXg(oeG^a;p@rewh zi6WELPfQNaoPB6_`;rHK>Gc!lWyQnkh&22?3_X(9x5gYPv|Ey;}T>h_Y`+rr9 z+W9V3JRYq}=We!B5qWQ%+Q~K`QNwJUBA-!WLC$YomyvpTG+^&z6$v;@zOp)t937*eEYWdKU?$(?%5ynKh)cs z-3Z^hr0*MV@tuAW5KcP@x|Rs`(cpqtn|!8#yc&aGCWb(b;I`TfeOH|BNR3UpdFA+zGG;>;DLkxSUK z?wA~L&y#w*)9h7QO_)#G!xNe-Vke(`yo%4S-eGQN+6jjWXmTk83WNG1rSH`5)fDG3$Hw>b@gW(;h_e zudeY8dZ}^1@PD=Ty)%DV-b61rXBH?MxS*&d(0S^dq=}CU?`hsR-~4#_!&j`YjG526 z-L`A{VYX7W-n2wJPV`}&&vEOzZQVLYCVqV)VCyEnS+62rd#8(E!g;SEo-9@;_QfXs ziT^JvDNldtv5EcpiG_Z%l)ae*+qkP+s#~faHWkeIBH-2X*lAyY)8>T57G8I{-1+u& z*L~IKRQ>uz=h9yOeXTk-^zSG}oQSmG+xkR1#&zSqYV}J!x+nW7r>>cDx4FEp z} zNNGW85c5MF-uH@McU-IZT5eJz=)ba}bIQY0$|uuS#uc54i*cQJBdn^!%v-y2{+tH( z1gkj@m9X_6+e`;B&1Txx9@awYl%$1@rmMz*-pzGx2XynqI&ZPb&7$2V{9@{x^dgye4!t@;dW+VGZg!p@dj3>xZn)(Zi~5JxU4Ezv zuM0VRUhGHb;Z(kQ|Carc57u8T&=jnzd1dy|olkuA!}hF@u5{u&xx%twKAMT&?q0+SG*ux%XySd!U?>Fo( z_`$Nf-bmbU?w(*9Q*pn0oa+BXROdaY|FB%+$Be`7GImcsxbq!f{jfRN@DaOA-_ZlV zXBJF3{IIp`;|A$_ysd70?G}3uKg{K0x7>5|Aw#>l<(uap*xGB$GFEtqNLyUl|F13L z{KKz%A0N4Xzv24n*1hXhyQ+`>yp^!tzd~2=TfvQ3-rW{&o&+3!7|LhO^-z>Mr`|H~ zP8>(Q{&lM((}fpaW7Jo?sK51q8J~X9jZ=x;%x1+mwp>)oJJr%FR zs#wJ`)-auV=a_?ul~*kae@buI6SJbqDsYVyd#K&Rm-Q|Y#}Z#oTK6c3&wJOQ;1kb+ zw;z1u>Q=Z>SWI%EP=1J*6RX!MB|))U5BX)p&TQ}Iwm7r>unpf+nNt=&Zq?bBulrHW zSN#0Y948IgmqOmciDvDQGV`_^@DV;^a6ni_F28Y-Sj8IFW!oJL+cRbAjz7F;QPFbv z@VB~0hm{k!+D{hOCkn-h6wWxYaKd30zQ>j#Uz+}iRa{_sY;os&qE5TvV%|qH#3XVX zemf~2`E8Ua(q3KowEFi!5AmJ_W`!;mcOJLwW^eB(FFs*>C|0J8e{<(yOPoJ@2u{j$Ez+TzZ+T^-xEJM1@qu&rM4!Q%_lo##i( zCdsScnUyV?U+R#Tzj?;%?9ER3#$O&yIJ_{OfBB8gtW_o3J8kD&u)VEOw%u`?;I5+% z(XTNW-kWYO|y!3BhYt?$@$r>Cxq6R3r}$tLhZtX4@QRFQ35V`23os}$ zxZ!Z#vHfm|Op3ZktHJ|kM$-eV72+0YE6paBGG;L!YP@#&Q0wW8TmN|-{#|DIQ}5kz zOlD=@|K6!yd`W7yHDPO>T#Gnk#MtiQ#3Ob)-T9T1TVI|++qP!g<2)-jugZP&+H#rZ z=9}{-7jtjiYL*_*xpPjRLB7!Gch6oN^wNH&AMs*++}^C-CqHI=-&xT3{okwVsvq5( zw``QWe!J(jZ}9c*uaT*#wljqmJ9AtVjM4vI|LJR`^Cl-*J@v_Z@5?{CxhU;ng!y=&$EB0J}+$+J!AE{vbk*Pr+Mvc-*U;!YN!v zcIVoiXVD(NJZ;R=0;=a|pL@6Uc&EFq!&t%vljCwRJ{lz@~j?9r*?O zDid4Iv%Ef8&&2uFsHw%<kCcpVn7O1@ ze`O+*YSUZerT;yjaEj}t1U9a}(R9{JwScYu?E%vb?_Z}d1}_sYh+7ge_jdKOBb?_9 ztJmgO>xU$sF`s1c#K7f~b;tcJhc=qTyx(@L;kKw~-(tPT7SZ*WpQhbD$|H6DjhTDc zvcq}FCubhXQ@&U6)I2(9bDr$KHPPi$U;UiAI{c^TD)G-h3jZ<69nTa?GN1MIx$2&# z@J~X!dd@A{{L-lPT5$w-&(SVM(F4z4vDY>JJX&e=@pb9ONzxuVrnmgIz2*olS+djc zgvYa|X-{NU?=$>Z(l5G(NxJ^J@;SXU^)$`f2VN%plbQR&PWH~Oonac15%wFNhUoe= zL^c>mSe8xxy6;KH2@Q_&+l9?<7ioOiyf+~}^_z7##|&Pf*^PnC)@Rs)wyZ7nn4A4% z!mbMwzw@sw?%N};$>>@v?QbS;lTrR;OK@@IQnziL_9q_kZ#kE{x&5g^?!GhC^{s)A zG%B|lM9&cnl~hb#C!y$)eI#IE|Jn~N zKVsAN*DZbWtvakK+`~I|c98R3MV2{&?^OEF@}7&>J7pDzVo3`7|Ej6=A`DVtGqNJj z`c9ucD>puV%gT6MYnF(oV;(@vgL|~+X{a^ulM$`De+D# zy(%A;KQ}TaXx4-+#~=T!xISn4)Kl99$}5dui8$9AoEP^!o1n(hu6`PjM?O7r$>2WL} z(r~hk>yv<4Zj&;kN|qmf8M|!vnZ+4`wN2;RTCem(uUuX@A=Q0za4oaPoWOZ!{pVUq zRRs9t*zzBb-n^~Q{;d6ys`k%Lia~O|a$hQ2J#NM&cK3Zb?YBR7SY zEp@<`$4v9sHLv_T;oztt;q4kp-+aEB3jt5^4HeAs8(OIx7@aq*XDHY7Bf8}6{&Sy1KhL{)WPQxe zL+f+o>USg^{4-VixqigKd$G}xJHP$xI<^FIv)D(^mLqnS#_qd?iiRq7_j1Fw zbDn(P!4m&@zEzp#7Q;t#Pc#Rz-(_EXzcaIEYPRfe+51)u_1{;vuL|rF;C?4?@}1kJ zrfT8&F4i}kBJE`+_`b05~Xu~fHH*%}Qq#w4JyYk1&(+3$= zL_A(9tUkp+qhhDjOL>v=^$$2Yc|MmWnJO@>RSXI6>*<>0k)^V6zJ`I9%MG=?8o_Qt zEbDucPKJE@R#Y5u!IQ)Ogjwuw#XkmWapz-9W_P8p`=|6GYR8EOLh%Q~-z}BTUn)O& z_O-pIr>k8R@L_XUbFkEtH#KCHpz5&z-bXp4pUl-XXi)lsI5QxVBz7Z z&YM?cGgl zZX3tFdU$KzhoJu3?Wv{Rohq7qns=rM%@mt#bYkK)&D4PB(d{?wvh#AcE!2D}7xvXb z@oCE9qSx({+jHN?opD*-95vsu?aIrGmi05IoX8BWYb`l)!TjWgZOs}Di5>gqeUr02 z{^QHFhZ_%HZvQT;w_xJ^1%E>>E>JPyG?}#Fjm~Z>( zVNm8iS?JJ(c{wbqAHK}FlzKY-{HmnFo#*{;{d>NDkIl+uCzRom6b;q-9Nwh`<}{4c0pE8^#1YvZ*vXFz4hnICEeLS z4A(Mi?M#$C_TIZFAex^P001btE3t8yx)XBvWy-u5P00r%UT2&dnENm5VCz+QOog;O;G@`}p$( zv;L%%JhyYDNBnGFc<&WV+3L9emSO)ovpquD=C2|=T-JE#ybCZqbT`!h4xgf*q0yP% zt3J-Y+h&Qn&zijPv{b)M!Zq2DDcQY8c1X|7l!^6xe`8DDUA2Alm#vN3m{c!nZuHQ} z`{$gi(La{1Zp^=N@xb$`-qZ9_T=$&ca(tP1zU_|Uk8aDK-`M=Vw(Hf(3%glfnoe40 zH+@S~!s?SYce5UfHhX+EI+k#7NkI9gjUT0AC3h#C^_V6Vf4hHifOtoWJCo&`>DTPM ze#it!Tg2bC+by1ceR9bB#kFM*Mdx~4U0Kh2aAk9z_sknvdkT}@2TDk5q*XTV`aac? z{Tf@me^blkBO%G|1VBow0SZAl(#}wO4aSlzFl5u&HQOqoX|>zms47M9J{Vw;grp5 z&lbuHsJA<+`R-EVqMGv-lA<%>#7_ks5sBr$7W-z|?#a9Y`Pc8BR@*39M#lu#Ei9HYK}SqhPQDZSLa%K4rl&lYa~}x*>b5(n{@drVV*Csp zze(v`N0+H?aH)w}HEFd^cH-PEe)qW7pL~C4_ow*}g?~!_s1N($_Q!7*)BcYAUUQH8 zsy|+~SRrH1qRU-2H>$K)7AZ{nFLT1_CX3kSnT?HWpWke6jJ~y0<>;a*K2LPx%{QF3 znt4<6vZi`MRkPgLn+x8jPmp{O5Mrf#OIK=b#)Lag^2}amdW5n=O`<2xm{i&pYkohw z(Iq4$EGICs&R9Dx8+j-XCd!NwndW;CcjWimtMBPa!ts*RE_k{Pk**O)d^YP z#<4K)!P2>kk<%BwcPmbulGjtW=w0Gpp1V4KZFcd5U4DMB(@kd4h8{_+uUa*i^-A<~ z7suXvntHqmUM1h>!P6h{MJ`x@2hts_en_3Tz01K-wydrT|M<(LQ`i( zztG$Mx-feKL--b@=WJ^Owkn1NX2-jgUkjEy@oviU4N5!boD{n1ttX|)yP$c=?-v=F zN^_OYP7=Gce96_4wObBm1ZOTQ)A=6Kl)Glv#CR^7o-J{`qUW|Q;e5XCn9^LY-ra)5 zb5lN5$FIA2M8{1`SbFhgrW1e2`Ab4b8p%fzkZ|t|BU@}ak+mR_iXvOG5lNjI^DmY zYNftiuAkz6^z8d@FXHTy4;{JDvE_2LfBnRX{{sIrcGvyiZMpGbEZ@GG9h#h`o7b|| zp5DI0-bk|N=7d*~EBNnr`Cqx|m$a;RcHvG-W9HNBhdp|ZSuNU=azRiwOd!jIPw`ho zlJmx6r_A>H$FI4+;Cg@aH*=XuACezD+IYaV#71|y%hmXj`XeTtd)|4AiPSTeu{riH ztl8F8=ICiJ@M5(KXN-%B{&m6a0h<3huRVV$a#zBCf$$y8JDPp1J2xH-J#|7oM1@uR*QuXe6sZ$_LbTe0*|YxEd|n~8&T_Bh6B;W!da9s3?3+&o;&sD>9{#=i5zb7dt^(IzvS8DoXZMb18y7BiN-g{NQFBu4P zHWzG;OYdQwySZLy)^m3C*~Z4rVz+LT>=ZqGHgRj-#^)NA|K1xdGEOo|GPaQ{l>Asb zx%T2diTE|=A9sH~`rP&T(I-crReq{}^y$&(N!^ph-NKKB91E+`dMtKK-~XF>yjR~# ziCcy%k{#-N>T>p}+pn`&X0h!?uuQmIuw3*z&3^9v8;|Xt`fWq?=Jl!#h35NoIa1lr z<;^#LyR+@ruYJG8bM3`;c-`#I{`9?XPDR3-`}!N#EV}ejb@}%te7t8?>uOb0Kc3Al zn_BO^t8A^WUtVa+R{qqjkwuT%c6j>7vzW|S^Q*1kdQf)lm028*3hqeeO)8nlylCo# zX_b+^Gkv{m%YP&tyZ2|tCy9C88lyo3m-|q97?o)ylyRM(R z#P1P(n|-NuwW-O5FUjJ1GKZV*21gwD{yARYySPYRy>phv%zI^WdJp8>ChXg%5#%V& zIgRVHl5=xx2FKyE-BV=Kj4s@?zjW=^l$(M5A5V+t+Q**xAh|Yh+lo&so(BHYn0W6& z!IU>QmR?Id_)0>LCw}_&-ya^{t$f|af7?j!fW2eA{@vYyC+^LgR`&15$IqFWTRAyT zOlY;r(AW3kUMX~}Ua5B5f-OqY9Y=2}yqXsE>ZKHia7W+W%yNm}?yKK(8ym0pT>Vik zbKz#bJT+&l0_OZPpAwU6YuC>$?%O(XTXJz3Ti=2^+j5<69*q0;VEx(KT-+~YL)t6c z8OyhJub;clXv(z7E)$dDnF<4ITk|HqsOs$ERr}>u(Y*NHmMgMPldS5e_wgk1W!G=_ zN?QEuo}XachpC63CYa8?WxmVemi*tqFH18v>c8~T_IAM!QUIp9sNuj9eb!CA3wm)?imJ9+=5 z`7WungB+x>o1{ZIA3nN?c(bHDt3rT<$#ZV{jMp6RV=ozq$&!3{d%T!HCM z9_Cy0Z5g?zYUzHfx-7-K%p+yTqeeCh(ceL5wZki8lgblx?oNFlBwW4T*2Bnb)0Ohj zg-(fo=UA@ZZf^4B_TMA7+iyNQz?}29?JK((yOwHteZt=(hvY?n?(OY9KWWaZ*$pN= zwd^f(Q|yIT`YAB4+sm1emFrd=s1!ZVPJXfUt?o$-+WKtw*0~FOTK`M+`F4x=+md!( zbI*IMdBQjU_KB1v%Y;eVN6k;xUG|%OPR*-)=E`#}Y&WjloLwYy{r>kAvm3Z?)_$|8 z-naLLT+9j0t^5Dh^XTn(xs`FdkL0R#yS_|rHf25Usd;~!_Xf6bvjyhN=$Mnx!JKh*sw?q}>z;h*(fF-_WsR273x zu9(^q)tRzm@o%Y!N8cM|-%RvwROX$uxs=OReuvDBb9)-kPrU!yTKvCN3&hmMxL0jG!g}PNQu*<4%@l`8Z z?Wg|qX6ZGXa*JJM>!TvJC1tJJSAY2Y+1y=gmObd=`sHLK^hu~oc$)eT!TVkovoB0r z;668G>xI(AX>s9!;Q_9}zvburf7RG_eBO#%w?5oBlNv28_3PT%S$~am<{3X;t}ohJ zyU;B#Vq4wg&-uArAzVML)q3X_pN*T)EO6;>TJ_EU{co49JRJA!$!@!njrXH^Ue_lY z*6-MmWZ)?OtL0Vxp$qq0Jb3voC9Dd!V=an!&(^;Bf4&{-r^?@MQziwi^wGOK_2p@v z)MG)xGk2Z|O4Hpu^`^J%zYcG~Z?o@TzV{_D@Z_qu`s?F%*8V=_edVBj+%N6_6K_3y z)4JGQzvlD3iTT+-s`t$N-@s?g{&&&jZy`yhRY%Tx)GrjO{qiV9Jk~h&3m#*?w%X~{_XWCt`xl64>X`LDG=Vs*{QARs2#GUWS z&`)=Z(N8zXkI=Euj_E!;b&ul4e^s`{{!`8=y`J;&Z?MQJzk4C)6R!O5^i+{Cziur1 zl*cnn=2*z~gSjQMcO_Z8$$69WYVG^n7rAe8jjB4@)i#JYcWn&zsNZFL_o}9es+y|V z)yY!RC&VPKFbJEWb%yWh>53jtwv!xc2FuT+z4spT&6~;ZGaJjF(_Fg4^ zZ|%F+-}YZ|_-_5$H{-A0_xzRnJKeVhW6rFUW-lbN3@$oy|&m8zS&b|{nKlzLS ze-mR;{ojPe=X<^?%N{;LyK_Ie+=;C(~c1?L7Bq;mLQ=qGu~$I>wj@l$S^s?jxBD;52JJ6+~Zv@Se$^i_{us+0a<&dr&gH%}%1>!0@f>CyXJ^B#UL zRG;th`*ODqr_ui3x~ezL5=>4DKH9MR#->dh_gvjyIzL`frN#1vPTIrz#sls)%Sum` zXWT!1rLcQyzL2({CiAC|pegV9@4I+U<5p)~Z=@Mrc4Ge@<^LO}rg9k`XHMVIyx(#D z^0Qx5cf@b(D|uR6eTF9{Lj3OI&F;JkiMs+;2VZ!pa7BT)H$=xMc%!Fu_`Zn`i(gzQ z$elXv1M@;_-Rv(Z`#mlf??1Qa#MS!q;>L!`*@u-fEycXewuS`7Wj$!z*0s-NYmDYB zrPjE_0_N$`uU^I7yc=MiDDijeLdTOl4r%yhRJ!q_S4Ph=2CIn_C~h`kxchsQdH! ze<6`1Pg%KB|0UHf$rVZcUZ4KlQ*qvTjqgVcf;x{|+N}Dx;^1O|{;w4wTkYeUj@xYW z{`p?$pk- zg#XJDzP)+M{$)psSAGg_|1Z_Ty+q{rJK?SqB{u%zvrc~b_H$V`-=_Yj6ShbC-Ih#D z-k>Y5{CmppL*ZHfYgf%-dt53tlP&HN^Zd6v%)Zw>l|Cqyv7nKS01<*$&Hw+64jy;U zvPUnQ^Z#;5CO+f-rz=Q#{HkYGIPO%sxy+iMrQX2KPNBG@C^a#ct71;W3OJ!ojmxaszGB7!Jh42`q9K3)1jB00yx5C_ycPcCY=-hw4OtSTV z;_go_i&sboM@sElt?N1GfDz%^*`TlQ@TB)yW-b5zdrx#`~EMTc1B~#bd^PmZn;+J{@T7s_2K56sp0ST zNiLsy*~VtyvUx3OVJnus(USVLNF{m(cjd+sb-|Ap=N?fG{=56WQ|Y?X^ABw*f3Roi z6{DG+vNM}{Jhd+{PBCnazQ8!eh&6gy2CtW+zK>vr@no(b+wkdL^+&JjmTHu0zrM&C zBIIf6fOi7k98B_1O05@QbUa4ct( zJ}jWuu_?^pU7lXfodmv%xmq@SdwCdS&+D7!U#aCiwR&qFbH&>qFQ2>T^V~Jx{Qg&@ zX?^|fs@>nYQu9(Efxem3FUEzngP7@CXBMgg;42>-@)ES#YEkV+2WQywHdIJLkBU1wmi;XNyFfB1R!cb>yV1j9hr4gzn z1_lPkMy41ZF*Y#T@OnU2F4bc z!DV7%f{_+X3^3D&i4jH$GcYkW!qjVSh$dEVU|?cljNxh%OGAv{GBq{FNJysUh8RH& zTFZ;?u~`&;VVpk)es1 T@npS77fwr4b1qd?SARDEK9_or delta 11483 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 Z7-ELThGvrwM!IlX7#eV?s=E5S0RXf-VoLx3 diff --git a/src/paperless_mail/tests/samples/html.eml.pdf.webp b/src/paperless_mail/tests/samples/html.eml.pdf.webp index b4481efd92e594f4fb8afeeedcc67d1df9e47db1..ab7cd85353078c6f493fc632404493639b719168 100644 GIT binary patch literal 6098 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 literal 6012 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 diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 3da54e364..4123e1cc8 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -19,12 +19,18 @@ class TestParser(TestCase): def test_get_parsed(self): # Check if exception is raised when parsing fails. - with pytest.raises(ParseError): - self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "na")) + self.assertRaises( + ParseError, + self.parser.get_parsed, + os.path.join(self.SAMPLE_FILES, "na"), + ) # Check if exception is raised when the mail is faulty. - with pytest.raises(ParseError): - self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "broken.eml")) + self.assertRaises( + ParseError, + self.parser.get_parsed, + os.path.join(self.SAMPLE_FILES, "broken.eml"), + ) # Parse Test file and check relevant content parsed1 = self.parser.get_parsed( @@ -210,18 +216,18 @@ class TestParser(TestCase): def test_parse_na(self): # Check if exception is raised when parsing fails. - with pytest.raises(ParseError): - self.parser.parse( - os.path.join(os.path.join(self.SAMPLE_FILES, "na")), - "message/rfc822", - ) + self.assertRaises( + ParseError, + self.parser.parse, + os.path.join(self.SAMPLE_FILES, "na"), + "message/rfc822", + ) @mock.patch("paperless_mail.parsers.MailDocumentParser.tika_parse") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") - @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_parse_html_eml(self, m, n, mock_tika_parse: mock.MagicMock): + def test_parse_html_eml(self, n, mock_tika_parse: mock.MagicMock): # Validate parsing returns the expected results - text_expected = "Some Text\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (0.59 MiB)\n\nHTML content: tika return" + text_expected = "Some Text\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (600.24 KiB)\n\nHTML content: tika return" mock_tika_parse.return_value = "tika return" self.parser.parse(os.path.join(self.SAMPLE_FILES, "html.eml"), "message/rfc822") @@ -241,8 +247,7 @@ class TestParser(TestCase): ) @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") - @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_parse_simple_eml(self, m, n): + def test_parse_simple_eml(self, n): # Validate parsing returns the expected results self.parser.parse( @@ -268,8 +273,7 @@ class TestParser(TestCase): self.assertTrue(os.path.isfile(self.parser.archive_path)) @mock.patch("paperless_mail.parsers.parser.from_buffer") - @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output - def test_tika_parse(self, m, mock_from_buffer: mock.MagicMock): + def test_tika_parse(self, mock_from_buffer: mock.MagicMock): html = '

Some Text

' expected_text = "Some Text" mock_from_buffer.return_value = {"content": expected_text} @@ -300,8 +304,11 @@ class TestParser(TestCase): # Check if exception is raised when the pdf can not be created. self.parser.gotenberg_server = "" - with pytest.raises(ParseError): - self.parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "html.eml")) + self.assertRaises( + ParseError, + self.parser.generate_pdf, + os.path.join(self.SAMPLE_FILES, "html.eml"), + ) @mock.patch("paperless_mail.parsers.requests.post") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") @@ -313,8 +320,11 @@ class TestParser(TestCase): mock_post: mock.MagicMock, ): # Check if exception is raised when the mail can not be parsed. - with pytest.raises(ParseError): - self.parser.generate_pdf(os.path.join(self.SAMPLE_FILES, "broken.eml")) + self.assertRaises( + ParseError, + self.parser.generate_pdf, + os.path.join(self.SAMPLE_FILES, "broken.eml"), + ) mock_generate_pdf_from_mail.return_value = b"Mail Return" mock_generate_pdf_from_html.return_value = b"HTML Return" @@ -430,8 +440,7 @@ class TestParser(TestCase): self.assertFalse(" Date: Sun, 20 Nov 2022 12:36:49 +0100 Subject: [PATCH 44/60] use html.escape instead of some self build functions --- src/paperless_mail/parsers.py | 8 ++------ src/paperless_mail/tests/test_parsers.py | 4 +++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index da0cf96b9..c4ecaf861 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -1,5 +1,6 @@ import os import re +from html import escape from io import BytesIO from io import StringIO @@ -198,12 +199,7 @@ class MailDocumentParser(DocumentParser): text = "\n".join([str(e) for e in text]) if type(text) != str: text = str(text) - text = text.replace("&", "&") - text = text.replace("<", "<") - text = text.replace(">", ">") - text = text.replace(" ", "  ") - text = text.replace("'", "'") - text = text.replace('"', """) + text = escape(text) text = clean(text) text = linkify(text, parse_email=True) text = text.replace("\n", "
") diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 4123e1cc8..1a348b472 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -364,11 +364,13 @@ class TestParser(TestCase): def test_mail_to_html(self): mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) html_handle = self.parser.mail_to_html(mail) + html_received = html_handle.read() with open( os.path.join(self.SAMPLE_FILES, "html.eml.html"), ) as html_expected_handle: - self.assertHTMLEqual(html_expected_handle.read(), html_handle.read()) + html_expected = html_expected_handle.read() + self.assertHTMLEqual(html_expected, html_received) @mock.patch("paperless_mail.parsers.requests.post") @mock.patch("paperless_mail.parsers.MailDocumentParser.mail_to_html") From d132eba1431fd2c8a9dd57904cb019c26dbcc2e2 Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 12:48:03 +0100 Subject: [PATCH 45/60] optimize regex --- src/paperless_mail/parsers.py | 5 ++--- src/paperless_mail/tests/test_parsers.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index c4ecaf861..902619fd7 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -105,9 +105,8 @@ class MailDocumentParser(DocumentParser): def parse(self, document_path, mime_type, file_name=None): def strip_text(text: str): - text = re.sub("\t", " ", text) - text = re.sub(" +", " ", text) - text = re.sub("(\n *)+", "\n", text) + text = re.sub(r"\s+", " ", text) + text = re.sub(r"(\n *)+", "\n", text) return text.strip() mail = self.get_parsed(document_path) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 1a348b472..5cd614197 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -227,7 +227,7 @@ class TestParser(TestCase): @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") def test_parse_html_eml(self, n, mock_tika_parse: mock.MagicMock): # Validate parsing returns the expected results - text_expected = "Some Text\nand an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (600.24 KiB)\n\nHTML content: tika return" + text_expected = "Some Text and an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (600.24 KiB)\n\nHTML content: tika return" mock_tika_parse.return_value = "tika return" self.parser.parse(os.path.join(self.SAMPLE_FILES, "html.eml"), "message/rfc822") From ebe21a01140aa5d492c34fc88be63cdfcc57b025 Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 14:22:30 +0100 Subject: [PATCH 46/60] eml parsing requires tika --- src/paperless_mail/apps.py | 4 +++- src/paperless_mail/parsers.py | 13 ++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/paperless_mail/apps.py b/src/paperless_mail/apps.py index fa6b1a267..719400e76 100644 --- a/src/paperless_mail/apps.py +++ b/src/paperless_mail/apps.py @@ -1,4 +1,5 @@ from django.apps import AppConfig +from django.conf import settings from django.utils.translation import gettext_lazy as _ from paperless_mail.signals import mail_consumer_declaration @@ -11,5 +12,6 @@ class PaperlessMailConfig(AppConfig): def ready(self): from documents.signals import document_consumer_declaration - document_consumer_declaration.connect(mail_consumer_declaration) + if settings.TIKA_ENABLED: + document_consumer_declaration.connect(mail_consumer_declaration) AppConfig.ready(self) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index 902619fd7..b325b79d5 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -159,7 +159,12 @@ class MailDocumentParser(DocumentParser): pdf_collection.append(("1_mail.pdf", self.generate_pdf_from_mail(mail))) - if mail.html != "": + if mail.html == "": + with open(pdf_path, "wb") as file: + file.write(pdf_collection[0][1]) + file.close() + return pdf_path + else: pdf_collection.append( ( "2_html.pdf", @@ -167,12 +172,6 @@ class MailDocumentParser(DocumentParser): ), ) - if len(pdf_collection) == 1: - with open(pdf_path, "wb") as file: - file.write(pdf_collection[0][1]) - file.close() - return pdf_path - files = {} for name, content in pdf_collection: files[name] = (name, BytesIO(content)) From 1fa735eb23afe98d156b5ba6bf7560cc65397202 Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 15:44:43 +0100 Subject: [PATCH 47/60] use imagehash instead of bitwise hashing --- Pipfile | 1 + Pipfile.lock | 228 ++++++++++-------- src/paperless_mail/tests/test_parsers.py | 1 - src/paperless_mail/tests/test_parsers_live.py | 27 +-- 4 files changed, 144 insertions(+), 113 deletions(-) diff --git a/Pipfile b/Pipfile index 8b57e7f15..4b32ad01e 100644 --- a/Pipfile +++ b/Pipfile @@ -79,3 +79,4 @@ black = "*" pre-commit = "*" sphinx-autobuild = "*" myst-parser = "*" +imagehash = "*" diff --git a/Pipfile.lock b/Pipfile.lock index d40977c95..009cf83f9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a9410a1269e5129c9114fde990a0e4a5367931093c5b7290051c3602aab61557" + "sha256": "a49c31147e62885e2c962cd0b19f18169d73aa4b89f9e1b27f61c996be3e3ea4" }, "pipfile-spec": 6, "requires": {}, @@ -126,6 +126,7 @@ "sha256:fafbd82934d30f8a004f81e8f7a062e31413a23d444be8ee3326553915958c6d" ], "index": "pypi", + "markers": null, "version": "==5.2.7" }, "certifi": { @@ -548,6 +549,14 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, + "imagehash": { + "hashes": [ + "sha256:5ad9a5cde14fe255745a8245677293ac0d67f09c330986a351f34b614ba62fb5", + "sha256:7038d1b7f9e0585beb3dd8c0a956f02b95a346c0b5f24a9e8cc03ebadaf0aa70" + ], + "index": "pypi", + "version": "==4.3.1" + }, "imap-tools": { "hashes": [ "sha256:6f5572b2e747a81a607438e0ef61ff28f323f6697820493c8ca4467465baeb76", @@ -772,37 +781,37 @@ }, "numpy": { "hashes": [ - "sha256:0fe563fc8ed9dc4474cbf70742673fc4391d70f4363f917599a7fa99f042d5a8", - "sha256:12ac457b63ec8ded85d85c1e17d85efd3c2b0967ca39560b307a35a6703a4735", - "sha256:2341f4ab6dba0834b685cce16dad5f9b6606ea8a00e6da154f5dbded70fdc4dd", - "sha256:296d17aed51161dbad3c67ed6d164e51fcd18dbcd5dd4f9d0a9c6055dce30810", - "sha256:488a66cb667359534bc70028d653ba1cf307bae88eab5929cd707c761ff037db", - "sha256:4d52914c88b4930dafb6c48ba5115a96cbab40f45740239d9f4159c4ba779962", - "sha256:5e13030f8793e9ee42f9c7d5777465a560eb78fa7e11b1c053427f2ccab90c79", - "sha256:61be02e3bf810b60ab74e81d6d0d36246dbfb644a462458bb53b595791251911", - "sha256:7607b598217745cc40f751da38ffd03512d33ec06f3523fb0b5f82e09f6f676d", - "sha256:7a70a7d3ce4c0e9284e92285cba91a4a3f5214d87ee0e95928f3614a256a1488", - "sha256:7ab46e4e7ec63c8a5e6dbf5c1b9e1c92ba23a7ebecc86c336cb7bf3bd2fb10e5", - "sha256:8981d9b5619569899666170c7c9748920f4a5005bf79c72c07d08c8a035757b0", - "sha256:8c053d7557a8f022ec823196d242464b6955a7e7e5015b719e76003f63f82d0f", - "sha256:926db372bc4ac1edf81cfb6c59e2a881606b409ddc0d0920b988174b2e2a767f", - "sha256:95d79ada05005f6f4f337d3bb9de8a7774f259341c70bc88047a1f7b96a4bcb2", - "sha256:95de7dc7dc47a312f6feddd3da2500826defdccbc41608d0031276a24181a2c0", - "sha256:a0882323e0ca4245eb0a3d0a74f88ce581cc33aedcfa396e415e5bba7bf05f68", - "sha256:a8365b942f9c1a7d0f0dc974747d99dd0a0cdfc5949a33119caf05cb314682d3", - "sha256:a8aae2fb3180940011b4862b2dd3756616841c53db9734b27bb93813cd79fce6", - "sha256:c237129f0e732885c9a6076a537e974160482eab8f10db6292e92154d4c67d71", - "sha256:c67b833dbccefe97cdd3f52798d430b9d3430396af7cdb2a0c32954c3ef73894", - "sha256:ce03305dd694c4873b9429274fd41fc7eb4e0e4dea07e0af97a933b079a5814f", - "sha256:d331afac87c92373826af83d2b2b435f57b17a5c74e6268b79355b970626e329", - "sha256:dada341ebb79619fe00a291185bba370c9803b1e1d7051610e01ed809ef3a4ba", - "sha256:ed2cc92af0efad20198638c69bb0fc2870a58dabfba6eb722c933b48556c686c", - "sha256:f260da502d7441a45695199b4e7fd8ca87db659ba1c78f2bbf31f934fe76ae0e", - "sha256:f2f390aa4da44454db40a1f0201401f9036e8d578a25f01a6e237cea238337ef", - "sha256:f76025acc8e2114bb664294a07ede0727aa75d63a06d2fae96bf29a81747e4a7" + "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d", + "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07", + "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df", + "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9", + "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d", + "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a", + "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719", + "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2", + "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280", + "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa", + "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387", + "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1", + "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43", + "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f", + "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398", + "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63", + "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de", + "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8", + "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481", + "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0", + "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d", + "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e", + "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96", + "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb", + "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6", + "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d", + "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a", + "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135" ], "index": "pypi", - "version": "==1.23.4" + "version": "==1.23.5" }, "ocrmypdf": { "hashes": [ @@ -1107,6 +1116,37 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==0.1.0.post0" }, + "pywavelets": { + "hashes": [ + "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b", + "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4", + "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4", + "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6", + "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de", + "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c", + "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa", + "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93", + "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875", + "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd", + "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356", + "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1", + "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c", + "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2", + "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784", + "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4", + "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b", + "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc", + "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966", + "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e", + "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426", + "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c", + "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202", + "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc", + "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, "pyyaml": { "hashes": [ "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", @@ -1265,6 +1305,7 @@ "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880" ], "index": "pypi", + "markers": null, "version": "==4.3.4" }, "regex": { @@ -1537,11 +1578,11 @@ }, "setuptools": { "hashes": [ - "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31", - "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f" + "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840", + "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d" ], "markers": "python_version >= '3.7'", - "version": "==65.5.1" + "version": "==65.6.0" }, "six": { "hashes": [ @@ -1663,11 +1704,12 @@ "standard" ], "hashes": [ - "sha256:cc277f7e73435748e69e075a721841f7c4a95dba06d12a72fe9874acced16f6f", - "sha256:cf538f3018536edb1f4a826311137ab4944ed741d52aeb98846f52215de57f25" + "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8", + "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd" ], "index": "pypi", - "version": "==0.19.0" + "markers": null, + "version": "==0.20.0" }, "uvloop": { "hashes": [ @@ -1951,49 +1993,45 @@ }, "zope.interface": { "hashes": [ - "sha256:026e7da51147910435950a46c55159d68af319f6e909f14873d35d411f4961db", - "sha256:061a41a3f96f076686d7f1cb87f3deec6f0c9f0325dcc054ac7b504ae9bb0d82", - "sha256:0eda7f61da6606a28b5efa5d8ad79b4b5bb242488e53a58993b2ec46c924ffee", - "sha256:13a7c6e3df8aa453583412de5725bf761217d06f66ff4ed776d44fbcd13ec4e4", - "sha256:185f0faf6c3d8f2203e8755f7ca16b8964d97da0abde89c367177a04e36f2568", - "sha256:2204a9d545fdbe0d9b0bf4d5e2fc67e7977de59666f7131c1433fde292fc3b41", - "sha256:27c53aa2f46d42940ccdcb015fd525a42bf73f94acd886296794a41f229d5946", - "sha256:3c293c5c0e1cabe59c33e0d02fcee5c3eb365f79a20b8199a26ca784e406bd0d", - "sha256:3e42b1c3f4fd863323a8275c52c78681281a8f2e1790f0e869d911c1c7b25c46", - "sha256:3e5540b7d703774fd171b7a7dc2a3cb70e98fc273b8b260b1bf2f7d3928f125b", - "sha256:4477930451521ac7da97cc31d49f7b83086d5ae76e52baf16aac659053119f6d", - "sha256:475b6e371cdbeb024f2302e826222bdc202186531f6dc095e8986c034e4b7961", - "sha256:489c4c46fcbd9364f60ff0dcb93ec9026eca64b2f43dc3b05d0724092f205e27", - "sha256:509a8d39b64a5e8d473f3f3db981f3ca603d27d2bc023c482605c1b52ec15662", - "sha256:58331d2766e8e409360154d3178449d116220348d46386430097e63d02a1b6d2", - "sha256:59a96d499ff6faa9b85b1309f50bf3744eb786e24833f7b500cbb7052dc4ae29", - "sha256:6cb8f9a1db47017929634264b3fc7ea4c1a42a3e28d67a14f14aa7b71deaa0d2", - "sha256:6d678475fdeb11394dc9aaa5c564213a1567cc663082e0ee85d52f78d1fbaab2", - "sha256:72a93445937cc71f0b8372b0c9e7c185328e0db5e94d06383a1cb56705df1df4", - "sha256:76cf472c79d15dce5f438a4905a1309be57d2d01bc1de2de30bda61972a79ab4", - "sha256:7b4547a2f624a537e90fb99cec4d8b3b6be4af3f449c3477155aae65396724ad", - "sha256:7f2e4ebe0a000c5727ee04227cf0ff5ae612fe599f88d494216e695b1dac744d", - "sha256:8343536ea4ee15d6525e3e726bb49ffc3f2034f828a49237a36be96842c06e7c", - "sha256:8de7bde839d72d96e0c92e8d1fdb4862e89b8fc52514d14b101ca317d9bcf87c", - "sha256:90f611d4cdf82fb28837fe15c3940255755572a4edf4c72e2306dbce7dcb3092", - "sha256:9ad58724fabb429d1ebb6f334361f0a3b35f96be0e74bfca6f7de8530688b2df", - "sha256:a1393229c9c126dd1b4356338421e8882347347ab6fe3230cb7044edc813e424", - "sha256:a20fc9cccbda2a28e8db8cabf2f47fead7e9e49d317547af6bf86a7269e4b9a1", - "sha256:a69f6d8b639f2317ba54278b64fef51d8250ad2c87acac1408b9cc461e4d6bb6", - "sha256:a6f51ffbdcf865f140f55c484001415505f5e68eb0a9eab1d37d0743b503b423", - "sha256:c9552ee9e123b7997c7630fb95c466ee816d19e721c67e4da35351c5f4032726", - "sha256:cd423d49abcf0ebf02c29c3daffe246ff756addb891f8aab717b3a4e2e1fd675", - "sha256:d0587d238b7867544134f4dcca19328371b8fd03fc2c56d15786f410792d0a68", - "sha256:d1f2d91c9c6cd54d750fa34f18bd73c71b372d0e6d06843bc7a5f21f5fd66fe0", - "sha256:d2f2ec42fbc21e1af5f129ec295e29fee6f93563e6388656975caebc5f851561", - "sha256:d743b03a72fefed807a4512c079fb1aa5e7777036cc7a4b6ff79ae4650a14f73", - "sha256:dd4b9251e95020c3d5d104b528dbf53629d09c146ce9c8dfaaf8f619ae1cce35", - "sha256:e4988d94962f517f6da2d52337170b84856905b31b7dc504ed9c7b7e4bab2fc3", - "sha256:e6a923d2dec50f2b4d41ce198af3516517f2e458220942cf393839d2f9e22000", - "sha256:e8c8764226daad39004b7873c3880eb4860c594ff549ea47c045cdf313e1bad5" + "sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32", + "sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0", + "sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c", + "sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c", + "sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d", + "sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf", + "sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b", + "sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc", + "sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f", + "sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d", + "sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e", + "sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16", + "sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f", + "sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9", + "sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296", + "sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a", + "sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d", + "sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d", + "sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189", + "sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4", + "sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452", + "sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a", + "sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0", + "sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5", + "sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671", + "sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e", + "sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f", + "sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396", + "sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7", + "sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b", + "sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf", + "sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f", + "sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6", + "sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188", + "sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7", + "sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.5.1" + "version": "==5.5.2" } }, "develop": { @@ -2174,11 +2212,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a", - "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2" + "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828", + "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec" ], "markers": "python_version < '3.11'", - "version": "==1.0.1" + "version": "==1.0.4" }, "execnet": { "hashes": [ @@ -2198,11 +2236,11 @@ }, "faker": { "hashes": [ - "sha256:4a3465624515a6807e8aa7e8eeb85bdd86a2fa53de4e258892dd6be95362462e", - "sha256:b9dd2fd9a9ac68a4e0c5040cd9e9bfaa099fa8dd15bae5f01f224a45431818d5" + "sha256:0094fe3340ad73c490d3ffccc59cc171b161acfccccd52925c70970ba23e6d6b", + "sha256:43da04aae745018e8bded768e74c84423d9dc38e4c498a53439e749d90e20bc0" ], "markers": "python_version >= '3.7'", - "version": "==15.3.1" + "version": "==15.3.2" }, "filelock": { "hashes": [ @@ -2214,11 +2252,11 @@ }, "identify": { "hashes": [ - "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440", - "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58" + "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f", + "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d" ], "markers": "python_version >= '3.7'", - "version": "==2.5.8" + "version": "==2.5.9" }, "idna": { "hashes": [ @@ -2548,11 +2586,11 @@ }, "setuptools": { "hashes": [ - "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31", - "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f" + "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840", + "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d" ], "markers": "python_version >= '3.7'", - "version": "==65.5.1" + "version": "==65.6.0" }, "six": { "hashes": [ @@ -2643,11 +2681,11 @@ }, "termcolor": { "hashes": [ - "sha256:91dd04fdf661b89d7169cefd35f609b19ca931eb033687eaa647cef1ff177c49", - "sha256:b80df54667ce4f48c03fe35df194f052dc27a541ebbf2544e4d6b47b5d6949c4" + "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b", + "sha256:fa852e957f97252205e105dd55bbc23b419a70fec0085708fc0515e399f304fd" ], "markers": "python_version >= '3.7'", - "version": "==2.1.0" + "version": "==2.1.1" }, "toml": { "hashes": [ @@ -2684,11 +2722,11 @@ }, "tox": { "hashes": [ - "sha256:89e4bc6df3854e9fc5582462e328dd3660d7d865ba625ae5881bbc63836a6324", - "sha256:d2c945f02a03d4501374a3d5430877380deb69b218b1df9b7f1d2f2a10befaf9" + "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04", + "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84" ], "index": "pypi", - "version": "==3.27.0" + "version": "==3.27.1" }, "typing-extensions": { "hashes": [ diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 5cd614197..892d1feb7 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -2,7 +2,6 @@ import datetime import os from unittest import mock -import pytest from django.test import TestCase from documents.parsers import ParseError from paperless_mail.parsers import MailDocumentParser diff --git a/src/paperless_mail/tests/test_parsers_live.py b/src/paperless_mail/tests/test_parsers_live.py index 9452a1186..ce3cfd3a3 100644 --- a/src/paperless_mail/tests/test_parsers_live.py +++ b/src/paperless_mail/tests/test_parsers_live.py @@ -1,4 +1,3 @@ -import hashlib import os from unittest import mock from urllib.error import HTTPError @@ -8,8 +7,10 @@ import pytest from django.test import TestCase from documents.parsers import ParseError from documents.parsers import run_convert +from imagehash import average_hash from paperless_mail.parsers import MailDocumentParser from pdfminer.high_level import extract_text +from PIL import Image class TestParserLive(TestCase): @@ -22,16 +23,8 @@ class TestParserLive(TestCase): self.parser.cleanup() @staticmethod - def hashfile(file): - buf_size = 65536 # An arbitrary (but fixed) buffer - sha256 = hashlib.sha256() - with open(file, "rb") as f: - while True: - data = f.read(buf_size) - if not data: - break - sha256.update(data) - return sha256.hexdigest() + def imagehash(file, hash_size=18): + return f"{average_hash(Image.open(file), hash_size)}" # Only run if convert is available @pytest.mark.skipif( @@ -53,8 +46,8 @@ class TestParserLive(TestCase): expected = os.path.join(self.SAMPLE_FILES, "simple_text.eml.pdf.webp") self.assertEqual( - self.hashfile(thumb), - self.hashfile(expected), + self.imagehash(thumb), + self.imagehash(expected), f"Created Thumbnail {thumb} differs from expected file {expected}", ) @@ -158,10 +151,10 @@ class TestParserLive(TestCase): logging_group=None, ) self.assertTrue(os.path.isfile(converted)) - thumb_hash = self.hashfile(converted) + thumb_hash = self.imagehash(converted) # The created pdf is not reproducible. But the converted image should always look the same. - expected_hash = self.hashfile( + expected_hash = self.imagehash( os.path.join(self.SAMPLE_FILES, "html.eml.pdf.webp"), ) self.assertEqual( @@ -244,10 +237,10 @@ class TestParserLive(TestCase): logging_group=None, ) self.assertTrue(os.path.isfile(converted)) - thumb_hash = self.hashfile(converted) + thumb_hash = self.imagehash(converted) # The created pdf is not reproducible. But the converted image should always look the same. - expected_hash = self.hashfile( + expected_hash = self.imagehash( os.path.join(self.SAMPLE_FILES, "sample.html.pdf.webp"), ) From df101f5e7a9eb97521d752e9755806a510e12d89 Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 16:09:46 +0100 Subject: [PATCH 48/60] split handle_message function --- src/paperless_mail/mail.py | 248 +++++++++++++++++++++---------------- 1 file changed, 142 insertions(+), 106 deletions(-) diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 6f14f51ca..d4a6703c6 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -351,11 +351,15 @@ class MailAccountHandler(LoggingMixin): return total_processed_files def handle_message(self, message, rule: MailRule) -> int: + processed_elements = 0 + + # Skip Message handling when only attachments are to be processed but + # message doesn't have any. if ( not message.attachments and rule.consumption_scope == MailRule.ConsumptionScope.ATTACHMENTS_ONLY ): - return 0 + return processed_elements self.log( "debug", @@ -368,130 +372,162 @@ class MailAccountHandler(LoggingMixin): tag_ids = [tag.id for tag in rule.assign_tags.all()] doc_type = rule.assign_document_type - processed_attachments = 0 - if ( rule.consumption_scope == MailRule.ConsumptionScope.EML_ONLY or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING ): - os.makedirs(settings.SCRATCH_DIR, exist_ok=True) - _, temp_filename = tempfile.mkstemp( - prefix="paperless-mail-", - dir=settings.SCRATCH_DIR, - suffix=".eml", + processed_elements += self.process_eml( + message, + rule, + correspondent, + tag_ids, + doc_type, ) - with open(temp_filename, "wb") as f: - # Move "From"-header to beginning of file - # TODO: This ugly workaround is needed because the parser is - # chosen only by the mime_type detected via magic - # (see documents/consumer.py "mime_type = magic.from_file") - # Unfortunately magic sometimes fails to detect the mime - # type of .eml files correctly as message/rfc822 and instead - # detects text/plain. - # This also effects direct file consumption of .eml files - # which are not treated with this workaround. - from_element = None - for i, header in enumerate(message.obj._headers): - if header[0] == "From": - from_element = i - if from_element: - new_headers = [message.obj._headers.pop(from_element)] - new_headers += message.obj._headers - message.obj._headers = new_headers - - f.write(message.obj.as_bytes()) - - self.log( - "info", - f"Rule {rule}: " - f"Consuming eml from mail " - f"{message.subject} from {message.from_}", - ) - - consume_file.delay( - path=temp_filename, - override_filename=pathvalidate.sanitize_filename( - message.subject + ".eml", - ), - override_title=message.subject, - override_correspondent_id=correspondent.id if correspondent else None, - override_document_type_id=doc_type.id if doc_type else None, - override_tag_ids=tag_ids, - ) - processed_attachments += 1 if ( rule.consumption_scope == MailRule.ConsumptionScope.ATTACHMENTS_ONLY or rule.consumption_scope == MailRule.ConsumptionScope.EVERYTHING ): - for att in message.attachments: + processed_elements += self.process_attachments( + message, + rule, + correspondent, + tag_ids, + doc_type, + ) - if ( - not att.content_disposition == "attachment" - and rule.attachment_type - == MailRule.AttachmentProcessing.ATTACHMENTS_ONLY + return processed_elements + + def process_attachments( + self, + message: MailMessage, + rule: MailRule, + correspondent, + tag_ids, + doc_type, + ): + processed_attachments = 0 + for att in message.attachments: + + if ( + not att.content_disposition == "attachment" + and rule.attachment_type + == MailRule.AttachmentProcessing.ATTACHMENTS_ONLY + ): + self.log( + "debug", + f"Rule {rule}: " + f"Skipping attachment {att.filename} " + f"with content disposition {att.content_disposition}", + ) + continue + + if 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(), ): - self.log( - "debug", - f"Rule {rule}: " - f"Skipping attachment {att.filename} " - f"with content disposition {att.content_disposition}", - ) continue - if 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) - title = self.get_title(message, att, rule) + # don't trust the content type of the attachment. Could be + # generic application/octet-stream. + mime_type = magic.from_buffer(att.payload, mime=True) - # don't trust the content type of the attachment. Could be - # generic application/octet-stream. - mime_type = magic.from_buffer(att.payload, mime=True) + if is_mime_type_supported(mime_type): - if is_mime_type_supported(mime_type): + os.makedirs(settings.SCRATCH_DIR, exist_ok=True) + _, temp_filename = tempfile.mkstemp( + prefix="paperless-mail-", + dir=settings.SCRATCH_DIR, + ) + with open(temp_filename, "wb") as f: + f.write(att.payload) - os.makedirs(settings.SCRATCH_DIR, exist_ok=True) - _, temp_filename = tempfile.mkstemp( - prefix="paperless-mail-", - dir=settings.SCRATCH_DIR, - ) - with open(temp_filename, "wb") as f: - f.write(att.payload) + self.log( + "info", + f"Rule {rule}: " + f"Consuming attachment {att.filename} from mail " + f"{message.subject} from {message.from_}", + ) - self.log( - "info", - f"Rule {rule}: " - f"Consuming attachment {att.filename} from mail " - f"{message.subject} from {message.from_}", - ) + consume_file.delay( + path=temp_filename, + override_filename=pathvalidate.sanitize_filename( + att.filename, + ), + override_title=title, + override_correspondent_id=correspondent.id + if correspondent + else None, + override_document_type_id=doc_type.id if doc_type else None, + override_tag_ids=tag_ids, + ) - consume_file.delay( - path=temp_filename, - override_filename=pathvalidate.sanitize_filename( - att.filename, - ), - override_title=title, - override_correspondent_id=correspondent.id - if correspondent - else None, - override_document_type_id=doc_type.id if doc_type else None, - override_tag_ids=tag_ids, - ) + processed_attachments += 1 + else: + self.log( + "debug", + f"Rule {rule}: " + f"Skipping attachment {att.filename} " + f"since guessed mime type {mime_type} is not supported " + f"by paperless", + ) - processed_attachments += 1 - else: - self.log( - "debug", - f"Rule {rule}: " - f"Skipping attachment {att.filename} " - f"since guessed mime type {mime_type} is not supported " - f"by paperless", - ) + def process_eml( + self, + message: MailMessage, + rule: MailRule, + correspondent, + tag_ids, + doc_type, + ): + os.makedirs(settings.SCRATCH_DIR, exist_ok=True) + _, temp_filename = tempfile.mkstemp( + prefix="paperless-mail-", + dir=settings.SCRATCH_DIR, + suffix=".eml", + ) + with open(temp_filename, "wb") as f: + # Move "From"-header to beginning of file + # TODO: This ugly workaround is needed because the parser is + # chosen only by the mime_type detected via magic + # (see documents/consumer.py "mime_type = magic.from_file") + # Unfortunately magic sometimes fails to detect the mime + # type of .eml files correctly as message/rfc822 and instead + # detects text/plain. + # This also effects direct file consumption of .eml files + # which are not treated with this workaround. + from_element = None + for i, header in enumerate(message.obj._headers): + if header[0] == "From": + from_element = i + if from_element: + new_headers = [message.obj._headers.pop(from_element)] + new_headers += message.obj._headers + message.obj._headers = new_headers - return processed_attachments + f.write(message.obj.as_bytes()) + + self.log( + "info", + f"Rule {rule}: " + f"Consuming eml from mail " + f"{message.subject} from {message.from_}", + ) + + consume_file.delay( + path=temp_filename, + override_filename=pathvalidate.sanitize_filename( + message.subject + ".eml", + ), + override_title=message.subject, + override_correspondent_id=correspondent.id if correspondent else None, + override_document_type_id=doc_type.id if doc_type else None, + override_tag_ids=tag_ids, + ) + processed_elements = 1 + return processed_elements From 85c41b79be2033bdec5226c9687a153d7f4edc06 Mon Sep 17 00:00:00 2001 From: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Date: Sun, 20 Nov 2022 08:02:06 -0800 Subject: [PATCH 49/60] Adds the new packages without updating other dependencies --- Pipfile.lock | 560 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 338 insertions(+), 222 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 009cf83f9..74f3a6a86 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a49c31147e62885e2c962cd0b19f18169d73aa4b89f9e1b27f61c996be3e3ea4" + "sha256": "548803b8c176073960d6fb5858949d1bb263b36f8811b2963d03a1a29ad65dd0" }, "pipfile-spec": 6, "requires": {}, @@ -126,7 +126,6 @@ "sha256:fafbd82934d30f8a004f81e8f7a062e31413a23d444be8ee3326553915958c6d" ], "index": "pypi", - "markers": null, "version": "==5.2.7" }, "certifi": { @@ -285,35 +284,36 @@ }, "cryptography": { "hashes": [ - "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d", - "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd", - "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146", - "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7", - "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436", - "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0", - "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828", - "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b", - "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55", - "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36", - "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50", - "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2", - "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a", - "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8", - "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0", - "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548", - "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320", - "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748", - "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249", - "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959", - "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f", - "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0", - "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd", - "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220", - "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c", - "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722" + "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a", + "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f", + "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0", + "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407", + "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7", + "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6", + "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153", + "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750", + "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad", + "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6", + "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b", + "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5", + "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a", + "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d", + "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d", + "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294", + "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0", + "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a", + "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac", + "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61", + "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013", + "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e", + "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb", + "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9", + "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd", + "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818" ], + "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==38.0.3" + "version": "==38.0.1" }, "daphne": { "hashes": [ @@ -549,14 +549,6 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, - "imagehash": { - "hashes": [ - "sha256:5ad9a5cde14fe255745a8245677293ac0d67f09c330986a351f34b614ba62fb5", - "sha256:7038d1b7f9e0585beb3dd8c0a956f02b95a346c0b5f24a9e8cc03ebadaf0aa70" - ], - "index": "pypi", - "version": "==4.3.1" - }, "imap-tools": { "hashes": [ "sha256:6f5572b2e747a81a607438e0ef61ff28f323f6697820493c8ca4467465baeb76", @@ -781,37 +773,37 @@ }, "numpy": { "hashes": [ - "sha256:01dd17cbb340bf0fc23981e52e1d18a9d4050792e8fb8363cecbf066a84b827d", - "sha256:06005a2ef6014e9956c09ba07654f9837d9e26696a0470e42beedadb78c11b07", - "sha256:09b7847f7e83ca37c6e627682f145856de331049013853f344f37b0c9690e3df", - "sha256:0aaee12d8883552fadfc41e96b4c82ee7d794949e2a7c3b3a7201e968c7ecab9", - "sha256:0cbe9848fad08baf71de1a39e12d1b6310f1d5b2d0ea4de051058e6e1076852d", - "sha256:1b1766d6f397c18153d40015ddfc79ddb715cabadc04d2d228d4e5a8bc4ded1a", - "sha256:33161613d2269025873025b33e879825ec7b1d831317e68f4f2f0f84ed14c719", - "sha256:5039f55555e1eab31124a5768898c9e22c25a65c1e0037f4d7c495a45778c9f2", - "sha256:522e26bbf6377e4d76403826ed689c295b0b238f46c28a7251ab94716da0b280", - "sha256:56e454c7833e94ec9769fa0f86e6ff8e42ee38ce0ce1fa4cbb747ea7e06d56aa", - "sha256:58f545efd1108e647604a1b5aa809591ccd2540f468a880bedb97247e72db387", - "sha256:5e05b1c973a9f858c74367553e236f287e749465f773328c8ef31abe18f691e1", - "sha256:7903ba8ab592b82014713c491f6c5d3a1cde5b4a3bf116404e08f5b52f6daf43", - "sha256:8969bfd28e85c81f3f94eb4a66bc2cf1dbdc5c18efc320af34bffc54d6b1e38f", - "sha256:92c8c1e89a1f5028a4c6d9e3ccbe311b6ba53694811269b992c0b224269e2398", - "sha256:9c88793f78fca17da0145455f0d7826bcb9f37da4764af27ac945488116efe63", - "sha256:a7ac231a08bb37f852849bbb387a20a57574a97cfc7b6cabb488a4fc8be176de", - "sha256:abdde9f795cf292fb9651ed48185503a2ff29be87770c3b8e2a14b0cd7aa16f8", - "sha256:af1da88f6bc3d2338ebbf0e22fe487821ea4d8e89053e25fa59d1d79786e7481", - "sha256:b2a9ab7c279c91974f756c84c365a669a887efa287365a8e2c418f8b3ba73fb0", - "sha256:bf837dc63ba5c06dc8797c398db1e223a466c7ece27a1f7b5232ba3466aafe3d", - "sha256:ca51fcfcc5f9354c45f400059e88bc09215fb71a48d3768fb80e357f3b457e1e", - "sha256:ce571367b6dfe60af04e04a1834ca2dc5f46004ac1cc756fb95319f64c095a96", - "sha256:d208a0f8729f3fb790ed18a003f3a57895b989b40ea4dce4717e9cf4af62c6bb", - "sha256:dbee87b469018961d1ad79b1a5d50c0ae850000b639bcb1b694e9981083243b6", - "sha256:e9f4c4e51567b616be64e05d517c79a8a22f3606499941d97bb76f2ca59f982d", - "sha256:f063b69b090c9d918f9df0a12116029e274daf0181df392839661c4c7ec9018a", - "sha256:f9a909a8bae284d46bbfdefbdd4a262ba19d3bc9921b1e76126b1d21c3c34135" + "sha256:0fe563fc8ed9dc4474cbf70742673fc4391d70f4363f917599a7fa99f042d5a8", + "sha256:12ac457b63ec8ded85d85c1e17d85efd3c2b0967ca39560b307a35a6703a4735", + "sha256:2341f4ab6dba0834b685cce16dad5f9b6606ea8a00e6da154f5dbded70fdc4dd", + "sha256:296d17aed51161dbad3c67ed6d164e51fcd18dbcd5dd4f9d0a9c6055dce30810", + "sha256:488a66cb667359534bc70028d653ba1cf307bae88eab5929cd707c761ff037db", + "sha256:4d52914c88b4930dafb6c48ba5115a96cbab40f45740239d9f4159c4ba779962", + "sha256:5e13030f8793e9ee42f9c7d5777465a560eb78fa7e11b1c053427f2ccab90c79", + "sha256:61be02e3bf810b60ab74e81d6d0d36246dbfb644a462458bb53b595791251911", + "sha256:7607b598217745cc40f751da38ffd03512d33ec06f3523fb0b5f82e09f6f676d", + "sha256:7a70a7d3ce4c0e9284e92285cba91a4a3f5214d87ee0e95928f3614a256a1488", + "sha256:7ab46e4e7ec63c8a5e6dbf5c1b9e1c92ba23a7ebecc86c336cb7bf3bd2fb10e5", + "sha256:8981d9b5619569899666170c7c9748920f4a5005bf79c72c07d08c8a035757b0", + "sha256:8c053d7557a8f022ec823196d242464b6955a7e7e5015b719e76003f63f82d0f", + "sha256:926db372bc4ac1edf81cfb6c59e2a881606b409ddc0d0920b988174b2e2a767f", + "sha256:95d79ada05005f6f4f337d3bb9de8a7774f259341c70bc88047a1f7b96a4bcb2", + "sha256:95de7dc7dc47a312f6feddd3da2500826defdccbc41608d0031276a24181a2c0", + "sha256:a0882323e0ca4245eb0a3d0a74f88ce581cc33aedcfa396e415e5bba7bf05f68", + "sha256:a8365b942f9c1a7d0f0dc974747d99dd0a0cdfc5949a33119caf05cb314682d3", + "sha256:a8aae2fb3180940011b4862b2dd3756616841c53db9734b27bb93813cd79fce6", + "sha256:c237129f0e732885c9a6076a537e974160482eab8f10db6292e92154d4c67d71", + "sha256:c67b833dbccefe97cdd3f52798d430b9d3430396af7cdb2a0c32954c3ef73894", + "sha256:ce03305dd694c4873b9429274fd41fc7eb4e0e4dea07e0af97a933b079a5814f", + "sha256:d331afac87c92373826af83d2b2b435f57b17a5c74e6268b79355b970626e329", + "sha256:dada341ebb79619fe00a291185bba370c9803b1e1d7051610e01ed809ef3a4ba", + "sha256:ed2cc92af0efad20198638c69bb0fc2870a58dabfba6eb722c933b48556c686c", + "sha256:f260da502d7441a45695199b4e7fd8ca87db659ba1c78f2bbf31f934fe76ae0e", + "sha256:f2f390aa4da44454db40a1f0201401f9036e8d578a25f01a6e237cea238337ef", + "sha256:f76025acc8e2114bb664294a07ede0727aa75d63a06d2fae96bf29a81747e4a7" ], "index": "pypi", - "version": "==1.23.5" + "version": "==1.23.4" }, "ocrmypdf": { "hashes": [ @@ -855,43 +847,43 @@ }, "pikepdf": { "hashes": [ - "sha256:0207442d9b943efec7eb07ea5b3f138d90cc61429a3c3243902ac909cb508fb2", - "sha256:0709832cc49ef51f004975e6e9bdc6daee8a8d68de621d428f13c95a83952e7f", - "sha256:07448689cc4c1e249e26ae694a2060210948e61035356cca3a5b8baf3a6a147d", - "sha256:0bbf45e702bb0556d705e1a4b5da391921e024cc1e6823675f2ea200acec1199", - "sha256:1376f3f4b1c34ac089a644af2e499ca713e4e4ec17035362350c0ec78ca6286e", - "sha256:15725f1bf572abb9a675f61874da66dc22144e74f374f7e8d023558e8c9c3f38", - "sha256:18383d8e6a620c52974b75034ad99c423f80468a434b52de456bb74d5ab51360", - "sha256:1dedbb95bb2c67d6923c91cdcd5a92703d10e4c4825d85cd7b8b474039978741", - "sha256:2035b39d2e5c97b6d9ede632f514403888e3f47d2b1e8b69b98420766ccb898b", - "sha256:2dd952e678dfc523f2c481c3d0a29b9823f07024f73dea7e9c03d2ac6592a61c", - "sha256:3d06dabf16592bb7975e1124000212c3c3bab1e97ed3f7c6534ea92efe9b621a", - "sha256:40999c3f48e5d0259662f6f708694d3ace43b01b4a2a197cfed5cf230557b116", - "sha256:46c7c9a7128d1751eedbe769dbc6c0a7983eccded74fd7d1e236d83a50c9cc58", - "sha256:529d4d099eecbdaa3e06490a032954ce96feae2596c1ea22f961dbf791444a3c", - "sha256:5887799a29510b53c7015b05d7276ee2e0f0ff1d782c75c3a3d1d9f68013665e", - "sha256:5c2de883986ef25e2e9b8ded8e5c285cb390950742164ce1bf116158009cd9c9", - "sha256:62a8c05876b9c7af4cad0ba9a8f22c77775bcceb118c35d682735955f5485297", - "sha256:6df4510606546c9c995afe3f799c506fe90798602b0628affffb7e1516fa1062", - "sha256:746897cbfc0c200de6be428a4e92dee72d0e03e1ff00d56006ee94fb59be199d", - "sha256:801100d8b4b885a203e76bc7266296f909944d621e6a0ac480fa2a0a0e0b1bb4", - "sha256:823f8b1cbad1182709d81afa32c23ac37b9c8ed33bdbc2b41f674be9420dc108", - "sha256:94057ca79525ba5eb5cc9c42337364f0e9e5f239887c0457dffb4ba3e6ac0187", - "sha256:9b1ba16cc5eb243c5e684c220752358a8e1e28a4e02ecdf2c3d24646f29c623f", - "sha256:9c9d75bb77dfe9b6f8915bc5339cfb0db427c3cb7cd75aa419b1da3c82f122ed", - "sha256:a0b78071d5fcd6b2288da469e89c030475095349dd57d82f5c40c37600d02e14", - "sha256:a1679c7d5b374895b6196784a75b8122ca0bb9248f5d97cd5ed77c569e264e88", - "sha256:ab8d610ca732a6369479605817cc55ee6f62d5b105ffe7e3749c3785c383631e", - "sha256:aff2ce52f0ab4ea8a1fbe57b06982b9fa9f997dd6bbec4b141091a1e71145a63", - "sha256:bc9b625f5ed454f445bf5012682b24d334adc9f853d41e44cfee7c52ddf92666", - "sha256:e0e66d49f8a85a4e0f915d42471643a5020bcdbef02586e49328ed417c13326a", - "sha256:e3dbcecc145d46d37738a407e0ddcce7cfb76d3e116ab3ba9c80f4dd14e71a3d", - "sha256:e99a90279a8254fa149d56cd307f94908c7844b2b8b42b61d241259804e40643", - "sha256:efc497cd01c55c5dbdd8a81766e317f44f728b3ceb65d7b6c6a064772c60e1c7", - "sha256:f35cecdab44cb01377e93a60a475bf4437854d98cb94379fcd65c6daa1c9a37e" + "sha256:053911bc19fbc987ff32e8e2836693480bef4c400785f700e8645729aecc7dae", + "sha256:0b10914b14667b7de19c9b27437cd798afee82690e16763965c7f7a2ab4da90a", + "sha256:0bed47afb90c78e2262e5b2e0d4722245b50c28bdaa317bdd39c4ef125d59a2c", + "sha256:0e51db2d246d277e3b11f3341b42c737deea60a49c3d8666388bff466fe1c4d0", + "sha256:3b9037377d9ca2b91c47b50ef9edb2fd295b0bf963ba14ee732da52006600677", + "sha256:475ef831d47e0aacd1bbfc366090c645bdc881bfdcf682f2a789860f645a1bf1", + "sha256:5233f8f1dee1bed95379c1b67af926725fe6d8ba210c587406f51a6bae651132", + "sha256:52fda5f8b730489a10cc492249465d52b4f190f7eb9b6c0cf80e49d5be3d8652", + "sha256:6129a5277f850ae6cdfd2cfff7e2c4a0b24618aee942a99763c84791dfbbd067", + "sha256:73281beedc1c234d50fe11f8beb55cc5ea2d43625ad7aa88dcdea54ef019939c", + "sha256:7e7ef427346a8324c32e9e2dbd6d10b0c9acaeb30d646223418206ff33594d7e", + "sha256:8338786c9912703dee8a501c1dd3b5e1065c22628cc4f781a548e378ae0f9f0c", + "sha256:8ba102fe535677c54c2e6cca65c9e3742ff7abb03934590a06c77a17c9e86827", + "sha256:91faf319bae1f7a9a0665b1622680296e6ec351497e8b3da1f7b83c8c2fbbc9c", + "sha256:93450025948474b656a6bef0f64c91f5d2f18b4234d7470c4bb8b77e8eb36dc4", + "sha256:972c6a21c9e49fc53a36b9550616b9240fc3291e07990db0e93d2ea4bfd7f5cd", + "sha256:9b403d7b24a09a090ebeb7760890c5386c515b86a6cc6d4f9c7b36eefe94b52f", + "sha256:9b94b0bb3adf6be2407aaca8693bb1589fa5021c019ac05b1a87db299feefebe", + "sha256:a0645e34902fe51205048e982f30e78e2b1fe767203988dd4b1395f6326cc4d9", + "sha256:a7e6f4ac418092a48dbd1809905edd5c6640f271c49981558a1c3da3753c0524", + "sha256:ae5aa1fd18373fd96b8ea6610d78d1c12a43e105291e45ceaaa54637e76a6929", + "sha256:b6066f3bcdef27e255be8f3b6e299ec4c04b50688872f6cabe8e27908574563c", + "sha256:ba4db2e76fe4292c3ea47e39acb72f417e7b31f4fc594c91426b88e6e01d4940", + "sha256:bb7d56298d74e307f0ace8d8e02eb9cd2a1421993542566eb605367aa4886d58", + "sha256:bf5b505b4ac50332eb550ae61cf64854f774f5ddb10339f5dd8b20cfa44fa8e9", + "sha256:c892bfd06b69ab26732daa53c2d85f864bff0a9422b0c03004cfb58797406b2a", + "sha256:c9c46bcae66c8ea181aa3fd961e98b46bc656e40cc33df9794c63a0c84d719b2", + "sha256:cff08868ee479ffbaa64fb425260ddce14bffbc21bdd2a3b3de941589abab741", + "sha256:dba7ad05626a0768708c1dfa11b28b8461c6750994159a06f2ac58e0045a9685", + "sha256:e089f720703d5e8c419634b08fad466d540d081005e41b6382afd7728d327029", + "sha256:f02c4f10a645f43bcde681b31d90f6313d8edce3480729cc6d78c5411f3e9772", + "sha256:f0ff1e4bfbafbeea902a0bfc23e8017443a3be485d02c92cd7dab9c16d50543c", + "sha256:f4539bc4a586ec8dce7ceba474be726ca64135c48ad61c47dfba98139a7aebfb", + "sha256:fa397d5ee36f357f1ba60103004c32befc9aa7e3143ef3a9fbf6e3686b2fed99" ], "index": "pypi", - "version": "==6.2.4" + "version": "==6.2.2" }, "pillow": { "hashes": [ @@ -1116,37 +1108,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==0.1.0.post0" }, - "pywavelets": { - "hashes": [ - "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b", - "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4", - "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4", - "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6", - "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de", - "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c", - "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa", - "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93", - "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875", - "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd", - "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356", - "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1", - "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c", - "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2", - "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784", - "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4", - "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b", - "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc", - "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966", - "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e", - "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426", - "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c", - "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202", - "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc", - "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd" - ], - "markers": "python_version >= '3.8'", - "version": "==1.4.1" - }, "pyyaml": { "hashes": [ "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", @@ -1305,7 +1266,6 @@ "sha256:ddf27071df4adf3821c4f2ca59d67525c3a82e5f268bed97b813cb4fabf87880" ], "index": "pypi", - "markers": null, "version": "==4.3.4" }, "regex": { @@ -1578,11 +1538,11 @@ }, "setuptools": { "hashes": [ - "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840", - "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d" + "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31", + "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f" ], "markers": "python_version >= '3.7'", - "version": "==65.6.0" + "version": "==65.5.1" }, "six": { "hashes": [ @@ -1672,7 +1632,7 @@ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" }, "tzdata": { @@ -1704,12 +1664,11 @@ "standard" ], "hashes": [ - "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8", - "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd" + "sha256:cc277f7e73435748e69e075a721841f7c4a95dba06d12a72fe9874acced16f6f", + "sha256:cf538f3018536edb1f4a826311137ab4944ed741d52aeb98846f52215de57f25" ], "index": "pypi", - "markers": null, - "version": "==0.20.0" + "version": "==0.19.0" }, "uvloop": { "hashes": [ @@ -1993,45 +1952,49 @@ }, "zope.interface": { "hashes": [ - "sha256:008b0b65c05993bb08912f644d140530e775cf1c62a072bf9340c2249e613c32", - "sha256:0217a9615531c83aeedb12e126611b1b1a3175013bbafe57c702ce40000eb9a0", - "sha256:0fb497c6b088818e3395e302e426850f8236d8d9f4ef5b2836feae812a8f699c", - "sha256:17ebf6e0b1d07ed009738016abf0d0a0f80388e009d0ac6e0ead26fc162b3b9c", - "sha256:311196634bb9333aa06f00fc94f59d3a9fddd2305c2c425d86e406ddc6f2260d", - "sha256:3218ab1a7748327e08ef83cca63eea7cf20ea7e2ebcb2522072896e5e2fceedf", - "sha256:404d1e284eda9e233c90128697c71acffd55e183d70628aa0bbb0e7a3084ed8b", - "sha256:4087e253bd3bbbc3e615ecd0b6dd03c4e6a1e46d152d3be6d2ad08fbad742dcc", - "sha256:40f4065745e2c2fa0dff0e7ccd7c166a8ac9748974f960cd39f63d2c19f9231f", - "sha256:5334e2ef60d3d9439c08baedaf8b84dc9bb9522d0dacbc10572ef5609ef8db6d", - "sha256:604cdba8f1983d0ab78edc29aa71c8df0ada06fb147cea436dc37093a0100a4e", - "sha256:6373d7eb813a143cb7795d3e42bd8ed857c82a90571567e681e1b3841a390d16", - "sha256:655796a906fa3ca67273011c9805c1e1baa047781fca80feeb710328cdbed87f", - "sha256:65c3c06afee96c654e590e046c4a24559e65b0a87dbff256cd4bd6f77e1a33f9", - "sha256:696f3d5493eae7359887da55c2afa05acc3db5fc625c49529e84bd9992313296", - "sha256:6e972493cdfe4ad0411fd9abfab7d4d800a7317a93928217f1a5de2bb0f0d87a", - "sha256:7579960be23d1fddecb53898035a0d112ac858c3554018ce615cefc03024e46d", - "sha256:765d703096ca47aa5d93044bf701b00bbce4d903a95b41fff7c3796e747b1f1d", - "sha256:7e66f60b0067a10dd289b29dceabd3d0e6d68be1504fc9d0bc209cf07f56d189", - "sha256:8a2ffadefd0e7206adc86e492ccc60395f7edb5680adedf17a7ee4205c530df4", - "sha256:959697ef2757406bff71467a09d940ca364e724c534efbf3786e86eee8591452", - "sha256:9d783213fab61832dbb10d385a319cb0e45451088abd45f95b5bb88ed0acca1a", - "sha256:a16025df73d24795a0bde05504911d306307c24a64187752685ff6ea23897cb0", - "sha256:a2ad597c8c9e038a5912ac3cf166f82926feff2f6e0dabdab956768de0a258f5", - "sha256:bfee1f3ff62143819499e348f5b8a7f3aa0259f9aca5e0ddae7391d059dce671", - "sha256:d169ccd0756c15bbb2f1acc012f5aab279dffc334d733ca0d9362c5beaebe88e", - "sha256:d514c269d1f9f5cd05ddfed15298d6c418129f3f064765295659798349c43e6f", - "sha256:d692374b578360d36568dd05efb8a5a67ab6d1878c29c582e37ddba80e66c396", - "sha256:dbaeb9cf0ea0b3bc4b36fae54a016933d64c6d52a94810a63c00f440ecb37dd7", - "sha256:dc26c8d44472e035d59d6f1177eb712888447f5799743da9c398b0339ed90b1b", - "sha256:e1574980b48c8c74f83578d1e77e701f8439a5d93f36a5a0af31337467c08fcf", - "sha256:e74a578172525c20d7223eac5f8ad187f10940dac06e40113d62f14f3adb1e8f", - "sha256:e945de62917acbf853ab968d8916290548df18dd62c739d862f359ecd25842a6", - "sha256:f0980d44b8aded808bec5059018d64692f0127f10510eca71f2f0ace8fb11188", - "sha256:f98d4bd7bbb15ca701d19b93263cc5edfd480c3475d163f137385f49e5b3a3a7", - "sha256:fb68d212efd057596dee9e6582daded9f8ef776538afdf5feceb3059df2d2e7b" + "sha256:026e7da51147910435950a46c55159d68af319f6e909f14873d35d411f4961db", + "sha256:061a41a3f96f076686d7f1cb87f3deec6f0c9f0325dcc054ac7b504ae9bb0d82", + "sha256:0eda7f61da6606a28b5efa5d8ad79b4b5bb242488e53a58993b2ec46c924ffee", + "sha256:13a7c6e3df8aa453583412de5725bf761217d06f66ff4ed776d44fbcd13ec4e4", + "sha256:185f0faf6c3d8f2203e8755f7ca16b8964d97da0abde89c367177a04e36f2568", + "sha256:2204a9d545fdbe0d9b0bf4d5e2fc67e7977de59666f7131c1433fde292fc3b41", + "sha256:27c53aa2f46d42940ccdcb015fd525a42bf73f94acd886296794a41f229d5946", + "sha256:3c293c5c0e1cabe59c33e0d02fcee5c3eb365f79a20b8199a26ca784e406bd0d", + "sha256:3e42b1c3f4fd863323a8275c52c78681281a8f2e1790f0e869d911c1c7b25c46", + "sha256:3e5540b7d703774fd171b7a7dc2a3cb70e98fc273b8b260b1bf2f7d3928f125b", + "sha256:4477930451521ac7da97cc31d49f7b83086d5ae76e52baf16aac659053119f6d", + "sha256:475b6e371cdbeb024f2302e826222bdc202186531f6dc095e8986c034e4b7961", + "sha256:489c4c46fcbd9364f60ff0dcb93ec9026eca64b2f43dc3b05d0724092f205e27", + "sha256:509a8d39b64a5e8d473f3f3db981f3ca603d27d2bc023c482605c1b52ec15662", + "sha256:58331d2766e8e409360154d3178449d116220348d46386430097e63d02a1b6d2", + "sha256:59a96d499ff6faa9b85b1309f50bf3744eb786e24833f7b500cbb7052dc4ae29", + "sha256:6cb8f9a1db47017929634264b3fc7ea4c1a42a3e28d67a14f14aa7b71deaa0d2", + "sha256:6d678475fdeb11394dc9aaa5c564213a1567cc663082e0ee85d52f78d1fbaab2", + "sha256:72a93445937cc71f0b8372b0c9e7c185328e0db5e94d06383a1cb56705df1df4", + "sha256:76cf472c79d15dce5f438a4905a1309be57d2d01bc1de2de30bda61972a79ab4", + "sha256:7b4547a2f624a537e90fb99cec4d8b3b6be4af3f449c3477155aae65396724ad", + "sha256:7f2e4ebe0a000c5727ee04227cf0ff5ae612fe599f88d494216e695b1dac744d", + "sha256:8343536ea4ee15d6525e3e726bb49ffc3f2034f828a49237a36be96842c06e7c", + "sha256:8de7bde839d72d96e0c92e8d1fdb4862e89b8fc52514d14b101ca317d9bcf87c", + "sha256:90f611d4cdf82fb28837fe15c3940255755572a4edf4c72e2306dbce7dcb3092", + "sha256:9ad58724fabb429d1ebb6f334361f0a3b35f96be0e74bfca6f7de8530688b2df", + "sha256:a1393229c9c126dd1b4356338421e8882347347ab6fe3230cb7044edc813e424", + "sha256:a20fc9cccbda2a28e8db8cabf2f47fead7e9e49d317547af6bf86a7269e4b9a1", + "sha256:a69f6d8b639f2317ba54278b64fef51d8250ad2c87acac1408b9cc461e4d6bb6", + "sha256:a6f51ffbdcf865f140f55c484001415505f5e68eb0a9eab1d37d0743b503b423", + "sha256:c9552ee9e123b7997c7630fb95c466ee816d19e721c67e4da35351c5f4032726", + "sha256:cd423d49abcf0ebf02c29c3daffe246ff756addb891f8aab717b3a4e2e1fd675", + "sha256:d0587d238b7867544134f4dcca19328371b8fd03fc2c56d15786f410792d0a68", + "sha256:d1f2d91c9c6cd54d750fa34f18bd73c71b372d0e6d06843bc7a5f21f5fd66fe0", + "sha256:d2f2ec42fbc21e1af5f129ec295e29fee6f93563e6388656975caebc5f851561", + "sha256:d743b03a72fefed807a4512c079fb1aa5e7777036cc7a4b6ff79ae4650a14f73", + "sha256:dd4b9251e95020c3d5d104b528dbf53629d09c146ce9c8dfaaf8f619ae1cce35", + "sha256:e4988d94962f517f6da2d52337170b84856905b31b7dc504ed9c7b7e4bab2fc3", + "sha256:e6a923d2dec50f2b4d41ce198af3516517f2e458220942cf393839d2f9e22000", + "sha256:e8c8764226daad39004b7873c3880eb4860c594ff549ea47c045cdf313e1bad5" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.5.2" + "version": "==5.5.1" } }, "develop": { @@ -2212,11 +2175,11 @@ }, "exceptiongroup": { "hashes": [ - "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828", - "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec" + "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a", + "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2" ], "markers": "python_version < '3.11'", - "version": "==1.0.4" + "version": "==1.0.1" }, "execnet": { "hashes": [ @@ -2236,11 +2199,11 @@ }, "faker": { "hashes": [ - "sha256:0094fe3340ad73c490d3ffccc59cc171b161acfccccd52925c70970ba23e6d6b", - "sha256:43da04aae745018e8bded768e74c84423d9dc38e4c498a53439e749d90e20bc0" + "sha256:4a3465624515a6807e8aa7e8eeb85bdd86a2fa53de4e258892dd6be95362462e", + "sha256:b9dd2fd9a9ac68a4e0c5040cd9e9bfaa099fa8dd15bae5f01f224a45431818d5" ], "markers": "python_version >= '3.7'", - "version": "==15.3.2" + "version": "==15.3.1" }, "filelock": { "hashes": [ @@ -2252,11 +2215,11 @@ }, "identify": { "hashes": [ - "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f", - "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d" + "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440", + "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58" ], "markers": "python_version >= '3.7'", - "version": "==2.5.9" + "version": "==2.5.8" }, "idna": { "hashes": [ @@ -2266,6 +2229,14 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, + "imagehash": { + "hashes": [ + "sha256:5ad9a5cde14fe255745a8245677293ac0d67f09c330986a351f34b614ba62fb5", + "sha256:7038d1b7f9e0585beb3dd8c0a956f02b95a346c0b5f24a9e8cc03ebadaf0aa70" + ], + "index": "pypi", + "version": "==4.3.1" + }, "imagesize": { "hashes": [ "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", @@ -2274,14 +2245,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.1" }, - "importlib-metadata": { - "hashes": [ - "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab", - "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43" - ], - "markers": "python_version < '3.10'", - "version": "==5.0.0" - }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -2396,6 +2359,40 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==1.7.0" }, + "numpy": { + "hashes": [ + "sha256:0fe563fc8ed9dc4474cbf70742673fc4391d70f4363f917599a7fa99f042d5a8", + "sha256:12ac457b63ec8ded85d85c1e17d85efd3c2b0967ca39560b307a35a6703a4735", + "sha256:2341f4ab6dba0834b685cce16dad5f9b6606ea8a00e6da154f5dbded70fdc4dd", + "sha256:296d17aed51161dbad3c67ed6d164e51fcd18dbcd5dd4f9d0a9c6055dce30810", + "sha256:488a66cb667359534bc70028d653ba1cf307bae88eab5929cd707c761ff037db", + "sha256:4d52914c88b4930dafb6c48ba5115a96cbab40f45740239d9f4159c4ba779962", + "sha256:5e13030f8793e9ee42f9c7d5777465a560eb78fa7e11b1c053427f2ccab90c79", + "sha256:61be02e3bf810b60ab74e81d6d0d36246dbfb644a462458bb53b595791251911", + "sha256:7607b598217745cc40f751da38ffd03512d33ec06f3523fb0b5f82e09f6f676d", + "sha256:7a70a7d3ce4c0e9284e92285cba91a4a3f5214d87ee0e95928f3614a256a1488", + "sha256:7ab46e4e7ec63c8a5e6dbf5c1b9e1c92ba23a7ebecc86c336cb7bf3bd2fb10e5", + "sha256:8981d9b5619569899666170c7c9748920f4a5005bf79c72c07d08c8a035757b0", + "sha256:8c053d7557a8f022ec823196d242464b6955a7e7e5015b719e76003f63f82d0f", + "sha256:926db372bc4ac1edf81cfb6c59e2a881606b409ddc0d0920b988174b2e2a767f", + "sha256:95d79ada05005f6f4f337d3bb9de8a7774f259341c70bc88047a1f7b96a4bcb2", + "sha256:95de7dc7dc47a312f6feddd3da2500826defdccbc41608d0031276a24181a2c0", + "sha256:a0882323e0ca4245eb0a3d0a74f88ce581cc33aedcfa396e415e5bba7bf05f68", + "sha256:a8365b942f9c1a7d0f0dc974747d99dd0a0cdfc5949a33119caf05cb314682d3", + "sha256:a8aae2fb3180940011b4862b2dd3756616841c53db9734b27bb93813cd79fce6", + "sha256:c237129f0e732885c9a6076a537e974160482eab8f10db6292e92154d4c67d71", + "sha256:c67b833dbccefe97cdd3f52798d430b9d3430396af7cdb2a0c32954c3ef73894", + "sha256:ce03305dd694c4873b9429274fd41fc7eb4e0e4dea07e0af97a933b079a5814f", + "sha256:d331afac87c92373826af83d2b2b435f57b17a5c74e6268b79355b970626e329", + "sha256:dada341ebb79619fe00a291185bba370c9803b1e1d7051610e01ed809ef3a4ba", + "sha256:ed2cc92af0efad20198638c69bb0fc2870a58dabfba6eb722c933b48556c686c", + "sha256:f260da502d7441a45695199b4e7fd8ca87db659ba1c78f2bbf31f934fe76ae0e", + "sha256:f2f390aa4da44454db40a1f0201401f9036e8d578a25f01a6e237cea238337ef", + "sha256:f76025acc8e2114bb664294a07ede0727aa75d63a06d2fae96bf29a81747e4a7" + ], + "index": "pypi", + "version": "==1.23.4" + }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", @@ -2406,19 +2403,86 @@ }, "pathspec": { "hashes": [ - "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5", - "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0" + "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93", + "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d" ], "markers": "python_version >= '3.7'", - "version": "==0.10.2" + "version": "==0.10.1" + }, + "pillow": { + "hashes": [ + "sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040", + "sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8", + "sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65", + "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2", + "sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627", + "sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07", + "sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef", + "sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535", + "sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c", + "sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc", + "sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3", + "sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1", + "sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c", + "sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa", + "sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32", + "sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502", + "sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4", + "sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f", + "sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812", + "sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636", + "sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20", + "sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c", + "sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91", + "sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe", + "sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b", + "sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad", + "sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9", + "sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72", + "sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4", + "sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de", + "sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29", + "sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee", + "sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c", + "sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7", + "sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11", + "sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c", + "sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c", + "sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448", + "sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b", + "sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20", + "sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228", + "sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd", + "sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699", + "sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b", + "sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2", + "sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4", + "sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c", + "sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f", + "sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2", + "sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c", + "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3", + "sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193", + "sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48", + "sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02", + "sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8", + "sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e", + "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f", + "sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b", + "sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74", + "sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb", + "sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0" + ], + "index": "pypi", + "version": "==9.3.0" }, "platformdirs": { "hashes": [ - "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7", - "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10" + "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb", + "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0" ], "markers": "python_version >= '3.7'", - "version": "==2.5.4" + "version": "==2.5.3" }, "pluggy": { "hashes": [ @@ -2531,6 +2595,37 @@ ], "version": "==2022.6" }, + "pywavelets": { + "hashes": [ + "sha256:030670a213ee8fefa56f6387b0c8e7d970c7f7ad6850dc048bd7c89364771b9b", + "sha256:058b46434eac4c04dd89aeef6fa39e4b6496a951d78c500b6641fd5b2cc2f9f4", + "sha256:231b0e0b1cdc1112f4af3c24eea7bf181c418d37922a67670e9bf6cfa2d544d4", + "sha256:23bafd60350b2b868076d976bdd92f950b3944f119b4754b1d7ff22b7acbf6c6", + "sha256:3f19327f2129fb7977bc59b966b4974dfd72879c093e44a7287500a7032695de", + "sha256:47cac4fa25bed76a45bc781a293c26ac63e8eaae9eb8f9be961758d22b58649c", + "sha256:578af438a02a86b70f1975b546f68aaaf38f28fb082a61ceb799816049ed18aa", + "sha256:6437af3ddf083118c26d8f97ab43b0724b956c9f958e9ea788659f6a2834ba93", + "sha256:64c6bac6204327321db30b775060fbe8e8642316e6bff17f06b9f34936f88875", + "sha256:67a0d28a08909f21400cb09ff62ba94c064882ffd9e3a6b27880a111211d59bd", + "sha256:71ab30f51ee4470741bb55fc6b197b4a2b612232e30f6ac069106f0156342356", + "sha256:7231461d7a8eb3bdc7aa2d97d9f67ea5a9f8902522818e7e2ead9c2b3408eeb1", + "sha256:754fa5085768227c4f4a26c1e0c78bc509a266d9ebd0eb69a278be7e3ece943c", + "sha256:7ab8d9db0fe549ab2ee0bea61f614e658dd2df419d5b75fba47baa761e95f8f2", + "sha256:875d4d620eee655346e3589a16a73790cf9f8917abba062234439b594e706784", + "sha256:88aa5449e109d8f5e7f0adef85f7f73b1ab086102865be64421a3a3d02d277f4", + "sha256:91d3d393cffa634f0e550d88c0e3f217c96cfb9e32781f2960876f1808d9b45b", + "sha256:9cb5ca8d11d3f98e89e65796a2125be98424d22e5ada360a0dbabff659fca0fc", + "sha256:ab7da0a17822cd2f6545626946d3b82d1a8e106afc4b50e3387719ba01c7b966", + "sha256:ad987748f60418d5f4138db89d82ba0cb49b086e0cbb8fd5c3ed4a814cfb705e", + "sha256:d0e56cd7a53aed3cceca91a04d62feb3a0aca6725b1912d29546c26f6ea90426", + "sha256:d854411eb5ee9cb4bc5d0e66e3634aeb8f594210f6a1bed96dbed57ec70f181c", + "sha256:da7b9c006171be1f9ddb12cc6e0d3d703b95f7f43cb5e2c6f5f15d3233fcf202", + "sha256:daf0aa79842b571308d7c31a9c43bc99a30b6328e6aea3f50388cd8f69ba7dbc", + "sha256:de7cd61a88a982edfec01ea755b0740e94766e00a1ceceeafef3ed4c85c605cd" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, "pyyaml": { "hashes": [ "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", @@ -2584,13 +2679,42 @@ "markers": "python_version >= '3.7' and python_version < '4'", "version": "==2.28.1" }, + "scipy": { + "hashes": [ + "sha256:02b567e722d62bddd4ac253dafb01ce7ed8742cf8031aea030a41414b86c1125", + "sha256:1166514aa3bbf04cb5941027c6e294a000bba0cf00f5cdac6c77f2dad479b434", + "sha256:1da52b45ce1a24a4a22db6c157c38b39885a990a566748fc904ec9f03ed8c6ba", + "sha256:23b22fbeef3807966ea42d8163322366dd89da9bebdc075da7034cee3a1441ca", + "sha256:28d2cab0c6ac5aa131cc5071a3a1d8e1366dad82288d9ec2ca44df78fb50e649", + "sha256:2ef0fbc8bcf102c1998c1f16f15befe7cffba90895d6e84861cd6c6a33fb54f6", + "sha256:3b69b90c9419884efeffaac2c38376d6ef566e6e730a231e15722b0ab58f0328", + "sha256:4b93ec6f4c3c4d041b26b5f179a6aab8f5045423117ae7a45ba9710301d7e462", + "sha256:4e53a55f6a4f22de01ffe1d2f016e30adedb67a699a310cdcac312806807ca81", + "sha256:6311e3ae9cc75f77c33076cb2794fb0606f14c8f1b1c9ff8ce6005ba2c283621", + "sha256:65b77f20202599c51eb2771d11a6b899b97989159b7975e9b5259594f1d35ef4", + "sha256:6cc6b33139eb63f30725d5f7fa175763dc2df6a8f38ddf8df971f7c345b652dc", + "sha256:70de2f11bf64ca9921fda018864c78af7147025e467ce9f4a11bc877266900a6", + "sha256:70ebc84134cf0c504ce6a5f12d6db92cb2a8a53a49437a6bb4edca0bc101f11c", + "sha256:83606129247e7610b58d0e1e93d2c5133959e9cf93555d3c27e536892f1ba1f2", + "sha256:93d07494a8900d55492401917a119948ed330b8c3f1d700e0b904a578f10ead4", + "sha256:9c4e3ae8a716c8b3151e16c05edb1daf4cb4d866caa385e861556aff41300c14", + "sha256:9dd4012ac599a1e7eb63c114d1eee1bcfc6dc75a29b589ff0ad0bb3d9412034f", + "sha256:9e3fb1b0e896f14a85aa9a28d5f755daaeeb54c897b746df7a55ccb02b340f33", + "sha256:a0aa8220b89b2e3748a2836fbfa116194378910f1a6e78e4675a095bcd2c762d", + "sha256:d3b3c8924252caaffc54d4a99f1360aeec001e61267595561089f8b5900821bb", + "sha256:e013aed00ed776d790be4cb32826adb72799c61e318676172495383ba4570aa4", + "sha256:f3e7a8867f307e3359cc0ed2c63b61a1e33a19080f92fe377bc7d49f646f2ec1" + ], + "index": "pypi", + "version": "==1.8.1" + }, "setuptools": { "hashes": [ - "sha256:6211d2f5eddad8757bd0484923ca7c0a6302ebc4ab32ea5e94357176e0ca0840", - "sha256:d1eebf881c6114e51df1664bc2c9133d022f78d12d5f4f665b9191f084e2862d" + "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31", + "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f" ], "markers": "python_version >= '3.7'", - "version": "==65.6.0" + "version": "==65.5.1" }, "six": { "hashes": [ @@ -2681,11 +2805,11 @@ }, "termcolor": { "hashes": [ - "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b", - "sha256:fa852e957f97252205e105dd55bbc23b419a70fec0085708fc0515e399f304fd" + "sha256:91dd04fdf661b89d7169cefd35f609b19ca931eb033687eaa647cef1ff177c49", + "sha256:b80df54667ce4f48c03fe35df194f052dc27a541ebbf2544e4d6b47b5d6949c4" ], "markers": "python_version >= '3.7'", - "version": "==2.1.1" + "version": "==2.1.0" }, "toml": { "hashes": [ @@ -2722,18 +2846,18 @@ }, "tox": { "hashes": [ - "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04", - "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84" + "sha256:89e4bc6df3854e9fc5582462e328dd3660d7d865ba625ae5881bbc63836a6324", + "sha256:d2c945f02a03d4501374a3d5430877380deb69b218b1df9b7f1d2f2a10befaf9" ], "index": "pypi", - "version": "==3.27.1" + "version": "==3.27.0" }, "typing-extensions": { "hashes": [ "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa", "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e" ], - "markers": "python_version < '3.10'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" }, "urllib3": { @@ -2746,19 +2870,11 @@ }, "virtualenv": { "hashes": [ - "sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e", - "sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29" + "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108", + "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e" ], "markers": "python_version >= '3.6'", - "version": "==20.16.7" - }, - "zipp": { - "hashes": [ - "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1", - "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8" - ], - "markers": "python_version < '3.9'", - "version": "==3.10.0" + "version": "==20.16.6" } } } From 538a4219bd951a03c403133087954ac64d4b53a2 Mon Sep 17 00:00:00 2001 From: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Date: Sun, 20 Nov 2022 09:10:44 -0800 Subject: [PATCH 50/60] Fixes missing return --- src/paperless_mail/mail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index d4a6703c6..9ac03db6e 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -476,6 +476,7 @@ class MailAccountHandler(LoggingMixin): f"since guessed mime type {mime_type} is not supported " f"by paperless", ) + return processed_attachments def process_eml( self, From f6a70b85f4cff8d490884a2a11ba45f44c286a98 Mon Sep 17 00:00:00 2001 From: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Date: Sun, 20 Nov 2022 09:13:08 -0800 Subject: [PATCH 51/60] Use Django templating engine --- src/paperless_mail/parsers.py | 25 +++++++------------ .../email_msg_template.html} | 3 +++ .../{mail_template => templates}/input.css | 0 .../{mail_template => templates}/output.css | 0 .../package-lock.json | 0 .../{mail_template => templates}/package.json | 0 .../tailwind.config.js | 0 src/paperless_mail/tests/test_parsers.py | 5 ++-- 8 files changed, 15 insertions(+), 18 deletions(-) rename src/paperless_mail/{mail_template/index.html => templates/email_msg_template.html} (97%) rename src/paperless_mail/{mail_template => templates}/input.css (100%) rename src/paperless_mail/{mail_template => templates}/output.css (100%) rename src/paperless_mail/{mail_template => templates}/package-lock.json (100%) rename src/paperless_mail/{mail_template => templates}/package.json (100%) rename src/paperless_mail/{mail_template => templates}/tailwind.config.js (100%) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index b325b79d5..c9c4e7e90 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -229,21 +229,14 @@ class MailDocumentParser(DocumentParser): data["date"] = clean_html(mail.date.astimezone().strftime("%Y-%m-%d %H:%M")) data["content"] = clean_html(mail.text.strip()) - html_file = os.path.join(os.path.dirname(__file__), "mail_template/index.html") - placeholder_pattern = re.compile(r"{{(.+)}}") - html = StringIO() - with open(html_file) as html_template_handle: - for line in html_template_handle.readlines(): - for placeholder in placeholder_pattern.findall(line): - line = re.sub( - "{{" + placeholder + "}}", - data.get(placeholder.strip(), ""), - line, - ) - html.write(line) - html.seek(0) + from django.template.loader import render_to_string + + rendered = render_to_string("email_msg_template.html", context=data) + + html.write(rendered) + html.seek(0) return html @@ -252,12 +245,12 @@ class MailDocumentParser(DocumentParser): url = self.gotenberg_server + "/forms/chromium/convert/html" self.log("info", "Converting mail to PDF") - css_file = os.path.join(os.path.dirname(__file__), "mail_template/output.css") + css_file = os.path.join(os.path.dirname(__file__), "templates/output.css") with open(css_file, "rb") as css_handle: files = { - "html": ("index.html", self.mail_to_html(mail)), + "html": ("email_msg_template.html", self.mail_to_html(mail)), "css": ("output.css", css_handle), } headers = {} @@ -302,7 +295,7 @@ class MailDocumentParser(DocumentParser): files.append((name_clean, BytesIO(a.payload))) html_clean = html_clean.replace(name_cid, name_clean) - files.append(("index.html", StringIO(html_clean))) + files.append(("email_msg_template.html", StringIO(html_clean))) return files diff --git a/src/paperless_mail/mail_template/index.html b/src/paperless_mail/templates/email_msg_template.html similarity index 97% rename from src/paperless_mail/mail_template/index.html rename to src/paperless_mail/templates/email_msg_template.html index d801f57ef..a22666957 100644 --- a/src/paperless_mail/mail_template/index.html +++ b/src/paperless_mail/templates/email_msg_template.html @@ -1,3 +1,4 @@ +{% autoescape off %} @@ -43,3 +44,5 @@ + +{% endautoescape %} diff --git a/src/paperless_mail/mail_template/input.css b/src/paperless_mail/templates/input.css similarity index 100% rename from src/paperless_mail/mail_template/input.css rename to src/paperless_mail/templates/input.css diff --git a/src/paperless_mail/mail_template/output.css b/src/paperless_mail/templates/output.css similarity index 100% rename from src/paperless_mail/mail_template/output.css rename to src/paperless_mail/templates/output.css diff --git a/src/paperless_mail/mail_template/package-lock.json b/src/paperless_mail/templates/package-lock.json similarity index 100% rename from src/paperless_mail/mail_template/package-lock.json rename to src/paperless_mail/templates/package-lock.json diff --git a/src/paperless_mail/mail_template/package.json b/src/paperless_mail/templates/package.json similarity index 100% rename from src/paperless_mail/mail_template/package.json rename to src/paperless_mail/templates/package.json diff --git a/src/paperless_mail/mail_template/tailwind.config.js b/src/paperless_mail/templates/tailwind.config.js similarity index 100% rename from src/paperless_mail/mail_template/tailwind.config.js rename to src/paperless_mail/templates/tailwind.config.js diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 892d1feb7..6de97b30e 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -369,6 +369,7 @@ class TestParser(TestCase): os.path.join(self.SAMPLE_FILES, "html.eml.html"), ) as html_expected_handle: html_expected = html_expected_handle.read() + self.assertHTMLEqual(html_expected, html_received) @mock.patch("paperless_mail.parsers.requests.post") @@ -436,7 +437,7 @@ class TestParser(TestCase): result = self.parser.transform_inline_html(html, attachments) resulting_html = result[-1][1].read() - self.assertTrue(result[-1][0] == "index.html") + self.assertTrue(result[-1][0] == "email_msg_template.html") self.assertTrue(result[0][0] in resulting_html) self.assertFalse(" Date: Sun, 20 Nov 2022 09:15:06 -0800 Subject: [PATCH 52/60] Fixes one more place which used manual size formatting --- src/paperless_mail/parsers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index c9c4e7e90..bf67314cb 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -85,7 +85,8 @@ class MailDocumentParser(DocumentParser): "prefix": "", "key": "attachments", "value": ", ".join( - f"{attachment.filename}({(attachment.size / 1024):.2f} KiB)" + f"{attachment.filename}" + f"({format_size(attachment.size, binary=True)})" for attachment in mail.attachments ), }, From af8a6c3764659efa285b24ede4759aeb99eb6bcb Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 19:53:57 +0100 Subject: [PATCH 53/60] fix filenames --- src/paperless_mail/parsers.py | 4 ++-- src/paperless_mail/tests/test_parsers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index bf67314cb..8f8dd2d37 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -251,7 +251,7 @@ class MailDocumentParser(DocumentParser): with open(css_file, "rb") as css_handle: files = { - "html": ("email_msg_template.html", self.mail_to_html(mail)), + "html": ("index.html", self.mail_to_html(mail)), "css": ("output.css", css_handle), } headers = {} @@ -296,7 +296,7 @@ class MailDocumentParser(DocumentParser): files.append((name_clean, BytesIO(a.payload))) html_clean = html_clean.replace(name_cid, name_clean) - files.append(("email_msg_template.html", StringIO(html_clean))) + files.append(("index.html", StringIO(html_clean))) return files diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 6de97b30e..0d533b4f1 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -437,7 +437,7 @@ class TestParser(TestCase): result = self.parser.transform_inline_html(html, attachments) resulting_html = result[-1][1].read() - self.assertTrue(result[-1][0] == "email_msg_template.html") + self.assertTrue(result[-1][0] == "index.html") self.assertTrue(result[0][0] in resulting_html) self.assertFalse(" Date: Sun, 20 Nov 2022 20:12:41 +0100 Subject: [PATCH 54/60] minor test improvements --- src/paperless_mail/tests/test_parsers.py | 99 +++++++++++++----------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 0d533b4f1..315e82a1a 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -92,125 +92,130 @@ class TestParser(TestCase): "message/rfc822", ) - self.assertTrue( - {"namespace": "", "prefix": "", "key": "attachments", "value": ""} - in metadata, + self.assertIn( + {"namespace": "", "prefix": "", "key": "attachments", "value": ""}, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "", "key": "date", "value": "2022-10-12 21:40:43 UTC+02:00", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "content-language", "value": "en-US", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "content-type", "value": "text/plain; charset=UTF-8; format=flowed", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "date", "value": "Wed, 12 Oct 2022 21:40:43 +0200", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "delivered-to", "value": "mail@someserver.de", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "from", "value": "Some One ", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "message-id", "value": "<6e99e34d-e20a-80c4-ea61-d8234b612be9@someserver.de>", - } - in metadata, + }, + metadata, ) - self.assertTrue( - {"namespace": "", "prefix": "header", "key": "mime-version", "value": "1.0"} - in metadata, + self.assertIn( + { + "namespace": "", + "prefix": "header", + "key": "mime-version", + "value": "1.0", + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "received", "value": "from mail.someserver.org ([::1])\n\tby e1acdba3bd07 with LMTP\n\tid KBKZGD2YR2NTCgQAjubtDA\n\t(envelope-from )\n\tfor ; Wed, 10 Oct 2022 11:40:46 +0200, from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with ESMTPSA id 2BC9064C1616\n\tfor ; Wed, 12 Oct 2022 21:40:46 +0200 (CEST)", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "return-path", "value": "", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "subject", "value": "Simple Text Mail", - } - in metadata, + }, + metadata, ) - self.assertTrue( - {"namespace": "", "prefix": "header", "key": "to", "value": "some@one.de"} - in metadata, + self.assertIn( + {"namespace": "", "prefix": "header", "key": "to", "value": "some@one.de"}, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "user-agent", "value": "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101\n Thunderbird/102.3.1", - } - in metadata, + }, + metadata, ) - self.assertTrue( + self.assertIn( { "namespace": "", "prefix": "header", "key": "x-last-tls-session-version", "value": "TLSv1.3", - } - in metadata, + }, + metadata, ) def test_parse_na(self): @@ -438,8 +443,8 @@ class TestParser(TestCase): resulting_html = result[-1][1].read() self.assertTrue(result[-1][0] == "index.html") - self.assertTrue(result[0][0] in resulting_html) - self.assertFalse(" Date: Sun, 20 Nov 2022 20:24:36 +0100 Subject: [PATCH 55/60] change order of elements in parsed Texts --- src/paperless_mail/parsers.py | 5 +++-- src/paperless_mail/tests/test_parsers.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/paperless_mail/parsers.py b/src/paperless_mail/parsers.py index 8f8dd2d37..d50217f2e 100644 --- a/src/paperless_mail/parsers.py +++ b/src/paperless_mail/parsers.py @@ -112,8 +112,7 @@ class MailDocumentParser(DocumentParser): mail = self.get_parsed(document_path) - self.text = f"{strip_text(mail.text)}\n\n" - self.text += f"Subject: {mail.subject}\n\n" + self.text = f"Subject: {mail.subject}\n\n" self.text += f"From: {mail.from_values.full}\n\n" self.text += f"To: {', '.join(address.full for address in mail.to_values)}\n\n" if len(mail.cc_values) >= 1: @@ -134,6 +133,8 @@ class MailDocumentParser(DocumentParser): if mail.html != "": self.text += "HTML content: " + strip_text(self.tika_parse(mail.html)) + self.text += f"\n\n{strip_text(mail.text)}" + self.date = mail.date self.archive_path = self.generate_pdf(document_path) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 315e82a1a..6e47c70ed 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -231,7 +231,7 @@ class TestParser(TestCase): @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") def test_parse_html_eml(self, n, mock_tika_parse: mock.MagicMock): # Validate parsing returns the expected results - text_expected = "Some Text and an embedded image.\n\nSubject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (600.24 KiB)\n\nHTML content: tika return" + text_expected = "Subject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (600.24 KiB)\n\nHTML content: tika return\n\nSome Text and an embedded image." mock_tika_parse.return_value = "tika return" self.parser.parse(os.path.join(self.SAMPLE_FILES, "html.eml"), "message/rfc822") @@ -258,7 +258,7 @@ class TestParser(TestCase): os.path.join(self.SAMPLE_FILES, "simple_text.eml"), "message/rfc822", ) - text_expected = "This is just a simple Text Mail.\n\nSubject: Simple Text Mail\n\nFrom: Some One \n\nTo: some@one.de\n\nCC: asdasd@æsdasd.de, asdadasdasdasda.asdasd@æsdasd.de\n\nBCC: fdf@fvf.de\n\n" + text_expected = "Subject: Simple Text Mail\n\nFrom: Some One \n\nTo: some@one.de\n\nCC: asdasd@æsdasd.de, asdadasdasdasda.asdasd@æsdasd.de\n\nBCC: fdf@fvf.de\n\n\n\nThis is just a simple Text Mail." self.assertEqual(text_expected, self.parser.text) self.assertEqual( datetime.datetime( From 0b1a16908fbbb88728301fa708d221a9138539ce Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 20:33:07 +0100 Subject: [PATCH 56/60] Include .eml reference in docs --- docs/configuration.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 32a302f0c..2523b0f5d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -497,8 +497,10 @@ Tika settings Paperless can make use of `Tika `_ and `Gotenberg `_ for parsing and -converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you -wish to use this, you must provide a Tika server and a Gotenberg server, +converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). +Tika and Gotenberg are also needed to allow parsing of E-Mails (.eml). + +If you wish to use this, you must provide a Tika server and a Gotenberg server, configure their endpoints, and enable the feature. PAPERLESS_TIKA_ENABLED= From 00f39d8b581c358f2484680275222f6ad909758c Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 22:49:42 +0100 Subject: [PATCH 57/60] add test comments --- src/paperless_mail/tests/test_parsers.py | 206 +++++++++++++++++++++-- 1 file changed, 191 insertions(+), 15 deletions(-) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 6e47c70ed..a2aab941e 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -16,7 +16,15 @@ class TestParser(TestCase): def tearDown(self) -> None: self.parser.cleanup() - def test_get_parsed(self): + def test_get_parsed_missing_file(self): + """ + GIVEN: + - Fresh parser + WHEN: + - A nonexistent file should be parsed + THEN: + - An Exception is thrown + """ # Check if exception is raised when parsing fails. self.assertRaises( ParseError, @@ -24,6 +32,15 @@ class TestParser(TestCase): os.path.join(self.SAMPLE_FILES, "na"), ) + def test_get_parsed_broken_file(self): + """ + GIVEN: + - Fresh parser + WHEN: + - A faulty file should be parsed + THEN: + - An Exception is thrown + """ # Check if exception is raised when the mail is faulty. self.assertRaises( ParseError, @@ -31,6 +48,15 @@ class TestParser(TestCase): os.path.join(self.SAMPLE_FILES, "broken.eml"), ) + def test_get_parsed_simple_text_mail(self): + """ + GIVEN: + - Fresh parser + WHEN: + - A .eml file should be parsed + THEN: + - The content of the mail should be available in the parse result. + """ # Parse Test file and check relevant content parsed1 = self.parser.get_parsed( os.path.join(self.SAMPLE_FILES, "simple_text.eml"), @@ -48,9 +74,22 @@ class TestParser(TestCase): self.assertEqual(parsed1.text, "This is just a simple Text Mail.\n") self.assertEqual(parsed1.to, ("some@one.de",)) + def test_get_parsed_reparse(self): + """ + GIVEN: + - An E-Mail was parsed + WHEN: + - Another .eml file should be parsed + THEN: + - The parser should not retry to parse and return the old results + """ + # Parse Test file and check relevant content + parsed1 = self.parser.get_parsed( + os.path.join(self.SAMPLE_FILES, "simple_text.eml"), + ) # Check if same parsed object as before is returned, even if another file is given. parsed2 = self.parser.get_parsed( - os.path.join(os.path.join(self.SAMPLE_FILES, "na")), + os.path.join(os.path.join(self.SAMPLE_FILES, "html.eml")), ) self.assertEqual(parsed1, parsed2) @@ -61,6 +100,14 @@ class TestParser(TestCase): mock_make_thumbnail_from_pdf: mock.MagicMock, mock_generate_pdf: mock.MagicMock, ): + """ + GIVEN: + - An E-Mail was parsed + WHEN: + - The Thumbnail is requested + THEN: + - The parser should call the functions which generate the thumbnail + """ mocked_return = "Passing the return value through.." mock_make_thumbnail_from_pdf.return_value = mocked_return @@ -81,11 +128,28 @@ class TestParser(TestCase): self.assertEqual(mocked_return, thumb) @mock.patch("documents.loggers.LoggingMixin.log") - def test_extract_metadata(self, m: mock.MagicMock): + def test_extract_metadata_fail(self, m: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - Metadata extraction is triggered for nonexistent file + THEN: + - A log warning should be generated + """ # Validate if warning is logged when parsing fails self.assertEqual([], self.parser.extract_metadata("na", "message/rfc822")) self.assertEqual("warning", m.call_args[0][0]) + def test_extract_metadata(self): + """ + GIVEN: + - Fresh start + WHEN: + - Metadata extraction is triggered + THEN: + - metadata is returned + """ # Validate Metadata parsing returns the expected results metadata = self.parser.extract_metadata( os.path.join(self.SAMPLE_FILES, "simple_text.eml"), @@ -219,6 +283,14 @@ class TestParser(TestCase): ) def test_parse_na(self): + """ + GIVEN: + - Fresh start + WHEN: + - parsing is attempted with nonexistent file + THEN: + - Exception is thrown + """ # Check if exception is raised when parsing fails. self.assertRaises( ParseError, @@ -230,6 +302,14 @@ class TestParser(TestCase): @mock.patch("paperless_mail.parsers.MailDocumentParser.tika_parse") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") def test_parse_html_eml(self, n, mock_tika_parse: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - parsing is done with html mail + THEN: + - Tika is called, parsed information from non html parts is available + """ # Validate parsing returns the expected results text_expected = "Subject: HTML Message\n\nFrom: Name \n\nTo: someone@example.de\n\nAttachments: IntM6gnXFm00FEV5.png (6.89 KiB), 600+kbfile.txt (600.24 KiB)\n\nHTML content: tika return\n\nSome Text and an embedded image." mock_tika_parse.return_value = "tika return" @@ -252,6 +332,14 @@ class TestParser(TestCase): @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") def test_parse_simple_eml(self, n): + """ + GIVEN: + - Fresh start + WHEN: + - parsing is done with non html mail + THEN: + - parsed information is available + """ # Validate parsing returns the expected results self.parser.parse( @@ -277,22 +365,51 @@ class TestParser(TestCase): self.assertTrue(os.path.isfile(self.parser.archive_path)) @mock.patch("paperless_mail.parsers.parser.from_buffer") - def test_tika_parse(self, mock_from_buffer: mock.MagicMock): - html = '

Some Text

' - expected_text = "Some Text" - mock_from_buffer.return_value = {"content": expected_text} - + def test_tika_parse_unsuccessful(self, mock_from_buffer: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - tika parsing fails + THEN: + - the parser should return an empty string + """ # Check unsuccessful parsing mock_from_buffer.return_value = {"content": None} parsed = self.parser.tika_parse(None) self.assertEqual("", parsed) + @mock.patch("paperless_mail.parsers.parser.from_buffer") + def test_tika_parse(self, mock_from_buffer: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - tika parsing is called + THEN: + - a web request to tika shall be done and the reply es returned + """ + html = '

Some Text

' + expected_text = "Some Text" + # Check successful parsing mock_from_buffer.return_value = {"content": expected_text} parsed = self.parser.tika_parse(html) self.assertEqual(expected_text, parsed.strip()) mock_from_buffer.assert_called_with(html, self.parser.tika_server) + @mock.patch("paperless_mail.parsers.parser.from_buffer") + def test_tika_parse_exception(self, mock_from_buffer: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - tika parsing is called and an exception is thrown on the request + THEN: + - a ParseError Exception is thrown + """ + html = '

Some Text

' + # Check ParseError def my_side_effect(): raise Exception("Test") @@ -303,6 +420,14 @@ class TestParser(TestCase): @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html") def test_generate_pdf_parse_error(self, m: mock.MagicMock, n: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - pdf generation is requested but gotenberg can not be reached + THEN: + - a ParseError Exception is thrown + """ m.return_value = b"" n.return_value = b"" @@ -314,6 +439,22 @@ class TestParser(TestCase): os.path.join(self.SAMPLE_FILES, "html.eml"), ) + def test_generate_pdf_exception(self): + """ + GIVEN: + - Fresh start + WHEN: + - pdf generation is requested but parsing throws an exception + THEN: + - a ParseError Exception is thrown + """ + # Check if exception is raised when the mail can not be parsed. + self.assertRaises( + ParseError, + self.parser.generate_pdf, + os.path.join(self.SAMPLE_FILES, "broken.eml"), + ) + @mock.patch("paperless_mail.parsers.requests.post") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html") @@ -323,13 +464,14 @@ class TestParser(TestCase): mock_generate_pdf_from_mail: mock.MagicMock, mock_post: mock.MagicMock, ): - # Check if exception is raised when the mail can not be parsed. - self.assertRaises( - ParseError, - self.parser.generate_pdf, - os.path.join(self.SAMPLE_FILES, "broken.eml"), - ) - + """ + GIVEN: + - Fresh start + WHEN: + - pdf generation is requested + THEN: + - gotenberg is called and the resulting file is returned + """ mock_generate_pdf_from_mail.return_value = b"Mail Return" mock_generate_pdf_from_html.return_value = b"HTML Return" @@ -366,6 +508,14 @@ class TestParser(TestCase): self.assertEqual(b"Content", file.read()) def test_mail_to_html(self): + """ + GIVEN: + - Fresh start + WHEN: + - conversion from eml to html is requested + THEN: + - html should be returned + """ mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) html_handle = self.parser.mail_to_html(mail) html_received = html_handle.read() @@ -384,6 +534,14 @@ class TestParser(TestCase): mock_mail_to_html: mock.MagicMock, mock_post: mock.MagicMock, ): + """ + GIVEN: + - Fresh start + WHEN: + - conversion of PDF from .eml is requested + THEN: + - gotenberg should be called with valid intermediary html files, the resulting pdf is returned + """ mock_response = mock.MagicMock() mock_response.content = b"Content" mock_post.return_value = mock_response @@ -425,6 +583,15 @@ class TestParser(TestCase): mock_response.raise_for_status.assert_called_once() def test_transform_inline_html(self): + """ + GIVEN: + - Fresh start + WHEN: + - transforming of html content from an email with an inline image attachment is requested + THEN: + - html is returned and sanitized + """ + class MailAttachmentMock: def __init__(self, payload, content_id): self.payload = payload @@ -448,6 +615,15 @@ class TestParser(TestCase): @mock.patch("paperless_mail.parsers.requests.post") def test_generate_pdf_from_html(self, mock_post: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - generating pdf from html with inline attachments is attempted + THEN: + - gotenberg is called with the correct parameters and the resulting pdf is returned + """ + class MailAttachmentMock: def __init__(self, payload, content_id): self.payload = payload From 4aa318598fd0dc6c5d4e08dd2a13e7bf614511ec Mon Sep 17 00:00:00 2001 From: phail Date: Sun, 20 Nov 2022 23:26:20 +0100 Subject: [PATCH 58/60] add test comments --- src/paperless_mail/tests/test_parsers.py | 15 +++ src/paperless_mail/tests/test_parsers_live.py | 121 +++++++++++++++--- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index a2aab941e..e02267970 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -417,6 +417,21 @@ class TestParser(TestCase): mock_from_buffer.side_effect = my_side_effect self.assertRaises(ParseError, self.parser.tika_parse, html) + def test_tika_parse_unreachable(self): + """ + GIVEN: + - Fresh start + WHEN: + - tika parsing is called but tika is not available + THEN: + - a ParseError Exception is thrown + """ + html = '

Some Text

' + + # Check if exception is raised when Tika cannot be reached. + self.parser.tika_server = "" + self.assertRaises(ParseError, self.parser.tika_parse, html) + @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_mail") @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf_from_html") def test_generate_pdf_parse_error(self, m: mock.MagicMock, n: mock.MagicMock): diff --git a/src/paperless_mail/tests/test_parsers_live.py b/src/paperless_mail/tests/test_parsers_live.py index ce3cfd3a3..9a9816f7d 100644 --- a/src/paperless_mail/tests/test_parsers_live.py +++ b/src/paperless_mail/tests/test_parsers_live.py @@ -33,6 +33,14 @@ class TestParserLive(TestCase): ) @mock.patch("paperless_mail.parsers.MailDocumentParser.generate_pdf") def test_get_thumbnail(self, mock_generate_pdf: mock.MagicMock): + """ + GIVEN: + - Fresh start + WHEN: + - The Thumbnail is requested + THEN: + - The returned thumbnail image file is as expected + """ mock_generate_pdf.return_value = os.path.join( self.SAMPLE_FILES, "simple_text.eml.pdf", @@ -55,26 +63,39 @@ class TestParserLive(TestCase): "TIKA_LIVE" not in os.environ, reason="No tika server", ) - def test_tika_parse(self): + def test_tika_parse_successful(self): + """ + GIVEN: + - Fresh start + WHEN: + - tika parsing is called + THEN: + - a web request to tika shall be done and the reply es returned + """ html = '

Some Text

' expected_text = "Some Text" - tika_server_original = self.parser.tika_server - - # Check if exception is raised when Tika cannot be reached. - self.parser.tika_server = "" - self.assertRaises(ParseError, self.parser.tika_parse, html) - - # Check unsuccessful parsing - self.parser.tika_server = tika_server_original - - parsed = self.parser.tika_parse(None) - self.assertEqual("", parsed) - # Check successful parsing parsed = self.parser.tika_parse(html) self.assertEqual(expected_text, parsed.strip()) + @pytest.mark.skipif( + "TIKA_LIVE" not in os.environ, + reason="No tika server", + ) + def test_tika_parse_unsuccessful(self): + """ + GIVEN: + - Fresh start + WHEN: + - tika parsing fails + THEN: + - the parser should return an empty string + """ + # Check unsuccessful parsing + parsed = self.parser.tika_parse(None) + self.assertEqual("", parsed) + @pytest.mark.skipif( "GOTENBERG_LIVE" not in os.environ, reason="No gotenberg server", @@ -86,7 +107,14 @@ class TestParserLive(TestCase): mock_generate_pdf_from_html: mock.MagicMock, mock_generate_pdf_from_mail: mock.MagicMock, ): - + """ + GIVEN: + - Intermediary pdfs to be merged + WHEN: + - pdf generation is requested with html file requiring merging of pdfs + THEN: + - gotenberg is called to merge files and the resulting file is returned + """ with open(os.path.join(self.SAMPLE_FILES, "first.pdf"), "rb") as first: mock_generate_pdf_from_mail.return_value = first.read() @@ -107,6 +135,14 @@ class TestParserLive(TestCase): reason="No gotenberg server", ) def test_generate_pdf_from_mail_no_convert(self): + """ + GIVEN: + - Fresh start + WHEN: + - pdf generation from simple eml file is requested + THEN: + - gotenberg is called and the resulting file is returned and contains the expected text. + """ mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) pdf_path = os.path.join(self.parser.tempdir, "html.eml.pdf") @@ -128,6 +164,14 @@ class TestParserLive(TestCase): reason="PAPERLESS_TEST_SKIP_CONVERT set, skipping Test", ) def test_generate_pdf_from_mail(self): + """ + GIVEN: + - Fresh start + WHEN: + - pdf generation from simple eml file is requested + THEN: + - gotenberg is called and the resulting file is returned and look as expected. + """ mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) pdf_path = os.path.join(self.parser.tempdir, "html.eml.pdf") @@ -168,6 +212,15 @@ class TestParserLive(TestCase): reason="No gotenberg server", ) def test_generate_pdf_from_html_no_convert(self): + """ + GIVEN: + - Fresh start + WHEN: + - pdf generation from html eml file is requested + THEN: + - gotenberg is called and the resulting file is returned and contains the expected text. + """ + class MailAttachmentMock: def __init__(self, payload, content_id): self.payload = payload @@ -203,6 +256,15 @@ class TestParserLive(TestCase): reason="PAPERLESS_TEST_SKIP_CONVERT set, skipping Test", ) def test_generate_pdf_from_html(self): + """ + GIVEN: + - Fresh start + WHEN: + - pdf generation from html eml file is requested + THEN: + - gotenberg is called and the resulting file is returned and look as expected. + """ + class MailAttachmentMock: def __init__(self, payload, content_id): self.payload = payload @@ -255,10 +317,19 @@ class TestParserLive(TestCase): "GOTENBERG_LIVE" not in os.environ, reason="No gotenberg server", ) - def test_is_online_image_still_available(self): + def test_online_image_exception_on_not_available(self): + """ + GIVEN: + - Fresh start + WHEN: + - nonexistent image is requested + THEN: + - An exception shall be thrown + """ """ A public image is used in the html sample file. We have no control - whether this image stays online forever, so here we check if it is still there + whether this image stays online forever, so here we check if we can detect if is not + available anymore. """ # Start by Testing if nonexistent URL really throws an Exception @@ -268,5 +339,23 @@ class TestParserLive(TestCase): "https://upload.wikimedia.org/wikipedia/en/f/f7/nonexistent.png", ) + @pytest.mark.skipif( + "GOTENBERG_LIVE" not in os.environ, + reason="No gotenberg server", + ) + def test_is_online_image_still_available(self): + """ + GIVEN: + - Fresh start + WHEN: + - A public image used in the html sample file is requested + THEN: + - No exception shall be thrown + """ + """ + A public image is used in the html sample file. We have no control + whether this image stays online forever, so here we check if it is still there + """ + # Now check the URL used in samples/sample.html urlopen("https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png") From fe2db4dbf7bbc21b287fd72cd124545160eaa7c0 Mon Sep 17 00:00:00 2001 From: phail Date: Wed, 30 Nov 2022 10:16:39 +0100 Subject: [PATCH 59/60] adapt compose file for eml parsing --- docker/compose/docker-compose.ci-test.yml | 5 ++++- docker/compose/docker-compose.mariadb-tika.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docker/compose/docker-compose.ci-test.yml b/docker/compose/docker-compose.ci-test.yml index 87bc8b7f2..b1b8d2179 100644 --- a/docker/compose/docker-compose.ci-test.yml +++ b/docker/compose/docker-compose.ci-test.yml @@ -11,9 +11,12 @@ services: container_name: gotenberg network_mode: host 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. command: - "gotenberg" - - "--chromium-disable-routes=true" + - "--chromium-disable-javascript=true" + - "--chromium-allow-list=file:///tmp/.*" tika: image: ghcr.io/paperless-ngx/tika:latest hostname: tika diff --git a/docker/compose/docker-compose.mariadb-tika.yml b/docker/compose/docker-compose.mariadb-tika.yml index 22f69ba4f..4bbb390f0 100644 --- a/docker/compose/docker-compose.mariadb-tika.yml +++ b/docker/compose/docker-compose.mariadb-tika.yml @@ -87,9 +87,12 @@ services: gotenberg: image: docker.io/gotenberg/gotenberg:7.6 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. command: - "gotenberg" - - "--chromium-disable-routes=true" + - "--chromium-disable-javascript=true" + - "--chromium-allow-list=file:///tmp/.*" tika: image: ghcr.io/paperless-ngx/tika:latest From 4b31e5d0b46639c5cb68005149e2e2f0dc13ca94 Mon Sep 17 00:00:00 2001 From: Trenton Holmes <797416+stumpylog@users.noreply.github.com> Date: Sun, 4 Dec 2022 14:00:59 -0800 Subject: [PATCH 60/60] Fixes my broken formatting --- docs/configuration.md | 18 +++++++++--------- docs/troubleshooting.md | 14 +++++++------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 93eaead36..bcde72e5f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -607,17 +607,17 @@ services: PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000 PAPERLESS_TIKA_ENDPOINT: http://tika:9998 - # ... + # ... gotenberg: - image: gotenberg/gotenberg:7.6 - 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. - command: - - "gotenberg" - - "--chromium-disable-javascript=true" - - "--chromium-allow-list=file:///tmp/.*" + image: gotenberg/gotenberg:7.6 + 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. + command: + - 'gotenberg' + - '--chromium-disable-javascript=true' + - '--chromium-allow-list=file:///tmp/.*' tika: image: ghcr.io/paperless-ngx/tika:latest diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 329de94db..f522058a5 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -125,13 +125,13 @@ using docker-compose, this is achieved by the following configuration change in the `docker-compose.yml` file: ```yaml - # The gotenberg chromium route is used to convert .eml files. We do not - # want to allow external content like tracking pixels or even javascript. - command: - - "gotenberg" - - "--chromium-disable-javascript=true" - - "--chromium-allow-list=file:///tmp/.*" - - "--api-timeout=60" +# The gotenberg chromium route is used to convert .eml files. We do not +# want to allow external content like tracking pixels or even javascript. +command: + - 'gotenberg' + - '--chromium-disable-javascript=true' + - '--chromium-allow-list=file:///tmp/.*' + - '--api-timeout=60' ``` ## Permission denied errors in the consumption directory