updated apps

This commit is contained in:
2026-07-14 23:57:03 -04:00
parent 6cc7212cef
commit 010e828e9c
797 changed files with 45153 additions and 4246 deletions
@@ -43,7 +43,12 @@ _MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
DOMAIN_LANG_MAP = {
"amazon.de": ["versandbestaetigung", "Geliefert:", "Zugestellt:"],
"amazon.it": ["conferma-spedizione", "Consegna effettuata:", "Arriverà"],
"amazon.it": [
"conferma-spedizione",
"Consegna effettuata:",
"Arriverà",
"Spedito:",
],
"amazon.nl": [
"update-bestelling",
"verzending-volgen",
@@ -374,7 +374,7 @@ def _get_courier_info(
),
]
base_path = f"{hass.config.path()}/{default_image_path(hass, config)}"
base_path = hass.config.path(default_image_path(hass, config))
for (
active,
@@ -5,6 +5,7 @@ import binascii
import logging
import re
import unicodedata
from urllib.parse import quote, unquote
import aioimaplib
from aioimaplib import (
@@ -115,6 +116,30 @@ def quote_folder(folder: str) -> str:
return folder if _is_imap_atom(folder) else f'"{folder}"'
def encode_folder_ref(folder: str) -> str:
"""Percent-encode a folder name for use in a composite ``folder/uid`` ID.
Multi-folder searches tag each UID with its source folder as
``folder/uid``. Those composite IDs are space-joined and re-split on
whitespace at several call sites, and split on ``/`` to recover the
folder — so the folder component must contain neither whitespace nor
``/``. A folder named ``# - Projects`` would otherwise shatter into
``#``, ``-``, ``Projects/55`` when the joined ID list is ``.split()``.
``quote(..., safe="")`` escapes both (and ``%`` itself, keeping the
round-trip lossless for any folder name).
"""
return quote(folder, safe="")
def decode_folder_ref(folder: str) -> str:
"""Decode the percent-encoded folder component of a composite ID.
Takes the already-split folder component (everything before the final
``/`` of a ``folder/uid`` ID), not the full composite ID.
"""
return unquote(folder)
class InvalidAuth(HomeAssistantError):
"""Raise exception for invalid credentials."""
@@ -382,7 +407,7 @@ def _parse_esearch_line(line_bytes: bytes) -> list[bytes]:
pass
else:
uids.append(part)
return [f"{mailbox}/{uid}".encode() for uid in uids]
return [f"{encode_folder_ref(mailbox)}/{uid}".encode() for uid in uids]
async def _execute_single_search(account: IMAP4_SSL, search_query: str) -> list[bytes]: # noqa: C901
@@ -446,7 +471,8 @@ async def _execute_single_search(account: IMAP4_SSL, search_query: str) -> list[
if res.result == "OK" and res.lines:
parsed = parse_search_response(res.lines)
all_uids.extend(
f"{folder}/{uid.decode()}".encode() for uid in parsed
f"{encode_folder_ref(folder)}/{uid.decode()}".encode()
for uid in parsed
)
except TimeoutError:
raise
@@ -570,7 +596,7 @@ async def email_fetch(account: IMAP4_SSL, num, parts: str = "(RFC822)") -> tuple
num_str = num.decode() if isinstance(num, bytes) else str(num)
if "/" in num_str:
folder, num_str = num_str.rsplit("/", 1)
await selectfolder(account, folder)
await selectfolder(account, decode_folder_ref(folder))
try:
res = await account.uid("FETCH", num_str, parts)
except TimeoutError:
@@ -597,7 +623,7 @@ async def email_fetch_headers(account: IMAP4_SSL, num) -> tuple:
num_str = num.decode() if isinstance(num, bytes) else str(num)
if "/" in num_str:
folder, num_str = num_str.rsplit("/", 1)
await selectfolder(account, folder)
await selectfolder(account, decode_folder_ref(folder))
try:
res = await account.uid("FETCH", num_str, "(BODY[HEADER.FIELDS (SUBJECT)])")
except TimeoutError:
@@ -627,7 +653,7 @@ async def email_fetch_text(account: IMAP4_SSL, num, parts: str = "(BODY[1])") ->
num_str = num.decode() if isinstance(num, bytes) else str(num)
if "/" in num_str:
folder, num_str = num_str.rsplit("/", 1)
await selectfolder(account, folder)
await selectfolder(account, decode_folder_ref(folder))
try:
res = await account.uid("FETCH", num_str, parts)
except TimeoutError:
@@ -688,6 +714,7 @@ async def email_fetch_batch( # noqa: C901
num_str = num.decode() if isinstance(num, bytes) else str(num)
if "/" in num_str:
folder, actual_num = num_str.rsplit("/", 1)
folder = decode_folder_ref(folder)
else:
folder, actual_num = None, num_str
folder_to_nums.setdefault(folder, []).append(actual_num)
@@ -114,8 +114,21 @@ def _find_tracking_in_body(
return None
def save_image_data_to_disk(shipper_name: str, path: str, image_data: bytes) -> bool:
def save_image_data_to_disk(
shipper_name: str, path: str, image_data: bytes | None
) -> bool:
"""Write image bytes to disk and verify."""
if not image_data:
# Extraction can hand us zero bytes (e.g. a bare/empty base64 data URI
# in the email HTML). Writing that produces a 0-byte "photo" the
# camera then serves as a broken image — report failure instead so
# the caller falls through to the next extraction pass.
_LOGGER.debug(
"%s - No image data extracted; not writing %s",
shipper_name,
path,
)
return False
try:
# Ensure directory exists
directory = Path(path).parent
@@ -240,8 +253,11 @@ def _extract_from_html(
else str(payload)
)
# Base64 check
if matches := re.findall(base64_pattern, content):
# Base64 check. Skip empty matches: a bare "data:image/...;base64,"
# URI (seen in real FedEx delivered emails) matches zero characters,
# and taking it would discard a real photo later in the same part.
matches = [m for m in re.findall(base64_pattern, content) if m]
if matches:
try:
base64_data = matches[0].replace(" ", "").replace("=3D", "=")
return save_image_data_to_disk(