##// END OF EJS Templates
py3: call SMTP.has_extn() with an str...
Denis Laxalde -
r43439:3941e706 default
parent child Browse files
Show More
@@ -1,470 +1,470
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 if not self.has_extn(b"starttls"):
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(b"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 = smtplib.SSLFakeFile(self.sock)
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 and not password:
149 149 password = ui.getpass()
150 150 if username and password:
151 151 ui.note(_(b'(authenticating to mail server as %s)\n') % username)
152 152 try:
153 153 s.login(username, password)
154 154 except smtplib.SMTPException as inst:
155 155 raise error.Abort(inst)
156 156
157 157 def send(sender, recipients, msg):
158 158 try:
159 159 return s.sendmail(sender, recipients, msg)
160 160 except smtplib.SMTPRecipientsRefused as inst:
161 161 recipients = [r[1] for r in inst.recipients.values()]
162 162 raise error.Abort(b'\n' + b'\n'.join(recipients))
163 163 except smtplib.SMTPException as inst:
164 164 raise error.Abort(inst)
165 165
166 166 return send
167 167
168 168
169 169 def _sendmail(ui, sender, recipients, msg):
170 170 '''send mail using sendmail.'''
171 171 program = ui.config(b'email', b'method')
172 172
173 173 def stremail(x):
174 174 return procutil.shellquote(stringutil.email(encoding.strtolocal(x)))
175 175
176 176 cmdline = b'%s -f %s %s' % (
177 177 program,
178 178 stremail(sender),
179 179 b' '.join(map(stremail, recipients)),
180 180 )
181 181 ui.note(_(b'sending mail: %s\n') % cmdline)
182 182 fp = procutil.popen(cmdline, b'wb')
183 183 fp.write(util.tonativeeol(msg))
184 184 ret = fp.close()
185 185 if ret:
186 186 raise error.Abort(
187 187 b'%s %s'
188 188 % (
189 189 os.path.basename(program.split(None, 1)[0]),
190 190 procutil.explainexit(ret),
191 191 )
192 192 )
193 193
194 194
195 195 def _mbox(mbox, sender, recipients, msg):
196 196 '''write mails to mbox'''
197 197 fp = open(mbox, b'ab+')
198 198 # Should be time.asctime(), but Windows prints 2-characters day
199 199 # of month instead of one. Make them print the same thing.
200 200 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime())
201 201 fp.write(
202 202 b'From %s %s\n'
203 203 % (encoding.strtolocal(sender), encoding.strtolocal(date))
204 204 )
205 205 fp.write(msg)
206 206 fp.write(b'\n\n')
207 207 fp.close()
208 208
209 209
210 210 def connect(ui, mbox=None):
211 211 '''make a mail connection. return a function to send mail.
212 212 call as sendmail(sender, list-of-recipients, msg).'''
213 213 if mbox:
214 214 open(mbox, b'wb').close()
215 215 return lambda s, r, m: _mbox(mbox, s, r, m)
216 216 if ui.config(b'email', b'method') == b'smtp':
217 217 return _smtp(ui)
218 218 return lambda s, r, m: _sendmail(ui, s, r, m)
219 219
220 220
221 221 def sendmail(ui, sender, recipients, msg, mbox=None):
222 222 send = connect(ui, mbox=mbox)
223 223 return send(sender, recipients, msg)
224 224
225 225
226 226 def validateconfig(ui):
227 227 '''determine if we have enough config data to try sending email.'''
228 228 method = ui.config(b'email', b'method')
229 229 if method == b'smtp':
230 230 if not ui.config(b'smtp', b'host'):
231 231 raise error.Abort(
232 232 _(
233 233 b'smtp specified as email transport, '
234 234 b'but no smtp host configured'
235 235 )
236 236 )
237 237 else:
238 238 if not procutil.findexe(method):
239 239 raise error.Abort(
240 240 _(b'%r specified as email transport, but not in PATH') % method
241 241 )
242 242
243 243
244 244 def codec2iana(cs):
245 245 ''''''
246 246 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
247 247
248 248 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
249 249 if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
250 250 return b"iso-" + cs[3:]
251 251 return cs
252 252
253 253
254 254 def mimetextpatch(s, subtype=b'plain', display=False):
255 255 '''Return MIME message suitable for a patch.
256 256 Charset will be detected by first trying to decode as us-ascii, then utf-8,
257 257 and finally the global encodings. If all those fail, fall back to
258 258 ISO-8859-1, an encoding with that allows all byte sequences.
259 259 Transfer encodings will be used if necessary.'''
260 260
261 261 cs = [b'us-ascii', b'utf-8', encoding.encoding, encoding.fallbackencoding]
262 262 if display:
263 263 cs = [b'us-ascii']
264 264 for charset in cs:
265 265 try:
266 266 s.decode(pycompat.sysstr(charset))
267 267 return mimetextqp(s, subtype, codec2iana(charset))
268 268 except UnicodeDecodeError:
269 269 pass
270 270
271 271 return mimetextqp(s, subtype, b"iso-8859-1")
272 272
273 273
274 274 def mimetextqp(body, subtype, charset):
275 275 '''Return MIME message.
276 276 Quoted-printable transfer encoding will be used if necessary.
277 277 '''
278 278 cs = email.charset.Charset(charset)
279 279 msg = email.message.Message()
280 280 msg.set_type(pycompat.sysstr(b'text/' + subtype))
281 281
282 282 for line in body.splitlines():
283 283 if len(line) > 950:
284 284 cs.body_encoding = email.charset.QP
285 285 break
286 286
287 287 # On Python 2, this simply assigns a value. Python 3 inspects
288 288 # body and does different things depending on whether it has
289 289 # encode() or decode() attributes. We can get the old behavior
290 290 # if we pass a str and charset is None and we call set_charset().
291 291 # But we may get into trouble later due to Python attempting to
292 292 # encode/decode using the registered charset (or attempting to
293 293 # use ascii in the absence of a charset).
294 294 msg.set_payload(body, cs)
295 295
296 296 return msg
297 297
298 298
299 299 def _charsets(ui):
300 300 '''Obtains charsets to send mail parts not containing patches.'''
301 301 charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
302 302 fallbacks = [
303 303 encoding.fallbackencoding.lower(),
304 304 encoding.encoding.lower(),
305 305 b'utf-8',
306 306 ]
307 307 for cs in fallbacks: # find unique charsets while keeping order
308 308 if cs not in charsets:
309 309 charsets.append(cs)
310 310 return [cs for cs in charsets if not cs.endswith(b'ascii')]
311 311
312 312
313 313 def _encode(ui, s, charsets):
314 314 '''Returns (converted) string, charset tuple.
315 315 Finds out best charset by cycling through sendcharsets in descending
316 316 order. Tries both encoding and fallbackencoding for input. Only as
317 317 last resort send as is in fake ascii.
318 318 Caveat: Do not use for mail parts containing patches!'''
319 319 sendcharsets = charsets or _charsets(ui)
320 320 if not isinstance(s, bytes):
321 321 # We have unicode data, which we need to try and encode to
322 322 # some reasonable-ish encoding. Try the encodings the user
323 323 # wants, and fall back to garbage-in-ascii.
324 324 for ocs in sendcharsets:
325 325 try:
326 326 return s.encode(pycompat.sysstr(ocs)), ocs
327 327 except UnicodeEncodeError:
328 328 pass
329 329 except LookupError:
330 330 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
331 331 else:
332 332 # Everything failed, ascii-armor what we've got and send it.
333 333 return s.encode('ascii', 'backslashreplace')
334 334 # We have a bytes of unknown encoding. We'll try and guess a valid
335 335 # encoding, falling back to pretending we had ascii even though we
336 336 # know that's wrong.
337 337 try:
338 338 s.decode('ascii')
339 339 except UnicodeDecodeError:
340 340 for ics in (encoding.encoding, encoding.fallbackencoding):
341 341 try:
342 342 u = s.decode(ics)
343 343 except UnicodeDecodeError:
344 344 continue
345 345 for ocs in sendcharsets:
346 346 try:
347 347 return u.encode(pycompat.sysstr(ocs)), ocs
348 348 except UnicodeEncodeError:
349 349 pass
350 350 except LookupError:
351 351 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
352 352 # if ascii, or all conversion attempts fail, send (broken) ascii
353 353 return s, b'us-ascii'
354 354
355 355
356 356 def headencode(ui, s, charsets=None, display=False):
357 357 '''Returns RFC-2047 compliant header from given string.'''
358 358 if not display:
359 359 # split into words?
360 360 s, cs = _encode(ui, s, charsets)
361 361 return str(email.header.Header(s, cs))
362 362 return s
363 363
364 364
365 365 def _addressencode(ui, name, addr, charsets=None):
366 366 assert isinstance(addr, bytes)
367 367 name = headencode(ui, name, charsets)
368 368 try:
369 369 acc, dom = addr.split(b'@')
370 370 acc.decode('ascii')
371 371 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
372 372 addr = b'%s@%s' % (acc, dom)
373 373 except UnicodeDecodeError:
374 374 raise error.Abort(_(b'invalid email address: %s') % addr)
375 375 except ValueError:
376 376 try:
377 377 # too strict?
378 378 addr.decode('ascii')
379 379 except UnicodeDecodeError:
380 380 raise error.Abort(_(b'invalid local address: %s') % addr)
381 381 return pycompat.bytesurl(
382 382 email.utils.formataddr((name, encoding.strfromlocal(addr)))
383 383 )
384 384
385 385
386 386 def addressencode(ui, address, charsets=None, display=False):
387 387 '''Turns address into RFC-2047 compliant header.'''
388 388 if display or not address:
389 389 return address or b''
390 390 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
391 391 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
392 392
393 393
394 394 def addrlistencode(ui, addrs, charsets=None, display=False):
395 395 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
396 396 A single element of input list may contain multiple addresses, but output
397 397 always has one address per item'''
398 398 for a in addrs:
399 399 assert isinstance(a, bytes), r'%r unexpectedly not a bytestr' % a
400 400 if display:
401 401 return [a.strip() for a in addrs if a.strip()]
402 402
403 403 result = []
404 404 for name, addr in email.utils.getaddresses(
405 405 [encoding.strfromlocal(a) for a in addrs]
406 406 ):
407 407 if name or addr:
408 408 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
409 409 result.append(r)
410 410 return result
411 411
412 412
413 413 def mimeencode(ui, s, charsets=None, display=False):
414 414 '''creates mime text object, encodes it if needed, and sets
415 415 charset and transfer-encoding accordingly.'''
416 416 cs = b'us-ascii'
417 417 if not display:
418 418 s, cs = _encode(ui, s, charsets)
419 419 return mimetextqp(s, b'plain', cs)
420 420
421 421
422 422 if pycompat.ispy3:
423 423
424 424 Generator = email.generator.BytesGenerator
425 425
426 426 def parse(fp):
427 427 ep = email.parser.Parser()
428 428 # disable the "universal newlines" mode, which isn't binary safe.
429 429 # I have no idea if ascii/surrogateescape is correct, but that's
430 430 # what the standard Python email parser does.
431 431 fp = io.TextIOWrapper(
432 432 fp, encoding=r'ascii', errors=r'surrogateescape', newline=chr(10)
433 433 )
434 434 try:
435 435 return ep.parse(fp)
436 436 finally:
437 437 fp.detach()
438 438
439 439
440 440 else:
441 441
442 442 Generator = email.generator.Generator
443 443
444 444 def parse(fp):
445 445 ep = email.parser.Parser()
446 446 return ep.parse(fp)
447 447
448 448
449 449 def headdecode(s):
450 450 '''Decodes RFC-2047 header'''
451 451 uparts = []
452 452 for part, charset in email.header.decode_header(s):
453 453 if charset is not None:
454 454 try:
455 455 uparts.append(part.decode(charset))
456 456 continue
457 457 except UnicodeDecodeError:
458 458 pass
459 459 # On Python 3, decode_header() may return either bytes or unicode
460 460 # depending on whether the header has =?<charset>? or not
461 461 if isinstance(part, type(u'')):
462 462 uparts.append(part)
463 463 continue
464 464 try:
465 465 uparts.append(part.decode('UTF-8'))
466 466 continue
467 467 except UnicodeDecodeError:
468 468 pass
469 469 uparts.append(part.decode('ISO-8859-1'))
470 470 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now