diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 74c992cd..60835125 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,6 +8,9 @@ on: - v* pull_request: +env: + CMAKE_POLICY_VERSION_MINIMUM: 3.5 + jobs: lint: runs-on: ubuntu-latest diff --git a/app/config.py b/app/config.py index fe5df771..64ae82b6 100644 --- a/app/config.py +++ b/app/config.py @@ -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)) diff --git a/app/email/checks.py b/app/email/checks.py new file mode 100644 index 00000000..1a402549 --- /dev/null +++ b/app/email/checks.py @@ -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 diff --git a/app/email/status.py b/app/email/status.py index e375bc46..de0f8b49 100644 --- a/app/email/status.py +++ b/app/email/status.py @@ -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 diff --git a/email_handler.py b/email_handler.py index 53ddad90..12306204 100644 --- a/email_handler.py +++ b/email_handler.py @@ -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) diff --git a/tests/email_tests/test_checks.py b/tests/email_tests/test_checks.py new file mode 100644 index 00000000..89f69d9f --- /dev/null +++ b/tests/email_tests/test_checks.py @@ -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