Skip to content
Open
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
3 changes: 3 additions & 0 deletions invenio.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,9 @@ COMMUNITIES_ADMINISTRATION_DISABLED = False
MAIL_DEFAULT_SENDER = "Zenodo <noreply@zenodo.org>"
"""Default e-mail address sender."""

MAIL_DEFAULT_SUPPORT = "Zenodo Support <support@zenodo.org>"
"""Default support e-mail address."""

REST_ENABLE_CORS = True
"""Enable CORS validation."""

Expand Down
3 changes: 3 additions & 0 deletions site/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions site/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ def app_config(app_config):
),
}

app_config["MAIL_DEFAULT_SUPPORT"] = "Zenodo Support <support@zenodo.org>"

return app_config


Expand Down
52 changes: 52 additions & 0 deletions site/tests/moderation/test_actions.py
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions site/zenodo_rdm/moderation/actions.py
Original file line number Diff line number Diff line change
@@ -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}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<p>{{ _("Dear %(name)s,", name=full_name or _("user")) }}</p>
<p>
{{ _("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='<a href="https://support.zenodo.org/help/en-gb/2-content/141-what-content-is-not-suitable-for-zenodo">%s</a>' | format(_("what content is not suitable for Zenodo")),
terms_link='<a href="https://about.zenodo.org/terms/">%s</a>' | format(_("Terms of Use")),
policies_link='<a href="https://about.zenodo.org/policies/">%s</a>' | format(_("General Policies"))
) | safe }}
</p>
<p>{{ _("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.") }}</p>
<p>{{ _("Best regards,") }}<br>{{ _("The Zenodo Team") }}</p>
<hr>
<p>
{{ _("When replying, please do not remove the information below:") }}<br>
{{ _("User ID:") }} {{ user_id }}<br>
</p>

Original file line number Diff line number Diff line change
@@ -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) }}