##// END OF EJS Templates
py3: call SMTP.has_extn() with an str...
Denis Laxalde -
r43439:3941e706 default
parent child Browse files
Show More
@@ -1,470 +1,470
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(b"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(b"STARTTLS")
55 (resp, reply) = self.docmd(b"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 = smtplib.SSLFakeFile(self.sock)
64 self.file = smtplib.SSLFakeFile(self.sock)
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 and not password:
149 password = ui.getpass()
149 password = ui.getpass()
150 if username and password:
150 if username and password:
151 ui.note(_(b'(authenticating to mail server as %s)\n') % username)
151 ui.note(_(b'(authenticating to mail server as %s)\n') % username)
152 try:
152 try:
153 s.login(username, password)
153 s.login(username, password)
154 except smtplib.SMTPException as inst:
154 except smtplib.SMTPException as inst:
155 raise error.Abort(inst)
155 raise error.Abort(inst)
156
156
157 def send(sender, recipients, msg):
157 def send(sender, recipients, msg):
158 try:
158 try:
159 return s.sendmail(sender, recipients, msg)
159 return s.sendmail(sender, recipients, msg)
160 except smtplib.SMTPRecipientsRefused as inst:
160 except smtplib.SMTPRecipientsRefused as inst:
161 recipients = [r[1] for r in inst.recipients.values()]
161 recipients = [r[1] for r in inst.recipients.values()]
162 raise error.Abort(b'\n' + b'\n'.join(recipients))
162 raise error.Abort(b'\n' + b'\n'.join(recipients))
163 except smtplib.SMTPException as inst:
163 except smtplib.SMTPException as inst:
164 raise error.Abort(inst)
164 raise error.Abort(inst)
165
165
166 return send
166 return send
167
167
168
168
169 def _sendmail(ui, sender, recipients, msg):
169 def _sendmail(ui, sender, recipients, msg):
170 '''send mail using sendmail.'''
170 '''send mail using sendmail.'''
171 program = ui.config(b'email', b'method')
171 program = ui.config(b'email', b'method')
172
172
173 def stremail(x):
173 def stremail(x):
174 return procutil.shellquote(stringutil.email(encoding.strtolocal(x)))
174 return procutil.shellquote(stringutil.email(encoding.strtolocal(x)))
175
175
176 cmdline = b'%s -f %s %s' % (
176 cmdline = b'%s -f %s %s' % (
177 program,
177 program,
178 stremail(sender),
178 stremail(sender),
179 b' '.join(map(stremail, recipients)),
179 b' '.join(map(stremail, recipients)),
180 )
180 )
181 ui.note(_(b'sending mail: %s\n') % cmdline)
181 ui.note(_(b'sending mail: %s\n') % cmdline)
182 fp = procutil.popen(cmdline, b'wb')
182 fp = procutil.popen(cmdline, b'wb')
183 fp.write(util.tonativeeol(msg))
183 fp.write(util.tonativeeol(msg))
184 ret = fp.close()
184 ret = fp.close()
185 if ret:
185 if ret:
186 raise error.Abort(
186 raise error.Abort(
187 b'%s %s'
187 b'%s %s'
188 % (
188 % (
189 os.path.basename(program.split(None, 1)[0]),
189 os.path.basename(program.split(None, 1)[0]),
190 procutil.explainexit(ret),
190 procutil.explainexit(ret),
191 )
191 )
192 )
192 )
193
193
194
194
195 def _mbox(mbox, sender, recipients, msg):
195 def _mbox(mbox, sender, recipients, msg):
196 '''write mails to mbox'''
196 '''write mails to mbox'''
197 fp = open(mbox, b'ab+')
197 fp = open(mbox, b'ab+')
198 # Should be time.asctime(), but Windows prints 2-characters day
198 # Should be time.asctime(), but Windows prints 2-characters day
199 # of month instead of one. Make them print the same thing.
199 # 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())
200 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime())
201 fp.write(
201 fp.write(
202 b'From %s %s\n'
202 b'From %s %s\n'
203 % (encoding.strtolocal(sender), encoding.strtolocal(date))
203 % (encoding.strtolocal(sender), encoding.strtolocal(date))
204 )
204 )
205 fp.write(msg)
205 fp.write(msg)
206 fp.write(b'\n\n')
206 fp.write(b'\n\n')
207 fp.close()
207 fp.close()
208
208
209
209
210 def connect(ui, mbox=None):
210 def connect(ui, mbox=None):
211 '''make a mail connection. return a function to send mail.
211 '''make a mail connection. return a function to send mail.
212 call as sendmail(sender, list-of-recipients, msg).'''
212 call as sendmail(sender, list-of-recipients, msg).'''
213 if mbox:
213 if mbox:
214 open(mbox, b'wb').close()
214 open(mbox, b'wb').close()
215 return lambda s, r, m: _mbox(mbox, s, r, m)
215 return lambda s, r, m: _mbox(mbox, s, r, m)
216 if ui.config(b'email', b'method') == b'smtp':
216 if ui.config(b'email', b'method') == b'smtp':
217 return _smtp(ui)
217 return _smtp(ui)
218 return lambda s, r, m: _sendmail(ui, s, r, m)
218 return lambda s, r, m: _sendmail(ui, s, r, m)
219
219
220
220
221 def sendmail(ui, sender, recipients, msg, mbox=None):
221 def sendmail(ui, sender, recipients, msg, mbox=None):
222 send = connect(ui, mbox=mbox)
222 send = connect(ui, mbox=mbox)
223 return send(sender, recipients, msg)
223 return send(sender, recipients, msg)
224
224
225
225
226 def validateconfig(ui):
226 def validateconfig(ui):
227 '''determine if we have enough config data to try sending email.'''
227 '''determine if we have enough config data to try sending email.'''
228 method = ui.config(b'email', b'method')
228 method = ui.config(b'email', b'method')
229 if method == b'smtp':
229 if method == b'smtp':
230 if not ui.config(b'smtp', b'host'):
230 if not ui.config(b'smtp', b'host'):
231 raise error.Abort(
231 raise error.Abort(
232 _(
232 _(
233 b'smtp specified as email transport, '
233 b'smtp specified as email transport, '
234 b'but no smtp host configured'
234 b'but no smtp host configured'
235 )
235 )
236 )
236 )
237 else:
237 else:
238 if not procutil.findexe(method):
238 if not procutil.findexe(method):
239 raise error.Abort(
239 raise error.Abort(
240 _(b'%r specified as email transport, but not in PATH') % method
240 _(b'%r specified as email transport, but not in PATH') % method
241 )
241 )
242
242
243
243
244 def codec2iana(cs):
244 def codec2iana(cs):
245 ''''''
245 ''''''
246 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
246 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
247
247
248 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
248 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
249 if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
249 if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
250 return b"iso-" + cs[3:]
250 return b"iso-" + cs[3:]
251 return cs
251 return cs
252
252
253
253
254 def mimetextpatch(s, subtype=b'plain', display=False):
254 def mimetextpatch(s, subtype=b'plain', display=False):
255 '''Return MIME message suitable for a patch.
255 '''Return MIME message suitable for a patch.
256 Charset will be detected by first trying to decode as us-ascii, then utf-8,
256 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
257 and finally the global encodings. If all those fail, fall back to
258 ISO-8859-1, an encoding with that allows all byte sequences.
258 ISO-8859-1, an encoding with that allows all byte sequences.
259 Transfer encodings will be used if necessary.'''
259 Transfer encodings will be used if necessary.'''
260
260
261 cs = [b'us-ascii', b'utf-8', encoding.encoding, encoding.fallbackencoding]
261 cs = [b'us-ascii', b'utf-8', encoding.encoding, encoding.fallbackencoding]
262 if display:
262 if display:
263 cs = [b'us-ascii']
263 cs = [b'us-ascii']
264 for charset in cs:
264 for charset in cs:
265 try:
265 try:
266 s.decode(pycompat.sysstr(charset))
266 s.decode(pycompat.sysstr(charset))
267 return mimetextqp(s, subtype, codec2iana(charset))
267 return mimetextqp(s, subtype, codec2iana(charset))
268 except UnicodeDecodeError:
268 except UnicodeDecodeError:
269 pass
269 pass
270
270
271 return mimetextqp(s, subtype, b"iso-8859-1")
271 return mimetextqp(s, subtype, b"iso-8859-1")
272
272
273
273
274 def mimetextqp(body, subtype, charset):
274 def mimetextqp(body, subtype, charset):
275 '''Return MIME message.
275 '''Return MIME message.
276 Quoted-printable transfer encoding will be used if necessary.
276 Quoted-printable transfer encoding will be used if necessary.
277 '''
277 '''
278 cs = email.charset.Charset(charset)
278 cs = email.charset.Charset(charset)
279 msg = email.message.Message()
279 msg = email.message.Message()
280 msg.set_type(pycompat.sysstr(b'text/' + subtype))
280 msg.set_type(pycompat.sysstr(b'text/' + subtype))
281
281
282 for line in body.splitlines():
282 for line in body.splitlines():
283 if len(line) > 950:
283 if len(line) > 950:
284 cs.body_encoding = email.charset.QP
284 cs.body_encoding = email.charset.QP
285 break
285 break
286
286
287 # On Python 2, this simply assigns a value. Python 3 inspects
287 # On Python 2, this simply assigns a value. Python 3 inspects
288 # body and does different things depending on whether it has
288 # body and does different things depending on whether it has
289 # encode() or decode() attributes. We can get the old behavior
289 # 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().
290 # 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
291 # But we may get into trouble later due to Python attempting to
292 # encode/decode using the registered charset (or attempting to
292 # encode/decode using the registered charset (or attempting to
293 # use ascii in the absence of a charset).
293 # use ascii in the absence of a charset).
294 msg.set_payload(body, cs)
294 msg.set_payload(body, cs)
295
295
296 return msg
296 return msg
297
297
298
298
299 def _charsets(ui):
299 def _charsets(ui):
300 '''Obtains charsets to send mail parts not containing patches.'''
300 '''Obtains charsets to send mail parts not containing patches.'''
301 charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
301 charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
302 fallbacks = [
302 fallbacks = [
303 encoding.fallbackencoding.lower(),
303 encoding.fallbackencoding.lower(),
304 encoding.encoding.lower(),
304 encoding.encoding.lower(),
305 b'utf-8',
305 b'utf-8',
306 ]
306 ]
307 for cs in fallbacks: # find unique charsets while keeping order
307 for cs in fallbacks: # find unique charsets while keeping order
308 if cs not in charsets:
308 if cs not in charsets:
309 charsets.append(cs)
309 charsets.append(cs)
310 return [cs for cs in charsets if not cs.endswith(b'ascii')]
310 return [cs for cs in charsets if not cs.endswith(b'ascii')]
311
311
312
312
313 def _encode(ui, s, charsets):
313 def _encode(ui, s, charsets):
314 '''Returns (converted) string, charset tuple.
314 '''Returns (converted) string, charset tuple.
315 Finds out best charset by cycling through sendcharsets in descending
315 Finds out best charset by cycling through sendcharsets in descending
316 order. Tries both encoding and fallbackencoding for input. Only as
316 order. Tries both encoding and fallbackencoding for input. Only as
317 last resort send as is in fake ascii.
317 last resort send as is in fake ascii.
318 Caveat: Do not use for mail parts containing patches!'''
318 Caveat: Do not use for mail parts containing patches!'''
319 sendcharsets = charsets or _charsets(ui)
319 sendcharsets = charsets or _charsets(ui)
320 if not isinstance(s, bytes):
320 if not isinstance(s, bytes):
321 # We have unicode data, which we need to try and encode to
321 # We have unicode data, which we need to try and encode to
322 # some reasonable-ish encoding. Try the encodings the user
322 # some reasonable-ish encoding. Try the encodings the user
323 # wants, and fall back to garbage-in-ascii.
323 # wants, and fall back to garbage-in-ascii.
324 for ocs in sendcharsets:
324 for ocs in sendcharsets:
325 try:
325 try:
326 return s.encode(pycompat.sysstr(ocs)), ocs
326 return s.encode(pycompat.sysstr(ocs)), ocs
327 except UnicodeEncodeError:
327 except UnicodeEncodeError:
328 pass
328 pass
329 except LookupError:
329 except LookupError:
330 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
330 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
331 else:
331 else:
332 # Everything failed, ascii-armor what we've got and send it.
332 # Everything failed, ascii-armor what we've got and send it.
333 return s.encode('ascii', 'backslashreplace')
333 return s.encode('ascii', 'backslashreplace')
334 # We have a bytes of unknown encoding. We'll try and guess a valid
334 # 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
335 # encoding, falling back to pretending we had ascii even though we
336 # know that's wrong.
336 # know that's wrong.
337 try:
337 try:
338 s.decode('ascii')
338 s.decode('ascii')
339 except UnicodeDecodeError:
339 except UnicodeDecodeError:
340 for ics in (encoding.encoding, encoding.fallbackencoding):
340 for ics in (encoding.encoding, encoding.fallbackencoding):
341 try:
341 try:
342 u = s.decode(ics)
342 u = s.decode(ics)
343 except UnicodeDecodeError:
343 except UnicodeDecodeError:
344 continue
344 continue
345 for ocs in sendcharsets:
345 for ocs in sendcharsets:
346 try:
346 try:
347 return u.encode(pycompat.sysstr(ocs)), ocs
347 return u.encode(pycompat.sysstr(ocs)), ocs
348 except UnicodeEncodeError:
348 except UnicodeEncodeError:
349 pass
349 pass
350 except LookupError:
350 except LookupError:
351 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
351 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
352 # if ascii, or all conversion attempts fail, send (broken) ascii
352 # if ascii, or all conversion attempts fail, send (broken) ascii
353 return s, b'us-ascii'
353 return s, b'us-ascii'
354
354
355
355
356 def headencode(ui, s, charsets=None, display=False):
356 def headencode(ui, s, charsets=None, display=False):
357 '''Returns RFC-2047 compliant header from given string.'''
357 '''Returns RFC-2047 compliant header from given string.'''
358 if not display:
358 if not display:
359 # split into words?
359 # split into words?
360 s, cs = _encode(ui, s, charsets)
360 s, cs = _encode(ui, s, charsets)
361 return str(email.header.Header(s, cs))
361 return str(email.header.Header(s, cs))
362 return s
362 return s
363
363
364
364
365 def _addressencode(ui, name, addr, charsets=None):
365 def _addressencode(ui, name, addr, charsets=None):
366 assert isinstance(addr, bytes)
366 assert isinstance(addr, bytes)
367 name = headencode(ui, name, charsets)
367 name = headencode(ui, name, charsets)
368 try:
368 try:
369 acc, dom = addr.split(b'@')
369 acc, dom = addr.split(b'@')
370 acc.decode('ascii')
370 acc.decode('ascii')
371 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
371 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
372 addr = b'%s@%s' % (acc, dom)
372 addr = b'%s@%s' % (acc, dom)
373 except UnicodeDecodeError:
373 except UnicodeDecodeError:
374 raise error.Abort(_(b'invalid email address: %s') % addr)
374 raise error.Abort(_(b'invalid email address: %s') % addr)
375 except ValueError:
375 except ValueError:
376 try:
376 try:
377 # too strict?
377 # too strict?
378 addr.decode('ascii')
378 addr.decode('ascii')
379 except UnicodeDecodeError:
379 except UnicodeDecodeError:
380 raise error.Abort(_(b'invalid local address: %s') % addr)
380 raise error.Abort(_(b'invalid local address: %s') % addr)
381 return pycompat.bytesurl(
381 return pycompat.bytesurl(
382 email.utils.formataddr((name, encoding.strfromlocal(addr)))
382 email.utils.formataddr((name, encoding.strfromlocal(addr)))
383 )
383 )
384
384
385
385
386 def addressencode(ui, address, charsets=None, display=False):
386 def addressencode(ui, address, charsets=None, display=False):
387 '''Turns address into RFC-2047 compliant header.'''
387 '''Turns address into RFC-2047 compliant header.'''
388 if display or not address:
388 if display or not address:
389 return address or b''
389 return address or b''
390 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
390 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
391 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
391 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
392
392
393
393
394 def addrlistencode(ui, addrs, charsets=None, display=False):
394 def addrlistencode(ui, addrs, charsets=None, display=False):
395 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
395 '''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
396 A single element of input list may contain multiple addresses, but output
397 always has one address per item'''
397 always has one address per item'''
398 for a in addrs:
398 for a in addrs:
399 assert isinstance(a, bytes), r'%r unexpectedly not a bytestr' % a
399 assert isinstance(a, bytes), r'%r unexpectedly not a bytestr' % a
400 if display:
400 if display:
401 return [a.strip() for a in addrs if a.strip()]
401 return [a.strip() for a in addrs if a.strip()]
402
402
403 result = []
403 result = []
404 for name, addr in email.utils.getaddresses(
404 for name, addr in email.utils.getaddresses(
405 [encoding.strfromlocal(a) for a in addrs]
405 [encoding.strfromlocal(a) for a in addrs]
406 ):
406 ):
407 if name or addr:
407 if name or addr:
408 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
408 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
409 result.append(r)
409 result.append(r)
410 return result
410 return result
411
411
412
412
413 def mimeencode(ui, s, charsets=None, display=False):
413 def mimeencode(ui, s, charsets=None, display=False):
414 '''creates mime text object, encodes it if needed, and sets
414 '''creates mime text object, encodes it if needed, and sets
415 charset and transfer-encoding accordingly.'''
415 charset and transfer-encoding accordingly.'''
416 cs = b'us-ascii'
416 cs = b'us-ascii'
417 if not display:
417 if not display:
418 s, cs = _encode(ui, s, charsets)
418 s, cs = _encode(ui, s, charsets)
419 return mimetextqp(s, b'plain', cs)
419 return mimetextqp(s, b'plain', cs)
420
420
421
421
422 if pycompat.ispy3:
422 if pycompat.ispy3:
423
423
424 Generator = email.generator.BytesGenerator
424 Generator = email.generator.BytesGenerator
425
425
426 def parse(fp):
426 def parse(fp):
427 ep = email.parser.Parser()
427 ep = email.parser.Parser()
428 # disable the "universal newlines" mode, which isn't binary safe.
428 # disable the "universal newlines" mode, which isn't binary safe.
429 # I have no idea if ascii/surrogateescape is correct, but that's
429 # I have no idea if ascii/surrogateescape is correct, but that's
430 # what the standard Python email parser does.
430 # what the standard Python email parser does.
431 fp = io.TextIOWrapper(
431 fp = io.TextIOWrapper(
432 fp, encoding=r'ascii', errors=r'surrogateescape', newline=chr(10)
432 fp, encoding=r'ascii', errors=r'surrogateescape', newline=chr(10)
433 )
433 )
434 try:
434 try:
435 return ep.parse(fp)
435 return ep.parse(fp)
436 finally:
436 finally:
437 fp.detach()
437 fp.detach()
438
438
439
439
440 else:
440 else:
441
441
442 Generator = email.generator.Generator
442 Generator = email.generator.Generator
443
443
444 def parse(fp):
444 def parse(fp):
445 ep = email.parser.Parser()
445 ep = email.parser.Parser()
446 return ep.parse(fp)
446 return ep.parse(fp)
447
447
448
448
449 def headdecode(s):
449 def headdecode(s):
450 '''Decodes RFC-2047 header'''
450 '''Decodes RFC-2047 header'''
451 uparts = []
451 uparts = []
452 for part, charset in email.header.decode_header(s):
452 for part, charset in email.header.decode_header(s):
453 if charset is not None:
453 if charset is not None:
454 try:
454 try:
455 uparts.append(part.decode(charset))
455 uparts.append(part.decode(charset))
456 continue
456 continue
457 except UnicodeDecodeError:
457 except UnicodeDecodeError:
458 pass
458 pass
459 # On Python 3, decode_header() may return either bytes or unicode
459 # On Python 3, decode_header() may return either bytes or unicode
460 # depending on whether the header has =?<charset>? or not
460 # depending on whether the header has =?<charset>? or not
461 if isinstance(part, type(u'')):
461 if isinstance(part, type(u'')):
462 uparts.append(part)
462 uparts.append(part)
463 continue
463 continue
464 try:
464 try:
465 uparts.append(part.decode('UTF-8'))
465 uparts.append(part.decode('UTF-8'))
466 continue
466 continue
467 except UnicodeDecodeError:
467 except UnicodeDecodeError:
468 pass
468 pass
469 uparts.append(part.decode('ISO-8859-1'))
469 uparts.append(part.decode('ISO-8859-1'))
470 return encoding.unitolocal(u' '.join(uparts))
470 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now