##// END OF EJS Templates
mail: stop using the smtplib.SSLFakeFile and use socket.socket.makefile...
Augie Fackler -
r39061:a5e70c14 default
parent child Browse files
Show More
@@ -1,395 +1,395
1 1 # mail.py - mail sending bits for mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import email
11 11 import email.charset
12 12 import email.header
13 13 import email.message
14 14 import email.parser
15 15 import io
16 16 import os
17 17 import smtplib
18 18 import socket
19 19 import time
20 20
21 21 from .i18n import _
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 pycompat,
26 26 sslutil,
27 27 util,
28 28 )
29 29 from .utils import (
30 30 procutil,
31 31 stringutil,
32 32 )
33 33
34 34 class STARTTLS(smtplib.SMTP):
35 35 '''Derived class to verify the peer certificate for STARTTLS.
36 36
37 37 This class allows to pass any keyword arguments to SSL socket creation.
38 38 '''
39 39 def __init__(self, ui, host=None, **kwargs):
40 40 smtplib.SMTP.__init__(self, **kwargs)
41 41 self._ui = ui
42 42 self._host = host
43 43
44 44 def starttls(self, keyfile=None, certfile=None):
45 45 if not self.has_extn("starttls"):
46 46 msg = "STARTTLS extension not supported by server"
47 47 raise smtplib.SMTPException(msg)
48 48 (resp, reply) = self.docmd("STARTTLS")
49 49 if resp == 220:
50 50 self.sock = sslutil.wrapsocket(self.sock, keyfile, certfile,
51 51 ui=self._ui,
52 52 serverhostname=self._host)
53 53 self.file = smtplib.SSLFakeFile(self.sock)
54 54 self.helo_resp = None
55 55 self.ehlo_resp = None
56 56 self.esmtp_features = {}
57 57 self.does_esmtp = 0
58 58 return (resp, reply)
59 59
60 60 class SMTPS(smtplib.SMTP):
61 61 '''Derived class to verify the peer certificate for SMTPS.
62 62
63 63 This class allows to pass any keyword arguments to SSL socket creation.
64 64 '''
65 65 def __init__(self, ui, keyfile=None, certfile=None, host=None,
66 66 **kwargs):
67 67 self.keyfile = keyfile
68 68 self.certfile = certfile
69 69 smtplib.SMTP.__init__(self, **kwargs)
70 70 self._host = host
71 71 self.default_port = smtplib.SMTP_SSL_PORT
72 72 self._ui = ui
73 73
74 74 def _get_socket(self, host, port, timeout):
75 75 if self.debuglevel > 0:
76 76 self._ui.debug('connect: %r\n' % (host, port))
77 77 new_socket = socket.create_connection((host, port), timeout)
78 78 new_socket = sslutil.wrapsocket(new_socket,
79 79 self.keyfile, self.certfile,
80 80 ui=self._ui,
81 81 serverhostname=self._host)
82 self.file = smtplib.SSLFakeFile(new_socket)
82 self.file = new_socket.makefile(r'rb')
83 83 return new_socket
84 84
85 85 def _pyhastls():
86 86 """Returns true iff Python has TLS support, false otherwise."""
87 87 try:
88 88 import ssl
89 89 getattr(ssl, 'HAS_TLS', False)
90 90 return True
91 91 except ImportError:
92 92 return False
93 93
94 94 def _smtp(ui):
95 95 '''build an smtp connection and return a function to send mail'''
96 96 local_hostname = ui.config('smtp', 'local_hostname')
97 97 tls = ui.config('smtp', 'tls')
98 98 # backward compatible: when tls = true, we use starttls.
99 99 starttls = tls == 'starttls' or stringutil.parsebool(tls)
100 100 smtps = tls == 'smtps'
101 101 if (starttls or smtps) and not _pyhastls():
102 102 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
103 103 mailhost = ui.config('smtp', 'host')
104 104 if not mailhost:
105 105 raise error.Abort(_('smtp.host not configured - cannot send mail'))
106 106 if smtps:
107 107 ui.note(_('(using smtps)\n'))
108 108 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
109 109 elif starttls:
110 110 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
111 111 else:
112 112 s = smtplib.SMTP(local_hostname=local_hostname)
113 113 if smtps:
114 114 defaultport = 465
115 115 else:
116 116 defaultport = 25
117 117 mailport = util.getport(ui.config('smtp', 'port', defaultport))
118 118 ui.note(_('sending mail: smtp host %s, port %d\n') %
119 119 (mailhost, mailport))
120 120 s.connect(host=mailhost, port=mailport)
121 121 if starttls:
122 122 ui.note(_('(using starttls)\n'))
123 123 s.ehlo()
124 124 s.starttls()
125 125 s.ehlo()
126 126 if starttls or smtps:
127 127 ui.note(_('(verifying remote certificate)\n'))
128 128 sslutil.validatesocket(s.sock)
129 129 username = ui.config('smtp', 'username')
130 130 password = ui.config('smtp', 'password')
131 131 if username and not password:
132 132 password = ui.getpass()
133 133 if username and password:
134 134 ui.note(_('(authenticating to mail server as %s)\n') %
135 135 (username))
136 136 try:
137 137 s.login(username, password)
138 138 except smtplib.SMTPException as inst:
139 139 raise error.Abort(inst)
140 140
141 141 def send(sender, recipients, msg):
142 142 try:
143 143 return s.sendmail(sender, recipients, msg)
144 144 except smtplib.SMTPRecipientsRefused as inst:
145 145 recipients = [r[1] for r in inst.recipients.values()]
146 146 raise error.Abort('\n' + '\n'.join(recipients))
147 147 except smtplib.SMTPException as inst:
148 148 raise error.Abort(inst)
149 149
150 150 return send
151 151
152 152 def _sendmail(ui, sender, recipients, msg):
153 153 '''send mail using sendmail.'''
154 154 program = ui.config('email', 'method')
155 155 cmdline = '%s -f %s %s' % (program, stringutil.email(sender),
156 156 ' '.join(map(stringutil.email, recipients)))
157 157 ui.note(_('sending mail: %s\n') % cmdline)
158 158 fp = procutil.popen(cmdline, 'wb')
159 159 fp.write(util.tonativeeol(msg))
160 160 ret = fp.close()
161 161 if ret:
162 162 raise error.Abort('%s %s' % (
163 163 os.path.basename(program.split(None, 1)[0]),
164 164 procutil.explainexit(ret)))
165 165
166 166 def _mbox(mbox, sender, recipients, msg):
167 167 '''write mails to mbox'''
168 168 fp = open(mbox, 'ab+')
169 169 # Should be time.asctime(), but Windows prints 2-characters day
170 170 # of month instead of one. Make them print the same thing.
171 171 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime())
172 172 fp.write('From %s %s\n' % (sender, date))
173 173 fp.write(msg)
174 174 fp.write('\n\n')
175 175 fp.close()
176 176
177 177 def connect(ui, mbox=None):
178 178 '''make a mail connection. return a function to send mail.
179 179 call as sendmail(sender, list-of-recipients, msg).'''
180 180 if mbox:
181 181 open(mbox, 'wb').close()
182 182 return lambda s, r, m: _mbox(mbox, s, r, m)
183 183 if ui.config('email', 'method') == 'smtp':
184 184 return _smtp(ui)
185 185 return lambda s, r, m: _sendmail(ui, s, r, m)
186 186
187 187 def sendmail(ui, sender, recipients, msg, mbox=None):
188 188 send = connect(ui, mbox=mbox)
189 189 return send(sender, recipients, msg)
190 190
191 191 def validateconfig(ui):
192 192 '''determine if we have enough config data to try sending email.'''
193 193 method = ui.config('email', 'method')
194 194 if method == 'smtp':
195 195 if not ui.config('smtp', 'host'):
196 196 raise error.Abort(_('smtp specified as email transport, '
197 197 'but no smtp host configured'))
198 198 else:
199 199 if not procutil.findexe(method):
200 200 raise error.Abort(_('%r specified as email transport, '
201 201 'but not in PATH') % method)
202 202
203 203 def codec2iana(cs):
204 204 ''''''
205 205 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
206 206
207 207 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
208 208 if cs.startswith("iso") and not cs.startswith("iso-"):
209 209 return "iso-" + cs[3:]
210 210 return cs
211 211
212 212 def mimetextpatch(s, subtype='plain', display=False):
213 213 '''Return MIME message suitable for a patch.
214 214 Charset will be detected by first trying to decode as us-ascii, then utf-8,
215 215 and finally the global encodings. If all those fail, fall back to
216 216 ISO-8859-1, an encoding with that allows all byte sequences.
217 217 Transfer encodings will be used if necessary.'''
218 218
219 219 cs = ['us-ascii', 'utf-8', encoding.encoding, encoding.fallbackencoding]
220 220 if display:
221 221 return mimetextqp(s, subtype, 'us-ascii')
222 222 for charset in cs:
223 223 try:
224 224 s.decode(pycompat.sysstr(charset))
225 225 return mimetextqp(s, subtype, codec2iana(charset))
226 226 except UnicodeDecodeError:
227 227 pass
228 228
229 229 return mimetextqp(s, subtype, "iso-8859-1")
230 230
231 231 def mimetextqp(body, subtype, charset):
232 232 '''Return MIME message.
233 233 Quoted-printable transfer encoding will be used if necessary.
234 234 '''
235 235 cs = email.charset.Charset(charset)
236 236 msg = email.message.Message()
237 237 msg.set_type(pycompat.sysstr('text/' + subtype))
238 238
239 239 for line in body.splitlines():
240 240 if len(line) > 950:
241 241 cs.body_encoding = email.charset.QP
242 242 break
243 243
244 244 msg.set_payload(body, cs)
245 245
246 246 return msg
247 247
248 248 def _charsets(ui):
249 249 '''Obtains charsets to send mail parts not containing patches.'''
250 250 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
251 251 fallbacks = [encoding.fallbackencoding.lower(),
252 252 encoding.encoding.lower(), 'utf-8']
253 253 for cs in fallbacks: # find unique charsets while keeping order
254 254 if cs not in charsets:
255 255 charsets.append(cs)
256 256 return [cs for cs in charsets if not cs.endswith('ascii')]
257 257
258 258 def _encode(ui, s, charsets):
259 259 '''Returns (converted) string, charset tuple.
260 260 Finds out best charset by cycling through sendcharsets in descending
261 261 order. Tries both encoding and fallbackencoding for input. Only as
262 262 last resort send as is in fake ascii.
263 263 Caveat: Do not use for mail parts containing patches!'''
264 264 sendcharsets = charsets or _charsets(ui)
265 265 if not isinstance(s, bytes):
266 266 # We have unicode data, which we need to try and encode to
267 267 # some reasonable-ish encoding. Try the encodings the user
268 268 # wants, and fall back to garbage-in-ascii.
269 269 for ocs in sendcharsets:
270 270 try:
271 271 return s.encode(pycompat.sysstr(ocs)), ocs
272 272 except UnicodeEncodeError:
273 273 pass
274 274 except LookupError:
275 275 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
276 276 else:
277 277 # Everything failed, ascii-armor what we've got and send it.
278 278 return s.encode('ascii', 'backslashreplace')
279 279 # We have a bytes of unknown encoding. We'll try and guess a valid
280 280 # encoding, falling back to pretending we had ascii even though we
281 281 # know that's wrong.
282 282 try:
283 283 s.decode('ascii')
284 284 except UnicodeDecodeError:
285 285 for ics in (encoding.encoding, encoding.fallbackencoding):
286 286 try:
287 287 u = s.decode(ics)
288 288 except UnicodeDecodeError:
289 289 continue
290 290 for ocs in sendcharsets:
291 291 try:
292 292 return u.encode(pycompat.sysstr(ocs)), ocs
293 293 except UnicodeEncodeError:
294 294 pass
295 295 except LookupError:
296 296 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
297 297 # if ascii, or all conversion attempts fail, send (broken) ascii
298 298 return s, 'us-ascii'
299 299
300 300 def headencode(ui, s, charsets=None, display=False):
301 301 '''Returns RFC-2047 compliant header from given string.'''
302 302 if not display:
303 303 # split into words?
304 304 s, cs = _encode(ui, s, charsets)
305 305 return str(email.header.Header(s, cs))
306 306 return s
307 307
308 308 def _addressencode(ui, name, addr, charsets=None):
309 309 name = headencode(ui, name, charsets)
310 310 try:
311 311 acc, dom = addr.split(r'@')
312 312 acc = acc.encode('ascii')
313 313 dom = dom.decode(encoding.encoding).encode('idna')
314 314 addr = '%s@%s' % (acc, dom)
315 315 except UnicodeDecodeError:
316 316 raise error.Abort(_('invalid email address: %s') % addr)
317 317 except ValueError:
318 318 try:
319 319 # too strict?
320 320 addr = addr.encode('ascii')
321 321 except UnicodeDecodeError:
322 322 raise error.Abort(_('invalid local address: %s') % addr)
323 323 return pycompat.bytesurl(
324 324 email.utils.formataddr((name, encoding.strfromlocal(addr))))
325 325
326 326 def addressencode(ui, address, charsets=None, display=False):
327 327 '''Turns address into RFC-2047 compliant header.'''
328 328 if display or not address:
329 329 return address or ''
330 330 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
331 331 return _addressencode(ui, name, addr, charsets)
332 332
333 333 def addrlistencode(ui, addrs, charsets=None, display=False):
334 334 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
335 335 A single element of input list may contain multiple addresses, but output
336 336 always has one address per item'''
337 337 for a in addrs:
338 338 assert isinstance(a, bytes), (r'%r unexpectedly not a bytestr' % a)
339 339 if display:
340 340 return [a.strip() for a in addrs if a.strip()]
341 341
342 342 result = []
343 343 for name, addr in email.utils.getaddresses(
344 344 [encoding.strfromlocal(a) for a in addrs]):
345 345 if name or addr:
346 346 result.append(_addressencode(ui, name, addr, charsets))
347 347 return [pycompat.bytesurl(r) for r in result]
348 348
349 349 def mimeencode(ui, s, charsets=None, display=False):
350 350 '''creates mime text object, encodes it if needed, and sets
351 351 charset and transfer-encoding accordingly.'''
352 352 cs = 'us-ascii'
353 353 if not display:
354 354 s, cs = _encode(ui, s, charsets)
355 355 return mimetextqp(s, 'plain', cs)
356 356
357 357 if pycompat.ispy3:
358 358 def parse(fp):
359 359 ep = email.parser.Parser()
360 360 # disable the "universal newlines" mode, which isn't binary safe.
361 361 # I have no idea if ascii/surrogateescape is correct, but that's
362 362 # what the standard Python email parser does.
363 363 fp = io.TextIOWrapper(fp, encoding=r'ascii',
364 364 errors=r'surrogateescape', newline=chr(10))
365 365 try:
366 366 return ep.parse(fp)
367 367 finally:
368 368 fp.detach()
369 369 else:
370 370 def parse(fp):
371 371 ep = email.parser.Parser()
372 372 return ep.parse(fp)
373 373
374 374 def headdecode(s):
375 375 '''Decodes RFC-2047 header'''
376 376 uparts = []
377 377 for part, charset in email.header.decode_header(s):
378 378 if charset is not None:
379 379 try:
380 380 uparts.append(part.decode(charset))
381 381 continue
382 382 except UnicodeDecodeError:
383 383 pass
384 384 # On Python 3, decode_header() may return either bytes or unicode
385 385 # depending on whether the header has =?<charset>? or not
386 386 if isinstance(part, type(u'')):
387 387 uparts.append(part)
388 388 continue
389 389 try:
390 390 uparts.append(part.decode('UTF-8'))
391 391 continue
392 392 except UnicodeDecodeError:
393 393 pass
394 394 uparts.append(part.decode('ISO-8859-1'))
395 395 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now