##// END OF EJS Templates
mail: document behavior of Python 3...
Gregory Szorc -
r41450:9b3be572 default
parent child Browse files
Show More
@@ -1,399 +1,406 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 = new_socket.makefile(r'rb')
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 stremail = lambda x: stringutil.email(encoding.strtolocal(x))
155 stremail = lambda x: stringutil.email(encoding.strtolocal(x))
156 cmdline = '%s -f %s %s' % (program, stremail(sender),
156 cmdline = '%s -f %s %s' % (program, stremail(sender),
157 ' '.join(map(stremail, recipients)))
157 ' '.join(map(stremail, recipients)))
158 ui.note(_('sending mail: %s\n') % cmdline)
158 ui.note(_('sending mail: %s\n') % cmdline)
159 fp = procutil.popen(cmdline, 'wb')
159 fp = procutil.popen(cmdline, 'wb')
160 fp.write(util.tonativeeol(msg))
160 fp.write(util.tonativeeol(msg))
161 ret = fp.close()
161 ret = fp.close()
162 if ret:
162 if ret:
163 raise error.Abort('%s %s' % (
163 raise error.Abort('%s %s' % (
164 os.path.basename(program.split(None, 1)[0]),
164 os.path.basename(program.split(None, 1)[0]),
165 procutil.explainexit(ret)))
165 procutil.explainexit(ret)))
166
166
167 def _mbox(mbox, sender, recipients, msg):
167 def _mbox(mbox, sender, recipients, msg):
168 '''write mails to mbox'''
168 '''write mails to mbox'''
169 fp = open(mbox, 'ab+')
169 fp = open(mbox, 'ab+')
170 # Should be time.asctime(), but Windows prints 2-characters day
170 # Should be time.asctime(), but Windows prints 2-characters day
171 # of month instead of one. Make them print the same thing.
171 # of month instead of one. Make them print the same thing.
172 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime())
172 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime())
173 fp.write('From %s %s\n' % (encoding.strtolocal(sender),
173 fp.write('From %s %s\n' % (encoding.strtolocal(sender),
174 encoding.strtolocal(date)))
174 encoding.strtolocal(date)))
175 fp.write(msg)
175 fp.write(msg)
176 fp.write('\n\n')
176 fp.write('\n\n')
177 fp.close()
177 fp.close()
178
178
179 def connect(ui, mbox=None):
179 def connect(ui, mbox=None):
180 '''make a mail connection. return a function to send mail.
180 '''make a mail connection. return a function to send mail.
181 call as sendmail(sender, list-of-recipients, msg).'''
181 call as sendmail(sender, list-of-recipients, msg).'''
182 if mbox:
182 if mbox:
183 open(mbox, 'wb').close()
183 open(mbox, 'wb').close()
184 return lambda s, r, m: _mbox(mbox, s, r, m)
184 return lambda s, r, m: _mbox(mbox, s, r, m)
185 if ui.config('email', 'method') == 'smtp':
185 if ui.config('email', 'method') == 'smtp':
186 return _smtp(ui)
186 return _smtp(ui)
187 return lambda s, r, m: _sendmail(ui, s, r, m)
187 return lambda s, r, m: _sendmail(ui, s, r, m)
188
188
189 def sendmail(ui, sender, recipients, msg, mbox=None):
189 def sendmail(ui, sender, recipients, msg, mbox=None):
190 send = connect(ui, mbox=mbox)
190 send = connect(ui, mbox=mbox)
191 return send(sender, recipients, msg)
191 return send(sender, recipients, msg)
192
192
193 def validateconfig(ui):
193 def validateconfig(ui):
194 '''determine if we have enough config data to try sending email.'''
194 '''determine if we have enough config data to try sending email.'''
195 method = ui.config('email', 'method')
195 method = ui.config('email', 'method')
196 if method == 'smtp':
196 if method == 'smtp':
197 if not ui.config('smtp', 'host'):
197 if not ui.config('smtp', 'host'):
198 raise error.Abort(_('smtp specified as email transport, '
198 raise error.Abort(_('smtp specified as email transport, '
199 'but no smtp host configured'))
199 'but no smtp host configured'))
200 else:
200 else:
201 if not procutil.findexe(method):
201 if not procutil.findexe(method):
202 raise error.Abort(_('%r specified as email transport, '
202 raise error.Abort(_('%r specified as email transport, '
203 'but not in PATH') % method)
203 'but not in PATH') % method)
204
204
205 def codec2iana(cs):
205 def codec2iana(cs):
206 ''''''
206 ''''''
207 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
207 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
208
208
209 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
209 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
210 if cs.startswith("iso") and not cs.startswith("iso-"):
210 if cs.startswith("iso") and not cs.startswith("iso-"):
211 return "iso-" + cs[3:]
211 return "iso-" + cs[3:]
212 return cs
212 return cs
213
213
214 def mimetextpatch(s, subtype='plain', display=False):
214 def mimetextpatch(s, subtype='plain', display=False):
215 '''Return MIME message suitable for a patch.
215 '''Return MIME message suitable for a patch.
216 Charset will be detected by first trying to decode as us-ascii, then utf-8,
216 Charset will be detected by first trying to decode as us-ascii, then utf-8,
217 and finally the global encodings. If all those fail, fall back to
217 and finally the global encodings. If all those fail, fall back to
218 ISO-8859-1, an encoding with that allows all byte sequences.
218 ISO-8859-1, an encoding with that allows all byte sequences.
219 Transfer encodings will be used if necessary.'''
219 Transfer encodings will be used if necessary.'''
220
220
221 cs = ['us-ascii', 'utf-8', encoding.encoding, encoding.fallbackencoding]
221 cs = ['us-ascii', 'utf-8', encoding.encoding, encoding.fallbackencoding]
222 if display:
222 if display:
223 cs = ['us-ascii']
223 cs = ['us-ascii']
224 for charset in cs:
224 for charset in cs:
225 try:
225 try:
226 s.decode(pycompat.sysstr(charset))
226 s.decode(pycompat.sysstr(charset))
227 return mimetextqp(s, subtype, codec2iana(charset))
227 return mimetextqp(s, subtype, codec2iana(charset))
228 except UnicodeDecodeError:
228 except UnicodeDecodeError:
229 pass
229 pass
230
230
231 return mimetextqp(s, subtype, "iso-8859-1")
231 return mimetextqp(s, subtype, "iso-8859-1")
232
232
233 def mimetextqp(body, subtype, charset):
233 def mimetextqp(body, subtype, charset):
234 '''Return MIME message.
234 '''Return MIME message.
235 Quoted-printable transfer encoding will be used if necessary.
235 Quoted-printable transfer encoding will be used if necessary.
236 '''
236 '''
237 cs = email.charset.Charset(charset)
237 cs = email.charset.Charset(charset)
238 msg = email.message.Message()
238 msg = email.message.Message()
239 msg.set_type(pycompat.sysstr('text/' + subtype))
239 msg.set_type(pycompat.sysstr('text/' + subtype))
240
240
241 for line in body.splitlines():
241 for line in body.splitlines():
242 if len(line) > 950:
242 if len(line) > 950:
243 cs.body_encoding = email.charset.QP
243 cs.body_encoding = email.charset.QP
244 break
244 break
245
245
246 # On Python 2, this simply assigns a value. Python 3 inspects
247 # body and does different things depending on whether it has
248 # encode() or decode() attributes. We can get the old behavior
249 # if we pass a str and charset is None and we call set_charset().
250 # But we may get into trouble later due to Python attempting to
251 # encode/decode using the registered charset (or attempting to
252 # use ascii in the absence of a charset).
246 msg.set_payload(body, cs)
253 msg.set_payload(body, cs)
247
254
248 return msg
255 return msg
249
256
250 def _charsets(ui):
257 def _charsets(ui):
251 '''Obtains charsets to send mail parts not containing patches.'''
258 '''Obtains charsets to send mail parts not containing patches.'''
252 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
259 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
253 fallbacks = [encoding.fallbackencoding.lower(),
260 fallbacks = [encoding.fallbackencoding.lower(),
254 encoding.encoding.lower(), 'utf-8']
261 encoding.encoding.lower(), 'utf-8']
255 for cs in fallbacks: # find unique charsets while keeping order
262 for cs in fallbacks: # find unique charsets while keeping order
256 if cs not in charsets:
263 if cs not in charsets:
257 charsets.append(cs)
264 charsets.append(cs)
258 return [cs for cs in charsets if not cs.endswith('ascii')]
265 return [cs for cs in charsets if not cs.endswith('ascii')]
259
266
260 def _encode(ui, s, charsets):
267 def _encode(ui, s, charsets):
261 '''Returns (converted) string, charset tuple.
268 '''Returns (converted) string, charset tuple.
262 Finds out best charset by cycling through sendcharsets in descending
269 Finds out best charset by cycling through sendcharsets in descending
263 order. Tries both encoding and fallbackencoding for input. Only as
270 order. Tries both encoding and fallbackencoding for input. Only as
264 last resort send as is in fake ascii.
271 last resort send as is in fake ascii.
265 Caveat: Do not use for mail parts containing patches!'''
272 Caveat: Do not use for mail parts containing patches!'''
266 sendcharsets = charsets or _charsets(ui)
273 sendcharsets = charsets or _charsets(ui)
267 if not isinstance(s, bytes):
274 if not isinstance(s, bytes):
268 # We have unicode data, which we need to try and encode to
275 # We have unicode data, which we need to try and encode to
269 # some reasonable-ish encoding. Try the encodings the user
276 # some reasonable-ish encoding. Try the encodings the user
270 # wants, and fall back to garbage-in-ascii.
277 # wants, and fall back to garbage-in-ascii.
271 for ocs in sendcharsets:
278 for ocs in sendcharsets:
272 try:
279 try:
273 return s.encode(pycompat.sysstr(ocs)), ocs
280 return s.encode(pycompat.sysstr(ocs)), ocs
274 except UnicodeEncodeError:
281 except UnicodeEncodeError:
275 pass
282 pass
276 except LookupError:
283 except LookupError:
277 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
284 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
278 else:
285 else:
279 # Everything failed, ascii-armor what we've got and send it.
286 # Everything failed, ascii-armor what we've got and send it.
280 return s.encode('ascii', 'backslashreplace')
287 return s.encode('ascii', 'backslashreplace')
281 # We have a bytes of unknown encoding. We'll try and guess a valid
288 # We have a bytes of unknown encoding. We'll try and guess a valid
282 # encoding, falling back to pretending we had ascii even though we
289 # encoding, falling back to pretending we had ascii even though we
283 # know that's wrong.
290 # know that's wrong.
284 try:
291 try:
285 s.decode('ascii')
292 s.decode('ascii')
286 except UnicodeDecodeError:
293 except UnicodeDecodeError:
287 for ics in (encoding.encoding, encoding.fallbackencoding):
294 for ics in (encoding.encoding, encoding.fallbackencoding):
288 try:
295 try:
289 u = s.decode(ics)
296 u = s.decode(ics)
290 except UnicodeDecodeError:
297 except UnicodeDecodeError:
291 continue
298 continue
292 for ocs in sendcharsets:
299 for ocs in sendcharsets:
293 try:
300 try:
294 return u.encode(pycompat.sysstr(ocs)), ocs
301 return u.encode(pycompat.sysstr(ocs)), ocs
295 except UnicodeEncodeError:
302 except UnicodeEncodeError:
296 pass
303 pass
297 except LookupError:
304 except LookupError:
298 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
305 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
299 # if ascii, or all conversion attempts fail, send (broken) ascii
306 # if ascii, or all conversion attempts fail, send (broken) ascii
300 return s, 'us-ascii'
307 return s, 'us-ascii'
301
308
302 def headencode(ui, s, charsets=None, display=False):
309 def headencode(ui, s, charsets=None, display=False):
303 '''Returns RFC-2047 compliant header from given string.'''
310 '''Returns RFC-2047 compliant header from given string.'''
304 if not display:
311 if not display:
305 # split into words?
312 # split into words?
306 s, cs = _encode(ui, s, charsets)
313 s, cs = _encode(ui, s, charsets)
307 return str(email.header.Header(s, cs))
314 return str(email.header.Header(s, cs))
308 return s
315 return s
309
316
310 def _addressencode(ui, name, addr, charsets=None):
317 def _addressencode(ui, name, addr, charsets=None):
311 assert isinstance(addr, bytes)
318 assert isinstance(addr, bytes)
312 name = headencode(ui, name, charsets)
319 name = headencode(ui, name, charsets)
313 try:
320 try:
314 acc, dom = addr.split('@')
321 acc, dom = addr.split('@')
315 acc.decode('ascii')
322 acc.decode('ascii')
316 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
323 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
317 addr = '%s@%s' % (acc, dom)
324 addr = '%s@%s' % (acc, dom)
318 except UnicodeDecodeError:
325 except UnicodeDecodeError:
319 raise error.Abort(_('invalid email address: %s') % addr)
326 raise error.Abort(_('invalid email address: %s') % addr)
320 except ValueError:
327 except ValueError:
321 try:
328 try:
322 # too strict?
329 # too strict?
323 addr.decode('ascii')
330 addr.decode('ascii')
324 except UnicodeDecodeError:
331 except UnicodeDecodeError:
325 raise error.Abort(_('invalid local address: %s') % addr)
332 raise error.Abort(_('invalid local address: %s') % addr)
326 return pycompat.bytesurl(
333 return pycompat.bytesurl(
327 email.utils.formataddr((name, encoding.strfromlocal(addr))))
334 email.utils.formataddr((name, encoding.strfromlocal(addr))))
328
335
329 def addressencode(ui, address, charsets=None, display=False):
336 def addressencode(ui, address, charsets=None, display=False):
330 '''Turns address into RFC-2047 compliant header.'''
337 '''Turns address into RFC-2047 compliant header.'''
331 if display or not address:
338 if display or not address:
332 return address or ''
339 return address or ''
333 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
340 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
334 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
341 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
335
342
336 def addrlistencode(ui, addrs, charsets=None, display=False):
343 def addrlistencode(ui, addrs, charsets=None, display=False):
337 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
344 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
338 A single element of input list may contain multiple addresses, but output
345 A single element of input list may contain multiple addresses, but output
339 always has one address per item'''
346 always has one address per item'''
340 for a in addrs:
347 for a in addrs:
341 assert isinstance(a, bytes), (r'%r unexpectedly not a bytestr' % a)
348 assert isinstance(a, bytes), (r'%r unexpectedly not a bytestr' % a)
342 if display:
349 if display:
343 return [a.strip() for a in addrs if a.strip()]
350 return [a.strip() for a in addrs if a.strip()]
344
351
345 result = []
352 result = []
346 for name, addr in email.utils.getaddresses(
353 for name, addr in email.utils.getaddresses(
347 [encoding.strfromlocal(a) for a in addrs]):
354 [encoding.strfromlocal(a) for a in addrs]):
348 if name or addr:
355 if name or addr:
349 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
356 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
350 result.append(r)
357 result.append(r)
351 return result
358 return result
352
359
353 def mimeencode(ui, s, charsets=None, display=False):
360 def mimeencode(ui, s, charsets=None, display=False):
354 '''creates mime text object, encodes it if needed, and sets
361 '''creates mime text object, encodes it if needed, and sets
355 charset and transfer-encoding accordingly.'''
362 charset and transfer-encoding accordingly.'''
356 cs = 'us-ascii'
363 cs = 'us-ascii'
357 if not display:
364 if not display:
358 s, cs = _encode(ui, s, charsets)
365 s, cs = _encode(ui, s, charsets)
359 return mimetextqp(s, 'plain', cs)
366 return mimetextqp(s, 'plain', cs)
360
367
361 if pycompat.ispy3:
368 if pycompat.ispy3:
362 def parse(fp):
369 def parse(fp):
363 ep = email.parser.Parser()
370 ep = email.parser.Parser()
364 # disable the "universal newlines" mode, which isn't binary safe.
371 # disable the "universal newlines" mode, which isn't binary safe.
365 # I have no idea if ascii/surrogateescape is correct, but that's
372 # I have no idea if ascii/surrogateescape is correct, but that's
366 # what the standard Python email parser does.
373 # what the standard Python email parser does.
367 fp = io.TextIOWrapper(fp, encoding=r'ascii',
374 fp = io.TextIOWrapper(fp, encoding=r'ascii',
368 errors=r'surrogateescape', newline=chr(10))
375 errors=r'surrogateescape', newline=chr(10))
369 try:
376 try:
370 return ep.parse(fp)
377 return ep.parse(fp)
371 finally:
378 finally:
372 fp.detach()
379 fp.detach()
373 else:
380 else:
374 def parse(fp):
381 def parse(fp):
375 ep = email.parser.Parser()
382 ep = email.parser.Parser()
376 return ep.parse(fp)
383 return ep.parse(fp)
377
384
378 def headdecode(s):
385 def headdecode(s):
379 '''Decodes RFC-2047 header'''
386 '''Decodes RFC-2047 header'''
380 uparts = []
387 uparts = []
381 for part, charset in email.header.decode_header(s):
388 for part, charset in email.header.decode_header(s):
382 if charset is not None:
389 if charset is not None:
383 try:
390 try:
384 uparts.append(part.decode(charset))
391 uparts.append(part.decode(charset))
385 continue
392 continue
386 except UnicodeDecodeError:
393 except UnicodeDecodeError:
387 pass
394 pass
388 # On Python 3, decode_header() may return either bytes or unicode
395 # On Python 3, decode_header() may return either bytes or unicode
389 # depending on whether the header has =?<charset>? or not
396 # depending on whether the header has =?<charset>? or not
390 if isinstance(part, type(u'')):
397 if isinstance(part, type(u'')):
391 uparts.append(part)
398 uparts.append(part)
392 continue
399 continue
393 try:
400 try:
394 uparts.append(part.decode('UTF-8'))
401 uparts.append(part.decode('UTF-8'))
395 continue
402 continue
396 except UnicodeDecodeError:
403 except UnicodeDecodeError:
397 pass
404 pass
398 uparts.append(part.decode('ISO-8859-1'))
405 uparts.append(part.decode('ISO-8859-1'))
399 return encoding.unitolocal(u' '.join(uparts))
406 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now