]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Allow users to set a combined certificte and key file for additional certificates...
authorTrenton H <797416+stumpylog@users.noreply.github.com>
Mon, 21 Aug 2023 20:21:02 +0000 (13:21 -0700)
committerTrenton H <797416+stumpylog@users.noreply.github.com>
Wed, 23 Aug 2023 15:22:01 +0000 (08:22 -0700)
docs/configuration.md
src/paperless/checks.py
src/paperless/settings.py
src/paperless/tests/test_checks.py
src/paperless_mail/mail.py

index c38221e50ea3d3a0cfcf186484685aae1637014d..13e628151132ad29bfc6c1529ec73ac04ed2d750 100644 (file)
@@ -501,6 +501,19 @@ HTTP header/value expected by Django, eg `'["HTTP_X_FORWARDED_PROTO", "https"]'`
     Settings this value has security implications.  Read the Django documentation
     and be sure you understand its usage before setting it.
 
+`PAPERLESS_EMAIL_CERTIFICATE_FILE=<path>`
+
+: Configures an additional SSL certificate file containing a [combined key and certificate](https://docs.python.org/3/library/ssl.html#combined-key-and-certificate) file
+for validating SSL connections against mail providers. This is for use with self-signed certificates against
+local IMAP servers.
+
+    Defaults to None.
+
+!!! warning
+
+    Settings this value has security implications for the security of your email.
+    Understand what it does and be sure you need to before setting.
+
 ## OCR settings {#ocr}
 
 Paperless uses [OCRmyPDF](https://ocrmypdf.readthedocs.io/en/latest/)
index cda14baadc9130324f41537553ae35da11af9638..d3009d0362e3a2568b401b115dd47683a1de6350 100644 (file)
@@ -177,6 +177,23 @@ def settings_values_check(app_configs, **kwargs):
             )
         return msgs
 
+    def _email_certificate_validate():
+        msgs = []
+        # Existence checks
+        if (
+            settings.EMAIL_CERTIFICATE_FILE is not None
+            and not settings.EMAIL_CERTIFICATE_FILE.is_file()
+        ):
+            msgs.append(
+                Error(
+                    f"Email cert {settings.EMAIL_CERTIFICATE_FILE} is not a file",
+                ),
+            )
+        return msgs
+
     return (
-        _ocrmypdf_settings_check() + _timezone_validate() + _barcode_scanner_validate()
+        _ocrmypdf_settings_check()
+        + _timezone_validate()
+        + _barcode_scanner_validate()
+        + _email_certificate_validate()
     )
index 7d2dda0d93e7e55223c4d4a07cfd9211bbf4c185..6b2ea56b26430a070006ddd47cf0da4bcb09ac82 100644 (file)
@@ -67,11 +67,20 @@ def __get_float(key: str, default: float) -> float:
     return float(os.getenv(key, default))
 
 
-def __get_path(key: str, default: Union[PathLike, str]) -> Path:
+def __get_path(
+    key: str,
+    default: Optional[Union[PathLike, str]] = None,
+) -> Optional[Path]:
     """
-    Return a normalized, absolute path based on the environment variable or a default
+    Return a normalized, absolute path based on the environment variable or a default,
+    if provided.  If not set and no default, returns None
     """
-    return Path(os.environ.get(key, default)).resolve()
+    if key in os.environ:
+        return Path(os.environ[key]).resolve()
+    elif default is not None:
+        return Path(default).resolve()
+    else:
+        return None
 
 
 def __get_list(
@@ -477,6 +486,8 @@ CSRF_COOKIE_NAME = f"{COOKIE_PREFIX}csrftoken"
 SESSION_COOKIE_NAME = f"{COOKIE_PREFIX}sessionid"
 LANGUAGE_COOKIE_NAME = f"{COOKIE_PREFIX}django_language"
 
+EMAIL_CERTIFICATE_FILE = __get_path("PAPERLESS_EMAIL_CERTIFICATE_FILE")
+
 
 ###############################################################################
 # Database                                                                    #
index cd706c532586fd8355fe21a883a785d9d7110ba4..6aac1a4c68b932aab9f3d2cc29fc16af2d47d356 100644 (file)
@@ -1,9 +1,11 @@
 import os
+from pathlib import Path
 
 from django.test import TestCase
 from django.test import override_settings
 
 from documents.tests.utils import DirectoriesMixin
+from documents.tests.utils import FileSystemAssertsMixin
 from paperless.checks import binaries_check
 from paperless.checks import debug_mode_check
 from paperless.checks import paths_check
@@ -57,7 +59,7 @@ class TestChecks(DirectoriesMixin, TestCase):
         self.assertEqual(len(debug_mode_check(None)), 1)
 
 
-class TestSettingsChecks(DirectoriesMixin, TestCase):
+class TestSettingsChecksAgainstDefaults(DirectoriesMixin, TestCase):
     def test_all_valid(self):
         """
         GIVEN:
@@ -70,6 +72,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
         msgs = settings_values_check(None)
         self.assertEqual(len(msgs), 0)
 
+
+class TestOcrSettingsChecks(DirectoriesMixin, TestCase):
     @override_settings(OCR_OUTPUT_TYPE="notapdf")
     def test_invalid_output_type(self):
         """
@@ -160,6 +164,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
 
         self.assertIn('OCR clean mode "cleanme"', msg.msg)
 
+
+class TestTimezoneSettingsChecks(DirectoriesMixin, TestCase):
     @override_settings(TIME_ZONE="TheMoon\\MyCrater")
     def test_invalid_timezone(self):
         """
@@ -178,6 +184,8 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
 
         self.assertIn('Timezone "TheMoon\\MyCrater"', msg.msg)
 
+
+class TestBarcodeSettingsChecks(DirectoriesMixin, TestCase):
     @override_settings(CONSUMER_BARCODE_SCANNER="Invalid")
     def test_barcode_scanner_invalid(self):
         msgs = settings_values_check(None)
@@ -200,3 +208,26 @@ class TestSettingsChecks(DirectoriesMixin, TestCase):
     def test_barcode_scanner_valid(self):
         msgs = settings_values_check(None)
         self.assertEqual(len(msgs), 0)
+
+
+class TestEmailCertSettingsChecks(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
+    @override_settings(EMAIL_CERTIFICATE_FILE=Path("/tmp/not_actually_here.pem"))
+    def test_not_valid_file(self):
+        """
+        GIVEN:
+            - Default settings
+            - Email certificate is set
+        WHEN:
+            - Email certificate file doesn't exist
+        THEN:
+            - system check error reported for email certificate
+        """
+        self.assertIsNotFile("/tmp/not_actually_here.pem")
+
+        msgs = settings_values_check(None)
+
+        self.assertEqual(len(msgs), 1)
+
+        msg = msgs[0]
+
+        self.assertIn("Email cert /tmp/not_actually_here.pem is not a file", msg.msg)
index a0bda19ba5519bd9081386aac965a5bb920c2914..fd66ac91d2f703d5a139c966a706b655d0c7726c 100644 (file)
@@ -395,12 +395,16 @@ def get_mailbox(server, port, security) -> MailBox:
     """
     Returns the correct MailBox instance for the given configuration.
     """
+    ssl_context = ssl.create_default_context()
+    if settings.EMAIL_CERTIFICATE_FILE is not None:  # pragma: nocover
+        ssl_context.load_cert_chain(certfile=settings.EMAIL_CERTIFICATE_FILE)
+
     if security == MailAccount.ImapSecurity.NONE:
         mailbox = MailBoxUnencrypted(server, port)
     elif security == MailAccount.ImapSecurity.STARTTLS:
-        mailbox = MailBoxTls(server, port, ssl_context=ssl.create_default_context())
+        mailbox = MailBoxTls(server, port, ssl_context=ssl_context)
     elif security == MailAccount.ImapSecurity.SSL:
-        mailbox = MailBox(server, port, ssl_context=ssl.create_default_context())
+        mailbox = MailBox(server, port, ssl_context=ssl_context)
     else:
         raise NotImplementedError("Unknown IMAP security")  # pragma: nocover
     return mailbox