1 # Copyright (C) 2009-2023 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <https://www.gnu.org/licenses/>.
18 """MTA connections."""
26 from contextlib
import suppress
27 from email
.message
import Message
28 from lazr
.config
import as_boolean
29 from mailman
.config
import config
30 from mailman
.interfaces
.configuration
import InvalidConfigurationError
31 from public
import public
34 log
= logging
.getLogger('mailman.smtp')
38 class SecureMode(enum
.Enum
):
42 # STARTTLS can be invoked prior to any message submission, but we don't do
43 # that -- we invoke immediately.
49 Convert a string to an enum value. Accepts any case.
56 raise InvalidConfigurationError('smtp_secure_mode', repr(s
))
60 """Manage a connection to the SMTP server."""
61 def __init__(self
, host
, port
, sessions_per_connection
,
62 smtp_user
=None, smtp_pass
=None,
63 secure_mode
=SecureMode
.INSECURE
,
64 verify_cert
=True, verify_hostname
=True):
65 """Create a connection manager.
67 :param host: The host name of the SMTP server to connect to.
69 :param port: The port number of the SMTP server to connect to.
71 :param sessions_per_connection: The number of SMTP sessions per
72 connection to the SMTP server. After this number of sessions
73 has been reached, the connection is closed and a new one is
74 opened. Set to zero for an unlimited number of sessions per
75 connection (i.e. your MTA has no limit).
76 :type sessions_per_connection: integer
77 :param smtp_user: Optional SMTP authentication user name. If given,
78 `smtp_pass` must also be given.
80 :param smtp_pass: Optional SMTP authentication password. If given,
81 `smtp_user` must also be given.
83 :param secure_mode: Whether to use implicit TLS (SMTPS), STARTTLS, or
84 an insecure connection.
85 :type secure_mode: SecureMode
86 :param verify_cert: Whether to require a server cert and verify it.
87 Verification in this context means that the server needs to supply
88 a valid certificate signed by a CA from a set of the system's
90 :type verify_cert: bool
91 :param verify_hostname: Whether to check that the server certificate
92 specifies the hostname as passed to this constructor.
93 RFC 2818 and RFC 6125 rules are followed.
94 :type verify_hostname: bool
96 With the exception of the parameters specified here, this class
97 uses the defaults provided by your version of the Python 'ssl'
100 If either of smtp_pass or smtp_user is omitted, the other will
101 be ignored. If secure_mode is INSECURE, verify_hostname and
102 verify_cert will be ignored. If secure_mode is not INSECURE,
103 verify_hostname will be ignored unless verify_cert is true.
107 self
._sessions
_per
_connection
= sessions_per_connection
108 self
.secure_mode
= secure_mode
109 self
.verify_cert
= verify_cert
110 self
.verify_hostname
= verify_hostname
and verify_cert
111 self
._username
= smtp_user
112 self
._password
= smtp_pass
114 self
._session
_count
= None
115 self
._connection
= None
116 if self
.secure_mode
== SecureMode
.INSECURE
:
117 self
._tls
_context
= None
119 self
._tls
_context
= self
._get
_tls
_context
(self
.verify_cert
,
120 self
.verify_hostname
)
122 def sendmail(self
, envsender
, recipients
, msg
):
123 """Mimic `smtplib.SMTP.sendmail`."""
124 if as_boolean(config
.devmode
.enabled
):
125 # Force the recipients to the specified address, but still deliver
126 # to the same number of recipients.
127 recipients
= [config
.devmode
.recipient
] * len(recipients
)
128 if self
._connection
is None:
131 # We accept a string, bytes or a Message object for the msg argument.
132 # A string is converted to ascii bytes with errors replaced and
133 # passed to smtplib.SMTP.sendmail. Bytes are passed as is and a
134 # Message is passed to smtplib.SMTP.send_message. Passing a Message
135 # object is preferred because of the treatment of line endings.
136 if isinstance(msg
, str):
137 msg
= msg
.encode('ascii', 'replace')
138 assert isinstance(msg
, bytes
) or isinstance(msg
, Message
), \
139 'Connection.sendmail received an invalid msg arg.'
141 log
.debug('envsender: %s, recipients: %s, size(msg): %s',
142 envsender
, recipients
, len(msg
))
143 if isinstance(msg
, Message
):
144 results
= self
._connection
.send_message(msg
, envsender
,
147 results
= self
._connection
.sendmail(envsender
, recipients
, msg
)
148 except smtplib
.SMTPException
:
149 # For safety, close this connection. The next send attempt will
150 # automatically re-open it. Pass the exception on up.
153 # This session has been successfully completed.
154 self
._session
_count
-= 1
155 # By testing exactly for equality to 0, we automatically handle the
156 # case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close
157 # the connection. We won't worry about wraparound <wink>.
158 if self
._session
_count
== 0:
163 """Mimic `smtplib.SMTP.quit`."""
164 if self
._connection
is None:
166 with
suppress(smtplib
.SMTPException
):
167 self
._connection
.quit()
168 self
._connection
= None
171 """Open a new connection."""
173 if self
.secure_mode
== SecureMode
.IMPLICIT
:
174 log
.debug('Connecting to %s:%s with implicit TLS',
175 self
._host
, self
._port
)
176 self
._connection
= smtplib
.SMTP_SSL(self
._host
, self
._port
,
177 context
=self
._tls
_context
)
179 log
.debug('Connecting to %s:%s', self
._host
, self
._port
)
180 self
._connection
= smtplib
.SMTP(self
._host
, self
._port
)
181 if self
.secure_mode
== SecureMode
.STARTTLS
:
182 log
.debug('Starttls')
183 self
._connection
.starttls(context
=self
._tls
_context
)
184 except (socket
.error
, IOError, smtplib
.SMTPException
):
187 except Exception as error
:
188 # This exception is kept here intentionally to make sure we log
189 # only when an exception other than 3 caught above happens and
191 # If ANYTHING fails here, after ensuring
192 # connection is closed, we'll let the exception bubble up so a
193 # message in process will be shunted.
194 log
.error('while connecting to SMTP: ' + str(error
))
197 self
._session
_count
= self
._sessions
_per
_connection
200 """Send login if both username and password are specified."""
201 if self
._username
is not None and self
._password
is not None:
202 log
.debug('logging in')
204 self
._connection
.login(self
._username
, self
._password
)
205 except smtplib
.SMTPException
:
206 # Ensure connection is closed and pass to BaseDelivery.
210 def _get_tls_context(self
, verify_cert
, verify_hostname
):
211 """Create and return a new SSLContext."""
212 ssl_context
= ssl
.create_default_context()
213 ssl_context
.check_hostname
= verify_hostname
215 ssl_context
.verify_mode
= ssl
.CERT_REQUIRED
217 ssl_context
.verify_mode
= ssl
.CERT_NONE