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