From 16f4552e0e32aa507cf2ad337504a4857ba573b9 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:41:25 -0800 Subject: [PATCH] Fix: use PAPERLESS_URL if set for pw reset emails (#5902) --- docs/usage.md | 3 +- src/documents/context_processors.py | 1 + .../templates/account/email/base_message.txt | 7 ++ src/locale/en_US/LC_MESSAGES/django.po | 78 +++++++++++-------- src/paperless/adapter.py | 16 ++++ src/paperless/settings.py | 28 ++++--- src/paperless/tests/test_adapter.py | 21 +++++ src/paperless/tests/test_settings.py | 25 ++++++ 8 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 src/documents/templates/account/email/base_message.txt diff --git a/docs/usage.md b/docs/usage.md index 11b1f710a..b39616844 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -253,7 +253,8 @@ permissions can be granted to limit access to certain parts of the UI (and corre ### Password reset In order to enable the password reset feature you will need to setup an SMTP backend, see -[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST) +[`PAPERLESS_EMAIL_HOST`](configuration.md#PAPERLESS_EMAIL_HOST). If your installation does not have +[`PAPERLESS_URL`](configuration.md#PAPERLESS_URL) set, the reset link included in emails will use the server host. ## Workflows diff --git a/src/documents/context_processors.py b/src/documents/context_processors.py index 0eaaa8e46..9f8dacfb3 100644 --- a/src/documents/context_processors.py +++ b/src/documents/context_processors.py @@ -7,4 +7,5 @@ def settings(request): or django_settings.EMAIL_HOST_USER != "", "DISABLE_REGULAR_LOGIN": django_settings.DISABLE_REGULAR_LOGIN, "ACCOUNT_ALLOW_SIGNUPS": django_settings.ACCOUNT_ALLOW_SIGNUPS, + "domain": getattr(django_settings, "PAPERLESS_URL", request.get_host()), } diff --git a/src/documents/templates/account/email/base_message.txt b/src/documents/templates/account/email/base_message.txt new file mode 100644 index 000000000..033ba0147 --- /dev/null +++ b/src/documents/templates/account/email/base_message.txt @@ -0,0 +1,7 @@ +{% load i18n %}{% autoescape off %}{% blocktrans with site_name="Paperless-ngx" %}Hello from {{ site_name }}!{% endblocktrans %} + +{% block content %}{% endblock content %} + +{% blocktrans with site_name="Paperless-ngx" site_domain=settings.domain %}Thank you for using {{ site_name }}! +{{ site_domain }}{% endblocktrans %} +{% endautoescape %} diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 523ca8f22..109940a59 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: paperless-ngx\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-02-26 00:10-0800\n" +"POT-Creation-Date: 2024-02-26 13:34-0800\n" "PO-Revision-Date: 2022-02-17 04:17\n" "Last-Translator: \n" "Language-Team: English\n" @@ -786,6 +786,18 @@ msgstr "" msgid "Invalid variable detected." msgstr "" +#: documents/templates/account/email/base_message.txt:1 +#, python-format +msgid "Hello from %(site_name)s!" +msgstr "" + +#: documents/templates/account/email/base_message.txt:5 +#, python-format +msgid "" +"Thank you for using %(site_name)s!\n" +"%(site_domain)s" +msgstr "" + #: documents/templates/account/login.html:5 msgid "Paperless-ngx sign in" msgstr "" @@ -1138,131 +1150,131 @@ msgstr "" msgid "paperless application settings" msgstr "" -#: paperless/settings.py:644 +#: paperless/settings.py:656 msgid "English (US)" msgstr "" -#: paperless/settings.py:645 +#: paperless/settings.py:657 msgid "Arabic" msgstr "" -#: paperless/settings.py:646 +#: paperless/settings.py:658 msgid "Afrikaans" msgstr "" -#: paperless/settings.py:647 +#: paperless/settings.py:659 msgid "Belarusian" msgstr "" -#: paperless/settings.py:648 +#: paperless/settings.py:660 msgid "Bulgarian" msgstr "" -#: paperless/settings.py:649 +#: paperless/settings.py:661 msgid "Catalan" msgstr "" -#: paperless/settings.py:650 +#: paperless/settings.py:662 msgid "Czech" msgstr "" -#: paperless/settings.py:651 +#: paperless/settings.py:663 msgid "Danish" msgstr "" -#: paperless/settings.py:652 +#: paperless/settings.py:664 msgid "German" msgstr "" -#: paperless/settings.py:653 +#: paperless/settings.py:665 msgid "Greek" msgstr "" -#: paperless/settings.py:654 +#: paperless/settings.py:666 msgid "English (GB)" msgstr "" -#: paperless/settings.py:655 +#: paperless/settings.py:667 msgid "Spanish" msgstr "" -#: paperless/settings.py:656 +#: paperless/settings.py:668 msgid "Finnish" msgstr "" -#: paperless/settings.py:657 +#: paperless/settings.py:669 msgid "French" msgstr "" -#: paperless/settings.py:658 +#: paperless/settings.py:670 msgid "Hungarian" msgstr "" -#: paperless/settings.py:659 +#: paperless/settings.py:671 msgid "Italian" msgstr "" -#: paperless/settings.py:660 +#: paperless/settings.py:672 msgid "Japanese" msgstr "" -#: paperless/settings.py:661 +#: paperless/settings.py:673 msgid "Luxembourgish" msgstr "" -#: paperless/settings.py:662 +#: paperless/settings.py:674 msgid "Norwegian" msgstr "" -#: paperless/settings.py:663 +#: paperless/settings.py:675 msgid "Dutch" msgstr "" -#: paperless/settings.py:664 +#: paperless/settings.py:676 msgid "Polish" msgstr "" -#: paperless/settings.py:665 +#: paperless/settings.py:677 msgid "Portuguese (Brazil)" msgstr "" -#: paperless/settings.py:666 +#: paperless/settings.py:678 msgid "Portuguese" msgstr "" -#: paperless/settings.py:667 +#: paperless/settings.py:679 msgid "Romanian" msgstr "" -#: paperless/settings.py:668 +#: paperless/settings.py:680 msgid "Russian" msgstr "" -#: paperless/settings.py:669 +#: paperless/settings.py:681 msgid "Slovak" msgstr "" -#: paperless/settings.py:670 +#: paperless/settings.py:682 msgid "Slovenian" msgstr "" -#: paperless/settings.py:671 +#: paperless/settings.py:683 msgid "Serbian" msgstr "" -#: paperless/settings.py:672 +#: paperless/settings.py:684 msgid "Swedish" msgstr "" -#: paperless/settings.py:673 +#: paperless/settings.py:685 msgid "Turkish" msgstr "" -#: paperless/settings.py:674 +#: paperless/settings.py:686 msgid "Ukrainian" msgstr "" -#: paperless/settings.py:675 +#: paperless/settings.py:687 msgid "Chinese Simplified" msgstr "" diff --git a/src/paperless/adapter.py b/src/paperless/adapter.py index 3d521bd66..add2bf45d 100644 --- a/src/paperless/adapter.py +++ b/src/paperless/adapter.py @@ -1,3 +1,5 @@ +from urllib.parse import quote + from allauth.account.adapter import DefaultAccountAdapter from allauth.core import context from allauth.socialaccount.adapter import DefaultSocialAccountAdapter @@ -45,6 +47,20 @@ class CustomAccountAdapter(DefaultAccountAdapter): return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts) + def get_reset_password_from_key_url(self, key): + """ + Return the URL to reset a password e.g. in reset email. + """ + if settings.PAPERLESS_URL is None: + return super().get_reset_password_from_key_url(key) + else: + path = reverse( + "account_reset_password_from_key", + kwargs={"uidb36": "UID", "key": "KEY"}, + ) + path = path.replace("UID-KEY", quote(key)) + return settings.PAPERLESS_URL + path + class CustomSocialAccountAdapter(DefaultSocialAccountAdapter): def is_open_for_signup(self, request, sociallogin): diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 24b3ae32f..d2ee531fc 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -437,6 +437,8 @@ SOCIALACCOUNT_PROVIDERS = json.loads( os.getenv("PAPERLESS_SOCIALACCOUNT_PROVIDERS", "{}"), ) +ACCOUNT_EMAIL_SUBJECT_PREFIX = "[Paperless-ngx] " + DISABLE_REGULAR_LOGIN = __get_boolean("PAPERLESS_DISABLE_REGULAR_LOGIN") AUTO_LOGIN_USERNAME = os.getenv("PAPERLESS_AUTO_LOGIN_USERNAME") @@ -498,18 +500,23 @@ if DEBUG: CORS_ALLOWED_ORIGINS.append("http://localhost:4200") ALLOWED_HOSTS = __get_list("PAPERLESS_ALLOWED_HOSTS", ["*"]) - -_paperless_url = os.getenv("PAPERLESS_URL") -if _paperless_url: - _paperless_uri = urlparse(_paperless_url) - CSRF_TRUSTED_ORIGINS.append(_paperless_url) - CORS_ALLOWED_ORIGINS.append(_paperless_url) - if ["*"] != ALLOWED_HOSTS: # always allow localhost. Necessary e.g. for healthcheck in docker. ALLOWED_HOSTS.append("localhost") - if _paperless_url: - ALLOWED_HOSTS.append(_paperless_uri.hostname) + + +def _parse_paperless_url(): + global CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, ALLOWED_HOSTS + url = os.getenv("PAPERLESS_URL") + if url: + CSRF_TRUSTED_ORIGINS.append(url) + CORS_ALLOWED_ORIGINS.append(url) + ALLOWED_HOSTS.append(urlparse(url).hostname) + + return url + + +PAPERLESS_URL = _parse_paperless_url() # For use with trusted proxies TRUSTED_PROXIES = __get_list("PAPERLESS_TRUSTED_PROXIES") @@ -1126,3 +1133,6 @@ DEFAULT_FROM_EMAIL: Final[str] = os.getenv("PAPERLESS_EMAIL_FROM", EMAIL_HOST_US EMAIL_USE_TLS: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_TLS") EMAIL_USE_SSL: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_SSL") EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] " +if DEBUG: # pragma: no cover + EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" + EMAIL_FILE_PATH = BASE_DIR / "sent_emails" diff --git a/src/paperless/tests/test_adapter.py b/src/paperless/tests/test_adapter.py index a77c55f23..8e73cafea 100644 --- a/src/paperless/tests/test_adapter.py +++ b/src/paperless/tests/test_adapter.py @@ -61,6 +61,27 @@ class TestCustomAccountAdapter(TestCase): with self.assertRaises(ValidationError): adapter.pre_authenticate(request) + def test_get_reset_password_from_key_url(self): + request = HttpRequest() + request.get_host = mock.Mock(return_value="foo.org") + with context.request_context(request): + adapter = get_adapter() + + # Test when PAPERLESS_URL is None + expected_url = f"https://foo.org{reverse('account_reset_password_from_key', kwargs={'uidb36': 'UID', 'key': 'KEY'})}" + self.assertEqual( + adapter.get_reset_password_from_key_url("UID-KEY"), + expected_url, + ) + + # Test when PAPERLESS_URL is not None + with override_settings(PAPERLESS_URL="https://bar.com"): + expected_url = f"https://bar.com{reverse('account_reset_password_from_key', kwargs={'uidb36': 'UID', 'key': 'KEY'})}" + self.assertEqual( + adapter.get_reset_password_from_key_url("UID-KEY"), + expected_url, + ) + class TestCustomSocialAccountAdapter(TestCase): def test_is_open_for_signup(self): diff --git a/src/paperless/tests/test_settings.py b/src/paperless/tests/test_settings.py index 41abb0a50..5b6335fd2 100644 --- a/src/paperless/tests/test_settings.py +++ b/src/paperless/tests/test_settings.py @@ -8,6 +8,7 @@ from celery.schedules import crontab from paperless.settings import _parse_beat_schedule from paperless.settings import _parse_db_settings from paperless.settings import _parse_ignore_dates +from paperless.settings import _parse_paperless_url from paperless.settings import _parse_redis_url from paperless.settings import default_threads_per_worker @@ -349,3 +350,27 @@ class TestDBSettings(TestCase): }, databases["sqlite"]["OPTIONS"], ) + + +class TestPaperlessURLSettings(TestCase): + def test_paperless_url(self): + """ + GIVEN: + - PAPERLESS_URL is set + WHEN: + - The URL is parsed + THEN: + - The URL is returned and present in related settings + """ + with mock.patch.dict( + os.environ, + { + "PAPERLESS_URL": "https://example.com", + }, + ): + url = _parse_paperless_url() + self.assertEqual("https://example.com", url) + from django.conf import settings + + self.assertIn(url, settings.CSRF_TRUSTED_ORIGINS) + self.assertIn(url, settings.CORS_ALLOWED_ORIGINS)