diff --git a/invenio.cfg b/invenio.cfg index 35dce33ea..ca5f34b05 100644 --- a/invenio.cfg +++ b/invenio.cfg @@ -949,6 +949,9 @@ COMMUNITIES_ADMINISTRATION_DISABLED = False MAIL_DEFAULT_SENDER = "Zenodo " """Default e-mail address sender.""" +MAIL_DEFAULT_SUPPORT = "Zenodo Support " +"""Default support e-mail address.""" + REST_ENABLE_CORS = True """Enable CORS validation.""" diff --git a/site/pyproject.toml b/site/pyproject.toml index df1e7e19f..cfc54d9be 100644 --- a/site/pyproject.toml +++ b/site/pyproject.toml @@ -95,6 +95,9 @@ funding = "zenodo_rdm.checks.funding:FundingCheck" eu_records_curation = "zenodo_rdm.curation.jobs:EURecordCuration" export_records = "zenodo_rdm.exporter.jobs:ExportRecords" +[project.entry-points."invenio_users_resources.moderation.actions"] +block = "zenodo_rdm.moderation.actions:on_block_notify" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/site/tests/conftest.py b/site/tests/conftest.py index d475e0c3b..5dbc0dd8f 100644 --- a/site/tests/conftest.py +++ b/site/tests/conftest.py @@ -140,6 +140,8 @@ def app_config(app_config): ), } + app_config["MAIL_DEFAULT_SUPPORT"] = "Zenodo Support " + return app_config diff --git a/site/tests/moderation/test_actions.py b/site/tests/moderation/test_actions.py new file mode 100644 index 000000000..11b3d480d --- /dev/null +++ b/site/tests/moderation/test_actions.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2026 CERN +# SPDX-License-Identifier: GPL-3.0-or-later +"""Tests for moderation actions.""" + +from unittest.mock import MagicMock, patch + +import pytest +from invenio_users_resources.proxies import current_actions_registry +from invenio_users_resources.services.users.tasks import execute_moderation_actions + +from zenodo_rdm.moderation.actions import on_block_notify + + +def test_block_triggers_notification(app, db, UserFixture, monkeypatch): + """on_block_notify is called and sends an email when execute_moderation_actions runs.""" + u = UserFixture(email="blocked@example.com", password="blockeduser") + u.create(app, db) + + monkeypatch.setitem(current_actions_registry, "block", [on_block_notify]) + + with app.extensions["mail"].record_messages() as outbox: + execute_moderation_actions(user_id=u.user.id, action="block") + + assert len(outbox) == 1 + msg = outbox[0] + assert msg.subject == "Notice of account suspension on Zenodo" + assert msg.recipients == ["blocked@example.com"] + assert msg.sender == app.config["MAIL_DEFAULT_SENDER"] + assert msg.reply_to == app.config["MAIL_DEFAULT_SUPPORT"] + assert msg.body.startswith("Dear user,") + + +@pytest.fixture() +def blocked_user(): + """Mock UserAggregate for a blocked user.""" + user = MagicMock() + user.id = 123 + user.email = "blocked@example.com" + user.profile = {"full_name": "Jane Doe"} + return user + + +def test_actor_triggered_block_sends_no_email(app, blocked_user): + """Human-triggered block (actor_id set) does not send an email.""" + with patch( + "zenodo_rdm.moderation.actions.UserAggregate.get_record", + return_value=blocked_user, + ): + with app.extensions["mail"].record_messages() as outbox: + on_block_notify(user_id=blocked_user.id, actor_id=456) + + assert len(outbox) == 0 diff --git a/site/zenodo_rdm/moderation/actions.py b/site/zenodo_rdm/moderation/actions.py new file mode 100644 index 000000000..5e116bd50 --- /dev/null +++ b/site/zenodo_rdm/moderation/actions.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2026 CERN +# SPDX-License-Identifier: GPL-3.0-or-later +"""RDM user moderation action.""" + +from flask import current_app +from flask_mail import Message +from invenio_app_rdm.utils.files import render_email_from_context +from invenio_users_resources.records.api import UserAggregate + +_BLOCK_NOTIFICATION_HTML = "zenodo_rdm/moderation/block_notification.html" +_BLOCK_NOTIFICATION_TXT = "zenodo_rdm/moderation/block_notification.txt" + + +def on_block_notify(user_id, uow=None, actor_id=None, **kwargs): + """Send a notification email to a user blocked by the automated system.""" + if actor_id is not None: + return + + user = UserAggregate.get_record(user_id) + + context = { + "full_name": (user.profile or {}).get("full_name", ""), + "user_id": user_id, + } + + mail_ext = current_app.extensions["mail"] + msg = Message( + subject="Notice of account suspension on Zenodo", + sender=current_app.config["MAIL_DEFAULT_SENDER"], + recipients=[user.email], + reply_to=current_app.config["MAIL_DEFAULT_SUPPORT"], + body=render_email_from_context(_BLOCK_NOTIFICATION_TXT, context), + html=render_email_from_context(_BLOCK_NOTIFICATION_HTML, context), + ) + try: + mail_ext.send(msg) + except Exception: + current_app.logger.exception( + "Failed to send block notification email.", + extra={"user_id": user_id} + ) diff --git a/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/moderation/block_notification.html b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/moderation/block_notification.html new file mode 100644 index 000000000..85a69f9df --- /dev/null +++ b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/moderation/block_notification.html @@ -0,0 +1,16 @@ +

{{ _("Dear %(name)s,", name=full_name or _("user")) }}

+

+{{ _("Your account has been blocked by our spam-detection system for uploading content that is not permitted on Zenodo. You can review %(content_link)s, our %(terms_link)s, and %(policies_link)s.", + content_link='%s' | format(_("what content is not suitable for Zenodo")), + terms_link='%s' | format(_("Terms of Use")), + policies_link='%s' | format(_("General Policies")) +) | safe }} +

+

{{ _("If you believe the content you submitted is legitimate research dissemination, please reply to this email and our support team will review your account as soon as possible.") }}

+

{{ _("Best regards,") }}
{{ _("The Zenodo Team") }}

+
+

+{{ _("When replying, please do not remove the information below:") }}
+{{ _("User ID:") }} {{ user_id }}
+

+ diff --git a/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/moderation/block_notification.txt b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/moderation/block_notification.txt new file mode 100644 index 000000000..893c336a7 --- /dev/null +++ b/site/zenodo_rdm/templates/semantic-ui/zenodo_rdm/moderation/block_notification.txt @@ -0,0 +1,11 @@ +{{ _('Dear %(name)s,', name=full_name or _('user')) }} + +{{ _('Your account has been blocked by our spam-detection system for uploading content that is not permitted on Zenodo. +If you believe the content you submitted is legitimate research dissemination, please reply to this email and our support team will review your account as soon as possible.') }} + +{{ _('Best regards, +The Zenodo Team') }} + +--- +{{ _('When replying, please do not remove the information below:') }} +{{ _('User ID: %(user_id)s', user_id=user_id) }}