Updated apps
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""Email parsing and validation utilities for Mail and Packages."""
|
||||
|
||||
import email
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from aioimaplib import IMAP4_SSL
|
||||
from voluptuous import Email, MultipleInvalid, Schema
|
||||
|
||||
from custom_components.mail_and_packages.const import SENSOR_DATA
|
||||
from custom_components.mail_and_packages.utils.cache import EmailCache
|
||||
|
||||
from .imap import email_fetch
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_email_address(email_address: str) -> bool:
|
||||
"""Validate the format of an email address.
|
||||
|
||||
Args:
|
||||
email_address (str): The email address to validate.
|
||||
|
||||
Returns:
|
||||
bool: `True` if the email address is valid, `False` otherwise.
|
||||
|
||||
"""
|
||||
try:
|
||||
schema = Schema(Email()) # pylint: disable=no-value-for-parameter
|
||||
schema(email_address)
|
||||
except MultipleInvalid:
|
||||
_LOGGER.error("'%s' does not look like a valid email address", email_address)
|
||||
return False
|
||||
|
||||
_LOGGER.debug("%s is a valid email address", email_address)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def generate_service_email_domains(amazon_fwds: list) -> set[str]:
|
||||
"""Generate a set of service email domains from amazon domains and SENSOR_DATA.
|
||||
|
||||
Returns:
|
||||
set[str]: A set of unique email domains.
|
||||
|
||||
"""
|
||||
domains = {fwd.split("@")[1] for fwd in amazon_fwds if "@" in fwd}
|
||||
for sensor in SENSOR_DATA.values():
|
||||
for address in sensor.get("email", []):
|
||||
if "@" not in address:
|
||||
continue
|
||||
domains.add(address.split("@")[1])
|
||||
return domains
|
||||
|
||||
|
||||
def _match_patterns(
|
||||
text: str,
|
||||
patterns: list[re.Pattern],
|
||||
body_count: bool,
|
||||
) -> tuple[int, int | None]:
|
||||
"""Apply patterns to text and return occurrence count and extracted value.
|
||||
|
||||
Returns:
|
||||
tuple[int, int | None]: (count_of_matches, extracted_value)
|
||||
|
||||
"""
|
||||
local_count = 0
|
||||
extracted_value = None
|
||||
|
||||
for pattern in patterns:
|
||||
if body_count:
|
||||
if (found := pattern.search(text)) and len(found.groups()) > 0:
|
||||
_LOGGER.debug(
|
||||
"Found (%s) in email result: %s",
|
||||
pattern.pattern,
|
||||
found.groups(),
|
||||
)
|
||||
extracted_value = int(found.group(1))
|
||||
elif (found := pattern.findall(text)) and len(found) > 0:
|
||||
_LOGGER.debug(
|
||||
"Found (%s) in email %s times.",
|
||||
pattern.pattern,
|
||||
len(found),
|
||||
)
|
||||
local_count += len(found)
|
||||
|
||||
return local_count, extracted_value
|
||||
|
||||
|
||||
async def _scan_email_for_text(
|
||||
account: type[IMAP4_SSL],
|
||||
email_id: str,
|
||||
patterns: list[re.Pattern],
|
||||
body_count: bool,
|
||||
cache: EmailCache | None = None,
|
||||
) -> tuple[int, int | None]:
|
||||
"""Scan a single email for terms.
|
||||
|
||||
Returns:
|
||||
tuple[int, int | None]: (total_matches, last_extracted_value)
|
||||
|
||||
"""
|
||||
total_matches = 0
|
||||
last_value = None
|
||||
|
||||
if cache:
|
||||
data = (await cache.fetch(email_id, "(RFC822)"))[1]
|
||||
else:
|
||||
data = (await email_fetch(account, email_id, "(RFC822)"))[1]
|
||||
for response_part in data:
|
||||
if not isinstance(response_part, (bytes, bytearray)):
|
||||
continue
|
||||
|
||||
msg = email.message_from_bytes(response_part)
|
||||
|
||||
for part in msg.walk():
|
||||
if part.get_content_type() not in ["text/html", "text/plain"]:
|
||||
continue
|
||||
|
||||
email_msg = part.get_payload(decode=True)
|
||||
try:
|
||||
email_msg = email_msg.decode("utf-8", "ignore")
|
||||
except (AttributeError, UnicodeError):
|
||||
continue
|
||||
|
||||
matches, value = _match_patterns(email_msg, patterns, body_count)
|
||||
total_matches += matches
|
||||
if value is not None:
|
||||
last_value = value
|
||||
|
||||
return total_matches, last_value
|
||||
|
||||
|
||||
async def find_text(
|
||||
sdata: Any,
|
||||
account: type[IMAP4_SSL],
|
||||
search_terms: list,
|
||||
body_count: bool,
|
||||
cache: EmailCache | None = None,
|
||||
) -> int:
|
||||
"""Filter for specific words in email."""
|
||||
_LOGGER.debug("Searching for (%s) in (%s) emails", search_terms, len(sdata))
|
||||
mail_list = sdata[0].split()
|
||||
count = 0
|
||||
|
||||
# Pre-compile regex patterns once
|
||||
patterns = [re.compile(rf"{term}") for term in search_terms]
|
||||
|
||||
for i in mail_list:
|
||||
matches, value = await _scan_email_for_text(
|
||||
account, i, patterns, body_count, cache
|
||||
)
|
||||
|
||||
if body_count:
|
||||
# If extracting a value, "last found value wins" (updates count)
|
||||
if value is not None:
|
||||
count = value
|
||||
else:
|
||||
# If counting occurrences, accumulate
|
||||
count += matches
|
||||
|
||||
return count
|
||||
|
||||
|
||||
async def find_text_matches(
|
||||
sdata: Any,
|
||||
account: type[IMAP4_SSL],
|
||||
search_terms: list,
|
||||
body_count: bool,
|
||||
cache: EmailCache | None = None,
|
||||
) -> tuple[int, list[bytes]]:
|
||||
"""Filter for specific words in email and return matches and matching IDs."""
|
||||
_LOGGER.debug("Searching for (%s) in (%s) emails", search_terms, len(sdata))
|
||||
mail_list = sdata[0].split()
|
||||
count = 0
|
||||
matching_ids = []
|
||||
|
||||
# Pre-compile regex patterns once
|
||||
patterns = [re.compile(rf"{term}") for term in search_terms]
|
||||
|
||||
for i in mail_list:
|
||||
matches, value = await _scan_email_for_text(
|
||||
account, i, patterns, body_count, cache
|
||||
)
|
||||
|
||||
matched = False
|
||||
if body_count:
|
||||
# If extracting a value, "last found value wins" (updates count)
|
||||
if value is not None:
|
||||
count = value
|
||||
matched = True
|
||||
# If counting occurrences, accumulate
|
||||
elif matches > 0:
|
||||
count += matches
|
||||
matched = True
|
||||
|
||||
if matched:
|
||||
matching_ids.append(i)
|
||||
|
||||
return count, matching_ids
|
||||
Reference in New Issue
Block a user