Format Python code with black

This commit is contained in:
kpj
2022-02-27 15:26:41 +01:00
parent f0ffc69010
commit c56cb25b5f
136 changed files with 6142 additions and 3811 deletions

View File

@@ -15,9 +15,9 @@ class MailAccountAdminForm(forms.ModelForm):
model = MailAccount
widgets = {
'password': forms.PasswordInput(),
"password": forms.PasswordInput(),
}
fields = '__all__'
fields = "__all__"
class MailAccountAdmin(admin.ModelAdmin):
@@ -25,15 +25,9 @@ class MailAccountAdmin(admin.ModelAdmin):
list_display = ("name", "imap_server", "username")
fieldsets = [
(None, {
'fields': ['name', 'imap_server', 'imap_port']
}),
(_("Authentication"), {
'fields': ['imap_security', 'username', 'password']
}),
(_("Advanced settings"), {
'fields': ['character_set']
})
(None, {"fields": ["name", "imap_server", "imap_port"]}),
(_("Authentication"), {"fields": ["imap_security", "username", "password"]}),
(_("Advanced settings"), {"fields": ["character_set"]}),
]
form = MailAccountAdminForm
@@ -44,56 +38,66 @@ class MailRuleAdmin(admin.ModelAdmin):
"attachment_type": admin.VERTICAL,
"action": admin.VERTICAL,
"assign_title_from": admin.VERTICAL,
"assign_correspondent_from": admin.VERTICAL
"assign_correspondent_from": admin.VERTICAL,
}
fieldsets = (
(None, {
'fields': ('name', 'order', 'account', 'folder')
}),
(_("Filter"), {
'description':
_("Paperless will only process mails that match ALL of the "
"filters given below."),
'fields':
('filter_from',
'filter_subject',
'filter_body',
'filter_attachment_filename',
'maximum_age',
'attachment_type')
}),
(_("Actions"), {
'description':
_("The action applied to the mail. This action is only "
"performed when documents were consumed from the mail. "
"Mails without attachments will remain entirely untouched."),
'fields': (
'action',
'action_parameter')
}),
(_("Metadata"), {
'description':
_("Assign metadata to documents consumed from this rule "
"automatically. If you do not assign tags, types or "
"correspondents here, paperless will still process all "
"matching rules that you have defined."),
"fields": (
'assign_title_from',
'assign_tag',
'assign_document_type',
'assign_correspondent_from',
'assign_correspondent')
})
(None, {"fields": ("name", "order", "account", "folder")}),
(
_("Filter"),
{
"description": _(
"Paperless will only process mails that match ALL of the "
"filters given below."
),
"fields": (
"filter_from",
"filter_subject",
"filter_body",
"filter_attachment_filename",
"maximum_age",
"attachment_type",
),
},
),
(
_("Actions"),
{
"description": _(
"The action applied to the mail. This action is only "
"performed when documents were consumed from the mail. "
"Mails without attachments will remain entirely untouched."
),
"fields": ("action", "action_parameter"),
},
),
(
_("Metadata"),
{
"description": _(
"Assign metadata to documents consumed from this rule "
"automatically. If you do not assign tags, types or "
"correspondents here, paperless will still process all "
"matching rules that you have defined."
),
"fields": (
"assign_title_from",
"assign_tag",
"assign_document_type",
"assign_correspondent_from",
"assign_correspondent",
),
},
),
)
list_filter = ("account",)
list_display = ("order", "name", "account", "folder", "action")
list_editable = ("order", )
list_editable = ("order",)
list_display_links = ("name", )
list_display_links = ("name",)
sortable_by = []

View File

@@ -4,6 +4,6 @@ from django.utils.translation import gettext_lazy as _
class PaperlessMailConfig(AppConfig):
name = 'paperless_mail'
name = "paperless_mail"
verbose_name = _('Paperless mail')
verbose_name = _("Paperless mail")

View File

