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