##// END OF EJS Templates
mail: delete conditional code for Python 2...
Gregory Szorc -
r49738:f0c445a8 default
parent child Browse files
Show More
@@ -1,531 +1,516 b''
1 1 # mail.py - mail sending bits for mercurial
2 2 #
3 3 # Copyright 2006 Olivia Mackall <olivia@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
9 9 import email
10 10 import email.charset
11 11 import email.generator
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 .pycompat import (
23 23 getattr,
24 24 open,
25 25 )
26 26 from . import (
27 27 encoding,
28 28 error,
29 29 pycompat,
30 30 sslutil,
31 31 util,
32 32 )
33 33 from .utils import (
34 34 procutil,
35 35 stringutil,
36 36 urlutil,
37 37 )
38 38
39 39 if pycompat.TYPE_CHECKING:
40 40 from typing import Any, List, Tuple, Union
41 41
42 42 # keep pyflakes happy
43 43 assert all((Any, List, Tuple, Union))
44 44
45 45
46 46 class STARTTLS(smtplib.SMTP):
47 47 """Derived class to verify the peer certificate for STARTTLS.
48 48
49 49 This class allows to pass any keyword arguments to SSL socket creation.
50 50 """
51 51
52 52 def __init__(self, ui, host=None, **kwargs):
53 53 smtplib.SMTP.__init__(self, **kwargs)
54 54 self._ui = ui
55 55 self._host = host
56 56
57 57 def starttls(self, keyfile=None, certfile=None):
58 58 if not self.has_extn("starttls"):
59 59 msg = b"STARTTLS extension not supported by server"
60 60 raise smtplib.SMTPException(msg)
61 61 (resp, reply) = self.docmd("STARTTLS")
62 62 if resp == 220:
63 63 self.sock = sslutil.wrapsocket(
64 64 self.sock,
65 65 keyfile,
66 66 certfile,
67 67 ui=self._ui,
68 68 serverhostname=self._host,
69 69 )
70 70 self.file = self.sock.makefile("rb")
71 71 self.helo_resp = None
72 72 self.ehlo_resp = None
73 73 self.esmtp_features = {}
74 74 self.does_esmtp = 0
75 75 return (resp, reply)
76 76
77 77
78 78 class SMTPS(smtplib.SMTP):
79 79 """Derived class to verify the peer certificate for SMTPS.
80 80
81 81 This class allows to pass any keyword arguments to SSL socket creation.
82 82 """
83 83
84 84 def __init__(self, ui, keyfile=None, certfile=None, host=None, **kwargs):
85 85 self.keyfile = keyfile
86 86 self.certfile = certfile
87 87 smtplib.SMTP.__init__(self, **kwargs)
88 88 self._host = host
89 89 self.default_port = smtplib.SMTP_SSL_PORT
90 90 self._ui = ui
91 91
92 92 def _get_socket(self, host, port, timeout):
93 93 if self.debuglevel > 0:
94 94 self._ui.debug(b'connect: %r\n' % ((host, port),))
95 95 new_socket = socket.create_connection((host, port), timeout)
96 96 new_socket = sslutil.wrapsocket(
97 97 new_socket,
98 98 self.keyfile,
99 99 self.certfile,
100 100 ui=self._ui,
101 101 serverhostname=self._host,
102 102 )
103 103 self.file = new_socket.makefile('rb')
104 104 return new_socket
105 105
106 106
107 107 def _pyhastls():
108 108 # type: () -> bool
109 109 """Returns true iff Python has TLS support, false otherwise."""
110 110 try:
111 111 import ssl
112 112
113 113 getattr(ssl, 'HAS_TLS', False)
114 114 return True
115 115 except ImportError:
116 116 return False
117 117
118 118
119 119 def _smtp(ui):
120 120 '''build an smtp connection and return a function to send mail'''
121 121 local_hostname = ui.config(b'smtp', b'local_hostname')
122 122 tls = ui.config(b'smtp', b'tls')
123 123 # backward compatible: when tls = true, we use starttls.
124 124 starttls = tls == b'starttls' or stringutil.parsebool(tls)
125 125 smtps = tls == b'smtps'
126 126 if (starttls or smtps) and not _pyhastls():
127 127 raise error.Abort(_(b"can't use TLS: Python SSL support not installed"))
128 128 mailhost = ui.config(b'smtp', b'host')
129 129 if not mailhost:
130 130 raise error.Abort(_(b'smtp.host not configured - cannot send mail'))
131 131 if smtps:
132 132 ui.note(_(b'(using smtps)\n'))
133 133 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
134 134 elif starttls:
135 135 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
136 136 else:
137 137 s = smtplib.SMTP(local_hostname=local_hostname)
138 138 if smtps:
139 139 defaultport = 465
140 140 else:
141 141 defaultport = 25
142 142 mailport = urlutil.getport(ui.config(b'smtp', b'port', defaultport))
143 143 ui.note(_(b'sending mail: smtp host %s, port %d\n') % (mailhost, mailport))
144 144 s.connect(host=mailhost, port=mailport)
145 145 if starttls:
146 146 ui.note(_(b'(using starttls)\n'))
147 147 s.ehlo()
148 148 s.starttls()
149 149 s.ehlo()
150 150 if starttls or smtps:
151 151 ui.note(_(b'(verifying remote certificate)\n'))
152 152 sslutil.validatesocket(s.sock)
153 153
154 154 try:
155 155 _smtp_login(ui, s, mailhost, mailport)
156 156 except smtplib.SMTPException as inst:
157 157 raise error.Abort(stringutil.forcebytestr(inst))
158 158
159 159 def send(sender, recipients, msg):
160 160 try:
161 161 return s.sendmail(sender, recipients, msg)
162 162 except smtplib.SMTPRecipientsRefused as inst:
163 163 recipients = [r[1] for r in inst.recipients.values()]
164 164 raise error.Abort(b'\n' + b'\n'.join(recipients))
165 165 except smtplib.SMTPException as inst:
166 166 raise error.Abort(stringutil.forcebytestr(inst))
167 167
168 168 return send
169 169
170 170
171 171 def _smtp_login(ui, smtp, mailhost, mailport):
172 172 """A hook for the keyring extension to perform the actual SMTP login.
173 173
174 174 An already connected SMTP object of the proper type is provided, based on
175 175 the current configuration. The host and port to which the connection was
176 176 established are provided for accessibility, since the SMTP object doesn't
177 177 provide an accessor. ``smtplib.SMTPException`` is raised on error.
178 178 """
179 179 username = ui.config(b'smtp', b'username')
180 180 password = ui.config(b'smtp', b'password')
181 181 if username:
182 182 if password:
183 183 password = encoding.strfromlocal(password)
184 184 else:
185 185 password = ui.getpass()
186 186 if password is not None:
187 187 password = encoding.strfromlocal(password)
188 188 if username and password:
189 189 ui.note(_(b'(authenticating to mail server as %s)\n') % username)
190 190 username = encoding.strfromlocal(username)
191 191 smtp.login(username, password)
192 192
193 193
194 194 def _sendmail(ui, sender, recipients, msg):
195 195 '''send mail using sendmail.'''
196 196 program = ui.config(b'email', b'method')
197 197
198 198 def stremail(x):
199 199 return procutil.shellquote(stringutil.email(encoding.strtolocal(x)))
200 200
201 201 cmdline = b'%s -f %s %s' % (
202 202 program,
203 203 stremail(sender),
204 204 b' '.join(map(stremail, recipients)),
205 205 )
206 206 ui.note(_(b'sending mail: %s\n') % cmdline)
207 207 fp = procutil.popen(cmdline, b'wb')
208 208 fp.write(util.tonativeeol(msg))
209 209 ret = fp.close()
210 210 if ret:
211 211 raise error.Abort(
212 212 b'%s %s'
213 213 % (
214 214 os.path.basename(procutil.shellsplit(program)[0]),
215 215 procutil.explainexit(ret),
216 216 )
217 217 )
218 218
219 219
220 220 def _mbox(mbox, sender, recipients, msg):
221 221 '''write mails to mbox'''
222 222 # TODO: use python mbox library for proper locking
223 223 with open(mbox, b'ab+') as fp:
224 224 # Should be time.asctime(), but Windows prints 2-characters day
225 225 # of month instead of one. Make them print the same thing.
226 226 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
227 227 fp.write(
228 228 b'From %s %s\n'
229 229 % (encoding.strtolocal(sender), encoding.strtolocal(date))
230 230 )
231 231 fp.write(msg)
232 232 fp.write(b'\n\n')
233 233
234 234
235 235 def connect(ui, mbox=None):
236 236 """make a mail connection. return a function to send mail.
237 237 call as sendmail(sender, list-of-recipients, msg)."""
238 238 if mbox:
239 239 open(mbox, b'wb').close()
240 240 return lambda s, r, m: _mbox(mbox, s, r, m)
241 241 if ui.config(b'email', b'method') == b'smtp':
242 242 return _smtp(ui)
243 243 return lambda s, r, m: _sendmail(ui, s, r, m)
244 244
245 245
246 246 def sendmail(ui, sender, recipients, msg, mbox=None):
247 247 send = connect(ui, mbox=mbox)
248 248 return send(sender, recipients, msg)
249 249
250 250
251 251 def validateconfig(ui):
252 252 '''determine if we have enough config data to try sending email.'''
253 253 method = ui.config(b'email', b'method')
254 254 if method == b'smtp':
255 255 if not ui.config(b'smtp', b'host'):
256 256 raise error.Abort(
257 257 _(
258 258 b'smtp specified as email transport, '
259 259 b'but no smtp host configured'
260 260 )
261 261 )
262 262 else:
263 263 if not procutil.findexe(method):
264 264 raise error.Abort(
265 265 _(b'%r specified as email transport, but not in PATH') % method
266 266 )
267 267
268 268
269 269 def codec2iana(cs):
270 270 # type: (str) -> str
271 271 ''' '''
272 272 cs = email.charset.Charset(cs).input_charset.lower()
273 273
274 274 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
275 275 if cs.startswith("iso") and not cs.startswith("iso-"):
276 276 return "iso-" + cs[3:]
277 277 return cs
278 278
279 279
280 280 def mimetextpatch(s, subtype='plain', display=False):
281 281 # type: (bytes, str, bool) -> email.message.Message
282 282 """Return MIME message suitable for a patch.
283 283 Charset will be detected by first trying to decode as us-ascii, then utf-8,
284 284 and finally the global encodings. If all those fail, fall back to
285 285 ISO-8859-1, an encoding with that allows all byte sequences.
286 286 Transfer encodings will be used if necessary."""
287 287
288 288 cs = [
289 289 'us-ascii',
290 290 'utf-8',
291 291 pycompat.sysstr(encoding.encoding),
292 292 pycompat.sysstr(encoding.fallbackencoding),
293 293 ]
294 294 if display:
295 295 cs = ['us-ascii']
296 296 for charset in cs:
297 297 try:
298 298 s.decode(charset)
299 299 return mimetextqp(s, subtype, codec2iana(charset))
300 300 except UnicodeDecodeError:
301 301 pass
302 302
303 303 return mimetextqp(s, subtype, "iso-8859-1")
304 304
305 305
306 306 def mimetextqp(body, subtype, charset):
307 307 # type: (bytes, str, str) -> email.message.Message
308 308 """Return MIME message.
309 309 Quoted-printable transfer encoding will be used if necessary.
310 310 """
311 311 cs = email.charset.Charset(charset)
312 312 msg = email.message.Message()
313 313 msg.set_type('text/' + subtype)
314 314
315 315 for line in body.splitlines():
316 316 if len(line) > 950:
317 317 cs.body_encoding = email.charset.QP
318 318 break
319 319
320 320 # On Python 2, this simply assigns a value. Python 3 inspects
321 321 # body and does different things depending on whether it has
322 322 # encode() or decode() attributes. We can get the old behavior
323 323 # if we pass a str and charset is None and we call set_charset().
324 324 # But we may get into trouble later due to Python attempting to
325 325 # encode/decode using the registered charset (or attempting to
326 326 # use ascii in the absence of a charset).
327 327 msg.set_payload(body, cs)
328 328
329 329 return msg
330 330
331 331
332 332 def _charsets(ui):
333 333 # type: (Any) -> List[str]
334 334 '''Obtains charsets to send mail parts not containing patches.'''
335 335 charsets = [
336 336 pycompat.sysstr(cs.lower())
337 337 for cs in ui.configlist(b'email', b'charsets')
338 338 ]
339 339 fallbacks = [
340 340 pycompat.sysstr(encoding.fallbackencoding.lower()),
341 341 pycompat.sysstr(encoding.encoding.lower()),
342 342 'utf-8',
343 343 ]
344 344 for cs in fallbacks: # find unique charsets while keeping order
345 345 if cs not in charsets:
346 346 charsets.append(cs)
347 347 return [cs for cs in charsets if not cs.endswith('ascii')]
348 348
349 349
350 350 def _encode(ui, s, charsets):
351 351 # type: (Any, bytes, List[str]) -> Tuple[bytes, str]
352 352 """Returns (converted) string, charset tuple.
353 353 Finds out best charset by cycling through sendcharsets in descending
354 354 order. Tries both encoding and fallbackencoding for input. Only as
355 355 last resort send as is in fake ascii.
356 356 Caveat: Do not use for mail parts containing patches!"""
357 357 sendcharsets = charsets or _charsets(ui)
358 358 if not isinstance(s, bytes):
359 359 # We have unicode data, which we need to try and encode to
360 360 # some reasonable-ish encoding. Try the encodings the user
361 361 # wants, and fall back to garbage-in-ascii.
362 362 for ocs in sendcharsets:
363 363 try:
364 364 return s.encode(ocs), ocs
365 365 except UnicodeEncodeError:
366 366 pass
367 367 except LookupError:
368 368 ui.warn(
369 369 _(b'ignoring invalid sendcharset: %s\n')
370 370 % pycompat.sysbytes(ocs)
371 371 )
372 372 else:
373 373 # Everything failed, ascii-armor what we've got and send it.
374 374 return s.encode('ascii', 'backslashreplace'), 'us-ascii'
375 375 # We have a bytes of unknown encoding. We'll try and guess a valid
376 376 # encoding, falling back to pretending we had ascii even though we
377 377 # know that's wrong.
378 378 try:
379 379 s.decode('ascii')
380 380 except UnicodeDecodeError:
381 381 for ics in (encoding.encoding, encoding.fallbackencoding):
382 382 ics = pycompat.sysstr(ics)
383 383 try:
384 384 u = s.decode(ics)
385 385 except UnicodeDecodeError:
386 386 continue
387 387 for ocs in sendcharsets:
388 388 try:
389 389 return u.encode(ocs), ocs
390 390 except UnicodeEncodeError:
391 391 pass
392 392 except LookupError:
393 393 ui.warn(
394 394 _(b'ignoring invalid sendcharset: %s\n')
395 395 % pycompat.sysbytes(ocs)
396 396 )
397 397 # if ascii, or all conversion attempts fail, send (broken) ascii
398 398 return s, 'us-ascii'
399 399
400 400
401 401 def headencode(ui, s, charsets=None, display=False):
402 402 # type: (Any, Union[bytes, str], List[str], bool) -> str
403 403 '''Returns RFC-2047 compliant header from given string.'''
404 404 if not display:
405 405 # split into words?
406 406 s, cs = _encode(ui, s, charsets)
407 407 return email.header.Header(s, cs).encode()
408 408 return encoding.strfromlocal(s)
409 409
410 410
411 411 def _addressencode(ui, name, addr, charsets=None):
412 412 # type: (Any, str, str, List[str]) -> str
413 413 addr = encoding.strtolocal(addr)
414 414 name = headencode(ui, name, charsets)
415 415 try:
416 416 acc, dom = addr.split(b'@')
417 417 acc.decode('ascii')
418 418 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
419 419 addr = b'%s@%s' % (acc, dom)
420 420 except UnicodeDecodeError:
421 421 raise error.Abort(_(b'invalid email address: %s') % addr)
422 422 except ValueError:
423 423 try:
424 424 # too strict?
425 425 addr.decode('ascii')
426 426 except UnicodeDecodeError:
427 427 raise error.Abort(_(b'invalid local address: %s') % addr)
428 428 return email.utils.formataddr((name, encoding.strfromlocal(addr)))
429 429
430 430
431 431 def addressencode(ui, address, charsets=None, display=False):
432 432 # type: (Any, bytes, List[str], bool) -> str
433 433 '''Turns address into RFC-2047 compliant header.'''
434 434 if display or not address:
435 435 return encoding.strfromlocal(address or b'')
436 436 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
437 437 return _addressencode(ui, name, addr, charsets)
438 438
439 439
440 440 def addrlistencode(ui, addrs, charsets=None, display=False):
441 441 # type: (Any, List[bytes], List[str], bool) -> List[str]
442 442 """Turns a list of addresses into a list of RFC-2047 compliant headers.
443 443 A single element of input list may contain multiple addresses, but output
444 444 always has one address per item"""
445 445 straddrs = []
446 446 for a in addrs:
447 447 assert isinstance(a, bytes), '%r unexpectedly not a bytestr' % a
448 448 straddrs.append(encoding.strfromlocal(a))
449 449 if display:
450 450 return [a.strip() for a in straddrs if a.strip()]
451 451
452 452 result = []
453 453 for name, addr in email.utils.getaddresses(straddrs):
454 454 if name or addr:
455 455 r = _addressencode(ui, name, addr, charsets)
456 456 result.append(r)
457 457 return result
458 458
459 459
460 460 def mimeencode(ui, s, charsets=None, display=False):
461 461 # type: (Any, bytes, List[str], bool) -> email.message.Message
462 462 """creates mime text object, encodes it if needed, and sets
463 463 charset and transfer-encoding accordingly."""
464 464 cs = 'us-ascii'
465 465 if not display:
466 466 s, cs = _encode(ui, s, charsets)
467 467 return mimetextqp(s, 'plain', cs)
468 468
469 469
470 if pycompat.ispy3:
471
472 Generator = email.generator.BytesGenerator
473
474 def parse(fp):
475 # type: (Any) -> email.message.Message
476 ep = email.parser.Parser()
477 # disable the "universal newlines" mode, which isn't binary safe.
478 # I have no idea if ascii/surrogateescape is correct, but that's
479 # what the standard Python email parser does.
480 fp = io.TextIOWrapper(
481 fp, encoding='ascii', errors='surrogateescape', newline=chr(10)
482 )
483 try:
484 return ep.parse(fp)
485 finally:
486 fp.detach()
487
488 def parsebytes(data):
489 # type: (bytes) -> email.message.Message
490 ep = email.parser.BytesParser()
491 return ep.parsebytes(data)
470 Generator = email.generator.BytesGenerator
492 471
493 472
494 else:
495
496 Generator = email.generator.Generator
473 def parse(fp):
474 # type: (Any) -> email.message.Message
475 ep = email.parser.Parser()
476 # disable the "universal newlines" mode, which isn't binary safe.
477 # I have no idea if ascii/surrogateescape is correct, but that's
478 # what the standard Python email parser does.
479 fp = io.TextIOWrapper(
480 fp, encoding='ascii', errors='surrogateescape', newline=chr(10)
481 )
482 try:
483 return ep.parse(fp)
484 finally:
485 fp.detach()
497 486
498 def parse(fp):
499 # type: (Any) -> email.message.Message
500 ep = email.parser.Parser()
501 return ep.parse(fp)
502 487
503 def parsebytes(data):
504 # type: (str) -> email.message.Message
505 ep = email.parser.Parser()
506 return ep.parsestr(data)
488 def parsebytes(data):
489 # type: (bytes) -> email.message.Message
490 ep = email.parser.BytesParser()
491 return ep.parsebytes(data)
507 492
508 493
509 494 def headdecode(s):
510 495 # type: (Union[email.header.Header, bytes]) -> bytes
511 496 '''Decodes RFC-2047 header'''
512 497 uparts = []
513 498 for part, charset in email.header.decode_header(s):
514 499 if charset is not None:
515 500 try:
516 501 uparts.append(part.decode(charset))
517 502 continue
518 503 except (UnicodeDecodeError, LookupError):
519 504 pass
520 505 # On Python 3, decode_header() may return either bytes or unicode
521 506 # depending on whether the header has =?<charset>? or not
522 507 if isinstance(part, type(u'')):
523 508 uparts.append(part)
524 509 continue
525 510 try:
526 511 uparts.append(part.decode('UTF-8'))
527 512 continue
528 513 except UnicodeDecodeError:
529 514 pass
530 515 uparts.append(part.decode('ISO-8859-1'))
531 516 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now