@@ -8,8 +8,13 @@ import pathvalidate
from django.conf import settings
from django.db import DatabaseError
from django_q.tasks import async_task
from imap_tools import MailBox, MailBoxUnencrypted, AND, MailMessageFlags, \
MailboxFolderSelectError
from imap_tools import (
MailBox,
MailBoxUnencrypted,
AND,
MailMessageFlags,
MailboxFolderSelectError,
)
from documents.loggers import LoggingMixin
from documents.models import Correspondent
@@ -22,7 +27,6 @@ class MailError(Exception):
class BaseMailAction:
def get_criteria(self):
return {}
@@ -31,30 +35,26 @@ class BaseMailAction:
class DeleteMailAction(BaseMailAction):
def post_consume(self, M, message_uids, parameter):
M.delete(message_uids)
class MarkReadMailAction(BaseMailAction):
def get_criteria(self):
return {'seen': False}
return {"seen": False}
def post_consume(self, M, message_uids, parameter):
M.seen(message_uids, True)
class MoveMailAction(BaseMailAction):
def post_consume(self, M, message_uids, parameter):
M.move(message_uids, parameter)
class FlagMailAction(BaseMailAction):
def get_criteria(self):
return {'flagged': False}
return {"flagged": False}
def post_consume(self, M, message_uids, parameter):
M.flag(message_uids, [MailMessageFlags.FLAGGED], True)
@@ -108,10 +108,7 @@ class MailAccountHandler(LoggingMixin):
try:
return Correspondent.objects.get_or_create(name=name)[0]
except DatabaseError as e:
self.log(
"error",
f"Error while retrieving correspondent {name}: {e}"
)
self.log("error", f"Error while retrieving correspondent {name}: {e}")
return None
def get_title(self, message, att, rule):
@@ -122,7 +119,9 @@ class MailAccountHandler(LoggingMixin):
return os.path.splitext(os.path.basename(att.filename))[0]
else:
raise NotImplementedError("Unknown title selector.") # pragma: nocover # NOQA: E501
raise NotImplementedError(
"Unknown title selector."
) # pragma: nocover # NOQA: E501
def get_correspondent(self, message, rule):
c_from = rule.assign_correspondent_from
@@ -134,9 +133,12 @@ class MailAccountHandler(LoggingMixin):
return self._correspondent_from_name(message.from_)
elif c_from == MailRule.CORRESPONDENT_FROM_NAME:
if message.from_values and 'name' in message.from_values and message.from_values['name']: # NOQA: E501
return self._correspondent_from_name(
message.from_values['name'])
if (
message.from_values
and "name" in message.from_values
and message.from_values["name"]
): # NOQA: E501
return self._correspondent_from_name(message.from_values["name"])
else:
return self._correspondent_from_name(message.from_)
@@ -144,69 +146,71 @@ class MailAccountHandler(LoggingMixin):
return rule.assign_correspondent
else:
raise NotImplementedError("Unknwown correspondent selector") # pragma: nocover # NOQA: E501
raise NotImplementedError(
"Unknwown correspondent selector"
) # pragma: nocover # NOQA: E501
def handle_mail_account(self, account):
self.renew_logging_group()
self.log('debug', f"Processing mail account {account}")
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:
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}")
raise MailError(f"Error while authenticating account {account}")
self.log('debug', f"Account {account}: Processing "
f"{account.rules.count()} rule(s)")
self.log(
"debug",
f"Account {account}: Processing " f"{account.rules.count()} rule(s)",
)
for rule in account.rules.order_by('order'):
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
exc_info=True,
)
return total_processed_files
def handle_mail_rule(self, M, rule):
self.log(
'debug',
f"Rule {rule}: Selecting folder {rule.folder}")
self.log("debug", f"Rule {rule}: Selecting folder {rule.folder}")
try:
M.folder.set(rule.folder)
except MailboxFolderSelectError:
raise MailError(
f"Rule {rule}: Folder {rule.folder} "
f"does not exist in account {rule.account}")
f"does not exist in account {rule.account}"
)
criterias = make_criterias(rule)
self.log(
'debug',
f"Rule {rule}: Searching folder with criteria "
f"{str(AND(**criterias))}")
"debug",
f"Rule {rule}: Searching folder with criteria " f"{str(AND(**criterias))}",
)
try:
messages = M.fetch(
criteria=AND(**criterias),
mark_seen=False,
charset=rule.account.character_set)
charset=rule.account.character_set,
)
except Exception:
raise MailError(
f"Rule {rule}: Error while fetching folder {rule.folder}")
raise MailError(f"Rule {rule}: Error while fetching folder {rule.folder}")
post_consume_messages = []
@@ -224,29 +228,27 @@ class MailAccountHandler(LoggingMixin):
except Exception as e:
self.log(
"error",
f"Rule {rule}: Error while processing mail "
f"{message.uid}: {e}",
exc_info=True)
f"Rule {rule}: Error while processing mail " f"{message.uid}: {e}",
exc_info=True,
)
self.log("debug", f"Rule {rule}: Processed {mails_processed} matching mail(s)")
self.log(
'debug',
f"Rule {rule}: Processed {mails_processed} matching mail(s)")
self.log(
'debug',
"debug",
f"Rule {rule}: Running mail actions on "
f"{len(post_consume_messages)} mails")
f"{len(post_consume_messages)} mails",
)
try:
get_rule_action(rule).post_consume(
M,
post_consume_messages,
rule.action_parameter)
M, post_consume_messages, rule.action_parameter
)
except Exception as e:
raise MailError(
f"Rule {rule}: Error while processing post-consume actions: "
f"{e}")
f"Rule {rule}: Error while processing post-consume actions: " f"{e}"
)
return total_processed_files
@@ -255,10 +257,11 @@ class MailAccountHandler(LoggingMixin):
return 0
self.log(
'debug',
"debug",
f"Rule {rule}: "
f"Processing mail {message.subject} from {message.from_} with "
f"{len(message.attachments)} attachment(s)")
f"{len(message.attachments)} attachment(s)",
)
correspondent = self.get_correspondent(message, rule)
tag = rule.assign_tag
@@ -268,12 +271,16 @@ class MailAccountHandler(LoggingMixin):
for att in message.attachments:
if not att.content_disposition == "attachment" and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY: # NOQA: E501
if (
not att.content_disposition == "attachment"
and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY
): # NOQA: E501
self.log(
'debug',
"debug",
f"Rule {rule}: "
f"Skipping attachment {att.filename} "
f"with content disposition {att.content_disposition}")
f"with content disposition {att.content_disposition}",
)
continue
if rule.filter_attachment_filename:
@@ -289,35 +296,44 @@ class MailAccountHandler(LoggingMixin):
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:
_, 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',
"info",
f"Rule {rule}: "
f"Consuming attachment {att.filename} from mail "
f"{message.subject} from {message.from_}")
f"{message.subject} from {message.from_}",
)
async_task(
"documents.tasks.consume_file",
path=temp_filename,
override_filename=pathvalidate.sanitize_filename(att.filename), # NOQA: E501
override_filename=pathvalidate.sanitize_filename(
att.filename
), # NOQA: E501
override_title=title,
override_correspondent_id=correspondent.id if correspondent else None, # NOQA: E501
override_document_type_id=doc_type.id if doc_type else None, # NOQA: E501
override_correspondent_id=correspondent.id
if correspondent
else None, # NOQA: E501
override_document_type_id=doc_type.id
if doc_type
else None, # NOQA: E501
override_tag_ids=[tag.id] if tag else None,
task_name=att.filename[:100]
task_name=att.filename[:100],
)
processed_attachments += 1
else:
self.log(
'debug',
"debug",
f"Rule {rule}: "
f"Skipping attachment {att.filename} "
f"since guessed mime type {mime_type} is not supported "
f"by paperless")
f"by paperless",
)
return processed_attachments

View File

