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