Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config flow for imap integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 import ssl
7 from typing import Any
8 
9 from aioimaplib import AioImapException
10 import voluptuous as vol
11 
12 from homeassistant.config_entries import (
13  ConfigEntry,
14  ConfigFlow,
15  ConfigFlowResult,
16  OptionsFlow,
17 )
18 from homeassistant.const import (
19  CONF_NAME,
20  CONF_PASSWORD,
21  CONF_PORT,
22  CONF_USERNAME,
23  CONF_VERIFY_SSL,
24 )
25 from homeassistant.core import HomeAssistant, callback
26 from homeassistant.data_entry_flow import AbortFlow
27 from homeassistant.helpers import config_validation as cv
29  BooleanSelector,
30  SelectSelector,
31  SelectSelectorConfig,
32  SelectSelectorMode,
33  TemplateSelector,
34  TemplateSelectorConfig,
35 )
36 from homeassistant.util.ssl import SSLCipherList
37 
38 from .const import (
39  CONF_CHARSET,
40  CONF_CUSTOM_EVENT_DATA_TEMPLATE,
41  CONF_ENABLE_PUSH,
42  CONF_EVENT_MESSAGE_DATA,
43  CONF_FOLDER,
44  CONF_MAX_MESSAGE_SIZE,
45  CONF_SEARCH,
46  CONF_SERVER,
47  CONF_SSL_CIPHER_LIST,
48  DEFAULT_MAX_MESSAGE_SIZE,
49  DEFAULT_PORT,
50  DOMAIN,
51  MAX_MESSAGE_SIZE_LIMIT,
52  MESSAGE_DATA_OPTIONS,
53 )
54 from .coordinator import connect_to_server
55 from .errors import InvalidAuth, InvalidFolder
56 
57 BOOLEAN_SELECTOR = BooleanSelector()
58 CIPHER_SELECTOR = SelectSelector(
60  options=list(SSLCipherList),
61  mode=SelectSelectorMode.DROPDOWN,
62  translation_key=CONF_SSL_CIPHER_LIST,
63  )
64 )
66 EVENT_MESSAGE_DATA_SELECTOR = SelectSelector(
68  options=MESSAGE_DATA_OPTIONS,
69  translation_key=CONF_EVENT_MESSAGE_DATA,
70  multiple=True,
71  )
72 )
73 
74 CONFIG_SCHEMA = vol.Schema(
75  {
76  vol.Required(CONF_USERNAME): str,
77  vol.Required(CONF_PASSWORD): str,
78  vol.Required(CONF_SERVER): str,
79  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
80  vol.Optional(CONF_CHARSET, default="utf-8"): str,
81  vol.Optional(CONF_FOLDER, default="INBOX"): str,
82  vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str,
83  # The default for new entries is to not include text and headers
84  vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): EVENT_MESSAGE_DATA_SELECTOR,
85  }
86 )
87 CONFIG_SCHEMA_ADVANCED = {
88  vol.Optional(
89  CONF_SSL_CIPHER_LIST, default=SSLCipherList.PYTHON_DEFAULT
90  ): CIPHER_SELECTOR,
91  vol.Optional(CONF_VERIFY_SSL, default=True): BOOLEAN_SELECTOR,
92 }
93 
94 OPTIONS_SCHEMA = vol.Schema(
95  {
96  vol.Optional(CONF_FOLDER, default="INBOX"): str,
97  vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str,
98  # The default for older entries is to include text and headers
99  vol.Optional(
100  CONF_EVENT_MESSAGE_DATA, default=MESSAGE_DATA_OPTIONS
101  ): EVENT_MESSAGE_DATA_SELECTOR,
102  }
103 )
104 
105 OPTIONS_SCHEMA_ADVANCED = {
106  vol.Optional(CONF_CUSTOM_EVENT_DATA_TEMPLATE): TEMPLATE_SELECTOR,
107  vol.Optional(CONF_MAX_MESSAGE_SIZE, default=DEFAULT_MAX_MESSAGE_SIZE): vol.All(
108  cv.positive_int,
109  vol.Range(min=DEFAULT_MAX_MESSAGE_SIZE, max=MAX_MESSAGE_SIZE_LIMIT),
110  ),
111  vol.Optional(CONF_ENABLE_PUSH, default=True): BOOLEAN_SELECTOR,
112 }
113 
114 
115 async def validate_input(
116  hass: HomeAssistant, user_input: dict[str, Any]
117 ) -> dict[str, str]:
118  """Validate user input."""
119  errors = {}
120 
121  try:
122  imap_client = await connect_to_server(user_input)
123  result, lines = await imap_client.search(
124  user_input[CONF_SEARCH],
125  charset=user_input[CONF_CHARSET],
126  )
127 
128  except InvalidAuth:
129  errors[CONF_USERNAME] = errors[CONF_PASSWORD] = "invalid_auth"
130  except InvalidFolder:
131  errors[CONF_FOLDER] = "invalid_folder"
132  except ssl.SSLError:
133  # The aioimaplib library 1.0.1 does not raise an ssl.SSLError correctly, but is logged
134  # See https://github.com/bamthomas/aioimaplib/issues/91
135  # This handler is added to be able to supply a better error message
136  errors["base"] = "ssl_error"
137  except (TimeoutError, AioImapException, ConnectionRefusedError):
138  errors["base"] = "cannot_connect"
139  else:
140  if result != "OK":
141  if "The specified charset is not supported" in lines[0].decode("utf-8"):
142  errors[CONF_CHARSET] = "invalid_charset"
143  else:
144  errors[CONF_SEARCH] = "invalid_search"
145 
146  return errors
147 
148 
149 class IMAPConfigFlow(ConfigFlow, domain=DOMAIN):
150  """Handle a config flow for imap."""
151 
152  VERSION = 1
153 
154  async def async_step_user(
155  self, user_input: dict[str, Any] | None = None
156  ) -> ConfigFlowResult:
157  """Handle the initial step."""
158 
159  schema = CONFIG_SCHEMA
160  if self.show_advanced_optionsshow_advanced_options:
161  schema = schema.extend(CONFIG_SCHEMA_ADVANCED)
162 
163  if user_input is None:
164  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=schema)
165 
166  self._async_abort_entries_match_async_abort_entries_match(
167  {
168  key: user_input[key]
169  for key in (CONF_USERNAME, CONF_SERVER, CONF_FOLDER, CONF_SEARCH)
170  }
171  )
172 
173  if not (errors := await validate_input(self.hass, user_input)):
174  title = user_input[CONF_USERNAME]
175 
176  return self.async_create_entryasync_create_entryasync_create_entry(title=title, data=user_input)
177 
178  schema = self.add_suggested_values_to_schemaadd_suggested_values_to_schema(schema, user_input)
179  return self.async_show_formasync_show_formasync_show_form(step_id="user", data_schema=schema, errors=errors)
180 
181  async def async_step_reauth(
182  self, entry_data: Mapping[str, Any]
183  ) -> ConfigFlowResult:
184  """Perform reauth upon an API authentication error."""
185  return await self.async_step_reauth_confirmasync_step_reauth_confirm()
186 
188  self, user_input: dict[str, str] | None = None
189  ) -> ConfigFlowResult:
190  """Confirm reauth dialog."""
191  errors = {}
192  reauth_entry = self._get_reauth_entry_get_reauth_entry()
193  if user_input is not None:
194  user_input = {**reauth_entry.data, **user_input}
195  if not (errors := await validate_input(self.hass, user_input)):
196  return self.async_update_reload_and_abortasync_update_reload_and_abort(reauth_entry, data=user_input)
197 
198  return self.async_show_formasync_show_formasync_show_form(
199  description_placeholders={
200  CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
201  CONF_NAME: reauth_entry.title,
202  },
203  step_id="reauth_confirm",
204  data_schema=vol.Schema(
205  {
206  vol.Required(CONF_PASSWORD): str,
207  }
208  ),
209  errors=errors,
210  )
211 
212  @staticmethod
213  @callback
215  config_entry: ConfigEntry,
216  ) -> ImapOptionsFlow:
217  """Get the options flow for this handler."""
218  return ImapOptionsFlow()
219 
220 
222  """Option flow handler."""
223 
224  async def async_step_init(
225  self, user_input: dict[str, Any] | None = None
226  ) -> ConfigFlowResult:
227  """Manage the options."""
228  errors: dict[str, str] | None = None
229  entry_data: dict[str, Any] = dict(self.config_entryconfig_entryconfig_entry.data)
230  if user_input is not None:
231  try:
232  self._async_abort_entries_match_async_abort_entries_match(
233  {
234  CONF_SERVER: self.config_entryconfig_entryconfig_entry.data[CONF_SERVER],
235  CONF_USERNAME: self.config_entryconfig_entryconfig_entry.data[CONF_USERNAME],
236  CONF_FOLDER: user_input[CONF_FOLDER],
237  CONF_SEARCH: user_input[CONF_SEARCH],
238  }
239  if user_input
240  else None
241  )
242  except AbortFlow as err:
243  errors = {"base": err.reason}
244  else:
245  entry_data.update(user_input)
246  errors = await validate_input(self.hass, entry_data)
247  if not errors:
248  self.hass.config_entries.async_update_entry(
249  self.config_entryconfig_entryconfig_entry, data=entry_data
250  )
251  self.hass.async_create_task(
252  self.hass.config_entries.async_reload(
253  self.config_entryconfig_entryconfig_entry.entry_id
254  )
255  )
256  return self.async_create_entryasync_create_entry(data={})
257 
258  schema = OPTIONS_SCHEMA
259  if self.show_advanced_optionsshow_advanced_options:
260  schema = schema.extend(OPTIONS_SCHEMA_ADVANCED)
261  schema = self.add_suggested_values_to_schemaadd_suggested_values_to_schema(schema, entry_data)
262 
263  return self.async_show_formasync_show_form(step_id="init", data_schema=schema, errors=errors)
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:156
ConfigFlowResult async_step_reauth(self, Mapping[str, Any] entry_data)
Definition: config_flow.py:183
ConfigFlowResult async_step_reauth_confirm(self, dict[str, str]|None user_input=None)
Definition: config_flow.py:189
ImapOptionsFlow async_get_options_flow(ConfigEntry config_entry)
Definition: config_flow.py:216
ConfigFlowResult async_step_init(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:226
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
ConfigFlowResult async_update_reload_and_abort(self, ConfigEntry entry, *str|None|UndefinedType unique_id=UNDEFINED, str|UndefinedType title=UNDEFINED, Mapping[str, Any]|UndefinedType data=UNDEFINED, Mapping[str, Any]|UndefinedType data_updates=UNDEFINED, Mapping[str, Any]|UndefinedType options=UNDEFINED, str|UndefinedType reason=UNDEFINED, bool reload_even_if_entry_is_unchanged=True)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
None _async_abort_entries_match(self, dict[str, Any]|None match_dict=None)
None config_entry(self, ConfigEntry value)
bool show_advanced_options(self)
vol.Schema add_suggested_values_to_schema(self, vol.Schema data_schema, Mapping[str, Any]|None suggested_values)
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
dict[str, str] validate_input(HomeAssistant hass, dict[str, Any] user_input)
Definition: config_flow.py:117
IMAP4_SSL connect_to_server(Mapping[str, Any] data)
Definition: coordinator.py:67