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