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