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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,20 @@ tests are set up to run on most individual functions and use test data in data.s
poetry install
poetry run coverage run -m pytest
```

deploying:
happens automatically on merge to major branches through GCP build triggers to a cloud run job

additonal environment variables needed:
CLASSIC_DB_URI
HALON_CREDS
SEND_EMAILS
ARCHIVAL_EMAIL
MOD_REPLY_TO

optional:
MAIL_FROM
ENV
LOG_LEVEL
REDIRECT_RECIPIENT

4 changes: 4 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Optional
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
Expand All @@ -9,6 +10,9 @@ class Settings(BaseSettings):
#email related
SEND_EMAILS: bool = False
MAIL_FROM: str = "e-prints@arxiv.org"
REDIRECT_RECIPIENT: Optional[str] = None
ARCHIVAL_EMAIL: Optional[str] = None
MOD_REPLY_TO: Optional[str] = None
HALON_CREDS: str = "smtps://user:pass@host:port"

model_config = SettingsConfigDict(
Expand Down
20 changes: 15 additions & 5 deletions app/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@

logger = logging.getLogger(__name__)

_OVERRIDE_RECIPIENT = "test-colab-group@arxiv.org"


def send_email(
to_emails: list[str],
Expand All @@ -22,18 +20,30 @@ def send_email(
) -> None:
"""Send a plain-text and HTML email via the Halon SMTP relay."""

reply_to_emails = (reply_to_emails or []) + ([settings.MOD_REPLY_TO] if settings.MOD_REPLY_TO else [])
bcc_emails: list[str] = [settings.ARCHIVAL_EMAIL] if settings.ARCHIVAL_EMAIL else []

if not settings.SEND_EMAILS:
logger.info(f"Email sending disabled. Would send to {to_emails}: {subject}")
return

# redirect emails while under development
if not settings.REDIRECT_RECIPIENT:
logger.error("REDIRECT_RECIPIENT not set — refusing to send to avoid misdirected email")
return

redirect_header = f"[TEST REDIRECT]\nOriginal To: {', '.join(to_emails)}"
if reply_to_emails:
redirect_header += f"\nOriginal Reply-To: {', '.join(reply_to_emails)}"
if bcc_emails:
redirect_header += f"\nOriginal Bcc: {', '.join(bcc_emails)}"
body = redirect_header + "\n\n" + body
html_body = redirect_header.replace("\n", "<br>\n") + "<br><br>\n" + html_body
to_emails = [_OVERRIDE_RECIPIENT]
to_emails = [settings.REDIRECT_RECIPIENT]
reply_to_emails = []
bcc_emails = []

#build email
msg = email.message.EmailMessage()
msg["Date"] = format_datetime(localtime())
msg["Message-ID"] = make_msgid()
Expand All @@ -51,7 +61,7 @@ def send_email(
sess.send_message(
msg,
from_addr=settings.MAIL_FROM,
to_addrs=to_emails,
to_addrs=to_emails + bcc_emails,
mail_options=("8bitmime",),
)
logger.info(f"Email sent to {to_emails}: {subject}")
logger.debug(f"Email sent to {to_emails + bcc_emails}: {subject}")
7 changes: 5 additions & 2 deletions app/email_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@
from zoneinfo import ZoneInfo
from sqlalchemy import select

from arxiv.config import settings as arxiv_settings
from arxiv.db import Session
from arxiv.db.models import Submission, SubmissionCategory
from arxiv.taxonomy.definitions import CATEGORY_ALIASES

from app.schema import SubEmailData, SimplifiedNotification, CommentData, PromoteData, NewPropData, PropRespData, EmailTask, UserContact

_ET = ZoneInfo("America/New_York")
_ET = ZoneInfo(arxiv_settings.ARXIV_BUSINESS_TZ)
def _fmt_time(dt: datetime) -> str:
et = dt.astimezone(_ET)
return et.strftime("%m-%d %H:%M ET")
return et.strftime("%m-%d %H:%M %Z")

from app.templates.comment import render_comment_block
from app.templates.promote import render_promote_block
Expand Down Expand Up @@ -55,6 +56,7 @@ def get_submission_info(submission_ids: set[int]) -> dict[int, SubEmailData]:
Submission.status,
Submission.submitter_name,
Submission.submitter_id,
Submission.submit_time,
).where(Submission.submission_id.in_(submission_ids))
).all()

Expand All @@ -77,6 +79,7 @@ def get_submission_info(submission_ids: set[int]) -> dict[int, SubEmailData]:
submitter_name=row.submitter_name or "",
submitter_id=row.submitter_id or 0,
submission_categories=_build_category_string(cats_by_sub.get(row.submission_id, [])),
submit_time=row.submit_time,
)
for row in rows
}
Expand Down
3 changes: 1 addition & 2 deletions app/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,8 @@ def _send_email_tasks(

#send email — failure skips ack (will redeliver)
try:
cat_str = " ".join(sorted(c.id for c in task.notifications.categories))
submitter = sub.submitter_name or f"user {sub.submitter_id}"
subject = f"Action Required: arXiv submission submit/{task.submission_id} to {cat_str} by {submitter}"
subject = f"Action Required: arXiv submission submit/{task.submission_id} to {sub.submission_categories} by {submitter}"
send_email(
to_emails=task.to_emails,
subject=subject,
Expand Down
5 changes: 3 additions & 2 deletions app/schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Literal, Union
from typing import Literal, Union, Optional
from enum import Enum
from pydantic import BaseModel
from dataclasses import dataclass, field
Expand Down Expand Up @@ -77,4 +77,5 @@ class SubEmailData:
status: int
submitter_name: str
submitter_id: int
submission_categories: str = ""
submission_categories: str = ""
submit_time: Optional[datetime] = None
8 changes: 8 additions & 0 deletions app/templates/email_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
MOD_HUB_URL = "https://arxiv-org.atlassian.net/wiki/spaces/ModRes/pages/812580865/Moderator+Hub"
MOD_HUB_TITLE = "Moderator Hub"

_FOOTER_LINE1 = "This email was generated by the moderator email system version 2.0"
Comment thread
bdc34 marked this conversation as resolved.
_FOOTER_LINE2 = "Some of your arXiv moderation emails may look different. We are in the process of transitioning email systems."


def render_body(
sub_text: str,
Expand All @@ -21,6 +24,9 @@ def render_body(
f"{CHECK_GUIDE_TITLE}: {CHECK_GUIDE_URL} \n"
f"{HOW_TO_MOD_TITLE}: {HOW_TO_MOD_URL} \n"
f"{MOD_HUB_TITLE}: {MOD_HUB_URL} \n"
f"\n"
f"{_FOOTER_LINE1}\n"
f"{_FOOTER_LINE2}\n"
)
body_html = (
f"{sub_html}\n"
Expand All @@ -32,5 +38,7 @@ def render_body(
f"<a href=\"{HOW_TO_MOD_URL}\">{HOW_TO_MOD_TITLE}</a> | "
f"<a href=\"{MOD_HUB_URL}\">{MOD_HUB_TITLE}</a>"
f"</p>\n"
f"<p>{_FOOTER_LINE1}<br>\n"
f"{_FOOTER_LINE2}</p>\n"
)
return body_text, body_html
22 changes: 17 additions & 5 deletions app/templates/submission.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import html
from zoneinfo import ZoneInfo

from arxiv.config import settings as arxiv_settings
from arxiv.submission.statuses import STATUS_NAMES

from app.schema import SubEmailData

CHECK_SUBMISSION_URL = "https://check.arxiv.org/submit/{submission_id}"
_ET = ZoneInfo(arxiv_settings.ARXIV_BUSINESS_TZ)


def render_submission_block(sub: SubEmailData) -> tuple[str, str]:
Expand All @@ -14,20 +17,29 @@ def render_submission_block(sub: SubEmailData) -> tuple[str, str]:
title = sub.title or "(no title)"
authors = sub.authors or "(no authors)"

submit_time_str = (
sub.submit_time.astimezone(_ET).strftime("%Y-%m-%d %H:%M %Z")
if sub.submit_time else None
)

text = (
f"Submission: submit/{sub.submission_id}\n"
f"Submission: submit/{sub.submission_id} | {check_url}\n"
f"Title: {title}\n"
f"Authors: {authors}\n"
f"Status: {status_label}\n"
f"Current Categories: {cat_list}\n"
f"View in Check: {check_url}\n"
)
if submit_time_str:
text += f"Submitted: {submit_time_str}\n"

html_out = (
f"<p><strong>Submission:</strong> submit/{sub.submission_id}<br>\n"
f"<p><strong>Submission:</strong> submit/{sub.submission_id} | <a href=\"{check_url}\">{check_url}</a><br>\n"
f"<strong>Title:</strong> {html.escape(title)}<br>\n"
f"<strong>Authors:</strong> {html.escape(authors)}<br>\n"
f"<strong>Status:</strong> {status_label}<br>\n"
f"<strong>Current Categories:</strong> {cat_list}<br>\n"
f"<strong>View in Check:</strong> <a href=\"{check_url}\">{check_url}</a></p>\n"
f"<strong>Current Categories:</strong> {cat_list}"
)
if submit_time_str:
html_out += f"<br>\n<strong>Submitted:</strong> {submit_time_str}"
html_out += "</p>\n"
return text, html_out
3 changes: 3 additions & 0 deletions tests/data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ INSERT INTO `tapir_nicknames` VALUES (10005,'jsmith',2,1,1,0,0,1);
INSERT INTO `arXiv_submissions` (submission_id, title, authors, status, remote_addr, remote_host, package) VALUES (123, 'A Test Paper on Machine Learning', 'Author One, Author Two', 1, '127.0.0.1', 'localhost', '');
INSERT INTO `arXiv_submissions` (submission_id, title, authors, status, remote_addr, remote_host, package) VALUES (124, 'Another Test Paper on Category Promotion', 'Author Three', 1, '127.0.0.1', 'localhost', '');
INSERT INTO `arXiv_submissions` (submission_id, title, authors, status, remote_addr, remote_host, package) VALUES (125, 'Paper With No Categories', 'Some Author', 1, '127.0.0.1', 'localhost', '');
INSERT INTO `arXiv_submissions` (submission_id, title, authors, status, remote_addr, remote_host, package) VALUES (126, 'A Math-Physics Paper', 'Author Math', 1, '127.0.0.1', 'localhost', '');

-- submission categories for get_submission_info tests
-- 123: cs.LG primary + cs.AI cross-list
Expand All @@ -68,3 +69,5 @@ INSERT INTO `arXiv_submission_category` VALUES (123, 'cs.AI', 0, NULL);
INSERT INTO `arXiv_submission_category` VALUES (124, 'cs.AI', 0, NULL);
INSERT INTO `arXiv_submission_category` VALUES (124, 'cs.LG', 0, NULL);
-- 125: no categories
-- 126: math-ph primary (math.MP alias should also appear)
INSERT INTO `arXiv_submission_category` VALUES (126, 'math-ph', 1, NULL);
55 changes: 43 additions & 12 deletions tests/test_email_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_render_comment_block():
text, html_out = render_comment_block(note, _USER)
assert "looks good to me" in text and "looks good to me" in html_out
assert _USER in text and _USER in html_out
assert "ET" in text
assert "EDT" in text

def test_render_comment_escapes_html():
note = _note(CommentData(comment="<script>alert('xss')</script> & done"))
Expand Down Expand Up @@ -98,7 +98,7 @@ def test_render_change_block_dispatches():
# ── submission block ──────────────────────────────────────────────────────────

def _mock_submission(submission_id=123, title="ML Paper", authors="Alice, Bob", status=1,
submission_categories="cs.LG cs.AI"):
submission_categories="cs.LG cs.AI", submit_time=_TIME):
return SubEmailData(
submission_id=submission_id,
title=title,
Expand All @@ -107,6 +107,7 @@ def _mock_submission(submission_id=123, title="ML Paper", authors="Alice, Bob",
submitter_name="",
submitter_id=0,
submission_categories=submission_categories,
submit_time=submit_time,
)

def test_render_submission_block():
Expand Down Expand Up @@ -163,7 +164,8 @@ def test_render_email_contains_all_sections_and_footer():
# ── exact output tests ────────────────────────────────────────────────────────
#will need to be updated whenever format changes
#feel free to delete if too annoying, but its kind of nice to see the whole output
_WHEN = "06-15 10:30 ET" # _TIME (2024-06-15 14:30 UTC) converted to EDT
_WHEN = "06-15 10:30 EDT" # _TIME (2024-06-15 14:30 UTC)
_SUBMIT_TIME_STR = "2024-06-15 10:30 EDT"
_CHECK_URL_123 = "https://check.arxiv.org/submit/123"


Expand Down Expand Up @@ -239,25 +241,49 @@ def test_submission_exact_text():
sub = _mock_submission()
text, _ = render_submission_block(sub)
assert text == (
"Submission: submit/123\n"
f"Submission: submit/123 | {_CHECK_URL_123}\n"
"Title: ML Paper\n"
"Authors: Alice, Bob\n"
"Status: submitted\n"
"Current Categories: cs.LG cs.AI\n"
f"Submitted: {_SUBMIT_TIME_STR}\n"
)


def test_submission_exact_text_no_submit_time():
sub = _mock_submission(submit_time=None)
text, _ = render_submission_block(sub)
assert text == (
f"Submission: submit/123 | {_CHECK_URL_123}\n"
"Title: ML Paper\n"
"Authors: Alice, Bob\n"
"Status: submitted\n"
"Current Categories: cs.LG cs.AI\n"
f"View in Check: {_CHECK_URL_123}\n"
)


def test_submission_exact_html():
sub = _mock_submission()
_, html_out = render_submission_block(sub)
assert html_out == (
"<p><strong>Submission:</strong> submit/123<br>\n"
f"<p><strong>Submission:</strong> submit/123 | <a href=\"{_CHECK_URL_123}\">{_CHECK_URL_123}</a><br>\n"
"<strong>Title:</strong> ML Paper<br>\n"
"<strong>Authors:</strong> Alice, Bob<br>\n"
"<strong>Status:</strong> submitted<br>\n"
"<strong>Current Categories:</strong> cs.LG cs.AI<br>\n"
f"<strong>View in Check:</strong> <a href=\"{_CHECK_URL_123}\">{_CHECK_URL_123}</a></p>\n"
f"<strong>Current Categories:</strong> cs.LG cs.AI<br>\n"
f"<strong>Submitted:</strong> {_SUBMIT_TIME_STR}</p>\n"
)


def test_submission_exact_html_no_submit_time():
sub = _mock_submission(submit_time=None)
_, html_out = render_submission_block(sub)
assert html_out == (
f"<p><strong>Submission:</strong> submit/123 | <a href=\"{_CHECK_URL_123}\">{_CHECK_URL_123}</a><br>\n"
"<strong>Title:</strong> ML Paper<br>\n"
"<strong>Authors:</strong> Alice, Bob<br>\n"
"<strong>Status:</strong> submitted<br>\n"
"<strong>Current Categories:</strong> cs.LG cs.AI</p>\n"
)


Expand All @@ -275,12 +301,12 @@ def test_full_email_exact_text():
promote_text, promote_html = render_change_block(promote, _USER)
body_text, _ = render_body(sub_text, sub_html, [comment_text, promote_text], [comment_html, promote_html])
assert body_text == (
"Submission: submit/123\n"
f"Submission: submit/123 | {_CHECK_URL}\n"
"Title: ML Paper\n"
"Authors: Alice, Bob\n"
"Status: submitted\n"
"Current Categories: cs.LG cs.AI\n"
f"View in Check: {_CHECK_URL}\n"
f"Submitted: {_SUBMIT_TIME_STR}\n"
"\n"
"----------------------------------------\n"
f"[{_WHEN}] {_USER} added a comment:\n"
Expand All @@ -293,6 +319,9 @@ def test_full_email_exact_text():
f"How to use Check: {_GUIDE_URL} \n"
f"How to moderate: {_MOD_URL} \n"
f"Moderator Hub: {_HUB_URL} \n"
"\n"
"This email was generated by the moderator email system version 2.0\n"
"Some of your arXiv moderation emails may look different. We are in the process of transitioning email systems.\n"
)


Expand All @@ -305,12 +334,12 @@ def test_full_email_exact_html():
promote_text, promote_html = render_change_block(promote, _USER)
_, body_html = render_body(sub_text, sub_html, [comment_text, promote_text], [comment_html, promote_html])
assert body_html == (
"<p><strong>Submission:</strong> submit/123<br>\n"
f"<p><strong>Submission:</strong> submit/123 | <a href=\"{_CHECK_URL}\">{_CHECK_URL}</a><br>\n"
"<strong>Title:</strong> ML Paper<br>\n"
"<strong>Authors:</strong> Alice, Bob<br>\n"
"<strong>Status:</strong> submitted<br>\n"
"<strong>Current Categories:</strong> cs.LG cs.AI<br>\n"
f"<strong>View in Check:</strong> <a href=\"{_CHECK_URL}\">{_CHECK_URL}</a></p>\n"
f"<strong>Submitted:</strong> {_SUBMIT_TIME_STR}</p>\n"
"\n"
"<hr>\n"
f"<p><strong>[{_WHEN}] {_USER}</strong> added a comment:<br>\n"
Expand All @@ -322,6 +351,8 @@ def test_full_email_exact_html():
f"<p><a href=\"{_GUIDE_URL}\">How to use Check</a> | "
f"<a href=\"{_MOD_URL}\">How to moderate</a> | "
f"<a href=\"{_HUB_URL}\">Moderator Hub</a></p>\n"
"<p>This email was generated by the moderator email system version 2.0<br>\n"
"Some of your arXiv moderation emails may look different. We are in the process of transitioning email systems.</p>\n"
)


Expand Down
Loading
Loading