diff --git a/src/documents/consumers/base.py b/src/documents/consumers/base.py index 4a72f906a..bd53b845d 100644 --- a/src/documents/consumers/base.py +++ b/src/documents/consumers/base.py @@ -12,10 +12,11 @@ from PIL import Image from django.conf import settings from django.utils import timezone +from django.template.defaultfilters import slugify from paperless.db import GnuPG -from ..models import Tag, Document +from ..models import Sender, Tag, Document from ..languages import ISO639 @@ -31,6 +32,19 @@ class Consumer(object): OCR = pyocr.get_available_tools()[0] DEFAULT_OCR_LANGUAGE = settings.OCR_LANGUAGE + REGEX_TITLE = re.compile( + r"^.*/(.*)\.(pdf|jpe?g|png|gif|tiff)$", + flags=re.IGNORECASE + ) + REGEX_SENDER_TITLE = re.compile( + r"^.*/(.*) - (.*)\.(pdf|jpe?g|png|gif|tiff)", + flags=re.IGNORECASE + ) + REGEX_SENDER_TITLE_TAGS = re.compile( + r"^.*/(.*) - (.*) - ([a-z\-,])\.(pdf|jpe?g|png|gif|tiff)", + flags=re.IGNORECASE + ) + def __init__(self, verbosity=1): self.verbosity = verbosity @@ -105,13 +119,48 @@ class Consumer(object): # Strip out excess white space to allow matching to go smoother return re.sub(r"\s+", " ", r) - def _guess_file_attributes(self, doc): - raise NotImplementedError( - "At the very least a consumer should determine the file type.") + def _guess_attributes_from_name(self, parseable): + """ + We use a crude naming convention to make handling the sender, title, and + tags easier: + " - - <tags>.<suffix>" + "<sender> - <title>.<suffix>" + "<title>.<suffix>" + """ + + def get_sender(sender_name): + return Sender.objects.get_or_create( + name=sender_name, defaults={"slug": slugify(sender_name)})[0] + + def get_tags(tags): + r = [] + for t in tags.split(","): + r.append( + Tag.objects.get_or_create(slug=t, defaults={"name": t})[0]) + return r + + # First attempt: "<sender> - <title> - <tags>.<suffix>" + m = re.match(self.REGEX_SENDER_TITLE_TAGS, parseable) + if m: + return ( + get_sender(m.group(1)), + m.group(2), + get_tags(m.group(3)), + m.group(4) + ) + + # Second attempt: "<sender> - <title>.<suffix>" + m = re.match(self.REGEX_SENDER_TITLE, parseable) + if m: + return get_sender(m.group(1)), m.group(2), [], m.group(3) + + # That didn't work, so we assume sender and tags are None + m = re.match(self.REGEX_TITLE, parseable) + return None, m.group(1), [], m.group(2) def _store(self, text, doc): - sender, title, file_type = self._guess_file_attributes(doc) + sender, title, file_type = self._guess_attributes_from_name(doc) lower_text = text.lower() relevant_tags = [t for t in Tag.objects.all() if t.matches(lower_text)] diff --git a/src/documents/consumers/file.py b/src/documents/consumers/file.py index e8a8737f4..77eaea25b 100644 --- a/src/documents/consumers/file.py +++ b/src/documents/consumers/file.py @@ -2,10 +2,8 @@ import os import re from django.conf import settings -from django.template.defaultfilters import slugify -from ..models import Sender -from . import Consumer, OCRError +from .base import Consumer, OCRError class FileConsumerError(Exception): @@ -16,11 +14,6 @@ class FileConsumer(Consumer): CONSUME = settings.CONSUMPTION_DIR - PARSER_REGEX_TITLE = re.compile( - r"^.*/(.*)\.(pdf|jpe?g|png|gif|tiff)$", flags=re.IGNORECASE) - PARSER_REGEX_SENDER_TITLE = re.compile( - r"^.*/(.*) - (.*)\.(pdf|jpe?g|png|gif|tiff)", flags=re.IGNORECASE) - def __init__(self, *args, **kwargs): Consumer.__init__(self, *args, **kwargs) @@ -47,7 +40,7 @@ class FileConsumer(Consumer): if not os.path.isfile(doc): continue - if not re.match(self.PARSER_REGEX_TITLE, doc): + if not re.match(self.REGEX_TITLE, doc): continue if doc in self._ignore: @@ -85,22 +78,3 @@ class FileConsumer(Consumer): self.stats[doc] = t return False - - def _guess_file_attributes(self, doc): - """ - We use a crude naming convention to make handling the sender and title - easier: - "<sender> - <title>.<suffix>" - """ - - # First we attempt "<sender> - <title>.<suffix>" - m = re.match(self.PARSER_REGEX_SENDER_TITLE, doc) - if m: - sender_name, title, file_type = m.group(1), m.group(2), m.group(3) - sender, __ = Sender.objects.get_or_create( - name=sender_name, defaults={"slug": slugify(sender_name)}) - return sender, title, file_type - - # That didn't work, so we assume sender is None - m = re.match(self.PARSER_REGEX_TITLE, doc) - return None, m.group(1), m.group(2) diff --git a/src/documents/consumers/mail.py b/src/documents/consumers/mail.py index 99106f0eb..c7ad0a23a 100644 --- a/src/documents/consumers/mail.py +++ b/src/documents/consumers/mail.py @@ -1,6 +1,10 @@ import datetime +import email import imaplib +from base64 import b64decode +from io import BytesIO + from django.conf import settings from . import Consumer @@ -49,7 +53,8 @@ class MailConsumer(Consumer): def consume(self): if self._enabled: - self.get_messages() + for message in self.get_messages(): + pass self.last_checked = datetime.datetime.now() @@ -58,12 +63,64 @@ class MailConsumer(Consumer): self._connect() self._login() - for message in self._fetch(): - print(message) # Now we have to do something with the attachment + messages = [] + for data in self._fetch(): + message = self._parse_message(data) + if message: + messages.append(message) self._connection.expunge() self._connection.close() self._connection.logout() - def _guess_file_attributes(self, doc): - return None, None, "jpg" + return messages + + @staticmethod + def _parse_message(data): + """ + Cribbed heavily from + https://www.ianlewis.org/en/parsing-email-attachments-python + """ + + r = [] + message = email.message_from_string(data) + + for part in message.walk(): + + content_disposition = part.get("Content-Disposition") + if not content_disposition: + continue + + dispositions = content_disposition.strip().split(";") + if not dispositions[0].lower() == "attachment": + continue + + file_data = part.get_payload() + attachment = BytesIO(b64decode(file_data)) + attachment.content_type = part.get_content_type() + attachment.size = len(file_data) + attachment.name = None + attachment.create_date = None + attachment.mod_date = None + attachment.read_date = None + + for param in dispositions[1:]: + + name, value = param.split("=") + name = name.lower() + + if name == "filename": + attachment.name = value + elif name == "create-date": + attachment.create_date = value + elif name == "modification-date": + attachment.mod_date = value + elif name == "read-date": + attachment.read_date = value + + r.append({ + "subject": message.get("Subject"), + "attachment": attachment, + }) + + return r diff --git a/src/documents/models.py b/src/documents/models.py index 960644854..78ed64832 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -157,5 +157,9 @@ class Document(models.Model): @property def parseable_file_name(self): if self.sender and self.title: - return "{} - {}.{}".format(self.sender, self.title, self.file_types) + tags = ",".join([t.slug for t in self.tags.all()]) + if tags: + return "{} - {} - {}.{}".format( + self.sender, self.title, tags, self.file_type) + return "{} - {}.{}".format(self.sender, self.title, self.file_type) return os.path.basename(self.source_path) diff --git a/src/documents/tests.py b/src/documents/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/src/documents/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/src/documents/tests/__init__.py b/src/documents/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/documents/tests/consumers/__init__.py b/src/documents/tests/consumers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/documents/tests/consumers/mail.py b/src/documents/tests/consumers/mail.py new file mode 100644 index 000000000..5bf4d1371 --- /dev/null +++ b/src/documents/tests/consumers/mail.py @@ -0,0 +1,43 @@ +import os +import magic + +from hashlib import md5 + +from django.conf import settings +from django.test import TestCase + +from ...consumers.mail import MailConsumer + + +class TestMailConsumer(TestCase): + + def __init__(self, *args, **kwargs): + + TestCase.__init__(self, *args, **kwargs) + self.sample = os.path.join( + settings.BASE_DIR, + "documents", + "tests", + "consumers", + "samples", + "mail.txt" + ) + + def test_parse(self): + consumer = MailConsumer() + with open(self.sample) as f: + + messages = consumer._parse_message(f.read()) + + self.assertTrue(len(messages), 1) + self.assertEqual(messages[0]["subject"], "Test 0") + + attachment = messages[0]["attachment"] + data = attachment.read() + + self.assertEqual( + md5(data).hexdigest(), "7c89655f9e9eb7dd8cde8568e8115d59") + + self.assertEqual(attachment.content_type, "application/pdf") + with magic.Magic(flags=magic.MAGIC_MIME_TYPE) as m: + self.assertEqual(m.id_buffer(data), "application/pdf") diff --git a/src/documents/tests/consumers/samples/mail.txt b/src/documents/tests/consumers/samples/mail.txt new file mode 100644 index 000000000..a4e2a267d --- /dev/null +++ b/src/documents/tests/consumers/samples/mail.txt @@ -0,0 +1,208 @@ +Return-Path: <sender@example.com> +X-Original-To: sender@mailbox4.mailhost.com +Delivered-To: sender@mailbox4.mailhost.com +Received: from mx8.mailhost.com (mail8.mailhost.com [75.126.24.68]) + by mailbox4.mailhost.com (Postfix) with ESMTP id B62BD5498001 + for <sender@mailbox4.mailhost.com>; Thu, 4 Feb 2016 22:01:17 +0000 (UTC) +Received: from localhost (localhost.localdomain [127.0.0.1]) + by mx8.mailhost.com (Postfix) with ESMTP id B41796F190D + for <sender@mailbox4.mailhost.com>; Thu, 4 Feb 2016 22:01:17 +0000 (UTC) +X-Spam-Flag: NO +X-Spam-Score: 0 +X-Spam-Level: +X-Spam-Status: No, score=0 tagged_above=-999 required=3 + tests=[RCVD_IN_DNSWL_NONE=-0.0001] +Received: from mx8.mailhost.com ([127.0.0.1]) + by localhost (mail8.mailhost.com [127.0.0.1]) (amavisd-new, port 10024) + with ESMTP id 3cj6d28FXsS3 for <sender@mailbox4.mailhost.com>; + Thu, 4 Feb 2016 22:01:17 +0000 (UTC) +Received: from smtp.mailhost.com (smtp.mailhost.com [74.55.86.74]) + by mx8.mailhost.com (Postfix) with ESMTP id 527D76F1529 + for <paperless@example.com>; Thu, 4 Feb 2016 22:01:17 +0000 (UTC) +Received: from [10.114.0.19] (nl3x.mullvad.net [46.166.136.162]) + by smtp.mailhost.com (Postfix) with ESMTP id 9C52420C6FDA + for <paperless@example.com>; Thu, 4 Feb 2016 22:01:16 +0000 (UTC) +To: paperless@example.com +From: Daniel Quinn <sender@example.com> +Subject: Test 0 +Message-ID: <56B3CA2A.6030806@example.com> +Date: Thu, 4 Feb 2016 22:01:14 +0000 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 + Thunderbird/38.5.0 +MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="------------090701020702030809070008" + +This is a multi-part message in MIME format. +--------------090701020702030809070008 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +This is the test body. + +--------------090701020702030809070008 +Content-Type: application/pdf; + name="test0.pdf" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; + filename="test0.pdf" + +JVBERi0xLjQKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0 +ZURlY29kZT4+CnN0cmVhbQp4nFWLQQvCMAyF7/kVOQutSdeuHZSA0+3gbVDwIN6c3gR38e/b +bF4kkPfyvReyjB94IyFVF7pgG0ze4TLDZYevLamzPKEvEFqbMEZfq+WO+5GRHZbHNROLy+So +UfFi6g7/RyusEpUl9VsQxQTlHR2oV3wUEzOdhOnXG1aw/o1yK2cYCkww4RdbUCevCmVuZHN0 +cmVhbQplbmRvYmoKCjMgMCBvYmoKMTM5CmVuZG9iagoKNSAwIG9iago8PC9MZW5ndGggNiAw +IFIvRmlsdGVyL0ZsYXRlRGVjb2RlL0xlbmd0aDEgMTA4MjQ+PgpzdHJlYW0KeJzlOWt0G9WZ +95uRbNmWLckPWY4SaRTFedmybI8T4rw8sS3ZiZ1YfqWSCbFkS7YEtiQkJSE8GlNeOQ5pUmh5 +Zkt2l+XQNl3GhLaBpcWw0D19UGALLRRS0gM9nD0lxVBK9wCx97tXI0UJAc727L8d+c587/u9 +7p0rOZXYEyJaMkV4Io1OBuLOqmqBEPJLQqB0dG9K2NRTsQHhM4Rw/zkWH5+870e7PiRE9Rgh ++Y+NT+wf+/b3e4YI0YYJKX41HAoEfxj6vUjIIgltrA0jYef8/nzEr0F8WXgydY2bP7QO8WOI +SxOx0cDxxbUmxN9AfOlk4Jr4apWLI8SMKBGigcmQpYXrRBx9KtobjyVTQbJsgZDl91B+PBGK +d9838hzipwjhjyIN8EMvLYJ5FOd4lTovX1NQWKQtLtGR/3eX+jCpIJ3qTURH4ux+wcWfIFXk +XkIW3qXY+ft898LH/5deaNKPe8hD5DFymLxGrlAYbuIhEbIHKbnX0+QlpNLLQ4bId8n055g9 +QU4hPy3nJ0doJJe8PORucpL8xwWzeMgkuQ59+QF5DRrIz7BVYuQD0JAbyXNo9QOkbb+UKa4E +b2MMHMuhvk7u5w6RbdzbiNxLOZyT05NnyTHYjZZTGOfhbMQbP2P0NnID3vtJmOxFmF3qTZ/+ +jhQs/AWjuoFsI18jW8hEjsaT8ABfiPUbIA9gTp9mNGeGmd/JX8n9kOPO3YnIN8g4jgBg7Nxh +fsvnZOh/ffGDpBhW8dWk4FJcrono5j/mGhc+5JeRQjK4MJehLXQt/IUPzEdVw6rF6k2qX3zR +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 +Fpt7cad50ChWDBpAN6gXdYMcYKFFMujULeg4nW5Yd0DH60gL4aaMoIZTcHRmoL+mputU/kJf +l6zxXC7DQbm6n96l3iE576BMBocu984AfN13y+HDpHVJl9zY75X9S3xdchABiQJTCOiXzBhJ +qy+ZTNWwC2pqEN6Dd1KzpwaJu5NpKsnySU0SkrhHJZkS1FCBNA54r6E8JFA9QO3dSUJvlFmT +VqLaScUcU07fGGDa/T/LhW2oCmVuZHN0cmVhbQplbmRvYmoKCjYgMCBvYmoKNjI5MQplbmRv +YmoKCjcgMCBvYmoKPDwvVHlwZS9Gb250RGVzY3JpcHRvci9Gb250TmFtZS9CQUFBQUErTGli +ZXJhdGlvblNlcmlmCi9GbGFncyA0Ci9Gb250QkJveFstNTQzIC0zMDMgMTI3NyA5ODFdL0l0 +YWxpY0FuZ2xlIDAKL0FzY2VudCA4OTEKL0Rlc2NlbnQgLTIxNgovQ2FwSGVpZ2h0IDk4MQov +U3RlbVYgODAKL0ZvbnRGaWxlMiA1IDAgUgo+PgplbmRvYmoKCjggMCBvYmoKPDwvTGVuZ3Ro +IDI5Mi9GaWx0ZXIvRmxhdGVEZWNvZGU+PgpzdHJlYW0KeJxdkctuwyAQRfd8Bct0EfmROA/J +spQmseRFH6rbD3BgnCLVGGGy8N+XmUlbqQvQmZl7BxiSY3NqrAnJqx9VC0H2xmoP03jzCuQF +rsaKLJfaqHCPaFdD50QSve08BRga249lKZK3WJuCn+XioMcLPIjkxWvwxl7l4uPYxri9OfcF +A9ggU1FVUkMf+zx17rkbICHXstGxbMK8jJY/wfvsQOYUZ3wVNWqYXKfAd/YKokzTSpZ1XQmw ++l8tK9hy6dVn56M0i9I0LdZV5Jx4s0NeMe+R18TbFXJBnKfIG9ZkyFvWUJ8d5wvkPTPlD8w1 +8iMz9Tyyl/Qnzp+Qz8xn5JrPPdOj7rfH5+H8f8Ym1c37ODL6JJoVTslY+P1HNzp00foG7l+O +gwplbmRzdHJlYW0KZW5kb2JqCgo5IDAgb2JqCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVU +eXBlL0Jhc2VGb250L0JBQUFBQStMaWJlcmF0aW9uU2VyaWYKL0ZpcnN0Q2hhciAwCi9MYXN0 +Q2hhciAxNQovV2lkdGhzWzc3NyA2MTAgNTAwIDI3NyAzODkgMjUwIDQ0MyAyNzcgNDQzIDUw +MCA1MDAgNDQzIDUwMCA3NzcgNTAwIDI1MApdCi9Gb250RGVzY3JpcHRvciA3IDAgUgovVG9V +bmljb2RlIDggMCBSCj4+CmVuZG9iagoKMTAgMCBvYmoKPDwvRjEgOSAwIFIKPj4KZW5kb2Jq +CgoxMSAwIG9iago8PC9Gb250IDEwIDAgUgovUHJvY1NldFsvUERGL1RleHRdCj4+CmVuZG9i +agoKMSAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDQgMCBSL1Jlc291cmNlcyAxMSAwIFIv +TWVkaWFCb3hbMCAwIDU5NSA4NDJdL0dyb3VwPDwvUy9UcmFuc3BhcmVuY3kvQ1MvRGV2aWNl +UkdCL0kgdHJ1ZT4+L0NvbnRlbnRzIDIgMCBSPj4KZW5kb2JqCgo0IDAgb2JqCjw8L1R5cGUv +UGFnZXMKL1Jlc291cmNlcyAxMSAwIFIKL01lZGlhQm94WyAwIDAgNTk1IDg0MiBdCi9LaWRz +WyAxIDAgUiBdCi9Db3VudCAxPj4KZW5kb2JqCgoxMiAwIG9iago8PC9UeXBlL0NhdGFsb2cv +UGFnZXMgNCAwIFIKL09wZW5BY3Rpb25bMSAwIFIgL1hZWiBudWxsIG51bGwgMF0KL0xhbmco +ZW4tR0IpCj4+CmVuZG9iagoKMTMgMCBvYmoKPDwvQ3JlYXRvcjxGRUZGMDA1NzAwNzIwMDY5 +MDA3NDAwNjUwMDcyPgovUHJvZHVjZXI8RkVGRjAwNEMwMDY5MDA2MjAwNzIwMDY1MDA0RjAw +NjYwMDY2MDA2OTAwNjMwMDY1MDAyMDAwMzUwMDJFMDAzMD4KL0NyZWF0aW9uRGF0ZShEOjIw +MTYwMjA0MjIwMDAyWicpPj4KZW5kb2JqCgp4cmVmCjAgMTQKMDAwMDAwMDAwMCA2NTUzNSBm +IAowMDAwMDA3NTA5IDAwMDAwIG4gCjAwMDAwMDAwMTkgMDAwMDAgbiAKMDAwMDAwMDIyOSAw +MDAwMCBuIAowMDAwMDA3NjUyIDAwMDAwIG4gCjAwMDAwMDAyNDkgMDAwMDAgbiAKMDAwMDAw +NjYyNSAwMDAwMCBuIAowMDAwMDA2NjQ2IDAwMDAwIG4gCjAwMDAwMDY4NDEgMDAwMDAgbiAK +MDAwMDAwNzIwMiAwMDAwMCBuIAowMDAwMDA3NDIyIDAwMDAwIG4gCjAwMDAwMDc0NTQgMDAw +MDAgbiAKMDAwMDAwNzc1MSAwMDAwMCBuIAowMDAwMDA3ODQ4IDAwMDAwIG4gCnRyYWlsZXIK +PDwvU2l6ZSAxNC9Sb290IDEyIDAgUgovSW5mbyAxMyAwIFIKL0lEIFsgPDRFN0ZCMEZCMjA4 +ODBCNURBQkIzQTNEOTQxNDlBRTQ3Pgo8NEU3RkIwRkIyMDg4MEI1REFCQjNBM0Q5NDE0OUFF +NDc+IF0KL0RvY0NoZWNrc3VtIC8yQTY0RDMzNzRFQTVEODMwNTRDNEI2RDFEMUY4QzU1RQo+ +PgpzdGFydHhyZWYKODAxOAolJUVPRgo= +--------------090701020702030809070008-- diff --git a/src/documents/tests/tests.py b/src/documents/tests/tests.py new file mode 100644 index 000000000..1d8c9b53a --- /dev/null +++ b/src/documents/tests/tests.py @@ -0,0 +1 @@ +from .consumers.mail import TestMailConsumer diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 8c4a73d78..6d9da46fd 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -136,10 +136,9 @@ STATIC_URL = '/static/' MEDIA_URL = "/media/" -# # Paperless-specific stuffs # Change these paths if yours are different -# +# ---------------------------------------------------------------------------- # The default language that tesseract will attempt to use when parsing # documents. It should be a 3-letter language code consistent with ISO 639. @@ -180,3 +179,4 @@ MAIL_CONSUMPTION = { # DON'T FORGET TO SET THIS as leaving it blank may cause some strang things with # GPG, including an interesting case where it may "encrypt" zero-byte files. PASSPHRASE = os.environ.get("PAPERLESS_PASSPHRASE") + diff --git a/src/pytest.ini b/src/pytest.ini new file mode 100644 index 000000000..1d62c4621 --- /dev/null +++ b/src/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +python_files=tests.py +DJANGO_SETTINGS_MODULE=paperless.settings +