mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Allow users to set a combined certificte and key file for additional certificates in the SSL context
This commit is contained in:
		@@ -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/)
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -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                                                                    #
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user