This commit is contained in:
Home Assistant Version Control
2026-07-25 02:41:58 +00:00
parent 3dc00b2d26
commit b84bc6688e
52 changed files with 2002 additions and 219 deletions
+216 -123
View File
@@ -19,7 +19,7 @@ from homeassistant.const import (
CONF_RESOURCES,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_entry_oauth2_flow, selector
from .const import (
@@ -358,8 +358,17 @@ async def _get_mailboxes(
oauth_token=oauth_token,
)
except (TimeoutError, AioImapException, ConnectionRefusedError) as err:
_LOGGER.error("Unable to connect: %s", err)
except (TimeoutError, AioImapException, ConnectionRefusedError, InvalidAuth) as err:
_LOGGER.error(
"Unable to connect: %s (IMAP server %s:%s as %s, security: %s, oauth: %s, class: %s)",
err if str(err) else "Authentication failed or timed out",
host,
port,
user,
security,
oauth_token is not None,
type(err).__name__,
)
return []
_LOGGER.debug("Attempting to get mailbox list...")
@@ -493,6 +502,7 @@ async def _get_schema_step_2(
user_input: list,
default_dict: list,
hass: HomeAssistant,
entry: config_entries.ConfigEntry | None = None,
) -> Any:
"""Get a schema using the default_dict as a backup."""
if user_input is None:
@@ -502,6 +512,31 @@ async def _get_schema_step_2(
"""Get default value for key."""
return user_input.get(key, default_dict.get(key, fallback_default))
oauth_token = None
if entry:
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass,
entry,
)
)
session = config_entry_oauth2_flow.OAuth2Session(
hass,
entry,
implementation,
)
await session.async_ensure_token_valid()
oauth_token = session.token.get("access_token")
except Exception as err: # noqa: BLE001
_LOGGER.error("Error refreshing OAuth token: %s", err)
if not oauth_token:
if "token" in data and isinstance(data["token"], dict):
oauth_token = data["token"].get("access_token")
else:
oauth_token = data.get("access_token")
mailboxes = await _get_mailboxes(
hass,
data[CONF_HOST],
@@ -510,7 +545,7 @@ async def _get_schema_step_2(
data.get(CONF_PASSWORD, ""),
data[CONF_IMAP_SECURITY],
data[CONF_VERIFY_SSL],
data.get("token", {}).get("access_token"),
oauth_token,
)
default_folder = _get_default(CONF_FOLDER)
@@ -817,7 +852,15 @@ async def _validate_login(
except ssl.SSLError:
errors["base"] = "ssl_error"
except (TimeoutError, AioImapException, ConnectionRefusedError) as err:
_LOGGER.error("Unable to connect: %s", err)
_LOGGER.error(
"Unable to connect: %s (IMAP server %s:%s as %s, security: %s, oauth: False, class: %s)",
err if str(err) else "Authentication failed or timed out",
user_input[CONF_HOST],
user_input[CONF_PORT],
user_input[CONF_USERNAME],
user_input[CONF_IMAP_SECURITY],
type(err).__name__,
)
errors["base"] = "cannot_connect"
else:
if result != "OK":
@@ -840,6 +883,14 @@ class MailAndPackagesFlowHandler(
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
DOMAIN = DOMAIN
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get the options flow for this handler."""
return MailAndPackagesOptionsFlow(config_entry)
@property
def logger(self) -> logging.Logger:
"""Return logger."""
@@ -858,6 +909,12 @@ class MailAndPackagesFlowHandler(
"prompt": "consent",
}
)
elif auth_type == AUTH_TYPE_OAUTH_MICROSOFT:
data.update(
{
"prompt": "select_account",
}
)
return data
def __init__(self):
@@ -951,12 +1008,16 @@ class MailAndPackagesFlowHandler(
"""Handle OAuth2 completion — store token and continue to step 2."""
self._data.update(data)
if self._entry:
if self.source == config_entries.SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._entry,
data=self._data,
)
return await self.async_step_reconfig_2()
# Reconfigure or Reauth flow: update entry and reload
self.hass.config_entries.async_update_entry(
self._entry,
data=self._data,
)
await self.hass.config_entries.async_reload(self._entry.entry_id)
if self.context.get("source") == config_entries.SOURCE_REAUTH:
return self.async_abort(reason="reauth_successful")
_LOGGER.debug("%s reconfigured.", DOMAIN)
return self.async_abort(reason="reconfigure_successful")
return await self.async_step_config_2()
async def _show_auth_form(self, user_input):
@@ -1053,6 +1114,7 @@ class MailAndPackagesFlowHandler(
user_input,
defaults,
self.hass,
getattr(self, "_entry", None),
),
errors=self._errors,
)
@@ -1212,6 +1274,22 @@ class MailAndPackagesFlowHandler(
auth_type = self._data.get(CONF_AUTH_TYPE, AUTH_TYPE_PASSWORD)
if auth_type != AUTH_TYPE_PASSWORD:
if (
self._entry
and self._entry.data.get(CONF_AUTH_TYPE) == auth_type
and self._entry.data.get(CONF_HOST) == self._data.get(CONF_HOST)
and self._entry.data.get(CONF_USERNAME)
== self._data.get(CONF_USERNAME)
and "token" in self._data
):
self.hass.config_entries.async_update_entry(
self._entry,
data=self._data,
)
await self.hass.config_entries.async_reload(self._entry.entry_id)
_LOGGER.debug("%s reconfigured (oauth skip).", DOMAIN)
return self.async_abort(reason="reconfigure_successful")
self._data[CONF_VERIFY_SSL] = True
self.hass.data.setdefault(DOMAIN, {})
self.hass.data[DOMAIN]["oauth_provider"] = auth_type
@@ -1219,7 +1297,13 @@ class MailAndPackagesFlowHandler(
self._errors = await _validate_login(self.hass, self._data)
if self._errors == {}:
return await self.async_step_reconfig_2()
self.hass.config_entries.async_update_entry(
self._entry,
data=self._data,
)
await self.hass.config_entries.async_reload(self._entry.entry_id)
_LOGGER.debug("%s reconfigured.", DOMAIN)
return self.async_abort(reason="reconfigure_successful")
return await self._show_reconfig_imap_form(user_input)
@@ -1265,117 +1349,104 @@ class MailAndPackagesFlowHandler(
errors=self._errors,
)
async def async_step_reconfig_2(self, user_input=None):
"""Configure form step 2."""
class MailAndPackagesOptionsFlow(config_entries.OptionsFlow):
"""Options flow for Mail and Packages."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self._entry = config_entry
self._data = {**config_entry.data, **config_entry.options}
self._errors = {}
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Manage the options."""
self._errors = {}
_LOGGER.debug("Loading step 2...")
if user_input is not None:
self._data.update(user_input)
self._errors, user_input = await _validate_user_input(user_input, self.hass)
if self._data.get("generate_mp4", False):
if not await _check_ffmpeg():
self._errors["generate_mp4"] = "ffmpeg_not_found"
if len(self._errors) == 0:
if self._data.get(CONF_ALLOW_FORWARDED_EMAILS, False):
return await self.async_step_reconfig_forwarded_emails()
return await self.async_step_options_forwarded_emails()
if any(
sensor in self._data.get(CONF_RESOURCES, [])
for sensor in AMAZON_SENSORS
sensor in self._data[CONF_RESOURCES] for sensor in AMAZON_SENSORS
):
return await self.async_step_reconfig_amazon()
has_custom_image = (
self._data.get(CONF_CUSTOM_IMG)
or self._data.get(CONF_AMAZON_CUSTOM_IMG)
or self._data.get(CONF_UPS_CUSTOM_IMG)
or self._data.get(CONF_WALMART_CUSTOM_IMG)
or self._data.get(CONF_FEDEX_CUSTOM_IMG)
or self._data.get(CONF_GENERIC_CUSTOM_IMG)
or self._data.get(CONF_POST_DE_CUSTOM_IMG)
)
if has_custom_image:
return await self.async_step_reconfig_3()
return await self.async_step_options_amazon()
if self._data.get(CONF_CUSTOM_IMG, False):
return await self.async_step_options_3()
return await self.async_step_options_storage()
return await self.async_step_reconfig_storage()
return await self._show_options_2(user_input)
return await self._show_reconfig_2(user_input)
return await self._show_options_2(user_input)
return await self._show_reconfig_2(user_input)
async def _show_options_2(self, user_input):
"""Step 2 of options."""
if self._data.get(CONF_AMAZON_FWDS) == []:
self._data[CONF_AMAZON_FWDS] = "(none)"
async def _show_reconfig_2(self, user_input):
"""Step 2 setup."""
return self.async_show_form(
step_id="reconfig_2",
step_id="init",
data_schema=await _get_schema_step_2(
self._data,
user_input,
self._data,
self.hass,
self._data, user_input, self._data, self.hass, self._entry
),
errors=self._errors,
)
async def async_step_reconfig_3(self, user_input=None):
"""Configure form step 2."""
async def async_step_options_forwarded_emails(self, user_input=None):
"""Configure forwarded emails."""
self._errors = {}
if user_input is not None:
if user_input.get(CONF_FORWARDED_EMAILS) == "(none)":
user_input[CONF_FORWARDED_EMAILS] = []
self._data.update(user_input)
self._errors, user_input = await _validate_user_input(self._data, self.hass)
if len(self._errors) == 0:
return await self.async_step_reconfig_storage()
if any(
sensor in self._data[CONF_RESOURCES] for sensor in AMAZON_SENSORS
):
return await self.async_step_options_amazon()
if self._data.get(CONF_CUSTOM_IMG, False):
return await self.async_step_options_3()
return await self.async_step_options_storage()
return await self._show_options_forwarded_emails(user_input)
return await self._show_reconfig_3(user_input)
return await self._show_reconfig_3(user_input)
async def _show_reconfig_3(self, user_input=None): # pylint: disable=unused-argument
"""Step 3 setup."""
# Defaults
defaults = {
CONF_CUSTOM_IMG_FILE: DEFAULT_CUSTOM_IMG_FILE,
CONF_AMAZON_CUSTOM_IMG_FILE: DEFAULT_AMAZON_CUSTOM_IMG_FILE,
CONF_UPS_CUSTOM_IMG_FILE: DEFAULT_UPS_CUSTOM_IMG_FILE,
CONF_WALMART_CUSTOM_IMG_FILE: DEFAULT_WALMART_CUSTOM_IMG_FILE,
CONF_FEDEX_CUSTOM_IMG_FILE: DEFAULT_FEDEX_CUSTOM_IMG_FILE,
CONF_GENERIC_CUSTOM_IMG_FILE: DEFAULT_GENERIC_CUSTOM_IMG_FILE,
CONF_POST_DE_CUSTOM_IMG_FILE: DEFAULT_POST_DE_CUSTOM_IMG_FILE,
}
async def _show_options_forwarded_emails(self, user_input=None):
"""Step forwarded emails."""
if self._data.get(CONF_FORWARDED_EMAILS, []) == []:
self._data[CONF_FORWARDED_EMAILS] = "(none)"
return self.async_show_form(
step_id="reconfig_3",
data_schema=_get_schema_step_3(self._data, defaults),
step_id="options_forwarded_emails",
data_schema=_get_schema_step_forwarded_emails(user_input, self._data),
errors=self._errors,
)
async def async_step_reconfig_amazon(self, user_input=None):
"""Configure form step amazon."""
async def async_step_options_amazon(self, user_input=None):
"""Configure amazon options."""
self._errors = {}
if user_input is not None:
if user_input.get(CONF_AMAZON_FWDS) == "(none)":
user_input[CONF_AMAZON_FWDS] = []
self._data.update(user_input)
self._errors, user_input = await _validate_user_input(self._data, self.hass)
if len(self._errors) == 0:
has_custom_image = (
self._data.get(CONF_CUSTOM_IMG)
or self._data.get(CONF_AMAZON_CUSTOM_IMG)
or self._data.get(CONF_UPS_CUSTOM_IMG)
or self._data.get(CONF_WALMART_CUSTOM_IMG)
or self._data.get(CONF_FEDEX_CUSTOM_IMG)
or self._data.get(CONF_GENERIC_CUSTOM_IMG)
or self._data.get(CONF_POST_DE_CUSTOM_IMG)
)
if has_custom_image:
return await self.async_step_reconfig_3()
if self._data.get(CONF_CUSTOM_IMG, False):
return await self.async_step_options_3()
return await self.async_step_options_storage()
return await self._show_options_amazon(user_input)
return await self._show_options_amazon(user_input)
return await self.async_step_reconfig_storage()
return await self._show_reconfig_amazon(user_input)
return await self._show_reconfig_amazon(user_input)
async def _show_reconfig_amazon(self, user_input):
"""Step 3 setup."""
if self._data.get(CONF_AMAZON_FWDS, []) == []:
async def _show_options_amazon(self, user_input):
"""Step Amazon setup."""
if self._data.get(CONF_AMAZON_FWDS) == []:
self._data[CONF_AMAZON_FWDS] = "(none)"
return self.async_show_form(
step_id="reconfig_amazon",
step_id="options_amazon",
data_schema=_get_schema_step_amazon(
user_input,
self._data,
@@ -1384,61 +1455,83 @@ class MailAndPackagesFlowHandler(
errors=self._errors,
)
async def async_step_reconfig_forwarded_emails(
self,
user_input: dict[str, Any] | None = None,
):
"""Configure form step forwarded emails."""
async def async_step_options_3(self, user_input=None):
"""Configure custom image files."""
self._errors = {}
if user_input is not None:
self._data.update(user_input)
self._errors, user_input = await _validate_user_input(self._data, self.hass)
if len(self._errors) == 0:
if any(
sensor in self._data.get(CONF_RESOURCES, [])
for sensor in AMAZON_SENSORS
):
return await self.async_step_reconfig_amazon()
if self._data.get(CONF_CUSTOM_IMG, False):
return await self.async_step_reconfig_3()
return await self.async_step_reconfig_storage()
return await self._show_reconfig_forwarded_emails(user_input)
return await self._show_reconfig_forwarded_emails(user_input)
async def _show_reconfig_forwarded_emails(self, user_input=None):
"""Step forwarded emails."""
if self._data.get(CONF_FORWARDED_EMAILS, []) == []:
self._data[CONF_FORWARDED_EMAILS] = "(none)"
return await self.async_step_options_storage()
return await self._show_options_3(user_input)
return await self._show_options_3(user_input)
async def _show_options_3(self, user_input):
"""Step 3 setup."""
return self.async_show_form(
step_id="reconfig_forwarded_emails",
data_schema=_get_schema_step_forwarded_emails(user_input, self._data),
step_id="options_3",
data_schema=_get_schema_step_3(self._data, self._data),
errors=self._errors,
)
async def async_step_reconfig_storage(self, user_input=None):
"""Configure form step storage."""
async def async_step_options_storage(self, user_input=None):
"""Configure storage options."""
self._errors = {}
if user_input is not None:
self._data.update(user_input)
self._errors, user_input = await _validate_user_input(self._data, self.hass)
if len(self._errors) == 0:
self.hass.config_entries.async_update_entry(
self._entry,
data=self._data,
)
await self.hass.config_entries.async_reload(self._entry.entry_id)
_LOGGER.debug("%s reconfigured.", DOMAIN)
return self.async_abort(reason="reconfigure_successful")
# Remove config flow only fields to not pollute options
options_keys = {
CONF_FOLDER,
CONF_SCAN_INTERVAL,
CONF_RESOURCES,
CONF_CUSTOM_IMG,
CONF_AMAZON_CUSTOM_IMG,
CONF_UPS_CUSTOM_IMG,
CONF_WALMART_CUSTOM_IMG,
CONF_FEDEX_CUSTOM_IMG,
CONF_GENERIC_CUSTOM_IMG,
CONF_POST_DE_CUSTOM_IMG,
CONF_CUSTOM_IMG_FILE,
CONF_AMAZON_CUSTOM_IMG_FILE,
CONF_UPS_CUSTOM_IMG_FILE,
CONF_WALMART_CUSTOM_IMG_FILE,
CONF_FEDEX_CUSTOM_IMG_FILE,
CONF_GENERIC_CUSTOM_IMG_FILE,
CONF_POST_DE_CUSTOM_IMG_FILE,
CONF_ALLOW_FORWARDED_EMAILS,
CONF_FORWARDED_EMAILS,
CONF_FORWARDING_HEADER,
CONF_AMAZON_FWDS,
CONF_AMAZON_DOMAIN,
CONF_AMAZON_DAYS,
CONF_STORAGE,
"generate_mp4",
"generate_grid",
"gif_duration",
"custom_days",
"allow_external",
"image_security",
"imap_timeout",
"image_name",
"image_path",
"usps_placeholder",
}
for key in list(self._data.keys()):
if key not in options_keys:
self._data.pop(key, None)
return await self._show_reconfig_storage(user_input)
return self.async_create_entry(title="", data=self._data)
return await self._show_reconfig_storage(user_input)
return await self._show_options_storage(user_input)
async def _show_reconfig_storage(self, user_input):
"""Step 3 setup."""
return await self._show_options_storage(user_input)
async def _show_options_storage(self, user_input):
"""Step storage setup."""
return self.async_show_form(
step_id="reconfig_storage",
step_id="options_storage",
data_schema=_get_schema_step_storage(user_input, self._data),
errors=self._errors,
)