##// END OF EJS Templates
mail: suppress a pytype error that's just experimentally wrong...
Augie Fackler -
r43804:3b31ee53 default
parent child Browse files
Show More
@@ -1,483 +1,485 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.generator
12 import email.generator
13 import email.header
13 import email.header
14 import email.message
14 import email.message
15 import email.parser
15 import email.parser
16 import io
16 import io
17 import os
17 import os
18 import smtplib
18 import smtplib
19 import socket
19 import socket
20 import time
20 import time
21
21
22 from .i18n import _
22 from .i18n import _
23 from .pycompat import (
23 from .pycompat import (
24 getattr,
24 getattr,
25 open,
25 open,
26 )
26 )
27 from . import (
27 from . import (
28 encoding,
28 encoding,
29 error,
29 error,
30 pycompat,
30 pycompat,
31 sslutil,
31 sslutil,
32 util,
32 util,
33 )
33 )
34 from .utils import (
34 from .utils import (
35 procutil,
35 procutil,
36 stringutil,
36 stringutil,
37 )
37 )
38
38
39
39
40 class STARTTLS(smtplib.SMTP):
40 class STARTTLS(smtplib.SMTP):
41 '''Derived class to verify the peer certificate for STARTTLS.
41 '''Derived class to verify the peer certificate for STARTTLS.
42
42
43 This class allows to pass any keyword arguments to SSL socket creation.
43 This class allows to pass any keyword arguments to SSL socket creation.
44 '''
44 '''
45
45
46 def __init__(self, ui, host=None, **kwargs):
46 def __init__(self, ui, host=None, **kwargs):
47 smtplib.SMTP.__init__(self, **kwargs)
47 smtplib.SMTP.__init__(self, **kwargs)
48 self._ui = ui
48 self._ui = ui
49 self._host = host
49 self._host = host
50
50
51 def starttls(self, keyfile=None, certfile=None):
51 def starttls(self, keyfile=None, certfile=None):
52 if not self.has_extn("starttls"):
52 if not self.has_extn("starttls"):
53 msg = b"STARTTLS extension not supported by server"
53 msg = b"STARTTLS extension not supported by server"
54 raise smtplib.SMTPException(msg)
54 raise smtplib.SMTPException(msg)
55 (resp, reply) = self.docmd("STARTTLS")
55 (resp, reply) = self.docmd("STARTTLS")
56 if resp == 220:
56 if resp == 220:
57 self.sock = sslutil.wrapsocket(
57 self.sock = sslutil.wrapsocket(
58 self.sock,
58 self.sock,
59 keyfile,
59 keyfile,
60 certfile,
60 certfile,
61 ui=self._ui,
61 ui=self._ui,
62 serverhostname=self._host,
62 serverhostname=self._host,
63 )
63 )
64 self.file = self.sock.makefile("rb")
64 self.file = self.sock.makefile("rb")
65 self.helo_resp = None
65 self.helo_resp = None
66 self.ehlo_resp = None
66 self.ehlo_resp = None
67 self.esmtp_features = {}
67 self.esmtp_features = {}
68 self.does_esmtp = 0
68 self.does_esmtp = 0
69 return (resp, reply)
69 return (resp, reply)
70
70
71
71
72 class SMTPS(smtplib.SMTP):
72 class SMTPS(smtplib.SMTP):
73 '''Derived class to verify the peer certificate for SMTPS.
73 '''Derived class to verify the peer certificate for SMTPS.
74
74
75 This class allows to pass any keyword arguments to SSL socket creation.
75 This class allows to pass any keyword arguments to SSL socket creation.
76 '''
76 '''
77
77
78 def __init__(self, ui, keyfile=None, certfile=None, host=None, **kwargs):
78 def __init__(self, ui, keyfile=None, certfile=None, host=None, **kwargs):
79 self.keyfile = keyfile
79 self.keyfile = keyfile
80 self.certfile = certfile
80 self.certfile = certfile
81 smtplib.SMTP.__init__(self, **kwargs)
81 smtplib.SMTP.__init__(self, **kwargs)
82 self._host = host
82 self._host = host
83 self.default_port = smtplib.SMTP_SSL_PORT
83 self.default_port = smtplib.SMTP_SSL_PORT
84 self._ui = ui
84 self._ui = ui
85
85
86 def _get_socket(self, host, port, timeout):
86 def _get_socket(self, host, port, timeout):
87 if self.debuglevel > 0:
87 if self.debuglevel > 0:
88 self._ui.debug(b'connect: %r\n' % ((host, port),))
88 self._ui.debug(b'connect: %r\n' % ((host, port),))
89 new_socket = socket.create_connection((host, port), timeout)
89 new_socket = socket.create_connection((host, port), timeout)
90 new_socket = sslutil.wrapsocket(
90 new_socket = sslutil.wrapsocket(
91 new_socket,
91 new_socket,
92 self.keyfile,
92 self.keyfile,
93 self.certfile,
93 self.certfile,
94 ui=self._ui,
94 ui=self._ui,
95 serverhostname=self._host,
95 serverhostname=self._host,
96 )
96 )
97 self.file = new_socket.makefile(r'rb')
97 self.file = new_socket.makefile(r'rb')
98 return new_socket
98 return new_socket
99
99
100
100
101 def _pyhastls():
101 def _pyhastls():
102 """Returns true iff Python has TLS support, false otherwise."""
102 """Returns true iff Python has TLS support, false otherwise."""
103 try:
103 try:
104 import ssl
104 import ssl
105
105
106 getattr(ssl, 'HAS_TLS', False)
106 getattr(ssl, 'HAS_TLS', False)
107 return True
107 return True
108 except ImportError:
108 except ImportError:
109 return False
109 return False
110
110
111
111
112 def _smtp(ui):
112 def _smtp(ui):
113 '''build an smtp connection and return a function to send mail'''
113 '''build an smtp connection and return a function to send mail'''
114 local_hostname = ui.config(b'smtp', b'local_hostname')
114 local_hostname = ui.config(b'smtp', b'local_hostname')
115 tls = ui.config(b'smtp', b'tls')
115 tls = ui.config(b'smtp', b'tls')
116 # backward compatible: when tls = true, we use starttls.
116 # backward compatible: when tls = true, we use starttls.
117 starttls = tls == b'starttls' or stringutil.parsebool(tls)
117 starttls = tls == b'starttls' or stringutil.parsebool(tls)
118 smtps = tls == b'smtps'
118 smtps = tls == b'smtps'
119 if (starttls or smtps) and not _pyhastls():
119 if (starttls or smtps) and not _pyhastls():
120 raise error.Abort(_(b"can't use TLS: Python SSL support not installed"))
120 raise error.Abort(_(b"can't use TLS: Python SSL support not installed"))
121 mailhost = ui.config(b'smtp', b'host')
121 mailhost = ui.config(b'smtp', b'host')
122 if not mailhost:
122 if not mailhost:
123 raise error.Abort(_(b'smtp.host not configured - cannot send mail'))
123 raise error.Abort(_(b'smtp.host not configured - cannot send mail'))
124 if smtps:
124 if smtps:
125 ui.note(_(b'(using smtps)\n'))
125 ui.note(_(b'(using smtps)\n'))
126 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
126 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
127 elif starttls:
127 elif starttls:
128 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
128 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
129 else:
129 else:
130 s = smtplib.SMTP(local_hostname=local_hostname)
130 s = smtplib.SMTP(local_hostname=local_hostname)
131 if smtps:
131 if smtps:
132 defaultport = 465
132 defaultport = 465
133 else:
133 else:
134 defaultport = 25
134 defaultport = 25
135 mailport = util.getport(ui.config(b'smtp', b'port', defaultport))
135 mailport = util.getport(ui.config(b'smtp', b'port', defaultport))
136 ui.note(_(b'sending mail: smtp host %s, port %d\n') % (mailhost, mailport))
136 ui.note(_(b'sending mail: smtp host %s, port %d\n') % (mailhost, mailport))
137 s.connect(host=mailhost, port=mailport)
137 s.connect(host=mailhost, port=mailport)
138 if starttls:
138 if starttls:
139 ui.note(_(b'(using starttls)\n'))
139 ui.note(_(b'(using starttls)\n'))
140 s.ehlo()
140 s.ehlo()
141 s.starttls()
141 s.starttls()
142 s.ehlo()
142 s.ehlo()
143 if starttls or smtps:
143 if starttls or smtps:
144 ui.note(_(b'(verifying remote certificate)\n'))
144 ui.note(_(b'(verifying remote certificate)\n'))
145 sslutil.validatesocket(s.sock)
145 sslutil.validatesocket(s.sock)
146 username = ui.config(b'smtp', b'username')
146 username = ui.config(b'smtp', b'username')
147 password = ui.config(b'smtp', b'password')
147 password = ui.config(b'smtp', b'password')
148 if username:
148 if username:
149 if password:
149 if password:
150 password = encoding.strfromlocal(password)
150 password = encoding.strfromlocal(password)
151 else:
151 else:
152 password = ui.getpass()
152 password = ui.getpass()
153 if username and password:
153 if username and password:
154 ui.note(_(b'(authenticating to mail server as %s)\n') % username)
154 ui.note(_(b'(authenticating to mail server as %s)\n') % username)
155 username = encoding.strfromlocal(username)
155 username = encoding.strfromlocal(username)
156 try:
156 try:
157 s.login(username, password)
157 s.login(username, password)
158 except smtplib.SMTPException as inst:
158 except smtplib.SMTPException as inst:
159 raise error.Abort(inst)
159 raise error.Abort(inst)
160
160
161 def send(sender, recipients, msg):
161 def send(sender, recipients, msg):
162 try:
162 try:
163 return s.sendmail(sender, recipients, msg)
163 return s.sendmail(sender, recipients, msg)
164 except smtplib.SMTPRecipientsRefused as inst:
164 except smtplib.SMTPRecipientsRefused as inst:
165 recipients = [r[1] for r in inst.recipients.values()]
165 recipients = [r[1] for r in inst.recipients.values()]
166 raise error.Abort(b'\n' + b'\n'.join(recipients))
166 raise error.Abort(b'\n' + b'\n'.join(recipients))
167 except smtplib.SMTPException as inst:
167 except smtplib.SMTPException as inst:
168 raise error.Abort(inst)
168 raise error.Abort(inst)
169
169
170 return send
170 return send
171
171
172
172
173 def _sendmail(ui, sender, recipients, msg):
173 def _sendmail(ui, sender, recipients, msg):
174 '''send mail using sendmail.'''
174 '''send mail using sendmail.'''
175 program = ui.config(b'email', b'method')
175 program = ui.config(b'email', b'method')
176
176
177 def stremail(x):
177 def stremail(x):
178 return procutil.shellquote(stringutil.email(encoding.strtolocal(x)))
178 return procutil.shellquote(stringutil.email(encoding.strtolocal(x)))
179
179
180 cmdline = b'%s -f %s %s' % (
180 cmdline = b'%s -f %s %s' % (
181 program,
181 program,
182 stremail(sender),
182 stremail(sender),
183 b' '.join(map(stremail, recipients)),
183 b' '.join(map(stremail, recipients)),
184 )
184 )
185 ui.note(_(b'sending mail: %s\n') % cmdline)
185 ui.note(_(b'sending mail: %s\n') % cmdline)
186 fp = procutil.popen(cmdline, b'wb')
186 fp = procutil.popen(cmdline, b'wb')
187 fp.write(util.tonativeeol(msg))
187 fp.write(util.tonativeeol(msg))
188 ret = fp.close()
188 ret = fp.close()
189 if ret:
189 if ret:
190 raise error.Abort(
190 raise error.Abort(
191 b'%s %s'
191 b'%s %s'
192 % (
192 % (
193 os.path.basename(program.split(None, 1)[0]),
193 os.path.basename(program.split(None, 1)[0]),
194 procutil.explainexit(ret),
194 procutil.explainexit(ret),
195 )
195 )
196 )
196 )
197
197
198
198
199 def _mbox(mbox, sender, recipients, msg):
199 def _mbox(mbox, sender, recipients, msg):
200 '''write mails to mbox'''
200 '''write mails to mbox'''
201 fp = open(mbox, b'ab+')
201 fp = open(mbox, b'ab+')
202 # Should be time.asctime(), but Windows prints 2-characters day
202 # Should be time.asctime(), but Windows prints 2-characters day
203 # of month instead of one. Make them print the same thing.
203 # of month instead of one. Make them print the same thing.
204 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime())
204 date = time.strftime(r'%a %b %d %H:%M:%S %Y', time.localtime())
205 fp.write(
205 fp.write(
206 b'From %s %s\n'
206 b'From %s %s\n'
207 % (encoding.strtolocal(sender), encoding.strtolocal(date))
207 % (encoding.strtolocal(sender), encoding.strtolocal(date))
208 )
208 )
209 fp.write(msg)
209 fp.write(msg)
210 fp.write(b'\n\n')
210 fp.write(b'\n\n')
211 fp.close()
211 fp.close()
212
212
213
213
214 def connect(ui, mbox=None):
214 def connect(ui, mbox=None):
215 '''make a mail connection. return a function to send mail.
215 '''make a mail connection. return a function to send mail.
216 call as sendmail(sender, list-of-recipients, msg).'''
216 call as sendmail(sender, list-of-recipients, msg).'''
217 if mbox:
217 if mbox:
218 open(mbox, b'wb').close()
218 open(mbox, b'wb').close()
219 return lambda s, r, m: _mbox(mbox, s, r, m)
219 return lambda s, r, m: _mbox(mbox, s, r, m)
220 if ui.config(b'email', b'method') == b'smtp':
220 if ui.config(b'email', b'method') == b'smtp':
221 return _smtp(ui)
221 return _smtp(ui)
222 return lambda s, r, m: _sendmail(ui, s, r, m)
222 return lambda s, r, m: _sendmail(ui, s, r, m)
223
223
224
224
225 def sendmail(ui, sender, recipients, msg, mbox=None):
225 def sendmail(ui, sender, recipients, msg, mbox=None):
226 send = connect(ui, mbox=mbox)
226 send = connect(ui, mbox=mbox)
227 return send(sender, recipients, msg)
227 return send(sender, recipients, msg)
228
228
229
229
230 def validateconfig(ui):
230 def validateconfig(ui):
231 '''determine if we have enough config data to try sending email.'''
231 '''determine if we have enough config data to try sending email.'''
232 method = ui.config(b'email', b'method')
232 method = ui.config(b'email', b'method')
233 if method == b'smtp':
233 if method == b'smtp':
234 if not ui.config(b'smtp', b'host'):
234 if not ui.config(b'smtp', b'host'):
235 raise error.Abort(
235 raise error.Abort(
236 _(
236 _(
237 b'smtp specified as email transport, '
237 b'smtp specified as email transport, '
238 b'but no smtp host configured'
238 b'but no smtp host configured'
239 )
239 )
240 )
240 )
241 else:
241 else:
242 if not procutil.findexe(method):
242 if not procutil.findexe(method):
243 raise error.Abort(
243 raise error.Abort(
244 _(b'%r specified as email transport, but not in PATH') % method
244 _(b'%r specified as email transport, but not in PATH') % method
245 )
245 )
246
246
247
247
248 def codec2iana(cs):
248 def codec2iana(cs):
249 ''''''
249 ''''''
250 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
250 cs = pycompat.sysbytes(email.charset.Charset(cs).input_charset.lower())
251
251
252 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
252 # "latin1" normalizes to "iso8859-1", standard calls for "iso-8859-1"
253 if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
253 if cs.startswith(b"iso") and not cs.startswith(b"iso-"):
254 return b"iso-" + cs[3:]
254 return b"iso-" + cs[3:]
255 return cs
255 return cs
256
256
257
257
258 def mimetextpatch(s, subtype=b'plain', display=False):
258 def mimetextpatch(s, subtype=b'plain', display=False):
259 '''Return MIME message suitable for a patch.
259 '''Return MIME message suitable for a patch.
260 Charset will be detected by first trying to decode as us-ascii, then utf-8,
260 Charset will be detected by first trying to decode as us-ascii, then utf-8,
261 and finally the global encodings. If all those fail, fall back to
261 and finally the global encodings. If all those fail, fall back to
262 ISO-8859-1, an encoding with that allows all byte sequences.
262 ISO-8859-1, an encoding with that allows all byte sequences.
263 Transfer encodings will be used if necessary.'''
263 Transfer encodings will be used if necessary.'''
264
264
265 cs = [b'us-ascii', b'utf-8', encoding.encoding, encoding.fallbackencoding]
265 cs = [b'us-ascii', b'utf-8', encoding.encoding, encoding.fallbackencoding]
266 if display:
266 if display:
267 cs = [b'us-ascii']
267 cs = [b'us-ascii']
268 for charset in cs:
268 for charset in cs:
269 try:
269 try:
270 s.decode(pycompat.sysstr(charset))
270 s.decode(pycompat.sysstr(charset))
271 return mimetextqp(s, subtype, codec2iana(charset))
271 return mimetextqp(s, subtype, codec2iana(charset))
272 except UnicodeDecodeError:
272 except UnicodeDecodeError:
273 pass
273 pass
274
274
275 return mimetextqp(s, subtype, b"iso-8859-1")
275 return mimetextqp(s, subtype, b"iso-8859-1")
276
276
277
277
278 def mimetextqp(body, subtype, charset):
278 def mimetextqp(body, subtype, charset):
279 '''Return MIME message.
279 '''Return MIME message.
280 Quoted-printable transfer encoding will be used if necessary.
280 Quoted-printable transfer encoding will be used if necessary.
281 '''
281 '''
282 cs = email.charset.Charset(charset)
282 # Experimentally charset is okay as a bytes even if the type
283 # stubs disagree.
284 cs = email.charset.Charset(charset) # pytype: disable=wrong-arg-types
283 msg = email.message.Message()
285 msg = email.message.Message()
284 msg.set_type(pycompat.sysstr(b'text/' + subtype))
286 msg.set_type(pycompat.sysstr(b'text/' + subtype))
285
287
286 for line in body.splitlines():
288 for line in body.splitlines():
287 if len(line) > 950:
289 if len(line) > 950:
288 cs.body_encoding = email.charset.QP
290 cs.body_encoding = email.charset.QP
289 break
291 break
290
292
291 # On Python 2, this simply assigns a value. Python 3 inspects
293 # On Python 2, this simply assigns a value. Python 3 inspects
292 # body and does different things depending on whether it has
294 # body and does different things depending on whether it has
293 # encode() or decode() attributes. We can get the old behavior
295 # encode() or decode() attributes. We can get the old behavior
294 # if we pass a str and charset is None and we call set_charset().
296 # if we pass a str and charset is None and we call set_charset().
295 # But we may get into trouble later due to Python attempting to
297 # But we may get into trouble later due to Python attempting to
296 # encode/decode using the registered charset (or attempting to
298 # encode/decode using the registered charset (or attempting to
297 # use ascii in the absence of a charset).
299 # use ascii in the absence of a charset).
298 msg.set_payload(body, cs)
300 msg.set_payload(body, cs)
299
301
300 return msg
302 return msg
301
303
302
304
303 def _charsets(ui):
305 def _charsets(ui):
304 '''Obtains charsets to send mail parts not containing patches.'''
306 '''Obtains charsets to send mail parts not containing patches.'''
305 charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
307 charsets = [cs.lower() for cs in ui.configlist(b'email', b'charsets')]
306 fallbacks = [
308 fallbacks = [
307 encoding.fallbackencoding.lower(),
309 encoding.fallbackencoding.lower(),
308 encoding.encoding.lower(),
310 encoding.encoding.lower(),
309 b'utf-8',
311 b'utf-8',
310 ]
312 ]
311 for cs in fallbacks: # find unique charsets while keeping order
313 for cs in fallbacks: # find unique charsets while keeping order
312 if cs not in charsets:
314 if cs not in charsets:
313 charsets.append(cs)
315 charsets.append(cs)
314 return [cs for cs in charsets if not cs.endswith(b'ascii')]
316 return [cs for cs in charsets if not cs.endswith(b'ascii')]
315
317
316
318
317 def _encode(ui, s, charsets):
319 def _encode(ui, s, charsets):
318 '''Returns (converted) string, charset tuple.
320 '''Returns (converted) string, charset tuple.
319 Finds out best charset by cycling through sendcharsets in descending
321 Finds out best charset by cycling through sendcharsets in descending
320 order. Tries both encoding and fallbackencoding for input. Only as
322 order. Tries both encoding and fallbackencoding for input. Only as
321 last resort send as is in fake ascii.
323 last resort send as is in fake ascii.
322 Caveat: Do not use for mail parts containing patches!'''
324 Caveat: Do not use for mail parts containing patches!'''
323 sendcharsets = charsets or _charsets(ui)
325 sendcharsets = charsets or _charsets(ui)
324 if not isinstance(s, bytes):
326 if not isinstance(s, bytes):
325 # We have unicode data, which we need to try and encode to
327 # We have unicode data, which we need to try and encode to
326 # some reasonable-ish encoding. Try the encodings the user
328 # some reasonable-ish encoding. Try the encodings the user
327 # wants, and fall back to garbage-in-ascii.
329 # wants, and fall back to garbage-in-ascii.
328 for ocs in sendcharsets:
330 for ocs in sendcharsets:
329 try:
331 try:
330 return s.encode(pycompat.sysstr(ocs)), ocs
332 return s.encode(pycompat.sysstr(ocs)), ocs
331 except UnicodeEncodeError:
333 except UnicodeEncodeError:
332 pass
334 pass
333 except LookupError:
335 except LookupError:
334 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
336 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
335 else:
337 else:
336 # Everything failed, ascii-armor what we've got and send it.
338 # Everything failed, ascii-armor what we've got and send it.
337 return s.encode('ascii', 'backslashreplace')
339 return s.encode('ascii', 'backslashreplace')
338 # We have a bytes of unknown encoding. We'll try and guess a valid
340 # We have a bytes of unknown encoding. We'll try and guess a valid
339 # encoding, falling back to pretending we had ascii even though we
341 # encoding, falling back to pretending we had ascii even though we
340 # know that's wrong.
342 # know that's wrong.
341 try:
343 try:
342 s.decode('ascii')
344 s.decode('ascii')
343 except UnicodeDecodeError:
345 except UnicodeDecodeError:
344 for ics in (encoding.encoding, encoding.fallbackencoding):
346 for ics in (encoding.encoding, encoding.fallbackencoding):
345 ics = pycompat.sysstr(ics)
347 ics = pycompat.sysstr(ics)
346 try:
348 try:
347 u = s.decode(ics)
349 u = s.decode(ics)
348 except UnicodeDecodeError:
350 except UnicodeDecodeError:
349 continue
351 continue
350 for ocs in sendcharsets:
352 for ocs in sendcharsets:
351 try:
353 try:
352 return u.encode(pycompat.sysstr(ocs)), ocs
354 return u.encode(pycompat.sysstr(ocs)), ocs
353 except UnicodeEncodeError:
355 except UnicodeEncodeError:
354 pass
356 pass
355 except LookupError:
357 except LookupError:
356 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
358 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
357 # if ascii, or all conversion attempts fail, send (broken) ascii
359 # if ascii, or all conversion attempts fail, send (broken) ascii
358 return s, b'us-ascii'
360 return s, b'us-ascii'
359
361
360
362
361 def headencode(ui, s, charsets=None, display=False):
363 def headencode(ui, s, charsets=None, display=False):
362 '''Returns RFC-2047 compliant header from given string.'''
364 '''Returns RFC-2047 compliant header from given string.'''
363 if not display:
365 if not display:
364 # split into words?
366 # split into words?
365 s, cs = _encode(ui, s, charsets)
367 s, cs = _encode(ui, s, charsets)
366 return encoding.strtolocal(email.header.Header(s, cs).encode())
368 return encoding.strtolocal(email.header.Header(s, cs).encode())
367 return s
369 return s
368
370
369
371
370 def _addressencode(ui, name, addr, charsets=None):
372 def _addressencode(ui, name, addr, charsets=None):
371 assert isinstance(addr, bytes)
373 assert isinstance(addr, bytes)
372 name = encoding.strfromlocal(headencode(ui, name, charsets))
374 name = encoding.strfromlocal(headencode(ui, name, charsets))
373 try:
375 try:
374 acc, dom = addr.split(b'@')
376 acc, dom = addr.split(b'@')
375 acc.decode('ascii')
377 acc.decode('ascii')
376 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
378 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
377 addr = b'%s@%s' % (acc, dom)
379 addr = b'%s@%s' % (acc, dom)
378 except UnicodeDecodeError:
380 except UnicodeDecodeError:
379 raise error.Abort(_(b'invalid email address: %s') % addr)
381 raise error.Abort(_(b'invalid email address: %s') % addr)
380 except ValueError:
382 except ValueError:
381 try:
383 try:
382 # too strict?
384 # too strict?
383 addr.decode('ascii')
385 addr.decode('ascii')
384 except UnicodeDecodeError:
386 except UnicodeDecodeError:
385 raise error.Abort(_(b'invalid local address: %s') % addr)
387 raise error.Abort(_(b'invalid local address: %s') % addr)
386 return pycompat.bytesurl(
388 return pycompat.bytesurl(
387 email.utils.formataddr((name, encoding.strfromlocal(addr)))
389 email.utils.formataddr((name, encoding.strfromlocal(addr)))
388 )
390 )
389
391
390
392
391 def addressencode(ui, address, charsets=None, display=False):
393 def addressencode(ui, address, charsets=None, display=False):
392 '''Turns address into RFC-2047 compliant header.'''
394 '''Turns address into RFC-2047 compliant header.'''
393 if display or not address:
395 if display or not address:
394 return address or b''
396 return address or b''
395 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
397 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
396 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
398 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
397
399
398
400
399 def addrlistencode(ui, addrs, charsets=None, display=False):
401 def addrlistencode(ui, addrs, charsets=None, display=False):
400 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
402 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
401 A single element of input list may contain multiple addresses, but output
403 A single element of input list may contain multiple addresses, but output
402 always has one address per item'''
404 always has one address per item'''
403 for a in addrs:
405 for a in addrs:
404 assert isinstance(a, bytes), r'%r unexpectedly not a bytestr' % a
406 assert isinstance(a, bytes), r'%r unexpectedly not a bytestr' % a
405 if display:
407 if display:
406 return [a.strip() for a in addrs if a.strip()]
408 return [a.strip() for a in addrs if a.strip()]
407
409
408 result = []
410 result = []
409 for name, addr in email.utils.getaddresses(
411 for name, addr in email.utils.getaddresses(
410 [encoding.strfromlocal(a) for a in addrs]
412 [encoding.strfromlocal(a) for a in addrs]
411 ):
413 ):
412 if name or addr:
414 if name or addr:
413 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
415 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
414 result.append(r)
416 result.append(r)
415 return result
417 return result
416
418
417
419
418 def mimeencode(ui, s, charsets=None, display=False):
420 def mimeencode(ui, s, charsets=None, display=False):
419 '''creates mime text object, encodes it if needed, and sets
421 '''creates mime text object, encodes it if needed, and sets
420 charset and transfer-encoding accordingly.'''
422 charset and transfer-encoding accordingly.'''
421 cs = b'us-ascii'
423 cs = b'us-ascii'
422 if not display:
424 if not display:
423 s, cs = _encode(ui, s, charsets)
425 s, cs = _encode(ui, s, charsets)
424 return mimetextqp(s, b'plain', cs)
426 return mimetextqp(s, b'plain', cs)
425
427
426
428
427 if pycompat.ispy3:
429 if pycompat.ispy3:
428
430
429 Generator = email.generator.BytesGenerator
431 Generator = email.generator.BytesGenerator
430
432
431 def parse(fp):
433 def parse(fp):
432 ep = email.parser.Parser()
434 ep = email.parser.Parser()
433 # disable the "universal newlines" mode, which isn't binary safe.
435 # disable the "universal newlines" mode, which isn't binary safe.
434 # I have no idea if ascii/surrogateescape is correct, but that's
436 # I have no idea if ascii/surrogateescape is correct, but that's
435 # what the standard Python email parser does.
437 # what the standard Python email parser does.
436 fp = io.TextIOWrapper(
438 fp = io.TextIOWrapper(
437 fp, encoding=r'ascii', errors=r'surrogateescape', newline=chr(10)
439 fp, encoding=r'ascii', errors=r'surrogateescape', newline=chr(10)
438 )
440 )
439 try:
441 try:
440 return ep.parse(fp)
442 return ep.parse(fp)
441 finally:
443 finally:
442 fp.detach()
444 fp.detach()
443
445
444 def parsebytes(data):
446 def parsebytes(data):
445 ep = email.parser.BytesParser()
447 ep = email.parser.BytesParser()
446 return ep.parsebytes(data)
448 return ep.parsebytes(data)
447
449
448
450
449 else:
451 else:
450
452
451 Generator = email.generator.Generator
453 Generator = email.generator.Generator
452
454
453 def parse(fp):
455 def parse(fp):
454 ep = email.parser.Parser()
456 ep = email.parser.Parser()
455 return ep.parse(fp)
457 return ep.parse(fp)
456
458
457 def parsebytes(data):
459 def parsebytes(data):
458 ep = email.parser.Parser()
460 ep = email.parser.Parser()
459 return ep.parsestr(data)
461 return ep.parsestr(data)
460
462
461
463
462 def headdecode(s):
464 def headdecode(s):
463 '''Decodes RFC-2047 header'''
465 '''Decodes RFC-2047 header'''
464 uparts = []
466 uparts = []
465 for part, charset in email.header.decode_header(s):
467 for part, charset in email.header.decode_header(s):
466 if charset is not None:
468 if charset is not None:
467 try:
469 try:
468 uparts.append(part.decode(charset))
470 uparts.append(part.decode(charset))
469 continue
471 continue
470 except (UnicodeDecodeError, LookupError):
472 except (UnicodeDecodeError, LookupError):
471 pass
473 pass
472 # On Python 3, decode_header() may return either bytes or unicode
474 # On Python 3, decode_header() may return either bytes or unicode
473 # depending on whether the header has =?<charset>? or not
475 # depending on whether the header has =?<charset>? or not
474 if isinstance(part, type(u'')):
476 if isinstance(part, type(u'')):
475 uparts.append(part)
477 uparts.append(part)
476 continue
478 continue
477 try:
479 try:
478 uparts.append(part.decode('UTF-8'))
480 uparts.append(part.decode('UTF-8'))
479 continue
481 continue
480 except UnicodeDecodeError:
482 except UnicodeDecodeError:
481 pass
483 pass
482 uparts.append(part.decode('ISO-8859-1'))
484 uparts.append(part.decode('ISO-8859-1'))
483 return encoding.unitolocal(u' '.join(uparts))
485 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now