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