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