Coverage for custom_components/supernotify/methods/email.py: 88%

88 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-12-28 14:21 +0000

1import logging 

2import re 

3from typing import TYPE_CHECKING, Any 

4 

5from homeassistant.components.notify.const import ATTR_DATA, ATTR_MESSAGE, ATTR_TARGET, ATTR_TITLE 

6from homeassistant.const import CONF_EMAIL 

7from homeassistant.core import HomeAssistant 

8from jinja2 import Environment, FileSystemLoader 

9 

10from custom_components.supernotify import CONF_TEMPLATE, METHOD_EMAIL 

11from custom_components.supernotify.configuration import SupernotificationConfiguration 

12from custom_components.supernotify.delivery_method import DeliveryMethod 

13from custom_components.supernotify.envelope import Envelope 

14 

15if TYPE_CHECKING: 

16 from pathlib import Path 

17 

18RE_VALID_EMAIL = ( 

19 r"^[a-zA-Z0-9.+/=?^_-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$" 

20) 

21 

22_LOGGER = logging.getLogger(__name__) 

23 

24 

25class EmailDeliveryMethod(DeliveryMethod): 

26 method = METHOD_EMAIL 

27 

28 def __init__(self, hass: HomeAssistant, context: SupernotificationConfiguration, deliveries: dict | None = None) -> None: 

29 super().__init__(hass, context, deliveries) 

30 self.template_path: Path | None = None 

31 if self.context.template_path: 

32 self.template_path = self.context.template_path / "email" 

33 if not self.template_path.exists(): 

34 _LOGGER.warning("SUPERNOTIFY Email templates not available at %s", self.template_path) 

35 self.template_path = None 

36 if self.template_path is None: 

37 _LOGGER.warning("SUPERNOTIFY Email templates not available") 

38 else: 

39 _LOGGER.debug("SUPERNOTIFY Loading email templates from %s", self.template_path) 

40 

41 def select_target(self, target: str) -> bool: 

42 return re.fullmatch(RE_VALID_EMAIL, target) is not None 

43 

44 def recipient_target(self, recipient: dict) -> list[str]: 

45 email = recipient.get(CONF_EMAIL) 

46 return [email] if email else [] 

47 

48 async def deliver(self, envelope: Envelope) -> bool: 

49 _LOGGER.info("SUPERNOTIFY notify_email: %s %s", envelope.delivery_name, envelope.targets) 

50 

51 data: dict[str, Any] = envelope.data or {} 

52 config = self.delivery_config(envelope.delivery_name) 

53 html: str | None = data.get("html") 

54 template: str = data.get(CONF_TEMPLATE, config.get(CONF_TEMPLATE)) 

55 addresses: list[str] = envelope.targets or [] 

56 snapshot_url: str | None = data.get("snapshot_url") 

57 # TODO: centralize in config 

58 footer_template = data.get("footer") 

59 footer = footer_template.format(e=envelope) if footer_template else None 

60 

61 action_data: dict[str, Any] = envelope.core_action_data() 

62 

63 if len(addresses) > 0: 

64 action_data[ATTR_TARGET] = addresses 

65 # default to SMTP platform default recipients if no explicit addresses 

66 

67 if data and data.get("data"): 

68 action_data[ATTR_DATA] = data.get("data") 

69 

70 if not template or not self.template_path: 

71 if footer and action_data.get(ATTR_MESSAGE): 

72 action_data[ATTR_MESSAGE] = f"{action_data[ATTR_MESSAGE]}\n\n{footer}" 

73 

74 image_path: Path | None = await envelope.grab_image() 

75 if image_path: 

76 action_data.setdefault("data", {}) 

77 action_data["data"]["images"] = [str(image_path)] 

78 if envelope.message_html: 

79 action_data.setdefault("data", {}) 

80 html = envelope.message_html 

81 if image_path: 

82 image_name = image_path.name 

83 if html and "cid:%s" not in html and not html.endswith("</html"): 

84 if snapshot_url: 

85 html += f'<div><p><a href="{snapshot_url}">' 

86 html += f'<img src="cid:{image_name}"/></a>' 

87 html += "</p></div>" 

88 else: 

89 html += f'<div><p><img src="cid:{image_name}"></p></div>' 

90 

91 action_data["data"]["html"] = html 

92 else: 

93 html = self.render_template(template, envelope, action_data, snapshot_url, envelope.message_html) 

94 if html: 

95 action_data.setdefault("data", {}) 

96 action_data["data"]["html"] = html 

97 return await self.call_action(envelope, action_data=action_data) 

98 

99 def render_template( 

100 self, 

101 template: str, 

102 envelope: Envelope, 

103 action_data: dict[str, Any], 

104 snapshot_url: str | None, 

105 preformatted_html: str | None, 

106 ) -> str | None: 

107 alert = {} 

108 try: 

109 alert = { 

110 "message": action_data.get(ATTR_MESSAGE), 

111 "title": action_data.get(ATTR_TITLE), 

112 "envelope": envelope, 

113 "subheading": "Home Assistant Notification", 

114 "configuration": self.context, 

115 "preformatted_html": preformatted_html, 

116 "img": None, 

117 } 

118 if snapshot_url: 

119 alert["img"] = {"text": "Snapshot Image", "url": snapshot_url} 

120 env = Environment(loader=FileSystemLoader(self.template_path or ""), autoescape=True) 

121 template_obj = env.get_template(template) 

122 html = template_obj.render(alert=alert) 

123 if not html: 

124 _LOGGER.error("Empty result from template %s", template) 

125 else: 

126 return html 

127 except Exception as e: 

128 _LOGGER.error("SUPERNOTIFY Failed to generate html mail: %s", e) 

129 _LOGGER.debug("SUPERNOTIFY Template failure: %s", alert, exc_info=True) 

130 return None