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