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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Email helper.
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.
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.
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.
16Example::
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 }
29Usage in Python::
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"""
39import email.message
40import email.policy
41import email.utils
42import smtplib
43import ssl
45import gws
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)."""
58class SmtpConfig(gws.Config):
59 """SMTP server configuration."""
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."""
75@gws.ext.config.helper('email')
76class Config(gws.Config):
77 """Sending of emails via SMTP."""
79 smtp: SmtpConfig
80 """SMTP server configuration."""
81 mailFrom: str = ''
82 """Sender address for messages that do not set one."""
85class Message(gws.Data):
86 """Email message."""
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."""
102class Error(gws.Error):
103 """Raised when an email cannot be sent."""
105 pass
108##
110_DEFAULT_POLICY = {
111 'linesep': '\r\n',
112 'cte_type': '7bit',
113 'utf8': False,
114}
116_DEFAULT_ENCODING = 'quoted-printable'
118_DEFAULT_PORT = {
119 SmtpMode.plain: 25,
120 SmtpMode.ssl: 465,
121 SmtpMode.tls: 587,
122}
125class _SmtpServer(gws.Data):
126 """SMTP server settings."""
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."""
142@gws.ext.object.helper('email')
143class Object(gws.Node):
144 """Email helper."""
146 smtp: _SmtpServer
147 """SMTP server settings."""
148 mailFrom: str
149 """Default sender address."""
151 def configure(self):
152 self.mailFrom = self.cfg('mailFrom')
154 p = self.cfg('smtp')
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)
165 def send_mail(self, m: Message):
166 """Send an email message.
168 Args:
169 m: The message.
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))
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
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)
188 self._send(msg)
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
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 )
215 # conn.set_debuglevel(2)
217 if self.smtp.mode == SmtpMode.tls:
218 conn.starttls(context=ssl.create_default_context())
220 if self.smtp.login:
221 conn.login(self.smtp.login, self.smtp.password)
223 return conn