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