@@ -6,7 +6,9 @@ from paperless_mail import tasks
class Command(BaseCommand):
help = """
""".replace(" ", "")
""".replace(
" ", ""
)
def handle(self, *args, **options):

View File

@@ -9,40 +9,146 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('documents', '1002_auto_20201111_1105'),
("documents", "1002_auto_20201111_1105"),
]
operations = [
migrations.CreateModel(
name='MailAccount',
name="MailAccount",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256, unique=True)),
('imap_server', models.CharField(max_length=256)),
('imap_port', models.IntegerField(blank=True, null=True)),
('imap_security', models.PositiveIntegerField(choices=[(1, 'No encryption'), (2, 'Use SSL'), (3, 'Use STARTTLS')], default=2)),
('username', models.CharField(max_length=256)),
('password', models.CharField(max_length=256)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=256, unique=True)),
("imap_server", models.CharField(max_length=256)),
("imap_port", models.IntegerField(blank=True, null=True)),
(
"imap_security",
models.PositiveIntegerField(
choices=[
(1, "No encryption"),
(2, "Use SSL"),
(3, "Use STARTTLS"),
],
default=2,
),
),
("username", models.CharField(max_length=256)),
("password", models.CharField(max_length=256)),
],
),
migrations.CreateModel(
name='MailRule',
name="MailRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=256)),
('folder', models.CharField(default='INBOX', max_length=256)),
('filter_from', models.CharField(blank=True, max_length=256, null=True)),
('filter_subject', models.CharField(blank=True, max_length=256, null=True)),
('filter_body', models.CharField(blank=True, max_length=256, null=True)),
('maximum_age', models.PositiveIntegerField(default=30)),
('action', 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, help_text='The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched.')),
('action_parameter', models.CharField(blank=True, help_text='Additional parameter for the action selected above, i.e., the target folder of the move to folder action.', max_length=256, null=True)),
('assign_title_from', models.PositiveIntegerField(choices=[(1, 'Use subject as title'), (2, 'Use attachment filename as title')], default=1)),
('assign_correspondent_from', models.PositiveIntegerField(choices=[(1, 'Do not assign a correspondent'), (2, 'Use mail address'), (3, 'Use name (or mail address if not available)'), (4, 'Use correspondent selected below')], default=1)),
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='paperless_mail.mailaccount')),
('assign_correspondent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.correspondent')),
('assign_document_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.documenttype')),
('assign_tag', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.tag')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=256)),
("folder", models.CharField(default="INBOX", max_length=256)),
(
"filter_from",
models.CharField(blank=True, max_length=256, null=True),
),
(
"filter_subject",
models.CharField(blank=True, max_length=256, null=True),
),
(
"filter_body",
models.CharField(blank=True, max_length=256, null=True),
),
("maximum_age", models.PositiveIntegerField(default=30)),
(
"action",
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,
help_text="The action applied to the mail. This action is only performed when documents were consumed from the mail. Mails without attachments will remain entirely untouched.",
),
),
(
"action_parameter",
models.CharField(
blank=True,
help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.",
max_length=256,
null=True,
),
),
(
"assign_title_from",
models.PositiveIntegerField(
choices=[
(1, "Use subject as title"),
(2, "Use attachment filename as title"),
],
default=1,
),
),
(
"assign_correspondent_from",
models.PositiveIntegerField(
choices=[
(1, "Do not assign a correspondent"),
(2, "Use mail address"),
(3, "Use name (or mail address if not available)"),
(4, "Use correspondent selected below"),
],
default=1,
),
),
(
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="rules",
to="paperless_mail.mailaccount",
),
),
(
"assign_correspondent",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="documents.correspondent",
),
),
(
"assign_document_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="documents.documenttype",
),
),
(
"assign_tag",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="documents.tag",
),
),
],
),
]

View File

@@ -7,26 +7,23 @@ from django_q.tasks import schedule
def add_schedules(apps, schema_editor):
schedule('paperless_mail.tasks.process_mail_accounts',
name="Check all e-mail accounts",
schedule_type=Schedule.MINUTES,
minutes=10)
schedule(
"paperless_mail.tasks.process_mail_accounts",
name="Check all e-mail accounts",
schedule_type=Schedule.MINUTES,
minutes=10,
)
def remove_schedules(apps, schema_editor):
Schedule.objects.filter(
func='paperless_mail.tasks.process_mail_accounts').delete()
Schedule.objects.filter(func="paperless_mail.tasks.process_mail_accounts").delete()
class Migration(migrations.Migration):
dependencies = [
('paperless_mail', '0001_initial'),
('django_q', '0013_task_attempt_count'),
("paperless_mail", "0001_initial"),
("django_q", "0013_task_attempt_count"),
]
operations = [
RunPython(add_schedules, remove_schedules)
]
operations = [RunPython(add_schedules, remove_schedules)]

View File

@@ -6,18 +6,22 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('paperless_mail', '0002_auto_20201117_1334'),
("paperless_mail", "0002_auto_20201117_1334"),
]
operations = [
migrations.AlterField(
model_name='mailaccount',
name='imap_port',
field=models.IntegerField(blank=True, help_text='This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.', null=True),
model_name="mailaccount",
name="imap_port",
field=models.IntegerField(
blank=True,
help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.",
null=True,
),
),
migrations.AlterField(
model_name='mailrule',
name='name',
model_name="mailrule",
name="name",
field=models.CharField(max_length=256, unique=True),
),
]

View File

@@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('paperless_mail', '0003_auto_20201118_1940'),
("paperless_mail", "0003_auto_20201118_1940"),
]
operations = [
migrations.AddField(
model_name='mailrule',
name='order',
model_name="mailrule",
name="order",
field=models.IntegerField(default=0),
),
]

View File

@@ -6,18 +6,28 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('paperless_mail', '0004_mailrule_order'),
("paperless_mail", "0004_mailrule_order"),
]
operations = [
migrations.AlterField(
model_name='mailrule',
name='action',
field=models.PositiveIntegerField(choices=[(3, "Mark as read, don't process read mails"), (4, "Flag the mail, don't process flagged mails"), (2, 'Move to specified folder'), (1, 'Delete')], default=3),
model_name="mailrule",
name="action",
field=models.PositiveIntegerField(
choices=[
(3, "Mark as read, don't process read mails"),
(4, "Flag the mail, don't process flagged mails"),
(2, "Move to specified folder"),
(1, "Delete"),
],
default=3,
),
),
migrations.AlterField(
model_name='mailrule',
name='maximum_age',
field=models.PositiveIntegerField(default=30, help_text='Specified in days.'),
model_name="mailrule",
name="maximum_age",
field=models.PositiveIntegerField(
default=30, help_text="Specified in days."
),
),
]

View File

@@ -7,122 +7,198 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('documents', '1011_auto_20210101_2340'),
('paperless_mail', '0005_help_texts'),
("documents", "1011_auto_20210101_2340"),
("paperless_mail", "0005_help_texts"),
]
operations = [
migrations.AlterModelOptions(
name='mailaccount',
options={'verbose_name': 'mail account', 'verbose_name_plural': 'mail accounts'},
name="mailaccount",
options={
"verbose_name": "mail account",
"verbose_name_plural": "mail accounts",
},
),
migrations.AlterModelOptions(
name='mailrule',
options={'verbose_name': 'mail rule', 'verbose_name_plural': 'mail rules'},
name="mailrule",
options={"verbose_name": "mail rule", "verbose_name_plural": "mail rules"},
),
migrations.AlterField(
model_name='mailaccount',
name='imap_port',
field=models.IntegerField(blank=True, help_text='This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.', null=True, verbose_name='IMAP port'),
model_name="mailaccount",
name="imap_port",
field=models.IntegerField(
blank=True,
help_text="This is usually 143 for unencrypted and STARTTLS connections, and 993 for SSL connections.",
null=True,
verbose_name="IMAP port",
),
),
migrations.AlterField(
model_name='mailaccount',
name='imap_security',
field=models.PositiveIntegerField(choices=[(1, 'No encryption'), (2, 'Use SSL'), (3, 'Use STARTTLS')], default=2, verbose_name='IMAP security'),
model_name="mailaccount",
name="imap_security",
field=models.PositiveIntegerField(
choices=[(1, "No encryption"), (2, "Use SSL"), (3, "Use STARTTLS")],
default=2,
verbose_name="IMAP security",
),
),
migrations.AlterField(
model_name='mailaccount',
name='imap_server',
field=models.CharField(max_length=256, verbose_name='IMAP server'),
model_name="mailaccount",
name="imap_server",
field=models.CharField(max_length=256, verbose_name="IMAP server"),
),
migrations.AlterField(
model_name='mailaccount',
name='name',
field=models.CharField(max_length=256, unique=True, verbose_name='name'),
model_name="mailaccount",
name="name",
field=models.CharField(max_length=256, unique=True, verbose_name="name"),
),
migrations.AlterField(
model_name='mailaccount',
name='password',
field=models.CharField(max_length=256, verbose_name='password'),
model_name="mailaccount",
name="password",
field=models.CharField(max_length=256, verbose_name="password"),
),
migrations.AlterField(
model_name='mailaccount',
name='username',
field=models.CharField(max_length=256, verbose_name='username'),
model_name="mailaccount",
name="username",
field=models.CharField(max_length=256, verbose_name="username"),
),
migrations.AlterField(
model_name='mailrule',
name='account',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rules', to='paperless_mail.mailaccount', verbose_name='account'),
model_name="mailrule",
name="account",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="rules",
to="paperless_mail.mailaccount",
verbose_name="account",
),
),
migrations.AlterField(
model_name='mailrule',
name='action',
field=models.PositiveIntegerField(choices=[(3, "Mark as read, don't process read mails"), (4, "Flag the mail, don't process flagged mails"), (2, 'Move to specified folder'), (1, 'Delete')], default=3, verbose_name='action'),
model_name="mailrule",
name="action",
field=models.PositiveIntegerField(
choices=[
(3, "Mark as read, don't process read mails"),
(4, "Flag the mail, don't process flagged mails"),
(2, "Move to specified folder"),
(1, "Delete"),
],
default=3,
verbose_name="action",
),
),
migrations.AlterField(
model_name='mailrule',
name='action_parameter',
field=models.CharField(blank=True, help_text='Additional parameter for the action selected above, i.e., the target folder of the move to folder action.', max_length=256, null=True, verbose_name='action parameter'),
model_name="mailrule",
name="action_parameter",
field=models.CharField(
blank=True,
help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action.",
max_length=256,
null=True,
verbose_name="action parameter",
),
),
migrations.AlterField(
model_name='mailrule',
name='assign_correspondent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.correspondent', verbose_name='assign this correspondent'),
model_name="mailrule",
name="assign_correspondent",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="documents.correspondent",
verbose_name="assign this correspondent",
),
),
migrations.AlterField(
model_name='mailrule',
name='assign_correspondent_from',
field=models.PositiveIntegerField(choices=[(1, 'Do not assign a correspondent'), (2, 'Use mail address'), (3, 'Use name (or mail address if not available)'), (4, 'Use correspondent selected below')], default=1, verbose_name='assign correspondent from'),
model_name="mailrule",
name="assign_correspondent_from",
field=models.PositiveIntegerField(
choices=[
(1, "Do not assign a correspondent"),
(2, "Use mail address"),
(3, "Use name (or mail address if not available)"),
(4, "Use correspondent selected below"),
],
default=1,
verbose_name="assign correspondent from",
),
),
migrations.AlterField(
model_name='mailrule',
name='assign_document_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.documenttype', verbose_name='assign this document type'),
model_name="mailrule",
name="assign_document_type",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="documents.documenttype",
verbose_name="assign this document type",
),
),
migrations.AlterField(
model_name='mailrule',
name='assign_tag',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='documents.tag', verbose_name='assign this tag'),
model_name="mailrule",
name="assign_tag",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="documents.tag",
verbose_name="assign this tag",
),
),
migrations.AlterField(
model_name='mailrule',
name='assign_title_from',
field=models.PositiveIntegerField(choices=[(1, 'Use subject as title'), (2, 'Use attachment filename as title')], default=1, verbose_name='assign title from'),
model_name="mailrule",
name="assign_title_from",
field=models.PositiveIntegerField(
choices=[
(1, "Use subject as title"),
(2, "Use attachment filename as title"),
],
default=1,
verbose_name="assign title from",
),
),
migrations.AlterField(
model_name='mailrule',
name='filter_body',
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='filter body'),
model_name="mailrule",
name="filter_body",
field=models.CharField(
blank=True, max_length=256, null=True, verbose_name="filter body"
),
),
migrations.AlterField(
model_name='mailrule',
name='filter_from',
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='filter from'),
model_name="mailrule",
name="filter_from",
field=models.CharField(
blank=True, max_length=256, null=True, verbose_name="filter from"
),
),
migrations.AlterField(
model_name='mailrule',
name='filter_subject',
field=models.CharField(blank=True, max_length=256, null=True, verbose_name='filter subject'),
model_name="mailrule",
name="filter_subject",
field=models.CharField(
blank=True, max_length=256, null=True, verbose_name="filter subject"
),
),
migrations.AlterField(
model_name='mailrule',
name='folder',
field=models.CharField(default='INBOX', max_length=256, verbose_name='folder'),
model_name="mailrule",
name="folder",
field=models.CharField(
default="INBOX", max_length=256, verbose_name="folder"
),
),
migrations.AlterField(
model_name='mailrule',
name='maximum_age',
field=models.PositiveIntegerField(default=30, help_text='Specified in days.', verbose_name='maximum age'),
model_name="mailrule",
name="maximum_age",
field=models.PositiveIntegerField(
default=30, help_text="Specified in days.", verbose_name="maximum age"
),
),
migrations.AlterField(
model_name='mailrule',
name='name',
field=models.CharField(max_length=256, unique=True, verbose_name='name'),
model_name="mailrule",
name="name",
field=models.CharField(max_length=256, unique=True, verbose_name="name"),
),
migrations.AlterField(
model_name='mailrule',
name='order',
field=models.IntegerField(default=0, verbose_name='order'),
model_name="mailrule",
name="order",
field=models.IntegerField(default=0, verbose_name="order"),
),
]

View File

@@ -6,18 +6,32 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('paperless_mail', '0006_auto_20210101_2340'),
("paperless_mail", "0006_auto_20210101_2340"),
]
operations = [
migrations.AddField(
model_name='mailrule',
name='attachment_type',
field=models.PositiveIntegerField(choices=[(1, 'Only process attachments.'), (2, "Process all files, including 'inline' attachments.")], default=1, help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", verbose_name='attachment type'),
model_name="mailrule",
name="attachment_type",
field=models.PositiveIntegerField(
choices=[
(1, "Only process attachments."),
(2, "Process all files, including 'inline' attachments."),
],
default=1,
help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.",
verbose_name="attachment type",
),
),
migrations.AddField(
model_name='mailrule',
name='filter_attachment_filename',
field=models.CharField(blank=True, help_text='Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.', max_length=256, null=True, verbose_name='filter attachment filename'),
model_name="mailrule",
name="filter_attachment_filename",
field=models.CharField(
blank=True,
help_text="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.",
max_length=256,
null=True,
verbose_name="filter attachment filename",
),
),
]

View File

@@ -6,23 +6,39 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('paperless_mail', '0007_auto_20210106_0138'),
("paperless_mail", "0007_auto_20210106_0138"),
]
operations = [
migrations.AddField(
model_name='mailaccount',
name='character_set',
field=models.CharField(default='UTF-8', help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.", max_length=256, verbose_name='character set'),
model_name="mailaccount",
name="character_set",
field=models.CharField(
default="UTF-8",
help_text="The character set to use when communicating with the mail server, such as 'UTF-8' or 'US-ASCII'.",
max_length=256,
verbose_name="character set",
),
),
migrations.AlterField(
model_name='mailrule',
name='action_parameter',
field=models.CharField(blank=True, help_text='Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.', max_length=256, null=True, verbose_name='action parameter'),
model_name="mailrule",
name="action_parameter",
field=models.CharField(
blank=True,
help_text="Additional parameter for the action selected above, i.e., the target folder of the move to folder action. Subfolders must be separated by dots.",
max_length=256,
null=True,
verbose_name="action parameter",
),
),
migrations.AlterField(
model_name='mailrule',
name='folder',
field=models.CharField(default='INBOX', help_text='Subfolders must be separated by dots.', max_length=256, verbose_name='folder'),
model_name="mailrule",
name="folder",
field=models.CharField(
default="INBOX",
help_text="Subfolders must be separated by dots.",
max_length=256,
verbose_name="folder",
),
),
]

View File

@@ -6,7 +6,6 @@ from django.utils.translation import gettext_lazy as _
class MailAccount(models.Model):
class Meta:
verbose_name = _("mail account")
verbose_name_plural = _("mail accounts")
@@ -21,41 +20,36 @@ class MailAccount(models.Model):
(IMAP_SECURITY_STARTTLS, _("Use STARTTLS")),
)
name = models.CharField(
_("name"),
max_length=256, unique=True)
name = models.CharField(_("name"), max_length=256, unique=True)
imap_server = models.CharField(
_("IMAP server"),
max_length=256)
imap_server = models.CharField(_("IMAP server"), max_length=256)
imap_port = models.IntegerField(
_("IMAP port"),
blank=True,
null=True,
help_text=_("This is usually 143 for unencrypted and STARTTLS "
"connections, and 993 for SSL connections."))
imap_security = models.PositiveIntegerField(
_("IMAP security"),
choices=IMAP_SECURITY_OPTIONS,
default=IMAP_SECURITY_SSL
help_text=_(
"This is usually 143 for unencrypted and STARTTLS "
"connections, and 993 for SSL connections."
),
)
username = models.CharField(
_("username"),
max_length=256)
imap_security = models.PositiveIntegerField(
_("IMAP security"), choices=IMAP_SECURITY_OPTIONS, default=IMAP_SECURITY_SSL
)
password = models.CharField(
_("password"),
max_length=256)
username = models.CharField(_("username"), max_length=256)
password = models.CharField(_("password"), max_length=256)
character_set = models.CharField(
_("character set"),
max_length=256,
default="UTF-8",
help_text=_("The character set to use when communicating with the "
"mail server, such as 'UTF-8' or 'US-ASCII'.")
help_text=_(
"The character set to use when communicating with the "
"mail server, such as 'UTF-8' or 'US-ASCII'."
),
)
def __str__(self):
@@ -63,7 +57,6 @@ class MailAccount(models.Model):
class MailRule(models.Model):
class Meta:
verbose_name = _("mail rule")
verbose_name_plural = _("mail rules")
@@ -73,8 +66,10 @@ class MailRule(models.Model):
ATTACHMENT_TYPES = (
(ATTACHMENT_TYPE_ATTACHMENTS_ONLY, _("Only process attachments.")),
(ATTACHMENT_TYPE_EVERYTHING, _("Process all files, including 'inline' "
"attachments."))
(
ATTACHMENT_TYPE_EVERYTHING,
_("Process all files, including 'inline' " "attachments."),
),
)
ACTION_DELETE = 1
@@ -94,7 +89,7 @@ class MailRule(models.Model):
TITLE_SELECTOR = (
(TITLE_FROM_SUBJECT, _("Use subject as title")),
(TITLE_FROM_FILENAME, _("Use attachment filename as title"))
(TITLE_FROM_FILENAME, _("Use attachment filename as title")),
)
CORRESPONDENT_FROM_NOTHING = 1
@@ -103,66 +98,64 @@ class MailRule(models.Model):
CORRESPONDENT_FROM_CUSTOM = 4
CORRESPONDENT_SELECTOR = (
(CORRESPONDENT_FROM_NOTHING,
_("Do not assign a correspondent")),
(CORRESPONDENT_FROM_EMAIL,
_("Use mail address")),
(CORRESPONDENT_FROM_NAME,
_("Use name (or mail address if not available)")),
(CORRESPONDENT_FROM_CUSTOM,
_("Use correspondent selected below"))
(CORRESPONDENT_FROM_NOTHING, _("Do not assign a correspondent")),
(CORRESPONDENT_FROM_EMAIL, _("Use mail address")),
(CORRESPONDENT_FROM_NAME, _("Use name (or mail address if not available)")),
(CORRESPONDENT_FROM_CUSTOM, _("Use correspondent selected below")),
)
name = models.CharField(
_("name"),
max_length=256, unique=True)
name = models.CharField(_("name"), max_length=256, unique=True)
order = models.IntegerField(
_("order"),
default=0)
order = models.IntegerField(_("order"), default=0)
account = models.ForeignKey(
MailAccount,
related_name="rules",
on_delete=models.CASCADE,
verbose_name=_("account")
verbose_name=_("account"),
)
folder = models.CharField(
_("folder"),
default='INBOX', max_length=256,
help_text=_("Subfolders must be separated by dots.")
default="INBOX",
max_length=256,
help_text=_("Subfolders must be separated by dots."),
)
filter_from = models.CharField(
_("filter from"),
max_length=256, null=True, blank=True)
_("filter from"), max_length=256, null=True, blank=True
)
filter_subject = models.CharField(
_("filter subject"),
max_length=256, null=True, blank=True)
_("filter subject"), max_length=256, null=True, blank=True
)
filter_body = models.CharField(
_("filter body"),
max_length=256, null=True, blank=True)
_("filter body"), max_length=256, null=True, blank=True
)
filter_attachment_filename = models.CharField(
_("filter attachment filename"),
max_length=256, null=True, blank=True,
help_text=_("Only consume documents which entirely match this "
"filename if specified. Wildcards such as *.pdf or "
"*invoice* are allowed. Case insensitive.")
max_length=256,
null=True,
blank=True,
help_text=_(
"Only consume documents which entirely match this "
"filename if specified. Wildcards such as *.pdf or "
"*invoice* are allowed. Case insensitive."
),
)
maximum_age = models.PositiveIntegerField(
_("maximum age"),
default=30,
help_text=_("Specified in days."))
_("maximum age"), default=30, help_text=_("Specified in days.")
)
attachment_type = models.PositiveIntegerField(
_("attachment type"),
choices=ATTACHMENT_TYPES,
default=ATTACHMENT_TYPE_ATTACHMENTS_ONLY,
help_text=_("Inline attachments include embedded images, so it's best "
"to combine this option with a filename filter.")
help_text=_(
"Inline attachments include embedded images, so it's best "
"to combine this option with a filename filter."
),
)
action = models.PositiveIntegerField(
@@ -173,17 +166,19 @@ class MailRule(models.Model):
action_parameter = models.CharField(
_("action parameter"),
max_length=256, blank=True, null=True,
help_text=_("Additional parameter for the action selected above, "
"i.e., "
"the target folder of the move to folder action. "
"Subfolders must be separated by dots.")
max_length=256,
blank=True,
null=True,
help_text=_(
"Additional parameter for the action selected above, "
"i.e., "
"the target folder of the move to folder action. "
"Subfolders must be separated by dots."
),
)
assign_title_from = models.PositiveIntegerField(
_("assign title from"),
choices=TITLE_SELECTOR,
default=TITLE_FROM_SUBJECT
_("assign title from"), choices=TITLE_SELECTOR, default=TITLE_FROM_SUBJECT
)
assign_tag = models.ForeignKey(
@@ -205,7 +200,7 @@ class MailRule(models.Model):
assign_correspondent_from = models.PositiveIntegerField(
_("assign correspondent from"),
choices=CORRESPONDENT_SELECTOR,
default=CORRESPONDENT_FROM_NOTHING
default=CORRESPONDENT_FROM_NOTHING,
)
assign_correspondent = models.ForeignKey(
@@ -213,7 +208,7 @@ class MailRule(models.Model):
null=True,
blank=True,
on_delete=models.SET_NULL,
verbose_name=_("assign this correspondent")
verbose_name=_("assign this correspondent"),
)
def __str__(self):

View File

@@ -11,8 +11,7 @@ def process_mail_accounts():
total_new_documents = 0
for account in MailAccount.objects.all():
try:
total_new_documents += MailAccountHandler().handle_mail_account(
account)
total_new_documents += MailAccountHandler().handle_mail_account(account)
except MailError:
logger.exception(f"Error while processing mail account {account}")

View File

@@ -38,7 +38,7 @@ class BogusMailBox(ContextManager):
self.messages_spam = []
def login(self, username, password):
if not (username == 'admin' and password == 'secret'):
if not (username == "admin" and password == "secret"):
raise Exception()
folder = BogusFolderManager()
@@ -46,24 +46,24 @@ class BogusMailBox(ContextManager):
def fetch(self, criteria, mark_seen, charset=""):
msg = self.messages
criteria = str(criteria).strip('()').split(" ")
criteria = str(criteria).strip("()").split(" ")
if 'UNSEEN' in criteria:
if "UNSEEN" in criteria:
msg = filter(lambda m: not m.seen, msg)
if 'SUBJECT' in criteria:
subject = criteria[criteria.index('SUBJECT') + 1].strip('"')
if "SUBJECT" in criteria:
subject = criteria[criteria.index("SUBJECT") + 1].strip('"')
msg = filter(lambda m: subject in m.subject, msg)
if 'BODY' in criteria:
body = criteria[criteria.index('BODY') + 1].strip('"')
if "BODY" in criteria:
body = criteria[criteria.index("BODY") + 1].strip('"')
msg = filter(lambda m: body in m.body, msg)
if 'FROM' in criteria:
from_ = criteria[criteria.index('FROM') + 1].strip('"')
if "FROM" in criteria:
from_ = criteria[criteria.index("FROM") + 1].strip('"')
msg = filter(lambda m: from_ in m.from_, msg)
if 'UNFLAGGED' in criteria:
if "UNFLAGGED" in criteria:
msg = filter(lambda m: not m.flagged, msg)
return list(msg)
@@ -88,15 +88,20 @@ class BogusMailBox(ContextManager):
self.messages_spam.append(
filter(lambda m: m.uid in uid_list, self.messages)
)
self.messages = list(
filter(lambda m: m.uid not in uid_list, self.messages)
)
self.messages = list(filter(lambda m: m.uid not in uid_list, self.messages))
else:
raise Exception()
def create_message(num_attachments=1, body="", subject="the suject", from_="noone@mail.com", seen=False, flagged=False):
message = namedtuple('MailMessage', [])
def create_message(
num_attachments=1,
body="",
subject="the suject",
from_="noone@mail.com",
seen=False,
flagged=False,
):
message = namedtuple("MailMessage", [])
message.uid = uuid.uuid4()
message.subject = subject
@@ -112,8 +117,10 @@ def create_message(num_attachments=1, body="", subject="the suject", from_="noon
return message
def create_attachment(filename="the_file.pdf", content_disposition="attachment", payload=b"a PDF document"):
attachment = namedtuple('Attachment', [])
def create_attachment(
filename="the_file.pdf", content_disposition="attachment", payload=b"a PDF document"
):
attachment = namedtuple("Attachment", [])
attachment.filename = filename
attachment.content_disposition = content_disposition
attachment.payload = payload
@@ -123,25 +130,24 @@ def create_attachment(filename="the_file.pdf", content_disposition="attachment",
def fake_magic_from_buffer(buffer, mime=False):
if mime:
if 'PDF' in str(buffer):
return 'application/pdf'
if "PDF" in str(buffer):
return "application/pdf"
else:
return 'unknown/type'
return "unknown/type"
else:
return 'Some verbose file description'
return "Some verbose file description"
@mock.patch('paperless_mail.mail.magic.from_buffer', fake_magic_from_buffer)
@mock.patch("paperless_mail.mail.magic.from_buffer", fake_magic_from_buffer)
class TestMail(DirectoriesMixin, TestCase):
def setUp(self):
patcher = mock.patch('paperless_mail.mail.MailBox')
patcher = mock.patch("paperless_mail.mail.MailBox")
m = patcher.start()
self.bogus_mailbox = BogusMailBox()
m.return_value = self.bogus_mailbox
self.addCleanup(patcher.stop)
patcher = mock.patch('paperless_mail.mail.async_task')
patcher = mock.patch("paperless_mail.mail.async_task")
self.async_task = patcher.start()
self.addCleanup(patcher.stop)
@@ -153,28 +159,53 @@ class TestMail(DirectoriesMixin, TestCase):
def reset_bogus_mailbox(self):
self.bogus_mailbox.messages = []
self.bogus_mailbox.messages_spam = []
self.bogus_mailbox.messages.append(create_message(subject="Invoice 1", from_="amazon@amazon.de", body="cables", seen=True, flagged=False))
self.bogus_mailbox.messages.append(create_message(subject="Invoice 2", body="from my favorite electronic store", seen=False, flagged=True))
self.bogus_mailbox.messages.append(create_message(subject="Claim your $10M price now!", from_="amazon@amazon-some-indian-site.org", seen=False))
self.bogus_mailbox.messages.append(
create_message(
subject="Invoice 1",
from_="amazon@amazon.de",
body="cables",
seen=True,
flagged=False,
)
)
self.bogus_mailbox.messages.append(
create_message(
subject="Invoice 2",
body="from my favorite electronic store",
seen=False,
flagged=True,
)
)
self.bogus_mailbox.messages.append(
create_message(
subject="Claim your $10M price now!",
from_="amazon@amazon-some-indian-site.org",
seen=False,
)
)
def test_get_correspondent(self):
message = namedtuple('MailMessage', [])
message = namedtuple("MailMessage", [])
message.from_ = "someone@somewhere.com"
message.from_values = {'name': "Someone!", 'email': "someone@somewhere.com"}
message.from_values = {"name": "Someone!", "email": "someone@somewhere.com"}
message2 = namedtuple('MailMessage', [])
message2 = namedtuple("MailMessage", [])
message2.from_ = "me@localhost.com"
message2.from_values = {'name': "", 'email': "fake@localhost.com"}
message2.from_values = {"name": "", "email": "fake@localhost.com"}
me_localhost = Correspondent.objects.create(name=message2.from_)
someone_else = Correspondent.objects.create(name="someone else")
handler = MailAccountHandler()
rule = MailRule(name="a", assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NOTHING)
rule = MailRule(
name="a", assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NOTHING
)
self.assertIsNone(handler.get_correspondent(message, rule))
rule = MailRule(name="b", assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL)
rule = MailRule(
name="b", assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL
)
c = handler.get_correspondent(message, rule)
self.assertIsNotNone(c)
self.assertEqual(c.name, "someone@somewhere.com")
@@ -183,7 +214,9 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(c.name, "me@localhost.com")
self.assertEqual(c.id, me_localhost.id)
rule = MailRule(name="c", assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NAME)
rule = MailRule(
name="c", assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NAME
)
c = handler.get_correspondent(message, rule)
self.assertIsNotNone(c)
self.assertEqual(c.name, "Someone!")
@@ -191,14 +224,18 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertIsNotNone(c)
self.assertEqual(c.id, me_localhost.id)
rule = MailRule(name="d", assign_correspondent_from=MailRule.CORRESPONDENT_FROM_CUSTOM, assign_correspondent=someone_else)
rule = MailRule(
name="d",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_CUSTOM,
assign_correspondent=someone_else,
)
c = handler.get_correspondent(message, rule)
self.assertEqual(c, someone_else)
def test_get_title(self):
message = namedtuple('MailMessage', [])
message = namedtuple("MailMessage", [])
message.subject = "the message title"
att = namedtuple('Attachment', [])
att = namedtuple("Attachment", [])
att.filename = "this_is_the_file.pdf"
handler = MailAccountHandler()
@@ -209,7 +246,9 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(handler.get_title(message, att, rule), "the message title")
def test_handle_message(self):
message = create_message(subject="the message title", from_="Myself", num_attachments=2)
message = create_message(
subject="the message title", from_="Myself", num_attachments=2
)
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account)
@@ -223,18 +262,18 @@ class TestMail(DirectoriesMixin, TestCase):
args1, kwargs1 = self.async_task.call_args_list[0]
args2, kwargs2 = self.async_task.call_args_list[1]
self.assertTrue(os.path.isfile(kwargs1['path']), kwargs1['path'])
self.assertTrue(os.path.isfile(kwargs1["path"]), kwargs1["path"])
self.assertEqual(kwargs1['override_title'], "file_0")
self.assertEqual(kwargs1['override_filename'], "file_0.pdf")
self.assertEqual(kwargs1["override_title"], "file_0")
self.assertEqual(kwargs1["override_filename"], "file_0.pdf")
self.assertTrue(os.path.isfile(kwargs2['path']), kwargs1['path'])
self.assertTrue(os.path.isfile(kwargs2["path"]), kwargs1["path"])
self.assertEqual(kwargs2['override_title'], "file_1")
self.assertEqual(kwargs2['override_filename'], "file_1.pdf")
self.assertEqual(kwargs2["override_title"], "file_1")
self.assertEqual(kwargs2["override_filename"], "file_1.pdf")
def test_handle_empty_message(self):
message = namedtuple('MailMessage', [])
message = namedtuple("MailMessage", [])
message.attachments = []
rule = MailRule()
@@ -248,7 +287,10 @@ class TestMail(DirectoriesMixin, TestCase):
message = create_message()
message.attachments = [
create_attachment(filename="f1.pdf"),
create_attachment(filename="f2.json", payload=b"{'much': 'payload.', 'so': 'json', 'wow': true}")
create_attachment(
filename="f2.json",
payload=b"{'much': 'payload.', 'so': 'json', 'wow': true}",
),
]
account = MailAccount()
@@ -260,14 +302,14 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(self.async_task.call_count, 1)
args, kwargs = self.async_task.call_args
self.assertTrue(os.path.isfile(kwargs['path']), kwargs['path'])
self.assertEqual(kwargs['override_filename'], "f1.pdf")
self.assertTrue(os.path.isfile(kwargs["path"]), kwargs["path"])
self.assertEqual(kwargs["override_filename"], "f1.pdf")
def test_handle_disposition(self):
message = create_message()
message.attachments = [
create_attachment(filename="f1.pdf", content_disposition='inline'),
create_attachment(filename="f2.pdf", content_disposition='attachment')
create_attachment(filename="f1.pdf", content_disposition="inline"),
create_attachment(filename="f2.pdf", content_disposition="attachment"),
]
account = MailAccount()
@@ -279,17 +321,21 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(self.async_task.call_count, 1)
args, kwargs = self.async_task.call_args
self.assertEqual(kwargs['override_filename'], "f2.pdf")
self.assertEqual(kwargs["override_filename"], "f2.pdf")
def test_handle_inline_files(self):
message = create_message()
message.attachments = [
create_attachment(filename="f1.pdf", content_disposition='inline'),
create_attachment(filename="f2.pdf", content_disposition='attachment')
create_attachment(filename="f1.pdf", content_disposition="inline"),
create_attachment(filename="f2.pdf", content_disposition="attachment"),
]
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account, attachment_type=MailRule.ATTACHMENT_TYPE_EVERYTHING)
rule = MailRule(
assign_title_from=MailRule.TITLE_FROM_FILENAME,
account=account,
attachment_type=MailRule.ATTACHMENT_TYPE_EVERYTHING,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -316,19 +362,29 @@ class TestMail(DirectoriesMixin, TestCase):
for (pattern, matches) in tests:
self.async_task.reset_mock()
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account, filter_attachment_filename=pattern)
rule = MailRule(
assign_title_from=MailRule.TITLE_FROM_FILENAME,
account=account,
filter_attachment_filename=pattern,
)
result = self.mail_account_handler.handle_message(message, rule)
self.assertEqual(result, len(matches))
filenames = [a[1]['override_filename'] for a in self.async_task.call_args_list]
filenames = [
a[1]["override_filename"] for a in self.async_task.call_args_list
]
self.assertCountEqual(filenames, matches)
def test_handle_mail_account_mark_read(self):
account = MailAccount.objects.create(name="test", imap_server="", username="admin", password="secret")
account = MailAccount.objects.create(
name="test", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(name="testrule", account=account, action=MailRule.ACTION_MARK_READ)
rule = MailRule.objects.create(
name="testrule", account=account, action=MailRule.ACTION_MARK_READ
)
self.assertEqual(len(self.bogus_mailbox.messages), 3)
self.assertEqual(self.async_task.call_count, 0)
@@ -340,9 +396,16 @@ class TestMail(DirectoriesMixin, TestCase):
def test_handle_mail_account_delete(self):
account = MailAccount.objects.create(name="test", imap_server="", username="admin", password="secret")
account = MailAccount.objects.create(
name="test", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(name="testrule", account=account, action=MailRule.ACTION_DELETE, filter_subject="Invoice")
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_DELETE,
filter_subject="Invoice",
)
self.assertEqual(self.async_task.call_count, 0)
self.assertEqual(len(self.bogus_mailbox.messages), 3)
@@ -351,9 +414,16 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(len(self.bogus_mailbox.messages), 1)
def test_handle_mail_account_flag(self):
account = MailAccount.objects.create(name="test", imap_server="", username="admin", password="secret")
account = MailAccount.objects.create(
name="test", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(name="testrule", account=account, action=MailRule.ACTION_FLAG, filter_subject="Invoice")
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_FLAG,
filter_subject="Invoice",
)
self.assertEqual(len(self.bogus_mailbox.messages), 3)
self.assertEqual(self.async_task.call_count, 0)
@@ -364,9 +434,17 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(len(self.bogus_mailbox.messages), 3)
def test_handle_mail_account_move(self):
account = MailAccount.objects.create(name="test", imap_server="", username="admin", password="secret")
account = MailAccount.objects.create(
name="test", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(name="testrule", account=account, action=MailRule.ACTION_MOVE, action_parameter="spam", filter_subject="Claim")
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action_parameter="spam",
filter_subject="Claim",
)
self.assertEqual(self.async_task.call_count, 0)
self.assertEqual(len(self.bogus_mailbox.messages), 3)
@@ -377,7 +455,9 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(len(self.bogus_mailbox.messages_spam), 1)
def test_error_login(self):
account = MailAccount.objects.create(name="test", imap_server="", username="admin", password="wrong")
account = MailAccount.objects.create(
name="test", imap_server="", username="admin", password="wrong"
)
try:
self.mail_account_handler.handle_mail_account(account)
@@ -387,11 +467,20 @@ class TestMail(DirectoriesMixin, TestCase):
self.fail("Should raise exception")
def test_error_skip_account(self):
account_faulty = MailAccount.objects.create(name="test", imap_server="", username="admin", password="wroasdng")
account_faulty = MailAccount.objects.create(
name="test", imap_server="", username="admin", password="wroasdng"
)
account = MailAccount.objects.create(name="test2", imap_server="", username="admin", password="secret")
rule = MailRule.objects.create(name="testrule", account=account, action=MailRule.ACTION_MOVE,
action_parameter="spam", filter_subject="Claim")
account = MailAccount.objects.create(
name="test2", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action_parameter="spam",
filter_subject="Claim",
)
tasks.process_mail_accounts()
self.assertEqual(self.async_task.call_count, 1)
@@ -400,31 +489,51 @@ class TestMail(DirectoriesMixin, TestCase):
def test_error_skip_rule(self):
account = MailAccount.objects.create(name="test2", imap_server="", username="admin", password="secret")
rule = MailRule.objects.create(name="testrule", account=account, action=MailRule.ACTION_MOVE,
action_parameter="spam", filter_subject="Claim", order=1, folder="uuuhhhh")
rule2 = MailRule.objects.create(name="testrule2", account=account, action=MailRule.ACTION_MOVE,
action_parameter="spam", filter_subject="Claim", order=2)
account = MailAccount.objects.create(
name="test2", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action_parameter="spam",
filter_subject="Claim",
order=1,
folder="uuuhhhh",
)
rule2 = MailRule.objects.create(
name="testrule2",
account=account,
action=MailRule.ACTION_MOVE,
action_parameter="spam",
filter_subject="Claim",
order=2,
)
self.mail_account_handler.handle_mail_account(account)
self.assertEqual(self.async_task.call_count, 1)
self.assertEqual(len(self.bogus_mailbox.messages), 2)
self.assertEqual(len(self.bogus_mailbox.messages_spam), 1)
@mock.patch("paperless_mail.mail.MailAccountHandler.get_correspondent")
def test_error_skip_mail(self, m):
def get_correspondent_fake(message, rule):
if message.from_ == 'amazon@amazon.de':
if message.from_ == "amazon@amazon.de":
raise ValueError("Does not compute.")
else:
return None
m.side_effect = get_correspondent_fake
account = MailAccount.objects.create(name="test2", imap_server="", username="admin", password="secret")
rule = MailRule.objects.create(name="testrule", account=account, action=MailRule.ACTION_MOVE, action_parameter="spam")
account = MailAccount.objects.create(
name="test2", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action_parameter="spam",
)
self.mail_account_handler.handle_mail_account(account)
@@ -433,15 +542,21 @@ class TestMail(DirectoriesMixin, TestCase):
# faulty mail still in inbox, untouched
self.assertEqual(len(self.bogus_mailbox.messages), 1)
self.assertEqual(self.bogus_mailbox.messages[0].from_, 'amazon@amazon.de')
self.assertEqual(self.bogus_mailbox.messages[0].from_, "amazon@amazon.de")
def test_error_create_correspondent(self):
account = MailAccount.objects.create(name="test2", imap_server="", username="admin", password="secret")
account = MailAccount.objects.create(
name="test2", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(
name="testrule", filter_from="amazon@amazon.de",
account=account, action=MailRule.ACTION_MOVE, action_parameter="spam",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL)
name="testrule",
filter_from="amazon@amazon.de",
account=account,
action=MailRule.ACTION_MOVE,
action_parameter="spam",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL,
)
self.mail_account_handler.handle_mail_account(account)
@@ -450,7 +565,7 @@ class TestMail(DirectoriesMixin, TestCase):
c = Correspondent.objects.get(name="amazon@amazon.de")
# should work
self.assertEqual(kwargs['override_correspondent_id'], c.id)
self.assertEqual(kwargs["override_correspondent_id"], c.id)
self.async_task.reset_mock()
self.reset_bogus_mailbox()
@@ -462,13 +577,19 @@ class TestMail(DirectoriesMixin, TestCase):
args, kwargs = self.async_task.call_args
self.async_task.assert_called_once()
self.assertEqual(kwargs['override_correspondent_id'], None)
self.assertEqual(kwargs["override_correspondent_id"], None)
def test_filters(self):
account = MailAccount.objects.create(name="test3", imap_server="", username="admin", password="secret")
rule = MailRule.objects.create(name="testrule3", account=account, action=MailRule.ACTION_DELETE, filter_subject="Claim")
account = MailAccount.objects.create(
name="test3", imap_server="", username="admin", password="secret"
)
rule = MailRule.objects.create(
name="testrule3",
account=account,
action=MailRule.ACTION_DELETE,
filter_subject="Claim",
)
self.assertEqual(self.async_task.call_count, 0)
@@ -508,23 +629,29 @@ class TestMail(DirectoriesMixin, TestCase):
self.assertEqual(len(self.bogus_mailbox.messages), 2)
self.assertEqual(self.async_task.call_count, 5)
class TestManagementCommand(TestCase):
@mock.patch("paperless_mail.management.commands.mail_fetcher.tasks.process_mail_accounts")
class TestManagementCommand(TestCase):
@mock.patch(
"paperless_mail.management.commands.mail_fetcher.tasks.process_mail_accounts"
)
def test_mail_fetcher(self, m):
call_command("mail_fetcher")
m.assert_called_once()
class TestTasks(TestCase):
class TestTasks(TestCase):
@mock.patch("paperless_mail.tasks.MailAccountHandler.handle_mail_account")
def test_all_accounts(self, m):
m.side_effect = lambda account: 6
MailAccount.objects.create(name="A", imap_server="A", username="A", password="A")
MailAccount.objects.create(name="B", imap_server="A", username="A", password="A")
MailAccount.objects.create(
name="A", imap_server="A", username="A", password="A"
)
MailAccount.objects.create(
name="B", imap_server="A", username="A", password="A"
)
result = tasks.process_mail_accounts()
@@ -538,7 +665,9 @@ class TestTasks(TestCase):
@mock.patch("paperless_mail.tasks.MailAccountHandler.handle_mail_account")
def test_single_accounts(self, m):
MailAccount.objects.create(name="A", imap_server="A", username="A", password="A")
MailAccount.objects.create(
name="A", imap_server="A", username="A", password="A"
)
tasks.process_mail_account("A")