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