updated apps
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user