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