Source code for brunns.matchers.smtp

import email
import re
from dataclasses import dataclass
from re import Match
from typing import Union, cast

from deprecated import deprecated
from hamcrest import anything
from hamcrest.core.base_matcher import BaseMatcher
from hamcrest.core.description import Description
from hamcrest.core.helpers.wrap_matcher import wrap_matcher
from hamcrest.core.matcher import Matcher

from brunns.matchers.utils import (
    append_matcher_description,
    describe_field_match,
    describe_field_mismatch,
)

ANYTHING = anything()


[docs] @dataclass class Email: to_name: str to_address: str from_name: str from_address: str subject: str body_text: str
[docs] def is_email() -> "EmailWith": """Matches a string as an RFC 822 / MIME email message. This function returns an :class:`EmailWith` matcher which can be refined using builder methods (e.g., ``.with_subject(...)``). It parses the string using Python's :mod:`email` module. :return: A matcher for email strings. """ return EmailWith()
[docs] class EmailWith(BaseMatcher[str]): def __init__( self, to_name: Union[str, Matcher[str]] = ANYTHING, to_address: Union[str, Matcher[str]] = ANYTHING, from_name: Union[str, Matcher[str]] = ANYTHING, from_address: Union[str, Matcher[str]] = ANYTHING, subject: Union[str, Matcher[str]] = ANYTHING, body_text: Union[str, Matcher[str]] = ANYTHING, ) -> None: self.to_name: Matcher[str] = wrap_matcher(to_name) self.to_address: Matcher[str] = wrap_matcher(to_address) self.from_name: Matcher[str] = wrap_matcher(from_name) self.from_address: Matcher[str] = wrap_matcher(from_address) self.subject: Matcher[str] = wrap_matcher(subject) self.body_text: Matcher[str] = wrap_matcher(body_text) def _matches(self, actual_email: str) -> bool: email = self._parse_email(actual_email) return ( self.to_name.matches(email.to_name) and self.to_address.matches(email.to_address) and self.from_name.matches(email.from_name) and self.from_address.matches(email.from_address) and self.subject.matches(email.subject) and self.body_text.matches(email.body_text) ) @staticmethod def _parse_email(actual_email: str) -> Email: parsed = email.message_from_string(actual_email) # Handle cases where To/From might be missing or malformed gracefully? # Current implementation assumes valid format per regex below. actual_to_name, actual_to_address = cast("Match", re.match("(.*) <(.*)>", parsed["To"])).groups() actual_from_name, actual_from_address = cast("Match", re.match("(.*) <(.*)>", parsed["From"])).groups() actual_subject = parsed["Subject"] actual_body_text = cast("str", parsed.get_payload()) return Email( to_name=actual_to_name, to_address=actual_to_address, from_name=actual_from_name, from_address=actual_from_address, subject=actual_subject, body_text=actual_body_text, )
[docs] def describe_to(self, description: Description) -> None: description.append_text("email with") append_matcher_description(self.to_name, "to_name", description) append_matcher_description(self.to_address, "to_address", description) append_matcher_description(self.from_name, "from_name", description) append_matcher_description(self.from_address, "from_address", description) append_matcher_description(self.subject, "subject", description) append_matcher_description(self.body_text, "body_text", description)
[docs] def describe_mismatch(self, actual_email: str, mismatch_description: Description) -> None: email = self._parse_email(actual_email) mismatch_description.append_text("was email with") describe_field_mismatch(self.to_name, "to_name", email.to_name, mismatch_description) describe_field_mismatch(self.to_address, "to_address", email.to_address, mismatch_description) describe_field_mismatch(self.from_name, "from_name", email.from_name, mismatch_description) describe_field_mismatch(self.from_address, "from_address", email.from_address, mismatch_description) describe_field_mismatch(self.subject, "subject", email.subject, mismatch_description) describe_field_mismatch(self.body_text, "body", email.body_text, mismatch_description)
[docs] def describe_match(self, actual_email: str, match_description: Description) -> None: email = self._parse_email(actual_email) match_description.append_text("was email with") describe_field_match(self.to_name, "to_name", email.to_name, match_description) describe_field_match(self.to_address, "to_address", email.to_address, match_description) describe_field_match(self.from_name, "from_name", email.from_name, match_description) describe_field_match(self.from_address, "from_address", email.from_address, match_description) describe_field_match(self.subject, "subject", email.subject, match_description) describe_field_match(self.body_text, "body", email.body_text, match_description)
[docs] def with_to_name(self, to_name: Union[str, Matcher[str]]): """Matches if the email 'To' name matches the given value or matcher. :param to_name: The expected recipient name or matcher. :return: Self, for chaining. """ self.to_name = wrap_matcher(to_name) return self
[docs] def and_to_name(self, to_name: Union[str, Matcher[str]]): """Matches if the email 'To' name matches the given value or matcher. A synonym for :meth:`with_to_name`. :param to_name: The expected recipient name or matcher. :return: Self, for chaining. """ return self.with_to_name(to_name)
[docs] def with_to_address(self, to_address: Union[str, Matcher[str]]): """Matches if the email 'To' address matches the given value or matcher. :param to_address: The expected recipient email address or matcher. :return: Self, for chaining. """ self.to_address = wrap_matcher(to_address) return self
[docs] def and_to_address(self, to_address: Union[str, Matcher[str]]): """Matches if the email 'To' address matches the given value or matcher. A synonym for :meth:`with_to_address`. :param to_address: The expected recipient email address or matcher. :return: Self, for chaining. """ return self.with_to_address(to_address)
[docs] def with_from_name(self, from_name: Union[str, Matcher[str]]): """Matches if the email 'From' name matches the given value or matcher. :param from_name: The expected sender name or matcher. :return: Self, for chaining. """ self.from_name = wrap_matcher(from_name) return self
[docs] def and_from_name(self, from_name: Union[str, Matcher[str]]): """Matches if the email 'From' name matches the given value or matcher. A synonym for :meth:`with_from_name`. :param from_name: The expected sender name or matcher. :return: Self, for chaining. """ return self.with_from_name(from_name)
[docs] def with_from_address(self, from_address: Union[str, Matcher[str]]): """Matches if the email 'From' address matches the given value or matcher. :param from_address: The expected sender email address or matcher. :return: Self, for chaining. """ self.from_address = wrap_matcher(from_address) return self
[docs] def and_from_address(self, from_address: Union[str, Matcher[str]]): """Matches if the email 'From' address matches the given value or matcher. A synonym for :meth:`with_from_address`. :param from_address: The expected sender email address or matcher. :return: Self, for chaining. """ return self.with_from_address(from_address)
[docs] def with_subject(self, subject: Union[str, Matcher[str]]): """Matches if the email subject matches the given value or matcher. :param subject: The expected subject string or matcher. :return: Self, for chaining. """ self.subject = wrap_matcher(subject) return self
[docs] def and_subject(self, subject: Union[str, Matcher[str]]): """Matches if the email subject matches the given value or matcher. A synonym for :meth:`with_subject`. :param subject: The expected subject string or matcher. :return: Self, for chaining. """ return self.with_subject(subject)
[docs] def with_body_text(self, body_text: Union[str, Matcher[str]]): """Matches if the email body text matches the given value or matcher. :param body_text: The expected body string or matcher. :return: Self, for chaining. """ self.body_text = wrap_matcher(body_text) return self
[docs] def and_body_text(self, body_text: Union[str, Matcher[str]]): """Matches if the email body text matches the given value or matcher. A synonym for :meth:`with_body_text`. :param body_text: The expected body string or matcher. :return: Self, for chaining. """ return self.with_body_text(body_text)
[docs] @deprecated(version="2.3.0", reason="Use builder style is_email()") def email_with( to_name: Union[str, Matcher[str]] = ANYTHING, to_address: Union[str, Matcher[str]] = ANYTHING, from_name: Union[str, Matcher[str]] = ANYTHING, from_address: Union[str, Matcher[str]] = ANYTHING, subject: Union[str, Matcher[str]] = ANYTHING, body_text: Union[str, Matcher[str]] = ANYTHING, ) -> Matcher: # pragma: no cover """Matches an email string with specific attributes. .. deprecated:: 2.3.0 Use :func:`is_email` and its builder methods instead. :param to_name: Expected recipient name. :param to_address: Expected recipient email address. :param from_name: Expected sender name. :param from_address: Expected sender email address. :param subject: Expected subject line. :param body_text: Expected body content. :return: A matcher for email strings. """ return EmailWith( to_name=to_name, to_address=to_address, from_name=from_name, from_address=from_address, subject=subject, body_text=body_text, )