<commit_before>import datetime
import hashlib
import logging
import os
import smtplib
from email.headerregistry import Address
from email.parser import Parser
from email.policy import default
from email.utils import formataddr, parseaddr
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple

import backoff
import orjson
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.backends.smtp import EmailBackend
from django.core.mail.message import sanitize_address
from django.core.management import CommandError
from django.db import transaction
from django.http import HttpRequest
from django.template import loader
from django.template.exceptions import TemplateDoesNotExist
from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
from django.utils.translation import override as override_language

from confirmation.models import generate_key, one_click_unsubscribe_link
from scripts.setup.inline_email_css import inline_template
from zerver.lib.logging_util import log_to_file
from zerver.models import EMAIL_TYPES, Realm, ScheduledEmail, UserProfile, get_user_profile_by_id
from zproject.email_backends import EmailLogBackEnd, get_forward_address

MAX_CONNECTION_TRIES = 3

## Logging setup ##

logger = logging.getLogger("zulip.send_email")
log_to_file(logger, settings.EMAIL_LOG_PATH)


class FromAddress:
    SUPPORT = parseaddr(settings.ZULIP_ADMINISTRATOR)[1]
    NOREPLY = parseaddr(settings.NOREPLY_EMAIL_ADDRESS)[1]

    support_placeholder = "SUPPORT"
    no_reply_placeholder = "NO_REPLY"
    tokenized_no_reply_placeholder = "TOKENIZED_NO_REPLY"

    # Generates an unpredictable noreply address.
    @staticmethod
    def tokenized_no_reply_address() -> str:
        if settings.ADD_TOKENS_TO_NOREPLY_ADDRESS:
            return parseaddr(settings.TOKENIZED_NOREPLY_EMAIL_ADDRESS)[1].format(
                token=generate_key()
            )
        return FromAddress.NOREPLY

    @staticmethod
    def security_email_from_name(
        language: Optional[str] = None, user_profile: Optional[UserProfile] = None
    ) -> str:
        if language is None:
            assert user_profile is not None
            language = user_profile.default_language

        with override_language(language):
            return _("Zulip Account Security")


def build_email(
    template_prefix: str,
    to_user_ids: Optional[List[int]] = None,
    to_emails: Optional[List[str]] = None,
    from_name: Optional[str] = None,
    from_address: Optional[str] = None,
    reply_to_email: Optional[str] = None,
    language: Optional[str] = None,
    context: Mapping[str, Any] = {},
    realm: Optional[Realm] = None,
) -> EmailMultiAlternatives:
    # Callers should pass exactly one of to_user_id and to_email.
    assert (to_user_ids is None) ^ (to_emails is None)
    if to_user_ids is not None:
        to_users = [get_user_profile_by_id(to_user_id) for to_user_id in to_user_ids]
        if realm is None:
            assert len({to_user.realm_id for to_user in to_users}) == 1
            realm = to_users[0].realm
        to_emails = []
        for to_user in to_users:
            stringified = str(
                Address(display_name=to_user.full_name, addr_spec=to_user.delivery_email)
            )
            # Check ASCII encoding length.  Amazon SES rejects emails
            # with From or To values longer than 320 characters (which
            # appears to be a misinterpretation of the RFC); in that
            # case we drop the name part from the address, under the
            # theory that it's better to send the email with a
            # simplified field than not at all.
            if len(sanitize_address(stringified, "utf-8")) > 320:
                stringified = str(Address(addr_spec=to_user.delivery_email))
            to_emails.append(stringified)

    extra_headers = {}
    if realm is not None:
        # formaddr is meant for formatting (display_name, email_address) pair for headers like "To",
        # but we can use its utility for formatting the List-Id header, as it follows the same format,
        # except having just a domain instead of an email address.
        extra_headers["List-Id"] = formataddr((realm.name, realm.host))

    context = {
        **context,
        "support_email": FromAddress.SUPPORT,
        "email_images_base_uri": settings.ROOT_DOMAIN_URI + "/static/images/emails",
        "physical_address": settings.PHYSICAL_ADDRESS,
    }

    def render_templates() -> Tuple[str, str, str]:
        email_subject = (
            loader.render_to_string(
                template_prefix + ".subject.txt", context=context, using="Jinja2_plaintext"
            )
            .strip()
            .replace("\n", "")
        )
        message = loader.render_to_string(
            template_prefix + ".txt", context=context, using="Jinja2_plaintext"
        )

        try:<commit_msg>email: Attempt to suppress email auto-replies.<commit_after>import datetime
import hashlib
import logging
import os
import smtplib
from email.headerregistry import Address
from email.parser import Parser
from email.policy import default
from email.utils import formataddr, parseaddr
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple

