containing the configuration parameters. Be sure to use the correct format
and watch out for indentation if editing the YAML file.
+### Email Parsing
+
+#### [`PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT=<int>`(#PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT) {#PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT}
+
+: The default layout to use for emails that are consumed as documents. Must be one of the integer choices below. Note that mail
+rules can specify this setting, thus this fallback is used for the default selection and for .eml files consumed by other means.
+
+ - `1` = Text, then HTML
+ - `2` = HTML, then text
+ - `3` = HTML only
+ - `4` = Text only
+
## Paths and folders
#### [`PAPERLESS_CONSUMPTION_DIR=<path>`](#PAPERLESS_CONSUMPTION_DIR) {#PAPERLESS_CONSUMPTION_DIR}
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">75</context>
+ <context context-type="linenumber">76</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">74</context>
+ <context context-type="linenumber">75</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">64</context>
+ <context context-type="linenumber">88</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">43</context>
</context-group>
</trans-unit>
+ <trans-unit id="3842519365862452117" datatype="html">
+ <source>PDF layout</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
+ <context context-type="linenumber">44</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="2873939123535615966" datatype="html">
<source>Include only files matching</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">46</context>
+ <context context-type="linenumber">47</context>
</context-group>
</trans-unit>
<trans-unit id="7233407036155150477" datatype="html">
<source>Optional. Wildcards e.g. *.pdf or *invoice* allowed. Can be comma-separated list. Case insensitive.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">46</context>
+ <context context-type="linenumber">47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">47</context>
+ <context context-type="linenumber">48</context>
</context-group>
</trans-unit>
<trans-unit id="1546332577833742677" datatype="html">
<source>Exclude files matching</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">47</context>
+ <context context-type="linenumber">48</context>
</context-group>
</trans-unit>
<trans-unit id="9216117865911519658" datatype="html">
<source>Action</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">53</context>
+ <context context-type="linenumber">54</context>
</context-group>
</trans-unit>
<trans-unit id="7841986067387421166" datatype="html">
<source>Only performed if the mail is processed.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">53</context>
+ <context context-type="linenumber">54</context>
</context-group>
</trans-unit>
<trans-unit id="1261794314435932203" datatype="html">
<source>Action parameter</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">55</context>
+ <context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="6093797930511670257" datatype="html">
<source>Assign title from</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">57</context>
+ <context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="5232720756589450549" datatype="html">
<source>Assign owner from rule</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">58</context>
+ <context context-type="linenumber">59</context>
</context-group>
</trans-unit>
<trans-unit id="6695990587380209737" datatype="html">
<source>Assign document type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">62</context>
+ <context context-type="linenumber">63</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<source>Assign correspondent from</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">63</context>
+ <context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="4875491778188965469" datatype="html">
<source>Assign correspondent</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">65</context>
+ <context context-type="linenumber">66</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<source>Error</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
- <context context-type="linenumber">72</context>
+ <context context-type="linenumber">73</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<source>Only process attachments</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">38</context>
+ <context context-type="linenumber">39</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">49</context>
+ <context context-type="linenumber">50</context>
</context-group>
</trans-unit>
<trans-unit id="936923743212522897" datatype="html">
<source>Process all files, including 'inline' attachments</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">42</context>
+ <context context-type="linenumber">43</context>
</context-group>
</trans-unit>
<trans-unit id="9025522236384167767" datatype="html">
<source>Process message as .eml</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">53</context>
+ <context context-type="linenumber">54</context>
</context-group>
</trans-unit>
<trans-unit id="7411485377918318115" datatype="html">
<source>Process message as .eml and attachments separately</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">57</context>
+ <context context-type="linenumber">58</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="8776300244268604360" datatype="html">
+ <source>System default</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
+ <context context-type="linenumber">65</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="4812910224365219000" datatype="html">
+ <source>Text, then HTML</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
+ <context context-type="linenumber">69</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="3181744476823286470" datatype="html">
+ <source>HTML, then text</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
+ <context context-type="linenumber">73</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="9048933760263399623" datatype="html">
+ <source>HTML only</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
+ <context context-type="linenumber">77</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="3835211125655594627" datatype="html">
+ <source>Text only</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
+ <context context-type="linenumber">81</context>
</context-group>
</trans-unit>
<trans-unit id="2784260611081866636" datatype="html">
<source>Move to specified folder</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">68</context>
+ <context context-type="linenumber">92</context>
</context-group>
</trans-unit>
<trans-unit id="4593278936733161020" datatype="html">
<source>Mark as read, don't process read mails</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">72</context>
+ <context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="2378921144019636516" datatype="html">
<source>Flag the mail, don't process flagged mails</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">76</context>
+ <context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="6457024618858980302" datatype="html">
<source>Tag the mail with specified tag, don't process tagged mails</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">80</context>
+ <context context-type="linenumber">104</context>
</context-group>
</trans-unit>
<trans-unit id="4673329664686432878" datatype="html">
<source>Use subject as title</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">87</context>
+ <context context-type="linenumber">111</context>
</context-group>
</trans-unit>
<trans-unit id="8645471396972938185" datatype="html">
<source>Use attachment filename as title</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">91</context>
+ <context context-type="linenumber">115</context>
</context-group>
</trans-unit>
<trans-unit id="2881879110886196973" datatype="html">
<source>Do not assign title from this rule</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">95</context>
+ <context context-type="linenumber">119</context>
</context-group>
</trans-unit>
<trans-unit id="1568902914205618549" datatype="html">
<source>Do not assign a correspondent</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">102</context>
+ <context context-type="linenumber">126</context>
</context-group>
</trans-unit>
<trans-unit id="3567746385454588269" datatype="html">
<source>Use mail address</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">106</context>
+ <context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit id="445154175758965852" datatype="html">
<source>Use name (or mail address if not available)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">110</context>
+ <context context-type="linenumber">134</context>
</context-group>
</trans-unit>
<trans-unit id="1258862217749148424" datatype="html">
<source>Use correspondent selected below</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">114</context>
+ <context context-type="linenumber">138</context>
</context-group>
</trans-unit>
<trans-unit id="3147349817770432927" datatype="html">
<source>Create new mail rule</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">166</context>
+ <context context-type="linenumber">190</context>
</context-group>
</trans-unit>
<trans-unit id="3374331029704382439" datatype="html">
<source>Edit mail rule</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
- <context context-type="linenumber">170</context>
+ <context context-type="linenumber">194</context>
</context-group>
</trans-unit>
<trans-unit id="8911059720204770105" datatype="html">
<div class="col-md-6">
<pngx-input-select [horizontal]="true" i18n-title title="Consumption scope" [items]="consumptionScopeOptions" formControlName="consumption_scope" i18n-hint hint="See docs for .eml processing requirements"></pngx-input-select>
<pngx-input-select [horizontal]="true" i18n-title title="Attachment type" [items]="attachmentTypeOptions" formControlName="attachment_type"></pngx-input-select>
+ <pngx-input-select [horizontal]="true" i18n-title title="PDF layout" [items]="pdfLayoutOptions" formControlName="pdf_layout"></pngx-input-select>
</div>
<div class="col-md-6">
<pngx-input-text [horizontal]="true" i18n-title title="Include only files matching" formControlName="filter_attachment_filename_include" i18n-hint hint="Optional. Wildcards e.g. *.pdf or *invoice* allowed. Can be comma-separated list. Case insensitive." [error]="error?.filter_attachment_filename_include"></pngx-input-text>
MailMetadataTitleOption,
MailRule,
MailRuleConsumptionScope,
+ MailRulePdfLayout,
} from 'src/app/data/mail-rule'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
},
]
+const PDF_LAYOUT_OPTIONS = [
+ {
+ id: MailRulePdfLayout.Default,
+ name: $localize`System default`,
+ },
+ {
+ id: MailRulePdfLayout.TextHtml,
+ name: $localize`Text, then HTML`,
+ },
+ {
+ id: MailRulePdfLayout.HtmlText,
+ name: $localize`HTML, then text`,
+ },
+ {
+ id: MailRulePdfLayout.HtmlOnly,
+ name: $localize`HTML only`,
+ },
+ {
+ id: MailRulePdfLayout.TextOnly,
+ name: $localize`Text only`,
+ },
+]
+
const ACTION_OPTIONS = [
{
id: MailAction.Delete,
filter_attachment_filename_exclude: new FormControl(null),
maximum_age: new FormControl(null),
attachment_type: new FormControl(MailFilterAttachmentType.Attachments),
+ pdf_layout: new FormControl(MailRulePdfLayout.Default),
consumption_scope: new FormControl(MailRuleConsumptionScope.Attachments),
order: new FormControl(null),
action: new FormControl(MailAction.MarkRead),
get consumptionScopeOptions() {
return CONSUMPTION_SCOPE_OPTIONS
}
+
+ get pdfLayoutOptions() {
+ return PDF_LAYOUT_OPTIONS
+ }
}
Everything = 3,
}
+export enum MailRulePdfLayout {
+ Default = 0,
+ TextHtml = 1,
+ HtmlText = 2,
+ HtmlOnly = 3,
+ TextOnly = 4,
+}
+
export enum MailAction {
Delete = 1,
Move = 2,
attachment_type: MailFilterAttachmentType
+ pdf_layout: MailRulePdfLayout
+
action: MailAction
action_parameter?: string
from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats
from documents.utils import run_subprocess
+from paperless_mail.parsers import MailDocumentParser
class WorkflowTriggerPlugin(
ConsumerStatusShortMessage.PARSING_DOCUMENT,
)
self.log.debug(f"Parsing {self.filename}...")
- document_parser.parse(self.working_copy, mime_type, self.filename)
+ if (
+ isinstance(document_parser, MailDocumentParser)
+ and self.input_doc.mailrule_id
+ ):
+ document_parser.parse(
+ self.working_copy,
+ mime_type,
+ self.filename,
+ self.input_doc.mailrule_id,
+ )
+ else:
+ document_parser.parse(self.working_copy, mime_type, self.filename)
self.log.debug(f"Generating thumbnail for {self.filename}...")
self._send_progress(
from documents.consumer import ConsumerError
from documents.data_models import DocumentMetadataOverrides
+from documents.data_models import DocumentSource
from documents.models import Correspondent
from documents.models import CustomField
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin
from documents.tests.utils import GetConsumerMixin
+from paperless_mail.models import MailRule
+from paperless_mail.parsers import MailDocumentParser
class TestAttributes(UnittestTestCase):
return "image/png"
elif os.path.splitext(file)[1] == ".webp":
return "image/webp"
+ elif os.path.splitext(file)[1] == ".eml":
+ return "message/rfc822"
else:
return "unknown"
else:
self.assertEqual(command[0], "qpdf")
self.assertEqual(command[1], "--replace-input")
+ @mock.patch("paperless_mail.models.MailRule.objects.get")
+ @mock.patch("paperless_mail.parsers.MailDocumentParser.parse")
+ @mock.patch("documents.parsers.document_consumer_declaration.send")
+ def test_mail_parser_receives_mailrule(
+ self,
+ mock_consumer_declaration_send: mock.Mock,
+ mock_mail_parser_parse: mock.Mock,
+ mock_mailrule_get: mock.Mock,
+ ):
+ """
+ GIVEN:
+ - A mail document from a mail rule
+ WHEN:
+ - The consumer is run
+ THEN:
+ - The mail parser should receive the mail rule
+ """
+ mock_consumer_declaration_send.return_value = [
+ (
+ None,
+ {
+ "parser": MailDocumentParser,
+ "mime_types": {"message/rfc822": ".eml"},
+ "weight": 0,
+ },
+ ),
+ ]
+ mock_mailrule_get.return_value = mock.Mock(
+ pdf_layout=MailRule.PdfLayout.HTML_ONLY,
+ )
+ with self.get_consumer(
+ filepath=(
+ Path(__file__).parent.parent.parent
+ / Path("paperless_mail")
+ / Path("tests")
+ / Path("samples")
+ ).resolve()
+ / "html.eml",
+ source=DocumentSource.MailFetch,
+ mailrule_id=1,
+ ) as consumer:
+ # fails because no gotenberg
+ with self.assertRaises(
+ ConsumerError,
+ ):
+ consumer.run()
+ mock_mail_parser_parse.assert_called_once_with(
+ consumer.working_copy,
+ "message/rfc822",
+ file_name="sample.pdf",
+ mailrule=mock_mailrule_get.return_value,
+ )
+
@mock.patch("documents.consumer.magic.from_file", fake_magic_from_file)
class TestConsumerCreatedDate(DirectoriesMixin, GetConsumerMixin, TestCase):
dependencies = (
(
"paperless_mail",
- "0028_alter_mailaccount_password_and_more",
+ "0029_mailrule_pdf_layout",
),
)
filepath: Path,
overrides: DocumentMetadataOverrides | None = None,
source: DocumentSource = DocumentSource.ConsumeFolder,
+ mailrule_id: int | None = None,
) -> Generator[ConsumerPlugin, None, None]:
# Store this for verification
self.status = DummyProgressManager(filepath.name, None)
reader = ConsumerPlugin(
- ConsumableDocument(source, original_file=filepath),
+ ConsumableDocument(
+ source,
+ original_file=filepath,
+ mailrule_id=mailrule_id or None,
+ ),
overrides or DocumentMetadataOverrides(),
self.status, # type: ignore
self.dirs.scratch_dir,
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-01-27 08:19-0800\n"
+"POT-Creation-Date: 2025-01-28 12:17-0800\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
msgstr ""
#: documents/models.py:67 documents/models.py:433 documents/models.py:1493
-#: paperless_mail/models.py:23 paperless_mail/models.py:136
+#: paperless_mail/models.py:23 paperless_mail/models.py:143
msgid "name"
msgstr ""
msgid "warning"
msgstr ""
-#: documents/models.py:387 paperless_mail/models.py:350
+#: documents/models.py:387 paperless_mail/models.py:363
msgid "error"
msgstr ""
msgid "filter filename"
msgstr ""
-#: documents/models.py:1066 paperless_mail/models.py:193
+#: documents/models.py:1066 paperless_mail/models.py:200
msgid ""
"Only consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
"Assign a document title, can include some placeholders, see documentation."
msgstr ""
-#: documents/models.py:1287 paperless_mail/models.py:261
+#: documents/models.py:1287 paperless_mail/models.py:274
msgid "assign this tag"
msgstr ""
-#: documents/models.py:1296 paperless_mail/models.py:269
+#: documents/models.py:1296 paperless_mail/models.py:282
msgid "assign this document type"
msgstr ""
-#: documents/models.py:1305 paperless_mail/models.py:283
+#: documents/models.py:1305 paperless_mail/models.py:296
msgid "assign this correspondent"
msgstr ""
msgid "workflow actions"
msgstr ""
-#: documents/models.py:1495 paperless_mail/models.py:138
+#: documents/models.py:1495 paperless_mail/models.py:145
msgid "order"
msgstr ""
msgid "actions"
msgstr ""
-#: documents/models.py:1511 paperless_mail/models.py:147
+#: documents/models.py:1511 paperless_mail/models.py:154
msgid "enabled"
msgstr ""
msgstr ""
#: paperless_mail/models.py:119
-msgid "Delete"
+msgid "System default"
msgstr ""
#: paperless_mail/models.py:120
-msgid "Move to specified folder"
+msgid "Text, then HTML"
msgstr ""
#: paperless_mail/models.py:121
-msgid "Mark as read, don't process read mails"
+msgid "HTML, then text"
msgstr ""
#: paperless_mail/models.py:122
-msgid "Flag the mail, don't process flagged mails"
+msgid "HTML only"
msgstr ""
#: paperless_mail/models.py:123
-msgid "Tag the mail with specified tag, don't process tagged mails"
+msgid "Text only"
msgstr ""
#: paperless_mail/models.py:126
-msgid "Use subject as title"
+msgid "Delete"
msgstr ""
#: paperless_mail/models.py:127
-msgid "Use attachment filename as title"
+msgid "Move to specified folder"
msgstr ""
#: paperless_mail/models.py:128
+msgid "Mark as read, don't process read mails"
+msgstr ""
+
+#: paperless_mail/models.py:129
+msgid "Flag the mail, don't process flagged mails"
+msgstr ""
+
+#: paperless_mail/models.py:130
+msgid "Tag the mail with specified tag, don't process tagged mails"
+msgstr ""
+
+#: paperless_mail/models.py:133
+msgid "Use subject as title"
+msgstr ""
+
+#: paperless_mail/models.py:134
+msgid "Use attachment filename as title"
+msgstr ""
+
+#: paperless_mail/models.py:135
msgid "Do not assign title from rule"
msgstr ""
-#: paperless_mail/models.py:131
+#: paperless_mail/models.py:138
msgid "Do not assign a correspondent"
msgstr ""
-#: paperless_mail/models.py:132
+#: paperless_mail/models.py:139
msgid "Use mail address"
msgstr ""
-#: paperless_mail/models.py:133
+#: paperless_mail/models.py:140
msgid "Use name (or mail address if not available)"
msgstr ""
-#: paperless_mail/models.py:134
+#: paperless_mail/models.py:141
msgid "Use correspondent selected below"
msgstr ""
-#: paperless_mail/models.py:144
+#: paperless_mail/models.py:151
msgid "account"
msgstr ""
-#: paperless_mail/models.py:150 paperless_mail/models.py:305
+#: paperless_mail/models.py:157 paperless_mail/models.py:318
msgid "folder"
msgstr ""
-#: paperless_mail/models.py:154
+#: paperless_mail/models.py:161
msgid ""
"Subfolders must be separated by a delimiter, often a dot ('.') or slash "
"('/'), but it varies by mail server."
msgstr ""
-#: paperless_mail/models.py:160
+#: paperless_mail/models.py:167
msgid "filter from"
msgstr ""
-#: paperless_mail/models.py:167
+#: paperless_mail/models.py:174
msgid "filter to"
msgstr ""
-#: paperless_mail/models.py:174
+#: paperless_mail/models.py:181
msgid "filter subject"
msgstr ""
-#: paperless_mail/models.py:181
+#: paperless_mail/models.py:188
msgid "filter body"
msgstr ""
-#: paperless_mail/models.py:188
+#: paperless_mail/models.py:195
msgid "filter attachment filename inclusive"
msgstr ""
-#: paperless_mail/models.py:200
+#: paperless_mail/models.py:207
msgid "filter attachment filename exclusive"
msgstr ""
-#: paperless_mail/models.py:205
+#: paperless_mail/models.py:212
msgid ""
"Do not consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
-#: paperless_mail/models.py:212
+#: paperless_mail/models.py:219
msgid "maximum age"
msgstr ""
-#: paperless_mail/models.py:214
+#: paperless_mail/models.py:221
msgid "Specified in days."
msgstr ""
-#: paperless_mail/models.py:218
+#: paperless_mail/models.py:225
msgid "attachment type"
msgstr ""
-#: paperless_mail/models.py:222
+#: paperless_mail/models.py:229
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgstr ""
-#: paperless_mail/models.py:228
+#: paperless_mail/models.py:235
msgid "consumption scope"
msgstr ""
-#: paperless_mail/models.py:234
+#: paperless_mail/models.py:241
+msgid "pdf layout"
+msgstr ""
+
+#: paperless_mail/models.py:247
msgid "action"
msgstr ""
-#: paperless_mail/models.py:240
+#: paperless_mail/models.py:253
msgid "action parameter"
msgstr ""
-#: paperless_mail/models.py:245
+#: paperless_mail/models.py:258
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action. Subfolders must be separated by dots."
msgstr ""
-#: paperless_mail/models.py:253
+#: paperless_mail/models.py:266
msgid "assign title from"
msgstr ""
-#: paperless_mail/models.py:273
+#: paperless_mail/models.py:286
msgid "assign correspondent from"
msgstr ""
-#: paperless_mail/models.py:287
+#: paperless_mail/models.py:300
msgid "Assign the rule owner to documents"
msgstr ""
-#: paperless_mail/models.py:313
+#: paperless_mail/models.py:326
msgid "uid"
msgstr ""
-#: paperless_mail/models.py:321
+#: paperless_mail/models.py:334
msgid "subject"
msgstr ""
-#: paperless_mail/models.py:329
+#: paperless_mail/models.py:342
msgid "received"
msgstr ""
-#: paperless_mail/models.py:336
+#: paperless_mail/models.py:349
msgid "processed"
msgstr ""
-#: paperless_mail/models.py:342
+#: paperless_mail/models.py:355
msgid "status"
msgstr ""
GS_BINARY = os.getenv("PAPERLESS_GS_BINARY", "gs")
+# Fallback layout for .eml consumption
+EMAIL_PARSE_DEFAULT_LAYOUT = __get_int(
+ "PAPERLESS_EMAIL_PARSE_DEFAULT_LAYOUT",
+ 1, # MailRule.PdfLayout.TEXT_HTML but that can't be imported here
+)
# Pre-2.x versions of Paperless stored your documents locally with GPG
# encryption, but that is no longer the default. This behaviour is still
--- /dev/null
+# Generated by Django 5.1.3 on 2024-11-24 12:39
+
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("paperless_mail", "0028_alter_mailaccount_password_and_more"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="mailrule",
+ name="pdf_layout",
+ field=models.PositiveIntegerField(
+ choices=[
+ (0, "System default"),
+ (1, "Text, then HTML"),
+ (2, "HTML, then text"),
+ (3, "HTML only"),
+ (4, "Text only"),
+ ],
+ default=0,
+ verbose_name="pdf layout",
+ ),
+ ),
+ ]
ATTACHMENTS_ONLY = 1, _("Only process attachments.")
EVERYTHING = 2, _("Process all files, including 'inline' attachments.")
+ class PdfLayout(models.IntegerChoices):
+ DEFAULT = 0, _("System default")
+ TEXT_HTML = 1, _("Text, then HTML")
+ HTML_TEXT = 2, _("HTML, then text")
+ HTML_ONLY = 3, _("HTML only")
+ TEXT_ONLY = 4, _("Text only")
+
class MailAction(models.IntegerChoices):
DELETE = 1, _("Delete")
MOVE = 2, _("Move to specified folder")
default=ConsumptionScope.ATTACHMENTS_ONLY,
)
+ pdf_layout = models.PositiveIntegerField(
+ _("pdf layout"),
+ choices=PdfLayout.choices,
+ default=PdfLayout.DEFAULT,
+ )
+
action = models.PositiveIntegerField(
_("action"),
choices=MailAction.choices,
from documents.parsers import ParseError
from documents.parsers import make_thumbnail_from_pdf
from paperless.models import OutputTypeChoices
+from paperless_mail.models import MailRule
class MailDocumentParser(DocumentParser):
result.sort(key=lambda item: (item["prefix"], item["key"]))
return result
- def parse(self, document_path: Path, mime_type: str, file_name=None):
+ def parse(
+ self,
+ document_path: Path,
+ mime_type: str,
+ file_name=None,
+ mailrule_id: int | None = None,
+ ):
"""
Parses the given .eml into formatted text, based on the decoded email.
self.date = mail.date
self.log.debug("Creating a PDF from the email")
- self.archive_path = self.generate_pdf(mail)
+ if mailrule_id:
+ rule = MailRule.objects.get(pk=mailrule_id)
+ self.archive_path = self.generate_pdf(mail, rule.pdf_layout)
+ else:
+ self.archive_path = self.generate_pdf(mail)
@staticmethod
def parse_file_to_message(filepath: Path) -> MailMessage:
f"{settings.TIKA_ENDPOINT}: {err}",
) from err
- def generate_pdf(self, mail_message: MailMessage) -> Path:
+ def generate_pdf(
+ self,
+ mail_message: MailMessage,
+ pdf_layout: MailRule.PdfLayout | None = None,
+ ) -> Path:
archive_path = Path(self.tempdir) / "merged.pdf"
mail_pdf_file = self.generate_pdf_from_mail(mail_message)
+ pdf_layout = (
+ pdf_layout or settings.EMAIL_PARSE_DEFAULT_LAYOUT
+ ) # EMAIL_PARSE_DEFAULT_LAYOUT is a MailRule.PdfLayout
+
# If no HTML content, create the PDF from the message
# Otherwise, create 2 PDFs and merge them with Gotenberg
if not mail_message.html:
if pdf_a_format is not None:
route.pdf_format(pdf_a_format)
- route.merge([mail_pdf_file, pdf_of_html_content])
+ match pdf_layout:
+ case MailRule.PdfLayout.HTML_TEXT:
+ route.merge([pdf_of_html_content, mail_pdf_file])
+ case MailRule.PdfLayout.HTML_ONLY:
+ route.merge([pdf_of_html_content])
+ case MailRule.PdfLayout.TEXT_ONLY:
+ route.merge([mail_pdf_file])
+ case MailRule.PdfLayout.TEXT_HTML | _:
+ route.merge([mail_pdf_file, pdf_of_html_content])
try:
response = route.run()
"order",
"attachment_type",
"consumption_scope",
+ "pdf_layout",
"owner",
"user_can_change",
"permissions",
import datetime
import logging
from pathlib import Path
+from unittest import mock
import httpx
import pytest
request = httpx_mock.get_request()
assert str(request.url) == "http://localhost:3000/forms/chromium/convert/html"
+
+ @pytest.mark.httpx_mock(can_send_already_matched_responses=True)
+ @mock.patch("gotenberg_client._merge.MergeRoute.merge")
+ @mock.patch("paperless_mail.models.MailRule.objects.get")
+ def test_generate_pdf_layout_options(
+ self,
+ mock_mailrule_get: mock.Mock,
+ mock_merge_route: mock.Mock,
+ httpx_mock: HTTPXMock,
+ mail_parser: MailDocumentParser,
+ html_email_file: Path,
+ html_email_pdf_file: Path,
+ ):
+ """
+ GIVEN:
+ - Email message
+ WHEN:
+ - Email is parsed with different layout options
+ THEN:
+ - Gotenberg is called with the correct layout option
+ """
+ httpx_mock.add_response(
+ url="http://localhost:9998/tika/text",
+ method="PUT",
+ json={
+ "Content-Type": "text/html",
+ "X-TIKA:Parsed-By": [],
+ "X-TIKA:content": "This is some Tika HTML text",
+ },
+ )
+ httpx_mock.add_response(
+ url="http://localhost:3000/forms/chromium/convert/html",
+ method="POST",
+ content=html_email_pdf_file.read_bytes(),
+ )
+ httpx_mock.add_response(
+ url="http://localhost:3000/forms/pdfengines/merge",
+ method="POST",
+ content=b"Pretend merged PDF content",
+ )
+
+ def test_layout_option(layout_option, expected_calls, expected_pdf_names):
+ mock_mailrule_get.return_value = mock.Mock(pdf_layout=layout_option)
+ mail_parser.parse(
+ document_path=html_email_file,
+ mime_type="message/rfc822",
+ mailrule_id=1,
+ )
+ args, _ = mock_merge_route.call_args
+ assert len(args[0]) == expected_calls
+ for i, pdf in enumerate(expected_pdf_names):
+ assert args[0][i].name == pdf
+
+ # 1 = MailRule.PdfLayout.TEXT_HTML
+ test_layout_option(1, 2, ["email_as_pdf.pdf", "html.pdf"])
+
+ # 2 = MailRule.PdfLayout.HTML_TEXT
+ test_layout_option(2, 2, ["html.pdf", "email_as_pdf.pdf"])
+
+ # 3 = MailRule.PdfLayout.HTML_ONLY
+ test_layout_option(3, 1, ["html.pdf"])
+
+ # 4 = MailRule.PdfLayout.TEXT_ONLY
+ test_layout_option(4, 1, ["email_as_pdf.pdf"])