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