##// END OF EJS Templates
mail: convert addr to str early in addrlistencode()
Denis Laxalde -
r43977:599e25ad default
parent child Browse files
Show More
@@ -1,483 +1,483
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('rb')
97 self.file = new_socket.makefile('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('%a %b %d %H:%M:%S %Y', time.localtime())
204 date = time.strftime('%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 # Experimentally charset is okay as a bytes even if the type
282 # Experimentally charset is okay as a bytes even if the type
283 # stubs disagree.
283 # stubs disagree.
284 cs = email.charset.Charset(charset) # pytype: disable=wrong-arg-types
284 cs = email.charset.Charset(charset) # pytype: disable=wrong-arg-types
285 msg = email.message.Message()
285 msg = email.message.Message()
286 msg.set_type(pycompat.sysstr(b'text/' + subtype))
286 msg.set_type(pycompat.sysstr(b'text/' + subtype))
287
287
288 for line in body.splitlines():
288 for line in body.splitlines():
289 if len(line) > 950:
289 if len(line) > 950:
290 cs.body_encoding = email.charset.QP
290 cs.body_encoding = email.charset.QP
291 break
291 break
292
292
293 # On Python 2, this simply assigns a value. Python 3 inspects
293 # On Python 2, this simply assigns a value. Python 3 inspects
294 # body and does different things depending on whether it has
294 # body and does different things depending on whether it has
295 # encode() or decode() attributes. We can get the old behavior
295 # encode() or decode() attributes. We can get the old behavior
296 # 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().
297 # 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
298 # encode/decode using the registered charset (or attempting to
298 # encode/decode using the registered charset (or attempting to
299 # use ascii in the absence of a charset).
299 # use ascii in the absence of a charset).
300 msg.set_payload(body, cs)
300 msg.set_payload(body, cs)
301
301
302 return msg
302 return msg
303
303
304
304
305 def _charsets(ui):
305 def _charsets(ui):
306 '''Obtains charsets to send mail parts not containing patches.'''
306 '''Obtains charsets to send mail parts not containing patches.'''
307 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')]
308 fallbacks = [
308 fallbacks = [
309 encoding.fallbackencoding.lower(),
309 encoding.fallbackencoding.lower(),
310 encoding.encoding.lower(),
310 encoding.encoding.lower(),
311 b'utf-8',
311 b'utf-8',
312 ]
312 ]
313 for cs in fallbacks: # find unique charsets while keeping order
313 for cs in fallbacks: # find unique charsets while keeping order
314 if cs not in charsets:
314 if cs not in charsets:
315 charsets.append(cs)
315 charsets.append(cs)
316 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')]
317
317
318
318
319 def _encode(ui, s, charsets):
319 def _encode(ui, s, charsets):
320 '''Returns (converted) string, charset tuple.
320 '''Returns (converted) string, charset tuple.
321 Finds out best charset by cycling through sendcharsets in descending
321 Finds out best charset by cycling through sendcharsets in descending
322 order. Tries both encoding and fallbackencoding for input. Only as
322 order. Tries both encoding and fallbackencoding for input. Only as
323 last resort send as is in fake ascii.
323 last resort send as is in fake ascii.
324 Caveat: Do not use for mail parts containing patches!'''
324 Caveat: Do not use for mail parts containing patches!'''
325 sendcharsets = charsets or _charsets(ui)
325 sendcharsets = charsets or _charsets(ui)
326 if not isinstance(s, bytes):
326 if not isinstance(s, bytes):
327 # 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
328 # some reasonable-ish encoding. Try the encodings the user
328 # some reasonable-ish encoding. Try the encodings the user
329 # wants, and fall back to garbage-in-ascii.
329 # wants, and fall back to garbage-in-ascii.
330 for ocs in sendcharsets:
330 for ocs in sendcharsets:
331 try:
331 try:
332 return s.encode(pycompat.sysstr(ocs)), ocs
332 return s.encode(pycompat.sysstr(ocs)), ocs
333 except UnicodeEncodeError:
333 except UnicodeEncodeError:
334 pass
334 pass
335 except LookupError:
335 except LookupError:
336 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
336 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
337 else:
337 else:
338 # Everything failed, ascii-armor what we've got and send it.
338 # Everything failed, ascii-armor what we've got and send it.
339 return s.encode('ascii', 'backslashreplace')
339 return s.encode('ascii', 'backslashreplace')
340 # 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
341 # encoding, falling back to pretending we had ascii even though we
341 # encoding, falling back to pretending we had ascii even though we
342 # know that's wrong.
342 # know that's wrong.
343 try:
343 try:
344 s.decode('ascii')
344 s.decode('ascii')
345 except UnicodeDecodeError:
345 except UnicodeDecodeError:
346 for ics in (encoding.encoding, encoding.fallbackencoding):
346 for ics in (encoding.encoding, encoding.fallbackencoding):
347 ics = pycompat.sysstr(ics)
347 ics = pycompat.sysstr(ics)
348 try:
348 try:
349 u = s.decode(ics)
349 u = s.decode(ics)
350 except UnicodeDecodeError:
350 except UnicodeDecodeError:
351 continue
351 continue
352 for ocs in sendcharsets:
352 for ocs in sendcharsets:
353 try:
353 try:
354 return u.encode(pycompat.sysstr(ocs)), ocs
354 return u.encode(pycompat.sysstr(ocs)), ocs
355 except UnicodeEncodeError:
355 except UnicodeEncodeError:
356 pass
356 pass
357 except LookupError:
357 except LookupError:
358 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
358 ui.warn(_(b'ignoring invalid sendcharset: %s\n') % ocs)
359 # if ascii, or all conversion attempts fail, send (broken) ascii
359 # if ascii, or all conversion attempts fail, send (broken) ascii
360 return s, b'us-ascii'
360 return s, b'us-ascii'
361
361
362
362
363 def headencode(ui, s, charsets=None, display=False):
363 def headencode(ui, s, charsets=None, display=False):
364 '''Returns RFC-2047 compliant header from given string.'''
364 '''Returns RFC-2047 compliant header from given string.'''
365 if not display:
365 if not display:
366 # split into words?
366 # split into words?
367 s, cs = _encode(ui, s, charsets)
367 s, cs = _encode(ui, s, charsets)
368 return email.header.Header(s, cs).encode()
368 return email.header.Header(s, cs).encode()
369 return encoding.strfromlocal(s)
369 return encoding.strfromlocal(s)
370
370
371
371
372 def _addressencode(ui, name, addr, charsets=None):
372 def _addressencode(ui, name, addr, charsets=None):
373 assert isinstance(addr, bytes)
373 assert isinstance(addr, bytes)
374 name = headencode(ui, name, charsets)
374 name = headencode(ui, name, charsets)
375 try:
375 try:
376 acc, dom = addr.split(b'@')
376 acc, dom = addr.split(b'@')
377 acc.decode('ascii')
377 acc.decode('ascii')
378 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
378 dom = dom.decode(pycompat.sysstr(encoding.encoding)).encode('idna')
379 addr = b'%s@%s' % (acc, dom)
379 addr = b'%s@%s' % (acc, dom)
380 except UnicodeDecodeError:
380 except UnicodeDecodeError:
381 raise error.Abort(_(b'invalid email address: %s') % addr)
381 raise error.Abort(_(b'invalid email address: %s') % addr)
382 except ValueError:
382 except ValueError:
383 try:
383 try:
384 # too strict?
384 # too strict?
385 addr.decode('ascii')
385 addr.decode('ascii')
386 except UnicodeDecodeError:
386 except UnicodeDecodeError:
387 raise error.Abort(_(b'invalid local address: %s') % addr)
387 raise error.Abort(_(b'invalid local address: %s') % addr)
388 return email.utils.formataddr((name, encoding.strfromlocal(addr)))
388 return email.utils.formataddr((name, encoding.strfromlocal(addr)))
389
389
390
390
391 def addressencode(ui, address, charsets=None, display=False):
391 def addressencode(ui, address, charsets=None, display=False):
392 '''Turns address into RFC-2047 compliant header.'''
392 '''Turns address into RFC-2047 compliant header.'''
393 if display or not address:
393 if display or not address:
394 return encoding.strfromlocal(address or b'')
394 return encoding.strfromlocal(address or b'')
395 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
395 name, addr = email.utils.parseaddr(encoding.strfromlocal(address))
396 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
396 return _addressencode(ui, name, encoding.strtolocal(addr), charsets)
397
397
398
398
399 def addrlistencode(ui, addrs, charsets=None, display=False):
399 def addrlistencode(ui, addrs, charsets=None, display=False):
400 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
400 '''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
401 A single element of input list may contain multiple addresses, but output
402 always has one address per item'''
402 always has one address per item'''
403 straddrs = []
403 for a in addrs:
404 for a in addrs:
404 assert isinstance(a, bytes), '%r unexpectedly not a bytestr' % a
405 assert isinstance(a, bytes), '%r unexpectedly not a bytestr' % a
406 straddrs.append(encoding.strfromlocal(a))
405 if display:
407 if display:
406 return [encoding.strfromlocal(a.strip()) for a in addrs if a.strip()]
408 return [a.strip() for a in straddrs 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(straddrs):
410 [encoding.strfromlocal(a) for a in addrs]
411 ):
412 if name or addr:
412 if name or addr:
413 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
413 r = _addressencode(ui, name, encoding.strtolocal(addr), charsets)
414 result.append(r)
414 result.append(r)
415 return result
415 return result
416
416
417
417
418 def mimeencode(ui, s, charsets=None, display=False):
418 def mimeencode(ui, s, charsets=None, display=False):
419 '''creates mime text object, encodes it if needed, and sets
419 '''creates mime text object, encodes it if needed, and sets
420 charset and transfer-encoding accordingly.'''
420 charset and transfer-encoding accordingly.'''
421 cs = b'us-ascii'
421 cs = b'us-ascii'
422 if not display:
422 if not display:
423 s, cs = _encode(ui, s, charsets)
423 s, cs = _encode(ui, s, charsets)
424 return mimetextqp(s, b'plain', cs)
424 return mimetextqp(s, b'plain', cs)
425
425
426
426
427 if pycompat.ispy3:
427 if pycompat.ispy3:
428
428
429 Generator = email.generator.BytesGenerator
429 Generator = email.generator.BytesGenerator
430
430
431 def parse(fp):
431 def parse(fp):
432 ep = email.parser.Parser()
432 ep = email.parser.Parser()
433 # disable the "universal newlines" mode, which isn't binary safe.
433 # disable the "universal newlines" mode, which isn't binary safe.
434 # I have no idea if ascii/surrogateescape is correct, but that's
434 # I have no idea if ascii/surrogateescape is correct, but that's
435 # what the standard Python email parser does.
435 # what the standard Python email parser does.
436 fp = io.TextIOWrapper(
436 fp = io.TextIOWrapper(
437 fp, encoding='ascii', errors='surrogateescape', newline=chr(10)
437 fp, encoding='ascii', errors='surrogateescape', newline=chr(10)
438 )
438 )
439 try:
439 try:
440 return ep.parse(fp)
440 return ep.parse(fp)
441 finally:
441 finally:
442 fp.detach()
442 fp.detach()
443
443
444 def parsebytes(data):
444 def parsebytes(data):
445 ep = email.parser.BytesParser()
445 ep = email.parser.BytesParser()
446 return ep.parsebytes(data)
446 return ep.parsebytes(data)
447
447
448
448
449 else:
449 else:
450
450
451 Generator = email.generator.Generator
451 Generator = email.generator.Generator
452
452
453 def parse(fp):
453 def parse(fp):
454 ep = email.parser.Parser()
454 ep = email.parser.Parser()
455 return ep.parse(fp)
455 return ep.parse(fp)
456
456
457 def parsebytes(data):
457 def parsebytes(data):
458 ep = email.parser.Parser()
458 ep = email.parser.Parser()
459 return ep.parsestr(data)
459 return ep.parsestr(data)
460
460
461
461
462 def headdecode(s):
462 def headdecode(s):
463 '''Decodes RFC-2047 header'''
463 '''Decodes RFC-2047 header'''
464 uparts = []
464 uparts = []
465 for part, charset in email.header.decode_header(s):
465 for part, charset in email.header.decode_header(s):
466 if charset is not None:
466 if charset is not None:
467 try:
467 try:
468 uparts.append(part.decode(charset))
468 uparts.append(part.decode(charset))
469 continue
469 continue
470 except (UnicodeDecodeError, LookupError):
470 except (UnicodeDecodeError, LookupError):
471 pass
471 pass
472 # On Python 3, decode_header() may return either bytes or unicode
472 # On Python 3, decode_header() may return either bytes or unicode
473 # depending on whether the header has =?<charset>? or not
473 # depending on whether the header has =?<charset>? or not
474 if isinstance(part, type(u'')):
474 if isinstance(part, type(u'')):
475 uparts.append(part)
475 uparts.append(part)
476 continue
476 continue
477 try:
477 try:
478 uparts.append(part.decode('UTF-8'))
478 uparts.append(part.decode('UTF-8'))
479 continue
479 continue
480 except UnicodeDecodeError:
480 except UnicodeDecodeError:
481 pass
481 pass
482 uparts.append(part.decode('ISO-8859-1'))
482 uparts.append(part.decode('ISO-8859-1'))
483 return encoding.unitolocal(u' '.join(uparts))
483 return encoding.unitolocal(u' '.join(uparts))
General Comments 0
You need to be logged in to leave comments. Login now