feat: allow to limit max email recipients (#2436)

* feat: allow to limit max email recipients

* Set cmake min version

---------

Co-authored-by: Adrià Casajús <adria.casajus@proton.ch>
This commit is contained in:
Carlos Quintana 2025-04-14 16:03:02 +02:00 committed by GitHub
parent c7cbc7a4c8
commit 0e95f3d047
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 75 additions and 0 deletions

View file

@ -8,6 +8,9 @@ on:
- v*
pull_request:
env:
CMAKE_POLICY_VERSION_MINIMUM: 3.5
jobs:
lint:
runs-on: ubuntu-latest

View file

@ -685,3 +685,4 @@ MAILBOX_VERIFICATION_OVERRIDE_CODE: Optional[str] = os.environ.get(
AUDIT_LOG_MAX_DAYS = int(os.environ.get("AUDIT_LOG_MAX_DAYS", 30))
ALIAS_TRASH_DAYS = int(os.environ.get("ALIAS_TRASH_DAYS", 30))
ALLOWED_OAUTH_SCHEMES = get_env_csv("ALLOWED_OAUTH_SCHEMES", "auth.simplelogin,https")
MAX_EMAIL_FORWARD_RECIPIENTS = int(os.environ.get("MAX_EMAIL_FORWARD_RECIPIENTS", 30))

18
app/email/checks.py Normal file
View file

@ -0,0 +1,18 @@
from app.email import headers
from app.log import LOG
from email.message import Message
from flanker.addresslib import address
def check_recipient_limit(msg: Message, limit: int) -> bool:
# Count total recipients in TO and CC
to_addrs = address.parse_list(str(msg.get(headers.TO, "")))
cc_addrs = address.parse_list(str(msg.get(headers.CC, "")))
total_recipients = len(to_addrs) + len(cc_addrs)
if total_recipients > limit:
LOG.w(
f"Too many recipients ({total_recipients}). Max allowed: {limit}. Refusing to forward"
)
return False
return True

View file

@ -61,4 +61,5 @@ E522 = (
E523 = "550 SL E523 Unknown error"
E524 = "550 SL E524 Wrong use of reverse-alias"
E525 = "550 SL E525 Alias loop"
E526 = "550 SL E526 Too many recipients"
# endregion

View file

@ -86,9 +86,11 @@ from app.config import (
OLD_UNSUBSCRIBER,
ALERT_FROM_ADDRESS_IS_REVERSE_ALIAS,
ALERT_TO_NOREPLY,
MAX_EMAIL_FORWARD_RECIPIENTS,
)
from app.db import Session
from app.email import status, headers
from app.email.checks import check_recipient_limit
from app.email.rate_limit import rate_limited
from app.email.spam import get_spam_score
from app.email_utils import (
@ -911,6 +913,10 @@ def forward_email_to_mailbox(
add_or_replace_header(msg, "Reply-To", new_reply_to_header)
LOG.d("Reply-To header, new:%s, old:%s", new_reply_to_header, original_reply_to)
# Check recipient limit
if not check_recipient_limit(msg, MAX_EMAIL_FORWARD_RECIPIENTS):
return False, status.E526
# replace CC & To emails by reverse-alias for all emails that are not alias
try:
replace_header_when_forward(msg, alias, headers.CC)

View file

@ -0,0 +1,46 @@
from email.message import Message
from app.email.checks import check_recipient_limit
from app.email import headers
from tests.utils import random_email
def _email_list(size: int) -> str:
emails = []
for i in range(size):
emails.append(random_email())
return ", ".join(emails)
def _create_message(to: str, cc: str) -> Message:
message = Message()
message[headers.CC] = cc
message[headers.TO] = to
return message
def test_can_forward_if_below_limit():
msg = _create_message(to=_email_list(1), cc=_email_list(1))
assert check_recipient_limit(msg, 5)
def test_can_forward_if_just_limit():
msg = _create_message(to=_email_list(1), cc=_email_list(1))
assert check_recipient_limit(msg, 2)
def test_cannot_forward_if_single_list_above_limit():
msg = _create_message(to=_email_list(3), cc=_email_list(0))
assert check_recipient_limit(msg, 2) is False
def test_cannot_forward_if_both_lists_above_limit():
msg = _create_message(to=_email_list(3), cc=_email_list(3))
assert check_recipient_limit(msg, 2) is False
def test_cannot_forward_if_both_lists_add_up_to_limit():
msg = _create_message(to=_email_list(3), cc=_email_list(3))
assert check_recipient_limit(msg, 5) is False