##// END OF EJS Templates
mail: suppress a pytype error that's just experimentally wrong...
Augie Fackler -
r43804:3b31ee53 default
parent child Browse files
Show More
@@ -1,483 +1,485 b''
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(r'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(r'%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 cs = email.charset.Charset(charset)
282 # Experimentally charset is okay as a bytes even if the type
283 # stubs disagree.
284 cs = email.charset.Charset(charset) # pytype: disable=wrong-arg-types
283 285 msg = email.message.Message()
284 286 msg.set_type(pycompat.sysstr(b'text/' + subtype))
285 287
286 288 for line in body.splitlines():
287 289 if len(line) > 950:
288 290 cs.body_encoding = email.charset.QP
289 291 break
290 292
291 293 # On Python 2, this simply assigns a value. Python 3 inspects
292 294 # body and does different things depending on whether it has
293 295 # encode() or decode() attributes. We can get the old behavior
294 296 # if we pass a str and charset is None and we call set_charset().
295 297 # But we may get into trouble later due to Python attempting to
296 298 # encode/decode using the registered charset (or attempting to
297 299 # use ascii in the absence of a charset).
298 300 msg.set_payload(body, cs)
299 301
300 302 return msg
301 303
302 304
303 305 def _charsets(ui):
304 306 '''Obtains charsets to send mail parts not containing patches.'''
305 307 charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
306 308 fallbacks = [
307 309 encoding.fallbackencoding.lower(),
308 310 encoding.encoding.lower(),
309 311 b'utf-8',
310 312 ]
311 313 for cs in fallbacks: # find unique charsets while keeping order
312 314 if cs not in charsets:
313 315 charsets.append(cs)
314 316 return [cs for cs in charsets if not cs.endswith(b'ascii')]
315 317
316 318
317 319 def _encode(ui, s, charsets):
318 320 '''Returns (converted) string, charset tuple.
319 321 Finds out best charset by cycling through sendcharsets in descending
320 322 order. Tries both encoding and fallbackencoding for input. Only as
321 323 last resort send as is in fake ascii.
322 324 Caveat: Do not use for mail parts containing patches!'''
323 325 sendcharsets = charsets or _charsets(ui)
324 326 if not isinstance(s, bytes):
325 327 # We have unicode data, which we need to try and encode to
326 328 # some reasonable-ish encoding. Try the encodings the user
327 329 # wants, and fall back to garbage-in-ascii.
328 330 for ocs in sendcharsets:
329 331 try:
330 332 return s.encode(pycompat.sysstr(ocs)), ocs
331 333 except UnicodeEncodeError:
332 334 pass
333 335 except LookupError:
334 336 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
335 337 else:
336 338 # Everything failed, ascii-armor what we've got and send it.
337 339 return s.encode('ascii', 'backslashreplace')
338 340 # We have a bytes of unknown encoding. We'll try and guess a valid
339 341 # encoding, falling back to pretending we had ascii even though we
340 342 # know that's wrong.
341 343 try:
342 344 s.decode('ascii')
343 345 except UnicodeDecodeError:
344 346 for ics in (encoding.encoding, encoding.fallbackencoding):
345 347 ics = pycompat.sysstr(ics)
346 348 try:
347 349 u = s.decode(ics)
348 350 except UnicodeDecodeError:
349 351 continue
350 352 for ocs in sendcharsets:
351 353 try:
352 354 return u.encode(pycompat.sysstr(ocs)), ocs
353 355 except UnicodeEncodeError:
354 356 pass
355 357 except LookupError:
356 358 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
357 359 # if ascii, or all conversion attempts fail, send (broken) ascii
358 360 return s, b'us-ascii'
359 361
360 362
361 363 def headencode(ui, s, charsets=None, display=False):
362 364 '''Returns RFC-2047 compliant header from given string.'''
363 365 if not display:
364 366 # split into words?
365 367 s, cs = _encode(ui, s, charsets)
366 368 return encoding.strtolocal(email.header.Header(s, cs).encode())
367 369 return s
368 370
369 371
370 372 def _addressencode(ui, name, addr, charsets=None):
371 373 assert isinstance(addr, bytes)
372 374 name = encoding.strfromlocal(headencode(ui, name, charsets))
373 375 try:
374 376 acc, dom = addr.split(b'@')
375 377 acc.decode('ascii')
376 378 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
377 379 addr = b'%s@%s' % (acc, dom)
378 380 except UnicodeDecodeError:
379 381 raise error.Abort(_(b'invalid email address: %s') % addr)
380 382 except ValueError:
381 383 try:
382 384 # too strict?
383 385 addr.decode('ascii')
384 386 except UnicodeDecodeError:
385 387 raise error.Abort(_(b'invalid local address: %s') % addr)
386 388 return pycompat.bytesurl(
387 389 email.utils.formataddr((name, encoding.strfromlocal(addr)))
388 390 )
389 391
390 392
391 393 def addressencode(ui, address, charsets=None, display=False):
392 394 '''Turns address into RFC-2047 compliant header.'''
393 395 if display or not address:
394 396 return address or b''
395 397 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
396 398 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
397 399
398 400
399 401 def addrlistencode(ui, addrs, charsets=None, display=False):
400 402 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
401 403 A single element of input list may contain multiple addresses, but output
402 404 always has one address per item'''
403 405 for a in addrs:
404 406 assert isinstance(a, bytes), r'%r unexpectedly not a bytestr' % a
405 407 if display:
406 408 return [a.strip() for a in addrs if a.strip()]
407 409
408 410 result = []
409 411 for name, addr in email.utils.getaddresses(
410 412 [encoding.strfromlocal(a) for a in addrs]
411 413 ):
412 414 if name or addr:
413 415 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
414 416 result.append(r)
415 417 return result
416 418
417 419
418 420 def mimeencode(ui, s, charsets=None, display=False):
419 421 '''creates mime text object, encodes it if needed, and sets
420 422 charset and transfer-encoding accordingly.'''
421 423 cs = b'us-ascii'
422 424 if not display:
423 425 s, cs = _encode(ui, s, charsets)
424 426 return mimetextqp(s, b'plain', cs)
425 427
426 428
427 429 if pycompat.ispy3:
428 430
429 431 Generator = email.generator.BytesGenerator
430 432
431 433 def parse(fp):
432 434 ep = email.parser.Parser()
433 435 # disable the "universal newlines" mode, which isn't binary safe.
434 436 # I have no idea if ascii/surrogateescape is correct, but that's
435 437 # what the standard Python email parser does.
436 438 fp = io.TextIOWrapper(
437 439 fp, encoding=r'ascii', errors=r'surrogateescape', newline=chr(10)
438 440 )
439 441 try:
440 442 return ep.parse(fp)
441 443 finally:
442 444 fp.detach()
443 445
444 446 def parsebytes(data):
445 447 ep = email.parser.BytesParser()
446 448 return ep.parsebytes(data)
447 449
448 450
449 451 else:
450 452
451 453 Generator = email.generator.Generator
452 454
453 455 def parse(fp):
454 456 ep = email.parser.Parser()
455 457 return ep.parse(fp)
456 458
457 459 def parsebytes(data):
458 460 ep = email.parser.Parser()
459 461 return ep.parsestr(data)
460 462
461 463
462 464 def headdecode(s):
463 465 '''Decodes RFC-2047 header'''
464 466 uparts = []
465 467 for part, charset in email.header.decode_header(s):
466 468 if charset is not None:
467 469 try:
468 470 uparts.append(part.decode(charset))
469 471 continue
470 472 except (UnicodeDecodeError, LookupError):
471 473 pass
472 474 # On Python 3, decode_header() may return either bytes or unicode
473 475 # depending on whether the header has =?<charset>? or not
474 476 if isinstance(part, type(u'')):
475 477 uparts.append(part)
476 478 continue
477 479 try:
478 480 uparts.append(part.decode('UTF-8'))
479 481 continue
480 482 except UnicodeDecodeError:
481 483 pass
482 484 uparts.append(part.decode('ISO-8859-1'))
483 485 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now