Repository: paperless-ngx/paperless-ngx

Issue:
[BUG] Password reset email contains local IP of paperless-ngx server
_Apologies for deleting the original_

### Description

https://github.com/paperless-ngx/paperless-ngx/assets/73014371/87201d3c-276a-4d6e-92d2-c60bf661a274
I've set up paperless-ngx on my local network and used an Apache reverse proxy to make the server accessible publicly. I've also configured the PAPERLESS_URL setting accordingly.
https://github.com/paperless-ngx/paperless-ngx/assets/73014371/6f12861a-0f98-4651-8146-7636d4992b7d
However, I've noticed that the password reset email still contains the local IP address of the server. I've checked the configurations thoroughly but couldn't find any settings that might address this issue. I've attached screenshots for reference. Is there something I'm overlooking?

### Steps to reproduce

Install paperless with bash -c "$(wget -qLO - https://github.com/tteck/Proxmox/raw/main/ct/paperless-ngx.sh)" from Proxmox VE Helper Scripts.
Setup PAPERLESS_URL
Setup SMTP server
Restart

### Webserver logs

```
[Mon Feb 26 00:00:00.209652 2024] [mpm_prefork:notice] [pid 172] AH00163: Apache/2.4.57 (Debian) configured -- resuming normal operations
[Mon Feb 26 00:00:00.209681 2024] [core:notice] [pid 172] AH00094: Command line: '/usr/sbin/apache2'
[Mon Feb 26 13:14:55.909507 2024] [mpm_prefork:notice] [pid 172] AH00170: caught SIGWINCH, shutting down gracefully
[Mon Feb 26 13:15:30.605936 2024] [mpm_prefork:notice] [pid 173] AH00163: Apache/2.4.57 (Debian) configured -- resuming normal operations
[Mon Feb 26 13:15:30.611525 2024] [core:notice] [pid 173] AH00094: Command line: '/usr/sbin/apache2'
```

### Browser logs

No response

### Paperless-ngx version

2.4.0

### Host OS

Debian 12

### Installation method

Other (please describe above)

### Browser

Google Chrome

### Configuration changes

paperless.conf

### Other

No response

_Originally posted by Aniruddha Ghosh_


Discussion:

Resolving patch:
diff --git a/src/documents/context_processors.py b/src/documents/context_processors.py
--- 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/paperless/adapter.py b/src/paperless/adapter.py
--- 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 @@ def is_safe_url(self, url):
 
         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
--- a/src/paperless/settings.py
+++ b/src/paperless/settings.py
@@ -437,6 +437,8 @@ def _parse_beat_schedule() -> dict:
     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 @@ def _parse_remote_user_settings() -> str:
     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 @@ def _get_nltk_language_setting(ocr_lang: str) -> Optional[str]:
 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"


Test patch:
diff --git a/src/paperless/tests/test_adapter.py b/src/paperless/tests/test_adapter.py
--- a/src/paperless/tests/test_adapter.py
+++ b/src/paperless/tests/test_adapter.py
@@ -61,6 +61,27 @@ def test_pre_authenticate(self, mock_consume_rate):
         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
--- a/src/paperless/tests/test_settings.py
+++ b/src/paperless/tests/test_settings.py
@@ -8,6 +8,7 @@
 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 @@ def test_db_timeout_with_not_sqlite(self):
                 },
                 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)