Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#17](https://github.com/BIPLAT-CIBERINFEC/pathocore-api/pull/17) Notify use-case admins of pending access requests
- [#18](https://github.com/BIPLAT-CIBERINFEC/pathocore-api/pull/18) Use English copy in API response payloads
- [#26](https://github.com/BIPLAT-CIBERINFEC/pathocore-api/pull/26) Support proxy CSRF settings for admin forms behind HTTPS reverse proxies
- [#31](https://github.com/BIPLAT-CIBERINFEC/pathocore-api/pull/31/) Add a `send_test_email` management command and configurable Django email backend.

### `Fixed`

Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,10 @@ safe defaults and normally do not need to be changed.
| `EMAIL_PORT` | `1025` or `587` | SMTP port. |
| `EMAIL_HOST_USER` | SMTP username | Optional SMTP auth username. |
| `EMAIL_HOST_PASSWORD` | SMTP password | Optional SMTP auth password. |
| `EMAIL_BACKEND` | `django.core.mail.backends.smtp.EmailBackend` | Django email backend. Use `django.core.mail.backends.console.EmailBackend` for CLI-only development checks. |
| `EMAIL_USE_TLS` | `false` or `true` | Whether SMTP uses TLS. |
| `DEFAULT_FROM_EMAIL` | `no-reply@pathocore.local` | Sender shown in PathoCore API emails. |
| `ALLOWED_EMAIL_DOMAINS` | `ciberisciii.es,externos.isciii.es` | Optional comma-separated recipient domain allow-list for the test email command. |
| `PATHOCORE_ACCESS_REQUEST_ADMIN_EMAILS` | `admin@example.org` | Optional fallback/copy recipients if Keycloak use-case admins are not found. |

New access requests notify admins from the Keycloak group
Expand Down Expand Up @@ -527,6 +529,19 @@ In the local Docker test stack these messages are captured by Mailpit:
http://127.0.0.1:8025
```

Send a test email with the active Django settings:

```bash
python manage.py send_test_email user@example.org
```

For a console-only check, override the backend:

```bash
EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend \
python manage.py send_test_email user@example.org
```

Revocation removes the approved Keycloak group but does not disable the whole
account.

Expand Down
2 changes: 2 additions & 0 deletions conf/docker_production_settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ EMAIL_HOST="change_me_smtp_host"
EMAIL_PORT=587
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_TLS="true"
DEFAULT_FROM_EMAIL="no-reply@pathocore.local"
ALLOWED_EMAIL_DOMAINS=""

### Logs settings
LOG_TYPE="regular_folder"
Expand Down
2 changes: 2 additions & 0 deletions conf/docker_test_settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ EMAIL_HOST="mailpit"
EMAIL_PORT=1025
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_TLS="false"
DEFAULT_FROM_EMAIL="no-reply@pathocore.local"
ALLOWED_EMAIL_DOMAINS=""

### Logs settings
LOG_TYPE="regular_folder"
Expand Down
2 changes: 2 additions & 0 deletions conf/template_install_settings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ EMAIL_HOST=""
EMAIL_PORT=587
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_TLS="true"
DEFAULT_FROM_EMAIL="no-reply@pathocore.local"
ALLOWED_EMAIL_DOMAINS=""

### Logs settings
LOG_TYPE="symbolic_link" # can be symbolic link, or regular_folder
Expand Down
4 changes: 4 additions & 0 deletions conf/template_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,17 @@ def _csv_env(name, default=None):
EMAIL_PORT = _int_env("EMAIL_PORT", "emailport")
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "emailhostuser")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "emailhostpassword")
EMAIL_BACKEND = os.environ.get(
"EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend"
)
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "emailhosttls").lower() in (
"1",
"true",
"yes",
"on",
)
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "pathocore-api@localhost")
ALLOWED_EMAIL_DOMAINS = _csv_env("ALLOWED_EMAIL_DOMAINS")

SPECTACULAR_SETTINGS = {
"TITLE": "PathoCore API",
Expand Down
82 changes: 82 additions & 0 deletions core/management/commands/send_test_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from email.utils import parseaddr

from django.conf import settings
from django.core.mail import send_mail
from django.core.management.base import BaseCommand, CommandError


def _email_domain(address):
_, parsed_address = parseaddr(address or "")
if "@" not in parsed_address:
return ""
return parsed_address.rsplit("@", 1)[1].lower()


class Command(BaseCommand):
help = "Send a test email using the active Django email settings."

def add_arguments(self, parser):
parser.add_argument("recipient", help="Recipient email address.")
parser.add_argument(
"--subject",
default="PathoCore email test",
help="Email subject. Default: PathoCore email test",
)
parser.add_argument(
"--message",
default="This is a PathoCore email test.",
help="Plain-text email body.",
)
parser.add_argument(
"--from-email",
default=None,
help="Sender address. Defaults to DEFAULT_FROM_EMAIL.",
)
parser.add_argument(
"--ignore-domain-policy",
action="store_true",
help="Skip ALLOWED_EMAIL_DOMAINS validation for this test send.",
)

def handle(self, *args, **options):
recipient = options["recipient"].strip()
from_email = options["from_email"] or settings.DEFAULT_FROM_EMAIL
allowed_domains = {
domain.lower() for domain in getattr(settings, "ALLOWED_EMAIL_DOMAINS", [])
}

if not _email_domain(recipient):
raise CommandError("recipient must be a valid email address")

recipient_domain = _email_domain(recipient)
if (
allowed_domains
and not options["ignore_domain_policy"]
and recipient_domain not in allowed_domains
):
raise CommandError(
"recipient domain '%s' is not in ALLOWED_EMAIL_DOMAINS"
% recipient_domain
)

self.stdout.write("EMAIL_BACKEND=%s" % settings.EMAIL_BACKEND)
self.stdout.write("EMAIL_HOST=%s" % getattr(settings, "EMAIL_HOST", ""))
self.stdout.write("EMAIL_PORT=%s" % getattr(settings, "EMAIL_PORT", ""))
self.stdout.write("EMAIL_USE_TLS=%s" % getattr(settings, "EMAIL_USE_TLS", ""))
self.stdout.write("DEFAULT_FROM_EMAIL=%s" % settings.DEFAULT_FROM_EMAIL)
if allowed_domains:
self.stdout.write(
"ALLOWED_EMAIL_DOMAINS=%s" % ",".join(sorted(allowed_domains))
)

sent_count = send_mail(
options["subject"],
options["message"],
from_email,
[recipient],
fail_silently=False,
)

self.stdout.write(
self.style.SUCCESS("Sent %s test email(s) to %s" % (sent_count, recipient))
)
38 changes: 38 additions & 0 deletions core/tests.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import base64
from datetime import date
from io import StringIO
from unittest.mock import patch

from django.contrib.auth.models import User
from django.core import mail
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import SimpleTestCase, TestCase
from django.test import override_settings
from django.utils import timezone
Expand Down Expand Up @@ -1491,6 +1493,42 @@ def test_command_creates_or_updates_default_superuser(self):
self.assertTrue(user.check_password("new_pass"))


class SendTestEmailCommandTests(SimpleTestCase):
@override_settings(
ALLOWED_EMAIL_DOMAINS=["ciberisciii.es"],
DEFAULT_FROM_EMAIL="no-reply@pathocore.local",
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
EMAIL_HOST="mailpit",
EMAIL_PORT=1025,
EMAIL_USE_TLS=False,
)
def test_command_uses_configured_backend(self):
mail.outbox = []
stdout = StringIO()

call_command("send_test_email", "da.valle@ciberisciii.es", stdout=stdout)

self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].to, ["da.valle@ciberisciii.es"])
self.assertEqual(mail.outbox[0].from_email, "no-reply@pathocore.local")
self.assertIn(
"EMAIL_BACKEND=django.core.mail.backends.locmem.EmailBackend",
stdout.getvalue(),
)
self.assertIn("Sent 1 test email(s)", stdout.getvalue())

@override_settings(
ALLOWED_EMAIL_DOMAINS=["ciberisciii.es"],
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
)
def test_command_rejects_disallowed_recipient_domain(self):
with self.assertRaisesMessage(
CommandError,
"recipient domain 'example.org' is not in ALLOWED_EMAIL_DOMAINS",
):
call_command("send_test_email", "user@example.org")


@override_settings(
ROOT_URLCONF="conf.urls",
ALLOWED_HOSTS=["testserver", "localhost"],
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ services:
EMAIL_PORT: ${EMAIL_PORT:-1025}
EMAIL_HOST_USER: ${EMAIL_HOST_USER:-}
EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD:-}
EMAIL_BACKEND: ${EMAIL_BACKEND:-django.core.mail.backends.smtp.EmailBackend}
EMAIL_USE_TLS: ${EMAIL_USE_TLS:-false}
DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL:-no-reply@pathocore.local}
ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-}
DATABROWSER_CACHE_SCHEDULER_ENABLED: ${DATABROWSER_CACHE_SCHEDULER_ENABLED:-true}
# Python weekday: Monday=0, Friday=4.
DATABROWSER_CACHE_REFRESH_WEEKDAY: ${DATABROWSER_CACHE_REFRESH_WEEKDAY:-4}
Expand Down
4 changes: 4 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,14 @@ write_runtime_env_file() {
write_runtime_env_var "EMAIL_PORT" "${EMAIL_PORT:-587}"
write_runtime_env_var "EMAIL_HOST_USER" "${EMAIL_HOST_USER:-}"
write_runtime_env_var "EMAIL_HOST_PASSWORD" "${EMAIL_HOST_PASSWORD:-}"
write_runtime_env_var \
"EMAIL_BACKEND" \
"${EMAIL_BACKEND:-django.core.mail.backends.smtp.EmailBackend}"
write_runtime_env_var "EMAIL_USE_TLS" "${EMAIL_USE_TLS:-true}"
write_runtime_env_var \
"DEFAULT_FROM_EMAIL" \
"${DEFAULT_FROM_EMAIL:-no-reply@pathocore.local}"
write_runtime_env_var "ALLOWED_EMAIL_DOMAINS" "${ALLOWED_EMAIL_DOMAINS:-}"
for env_key in $(compgen -A variable KEYCLOAK_ | sort); do
write_runtime_env_var "$env_key" "${!env_key}"
done
Expand Down
Loading