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