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