import backoff
import orjson
from django.conf import settings
from django.core.mail import EmailMultiAlternatives, get_connection
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.backends.smtp import EmailBackend
from django.core.mail.message import sanitize_address
from django.core.management import CommandError
from django.db import transaction
from django.http import HttpRequest
from django.template import loader
from django.template.exceptions import TemplateDoesNotExist
from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _
from django.utils.translation import override as override_language

from confirmation.models import generate_key, one_click_unsubscribe_link
from scripts.setup.inline_email_css import inline_template
from zerver.lib.logging_util import log_to_file
from zerver.models import EMAIL_TYPES, Realm, ScheduledEmail, UserProfile, get_user_profile_by_id
from zproject.email_backends import EmailLogBackEnd, get_forward_address

MAX_CONNECTION_TRIES = 3

## Logging setup ##

logger = logging.getLogger("zulip.send_email")
log_to_file(logger, settings.EMAIL_LOG_PATH)


class FromAddress:
    SUPPORT = parseaddr(settings.ZULIP_ADMINISTRATOR)[1]
    NOREPLY = parseaddr(settings.NOREPLY_EMAIL_ADDRESS)[1]

    support_placeholder = "SUPPORT"
    no_reply_placeholder = "NO_REPLY"
    tokenized_no_reply_placeholder = "TOKENIZED_NO_REPLY"

    # Generates an unpredictable noreply address.
    @staticmethod
    def tokenized_no_reply_address() -> str:
        if settings.ADD_TOKENS_TO_NOREPLY_ADDRESS:
            return parseaddr(settings.TOKENIZED_NOREPLY_EMAIL_ADDRESS)[1].format(
                token=generate_key()
            )
        return FromAddress.NOREPLY

    @staticmethod
    def security_email_from_name(
        language: Optional[str] = None, user_profile: Optional[UserProfile] = None
    ) -> str:
        if language is None:
            assert user_profile is not None
            language = user_profile.default_language

        with override_language(language):
            return _("Zulip Account Security")


def build_email(
    template_prefix: str,
    to_user_ids: Optional[List[int]] = None,
    to_emails: Optional[List[str]] = None,
    from_name: Optional[str] = None,
    from_address: Optional[str] = None,
    reply_to_email: Optional[str] = None,
    language: Optional[str] = None,
    context: Mapping[str, Any] = {},
    realm: Optional[Realm] = None,
) -> EmailMultiAlternatives:
    # Callers should pass exactly one of to_user_id and to_email.
    assert (to_user_ids is None) ^ (to_emails is None)
    if to_user_ids is not None:
        to_users = [get_user_profile_by_id(to_user_id) for to_user_id in to_user_ids]
        if realm is None:
            assert len({to_user.realm_id for to_user in to_users}) == 1
            realm = to_users[0].realm
        to_emails = []
        for to_user in to_users:
            stringified = str(
                Address(display_name=to_user.full_name, addr_spec=to_user.delivery_email)
            )
            # Check ASCII encoding length.  Amazon SES rejects emails
            # with From or To values longer than 320 characters (which
            # appears to be a misinterpretation of the RFC); in that
            # case we drop the name part from the address, under the
            # theory that it's better to send the email with a
            # simplified field than not at all.
            if len(sanitize_address(stringified, "utf-8")) > 320:
                stringified = str(Address(addr_spec=to_user.delivery_email))
            to_emails.append(stringified)

    # Attempt to suppress all auto-replies.  This header originally
    # came out of Microsoft Outlook and friends, but seems reasonably
    # commonly-recognized.
    extra_headers = {"X-Auto-Response-Suppress": "All"}

    if realm is not None:
        # formaddr is meant for formatting (display_name, email_address) pair for headers like "To",
        # but we can use its utility for formatting the List-Id header, as it follows the same format,
        # except having just a domain instead of an email address.
        extra_headers["List-Id"] = formataddr((realm.name, realm.host))

    context = {
        **context,
        "support_email": FromAddress.SUPPORT,
        "email_images_base_uri": settings.ROOT_DOMAIN_URI + "/static/images/emails",
        "physical_address": settings.PHYSICAL_ADDRESS,
    }

    def render_templates() -> Tuple[str, str, str]:
        email_subject = (
            loader.render_to_string(
                template_prefix + ".subject.txt", context=context, using="Jinja2_plaintext"
            )
            .strip()
            .replace("\n", "")
        )
        message = loader.render_to_string(
            template_prefix + ".txt", context=context, using="Jinja2_plaintext"
        )

        try:
{'fraction_non_alphanumeric': 0.07465143299767622, 'fraction_numerical': 0.0029047250193648335, 'mean_word_length': 3.261138613861386, 'pattern_counts': {'":': 7, '<': 3, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4005690', 'n_tokens_mistral': 3001, 'n_tokens_neox': 2944, 'n_words': 835}