1 """Mail (SMTP) notification service."""
3 from __future__
import annotations
5 from email.mime.application
import MIMEApplication
6 from email.mime.image
import MIMEImage
7 from email.mime.multipart
import MIMEMultipart
8 from email.mime.text
import MIMEText
12 from pathlib
import Path
15 import voluptuous
as vol
22 PLATFORM_SCHEMA
as NOTIFY_PLATFORM_SCHEMA,
23 BaseNotificationService,
59 PLATFORMS = [Platform.NOTIFY]
61 _LOGGER = logging.getLogger(__name__)
63 PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
65 vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [vol.Email()]),
66 vol.Required(CONF_SENDER): vol.Email(),
67 vol.Optional(CONF_SERVER, default=DEFAULT_HOST): cv.string,
68 vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
69 vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
70 vol.Optional(CONF_ENCRYPTION, default=DEFAULT_ENCRYPTION): vol.In(
73 vol.Optional(CONF_USERNAME): cv.string,
74 vol.Optional(CONF_PASSWORD): cv.string,
75 vol.Optional(CONF_SENDER_NAME): cv.string,
76 vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean,
77 vol.Optional(CONF_VERIFY_SSL, default=
True): cv.boolean,
85 discovery_info: DiscoveryInfoType |
None =
None,
86 ) -> MailNotificationService |
None:
87 """Get the mail notification service."""
94 config[CONF_ENCRYPTION],
95 config.get(CONF_USERNAME),
96 config.get(CONF_PASSWORD),
97 config[CONF_RECIPIENT],
98 config.get(CONF_SENDER_NAME),
100 config[CONF_VERIFY_SSL],
103 if mail_service.connection_is_valid():
110 """Implement the notification service for E-mail messages."""
126 """Initialize the SMTP service."""
141 """Connect/authenticate to SMTP Server."""
144 mail = smtplib.SMTP_SSL(
152 mail.set_debuglevel(self.
debugdebug)
153 mail.ehlo_or_helo_if_needed()
155 mail.starttls(context=ssl_context)
162 """Check for valid config, verify connectivity."""
166 except (smtplib.socket.gaierror, ConnectionRefusedError):
169 "SMTP server not found or refused connection (%s:%s). Please check"
170 " the IP address, hostname, and availability of your SMTP server"
176 except smtplib.SMTPAuthenticationError:
178 "Login not possible. Please check your setting and/or your credentials"
189 """Build and send a message to a user.
191 Will send plain text normally, with pictures as attachments if images config is
192 defined, or will build a multipart HTML if html config is defined.
194 subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
196 if data := kwargs.get(ATTR_DATA):
197 if ATTR_HTML
in data:
202 images=data.get(ATTR_IMAGES, []),
206 self.hass, message, images=data.get(ATTR_IMAGES, [])
211 msg[
"Subject"] = subject
213 if not (recipients := kwargs.get(ATTR_TARGET)):
215 msg[
"To"] = recipients
if isinstance(recipients, str)
else ",".join(recipients)
217 msg[
"From"] = f
"{self._sender_name} <{self._sender}>"
219 msg[
"From"] = self.
_sender_sender
220 msg[
"X-Mailer"] =
"Home Assistant"
221 msg[
"Date"] = email.utils.format_datetime(dt_util.now())
222 msg[
"Message-Id"] = email.utils.make_msgid()
224 return self.
_send_email_send_email(msg, recipients)
227 """Send the message."""
229 for _
in range(self.
triestries):
231 mail.sendmail(self.
_sender_sender, recipients, msg.as_string())
233 except smtplib.SMTPServerDisconnected:
235 "SMTPServerDisconnected sending mail: retrying connection"
239 except smtplib.SMTPException:
240 _LOGGER.warning(
"SMTPException sending mail: retrying connection")
247 """Build plaintext email."""
248 _LOGGER.debug(
"Building plain text email")
249 return MIMEText(message)
253 """Create a message attachment.
255 If MIMEImage is successful and content_id is passed (HTML), add images in-line.
256 Otherwise add them as attachments.
259 file_path = Path(atch_name).parent
260 if os.path.exists(file_path)
and not hass.config.is_allowed_path(
263 allow_list =
"allowlist_external_dirs"
264 file_name = os.path.basename(atch_name)
265 url =
"https://www.home-assistant.io/docs/configuration/basic/"
267 translation_domain=DOMAIN,
268 translation_key=
"remote_path_not_allowed",
269 translation_placeholders={
270 "allow_list": allow_list,
271 "file_path": file_path,
272 "file_name": file_name,
276 with open(atch_name,
"rb")
as attachment_file:
277 file_bytes = attachment_file.read()
278 except FileNotFoundError:
279 _LOGGER.warning(
"Attachment %s not found. Skipping", atch_name)
283 attachment = MIMEImage(file_bytes)
286 "Attachment %s has an unknown MIME type. Falling back to file",
289 attachment = MIMEApplication(file_bytes, Name=os.path.basename(atch_name))
290 attachment[
"Content-Disposition"] = (
291 f
'attachment; filename="{os.path.basename(atch_name)}"'
295 attachment.add_header(
"Content-ID", f
"<{content_id}>")
297 attachment.add_header(
298 "Content-Disposition",
299 f
"attachment; filename={os.path.basename(atch_name)}",
306 """Build Multipart message with images as attachments."""
307 _LOGGER.debug(
"Building multipart email with image attachme_build_html_msgnt(s)")
308 msg = MIMEMultipart()
309 body_txt = MIMEText(message)
312 for atch_name
in images:
315 msg.attach(attachment)
321 """Build Multipart message with in-line images and rich HTML (UTF-8)."""
322 _LOGGER.debug(
"Building HTML rich email")
323 msg = MIMEMultipart(
"related")
324 alternative = MIMEMultipart(
"alternative")
325 alternative.attach(MIMEText(text, _charset=
"utf-8"))
326 alternative.attach(MIMEText(html, ATTR_HTML, _charset=
"utf-8"))
327 msg.attach(alternative)
329 for atch_name
in images:
330 name = os.path.basename(atch_name)
333 msg.attach(attachment)
def __init__(self, server, port, timeout, sender, encryption, username, password, recipients, sender_name, debug, verify_ssl)
def connection_is_valid(self)
def send_message(self, message="", **kwargs)
def _send_email(self, msg, recipients)
None open(self, **Any kwargs)
def _attach_file(hass, atch_name, content_id="")
def _build_text_msg(message)
MailNotificationService|None get_service(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
def _build_multipart_msg(hass, message, images)
def _build_html_msg(hass, text, html, images)
None setup_reload_service(HomeAssistant hass, str domain, Iterable[str] platforms)
ssl.SSLContext client_context(SSLCipherList ssl_cipher_list=SSLCipherList.PYTHON_DEFAULT)