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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 GkpBUk(v 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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Ä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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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 rXztKK)yt`iErF=`BuT~I6p}`zcg(#n6zgz_b9 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 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 iAA>f%ws-?V1KCMAa-tm02PkX+v 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/html.eml.pdf.webp b/src/paperless_mail/tests/samples/html.eml.pdf.webp new file mode 100644 index 0000000000000000000000000000000000000000..b4481efd92e594f4fb8afeeedcc67d1df9e47db1 GIT binary patch literal 6012 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 R1mu8nfxu`qG!lxE{|oGNHG%*D literal 0 HcmV?d00001 diff --git a/src/paperless_mail/tests/test_parsers.py b/src/paperless_mail/tests/test_parsers.py index 2bc0fbdad..3da54e364 100644 --- a/src/paperless_mail/tests/test_parsers.py +++ b/src/paperless_mail/tests/test_parsers.py @@ -6,7 +6,6 @@ import pytest from django.test import TestCase from documents.parsers import ParseError from paperless_mail.parsers import MailDocumentParser -from pdfminer.high_level import extract_text class TestParser(TestCase): @@ -217,13 +216,16 @@ class TestParser(TestCase): "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): + def test_parse_html_eml(self, m, 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" + mock_tika_parse.return_value = "tika return" + 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, self.parser.text) self.assertEqual( datetime.datetime( @@ -265,27 +267,30 @@ class TestParser(TestCase): # Just check if file exists, the unittest for generate_pdf() goes deeper. 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): + def test_tika_parse(self, m, mock_from_buffer: mock.MagicMock): html = '

Some Text

' expected_text = "Some Text" - - 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) + mock_from_buffer.return_value = {"content": expected_text} # Check unsuccessful parsing - self.parser.tika_server = tika_server_original - + mock_from_buffer.return_value = {"content": None} parsed = self.parser.tika_parse(None) self.assertEqual("", parsed) # 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) + + # Check ParseError + def my_side_effect(): + raise Exception("Test") + + mock_from_buffer.side_effect = my_side_effect + 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") @@ -373,25 +378,31 @@ class TestParser(TestCase): retval = self.parser.generate_pdf_from_mail(mail) self.assertEqual(b"Content", retval) - 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, - ) + mock_mail_to_html.assert_called_once_with(mail) self.assertEqual( - self.parser.gotenberg_server + "/forms/pdfengines/merge", + self.parser.gotenberg_server + "/forms/chromium/convert/html", 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(), + { + "marginTop": "0.1", + "marginBottom": "0.1", + "marginLeft": "0.1", + "marginRight": "0.1", + "paperWidth": "8.27", + "paperHeight": "11.7", + "scale": "1.0", + }, + mock_post.call_args.kwargs["data"], ) self.assertEqual( - b"HTML Return", - mock_post.call_args.kwargs["files"]["2_html.pdf"][1].read(), + "Testresponse", + mock_post.call_args.kwargs["files"]["html"][1], + ) + self.assertEqual( + "output.css", + mock_post.call_args.kwargs["files"]["css"][0], ) mock_response.raise_for_status.assert_called_once() diff --git a/src/paperless_mail/tests/test_parsers_live.py b/src/paperless_mail/tests/test_parsers_live.py index a0fa1f54d..653388300 100644 --- a/src/paperless_mail/tests/test_parsers_live.py +++ b/src/paperless_mail/tests/test_parsers_live.py @@ -59,6 +59,10 @@ class TestParserLive(TestCase): f"Created Thumbnail {thumb} differs from expected file {expected}", ) + @pytest.mark.skipif( + "TIKA_LIVE" not in os.environ, + reason="No tika server", + ) @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output def test_tika_parse(self, m): html = '

Some Text

' @@ -108,6 +112,28 @@ class TestParserLive(TestCase): ) self.assertEqual(expected, extracted) + @pytest.mark.skipif( + "GOTENBERG_LIVE" not in os.environ, + reason="No gotenberg server", + ) + @mock.patch("documents.loggers.LoggingMixin.log") # Disable log output + def test_generate_pdf_from_mail_no_convert(self, m): + mail = self.parser.get_parsed(os.path.join(self.SAMPLE_FILES, "html.eml")) + + pdf_path = os.path.join(self.parser.tempdir, "html.eml.pdf") + + with open(pdf_path, "wb") as file: + file.write(self.parser.generate_pdf_from_mail(mail)) + file.close() + + extracted = extract_text(pdf_path) + expected = extract_text(os.path.join(self.SAMPLE_FILES, "html.eml.pdf")) + self.assertEqual(expected, extracted) + + @pytest.mark.skipif( + "GOTENBERG_LIVE" not in os.environ, + reason="No gotenberg server", + ) # Only run if convert is available @pytest.mark.skipif( "PAPERLESS_TEST_SKIP_CONVERT" in os.environ, @@ -115,10 +141,9 @@ class TestParserLive(TestCase): ) @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") + pdf_path = os.path.join(self.parser.tempdir, "html.eml.pdf") with open(pdf_path, "wb") as file: file.write(self.parser.generate_pdf_from_mail(mail)) @@ -126,7 +151,7 @@ class TestParserLive(TestCase): converted = os.path.join( self.parser.tempdir, - "test_generate_pdf_from_mail.webp", + "html.eml.pdf.webp", ) run_convert( density=300, @@ -143,8 +168,8 @@ class TestParserLive(TestCase): thumb_hash = self.hashfile(converted) # The created pdf is not reproducible. But the converted image should always look the same. - expected_hash = ( - "8734a3f0a567979343824e468cd737bf29c02086bbfd8773e94feb986968ad32" + expected_hash = self.hashfile( + os.path.join(self.SAMPLE_FILES, "html.eml.pdf.webp"), ) self.assertEqual( thumb_hash, @@ -174,14 +199,14 @@ class TestParserLive(TestCase): ] result = self.parser.generate_pdf_from_html(html, attachments) - pdf_path = os.path.join(self.parser.tempdir, "test_generate_pdf_from_html.pdf") + pdf_path = os.path.join(self.parser.tempdir, "sample.html.pdf") with open(pdf_path, "wb") as file: file.write(result) file.close() 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" + expected = extract_text(os.path.join(self.SAMPLE_FILES, "sample.html.pdf")) self.assertEqual(expected, extracted) @pytest.mark.skipif( From b68906b14ed89832705397b6822cf4001de87626 Mon Sep 17 00:00:00 2001 From: phail 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 zcmaiaWl$Ymv}F?9-Q6MB72x6ocPF@eaJjg2u%N+7aDqc{cL)x_65O2-Jh;Pr^JdvRT2f?_&d;$Xev=9L(7f%w=%W4unE*Ou1fB=LR1_N^m zfdAtV0^>?rd71Evk1xsP6+d9u-k^)wUC)>p9!^M93NFe~yJ%0Du>sLC^pW<>7@hz4 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 delta 11483 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 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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
  • 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 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