Coverage for gws-app/gws/plugin/email_helper/__init__.py: 0%

86 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Email helper. 

2 

3The ``email`` helper sends email messages over SMTP. A ``Message`` has a 

4subject, recipients, an optional sender and Bcc addresses, a plain text part 

5and an optional HTML part. Both parts are encoded as quoted-printable. If the 

6message has no sender, the configured ``mailFrom`` is used. 

7 

8The SMTP connection mode is ``ssl`` (SMTP over SSL, default port 465), 

9``tls`` (STARTTLS, default port 587) or ``plain`` (no encryption, default 

10port 25). If ``login`` is set, the helper logs in before sending. 

11 

12The helper is used, for example, by the email multi-factor adapter 

13(``gws.plugin.auth_mfa.email``) and the account plugin. It must be configured 

14with an SMTP server. 

15 

16Example:: 

17 

18 helpers+ { 

19 type "email" 

20 mailFrom "gws@example.com" 

21 smtp { 

22 host "smtp.example.com" 

23 mode "tls" 

24 login "gws" 

25 password "secret" 

26 } 

27 } 

28 

29Usage in Python:: 

30 

31 helper = cast(gws.plugin.email_helper.Object, root.app.helper('email')) 

32 helper.send_mail(gws.plugin.email_helper.Message( 

33 subject='Hello', 

34 mailTo='user_1@example.com', 

35 text='Hello, world.', 

36 )) 

37""" 

38 

39import email.message 

40import email.policy 

41import email.utils 

42import smtplib 

43import ssl 

44 

45import gws 

46 

47 

48class SmtpMode(gws.Enum): 

49 """SMTP connection modes.""" 

50 plain = 'plain' 

51 """Plain connection without encryption.""" 

52 ssl = 'ssl' 

53 """SSL connection.""" 

54 tls = 'tls' 

55 """TLS connection (STARTTLS).""" 

56 

57 

58class SmtpConfig(gws.Config): 

59 """SMTP server configuration.""" 

60 

61 mode: SmtpMode = SmtpMode.ssl 

62 """Connection encryption mode.""" 

63 host: str 

64 """SMTP host name.""" 

65 port: int = 0 

66 """SMTP port.""" 

67 login: str = '' 

68 """Login name for the SMTP server.""" 

69 password: str = '' 

70 """Password for the SMTP server.""" 

71 timeout: gws.Duration = '30' 

72 """Connection timeout.""" 

73 

74 

75@gws.ext.config.helper('email') 

76class Config(gws.Config): 

77 """Sending of emails via SMTP.""" 

78 

79 smtp: SmtpConfig 

80 """SMTP server configuration.""" 

81 mailFrom: str = '' 

82 """Sender address for messages that do not set one.""" 

83 

84 

85class Message(gws.Data): 

86 """Email message.""" 

87 

88 subject: str 

89 """Subject.""" 

90 mailTo: str 

91 """To addresses, comma separated.""" 

92 mailFrom: str 

93 """From address (default if omitted).""" 

94 bcc: str 

95 """Bcc addresses.""" 

96 text: str 

97 """Plain text content.""" 

98 html: str 

99 """HTML content.""" 

100 

101 

102class Error(gws.Error): 

103 """Raised when an email cannot be sent.""" 

104 

105 pass 

106 

107 

108## 

109 

110_DEFAULT_POLICY = { 

111 'linesep': '\r\n', 

112 'cte_type': '7bit', 

113 'utf8': False, 

114} 

115 

116_DEFAULT_ENCODING = 'quoted-printable' 

117 

118_DEFAULT_PORT = { 

119 SmtpMode.plain: 25, 

120 SmtpMode.ssl: 465, 

121 SmtpMode.tls: 587, 

122} 

123 

124 

125class _SmtpServer(gws.Data): 

126 """SMTP server settings.""" 

127 

128 mode: SmtpMode 

129 """Connection mode.""" 

130 host: str 

131 """Host name.""" 

132 port: int 

133 """Port.""" 

134 login: str 

135 """Login name, empty for no login.""" 

136 password: str 

137 """Password.""" 

138 timeout: int 

139 """Connection timeout in seconds.""" 

140 

141 

142@gws.ext.object.helper('email') 

143class Object(gws.Node): 

144 """Email helper.""" 

145 

146 smtp: _SmtpServer 

147 """SMTP server settings.""" 

148 mailFrom: str 

149 """Default sender address.""" 

150 

151 def configure(self): 

152 self.mailFrom = self.cfg('mailFrom') 

153 

154 p = self.cfg('smtp') 

155 

156 self.smtp = _SmtpServer( 

157 mode=p.mode or SmtpMode.ssl, 

158 host=p.host, 

159 login=p.login, 

160 password=p.password, 

161 timeout=p.timeout, 

162 ) 

163 self.smtp.port = p.port or _DEFAULT_PORT.get(self.smtp.mode) 

164 

165 def send_mail(self, m: Message): 

166 """Send an email message. 

167 

168 Args: 

169 m: The message. 

170 

171 Raises: 

172 ``Error``: If the connection to the SMTP server or the sending fails. 

173 """ 

174 msg = email.message.EmailMessage(email.policy.EmailPolicy(**_DEFAULT_POLICY)) 

175 

176 msg['Subject'] = m.subject 

177 msg['To'] = m.mailTo 

178 msg['From'] = m.mailFrom or self.mailFrom 

179 msg['Date'] = email.utils.formatdate() 

180 if m.bcc: 

181 msg['Bcc'] = m.bcc 

182 

183 msg.set_content(m.text, cte=_DEFAULT_ENCODING) 

184 if m.html: 

185 # @TODO images 

186 msg.add_alternative(m.html, subtype='html', cte=_DEFAULT_ENCODING) 

187 

188 self._send(msg) 

189 

190 def _send(self, msg): 

191 """Send a message with the SMTP server.""" 

192 if self.smtp: 

193 try: 

194 with self._smtp_connection() as conn: 

195 conn.send_message(msg) 

196 except OSError as exc: 

197 raise Error('SMTP error') from exc 

198 

199 def _smtp_connection(self): 

200 """Open an SMTP connection and log in, if configured.""" 

201 if self.smtp.mode == SmtpMode.ssl: 

202 conn = smtplib.SMTP_SSL( 

203 host=self.smtp.host, 

204 port=self.smtp.port, 

205 timeout=self.smtp.timeout, 

206 context=ssl.create_default_context(), 

207 ) 

208 else: 

209 conn = smtplib.SMTP( 

210 host=self.smtp.host, 

211 port=self.smtp.port, 

212 timeout=self.smtp.timeout, 

213 ) 

214 

215 # conn.set_debuglevel(2) 

216 

217 if self.smtp.mode == SmtpMode.tls: 

218 conn.starttls(context=ssl.create_default_context()) 

219 

220 if self.smtp.login: 

221 conn.login(self.smtp.login, self.smtp.password) 

222 

223 return conn