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