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