##// END OF EJS Templates
mail: retain hostname for sslutil.wrapsocket (issue5203)...
timeless -
r28935:a4c5c23d default
parent child Browse files
Show More
@@ -1,352 +1,357 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, print_function
9 9
10 10 import email
11 11 import os
12 12 import quopri
13 13 import smtplib
14 14 import socket
15 15 import sys
16 16 import time
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 encoding,
21 21 error,
22 22 sslutil,
23 23 util,
24 24 )
25 25
26 26 _oldheaderinit = email.Header.Header.__init__
27 27 def _unifiedheaderinit(self, *args, **kw):
28 28 """
29 29 Python 2.7 introduces a backwards incompatible change
30 30 (Python issue1974, r70772) in email.Generator.Generator code:
31 31 pre-2.7 code passed "continuation_ws='\t'" to the Header
32 32 constructor, and 2.7 removed this parameter.
33 33
34 34 Default argument is continuation_ws=' ', which means that the
35 35 behavior is different in <2.7 and 2.7
36 36
37 37 We consider the 2.7 behavior to be preferable, but need
38 38 to have an unified behavior for versions 2.4 to 2.7
39 39 """
40 40 # override continuation_ws
41 41 kw['continuation_ws'] = ' '
42 42 _oldheaderinit(self, *args, **kw)
43 43
44 44 email.Header.Header.__dict__['__init__'] = _unifiedheaderinit
45 45
46 46 class STARTTLS(smtplib.SMTP):
47 47 '''Derived class to verify the peer certificate for STARTTLS.
48 48
49 49 This class allows to pass any keyword arguments to SSL socket creation.
50 50 '''
51 def __init__(self, sslkwargs, **kwargs):
51 def __init__(self, sslkwargs, host=None, **kwargs):
52 52 smtplib.SMTP.__init__(self, **kwargs)
53 53 self._sslkwargs = sslkwargs
54 self._host = host
54 55
55 56 def starttls(self, keyfile=None, certfile=None):
56 57 if not self.has_extn("starttls"):
57 58 msg = "STARTTLS extension not supported by server"
58 59 raise smtplib.SMTPException(msg)
59 60 (resp, reply) = self.docmd("STARTTLS")
60 61 if resp == 220:
61 62 self.sock = sslutil.wrapsocket(self.sock, keyfile, certfile,
63 serverhostname=self._host,
62 64 **self._sslkwargs)
63 65 self.file = smtplib.SSLFakeFile(self.sock)
64 66 self.helo_resp = None
65 67 self.ehlo_resp = None
66 68 self.esmtp_features = {}
67 69 self.does_esmtp = 0
68 70 return (resp, reply)
69 71
70 72 class SMTPS(smtplib.SMTP):
71 73 '''Derived class to verify the peer certificate for SMTPS.
72 74
73 75 This class allows to pass any keyword arguments to SSL socket creation.
74 76 '''
75 def __init__(self, sslkwargs, keyfile=None, certfile=None, **kwargs):
77 def __init__(self, sslkwargs, keyfile=None, certfile=None, host=None,
78 **kwargs):
76 79 self.keyfile = keyfile
77 80 self.certfile = certfile
78 81 smtplib.SMTP.__init__(self, **kwargs)
82 self._host = host
79 83 self.default_port = smtplib.SMTP_SSL_PORT
80 84 self._sslkwargs = sslkwargs
81 85
82 86 def _get_socket(self, host, port, timeout):
83 87 if self.debuglevel > 0:
84 88 print('connect:', (host, port), file=sys.stderr)
85 89 new_socket = socket.create_connection((host, port), timeout)
86 90 new_socket = sslutil.wrapsocket(new_socket,
87 91 self.keyfile, self.certfile,
92 serverhostname=self._host,
88 93 **self._sslkwargs)
89 94 self.file = smtplib.SSLFakeFile(new_socket)
90 95 return new_socket
91 96
92 97 def _smtp(ui):
93 98 '''build an smtp connection and return a function to send mail'''
94 99 local_hostname = ui.config('smtp', 'local_hostname')
95 100 tls = ui.config('smtp', 'tls', 'none')
96 101 # backward compatible: when tls = true, we use starttls.
97 102 starttls = tls == 'starttls' or util.parsebool(tls)
98 103 smtps = tls == 'smtps'
99 104 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
100 105 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
101 106 mailhost = ui.config('smtp', 'host')
102 107 if not mailhost:
103 108 raise error.Abort(_('smtp.host not configured - cannot send mail'))
104 109 verifycert = ui.config('smtp', 'verifycert', 'strict')
105 110 if verifycert not in ['strict', 'loose']:
106 111 if util.parsebool(verifycert) is not False:
107 112 raise error.Abort(_('invalid smtp.verifycert configuration: %s')
108 113 % (verifycert))
109 114 verifycert = False
110 115 if (starttls or smtps) and verifycert:
111 116 sslkwargs = sslutil.sslkwargs(ui, mailhost)
112 117 else:
113 118 # 'ui' is required by sslutil.wrapsocket() and set by sslkwargs()
114 119 sslkwargs = {'ui': ui}
115 120 if smtps:
116 121 ui.note(_('(using smtps)\n'))
117 s = SMTPS(sslkwargs, local_hostname=local_hostname)
122 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
118 123 elif starttls:
119 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
124 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
120 125 else:
121 126 s = smtplib.SMTP(local_hostname=local_hostname)
122 127 if smtps:
123 128 defaultport = 465
124 129 else:
125 130 defaultport = 25
126 131 mailport = util.getport(ui.config('smtp', 'port', defaultport))
127 132 ui.note(_('sending mail: smtp host %s, port %d\n') %
128 133 (mailhost, mailport))
129 134 s.connect(host=mailhost, port=mailport)
130 135 if starttls:
131 136 ui.note(_('(using starttls)\n'))
132 137 s.ehlo()
133 138 s.starttls()
134 139 s.ehlo()
135 140 if (starttls or smtps) and verifycert:
136 141 ui.note(_('(verifying remote certificate)\n'))
137 142 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
138 143 username = ui.config('smtp', 'username')
139 144 password = ui.config('smtp', 'password')
140 145 if username and not password:
141 146 password = ui.getpass()
142 147 if username and password:
143 148 ui.note(_('(authenticating to mail server as %s)\n') %
144 149 (username))
145 150 try:
146 151 s.login(username, password)
147 152 except smtplib.SMTPException as inst:
148 153 raise error.Abort(inst)
149 154
150 155 def send(sender, recipients, msg):
151 156 try:
152 157 return s.sendmail(sender, recipients, msg)
153 158 except smtplib.SMTPRecipientsRefused as inst:
154 159 recipients = [r[1] for r in inst.recipients.values()]
155 160 raise error.Abort('\n' + '\n'.join(recipients))
156 161 except smtplib.SMTPException as inst:
157 162 raise error.Abort(inst)
158 163
159 164 return send
160 165
161 166 def _sendmail(ui, sender, recipients, msg):
162 167 '''send mail using sendmail.'''
163 168 program = ui.config('email', 'method', 'smtp')
164 169 cmdline = '%s -f %s %s' % (program, util.email(sender),
165 170 ' '.join(map(util.email, recipients)))
166 171 ui.note(_('sending mail: %s\n') % cmdline)
167 172 fp = util.popen(cmdline, 'w')
168 173 fp.write(msg)
169 174 ret = fp.close()
170 175 if ret:
171 176 raise error.Abort('%s %s' % (
172 177 os.path.basename(program.split(None, 1)[0]),
173 178 util.explainexit(ret)[0]))
174 179
175 180 def _mbox(mbox, sender, recipients, msg):
176 181 '''write mails to mbox'''
177 182 fp = open(mbox, 'ab+')
178 183 # Should be time.asctime(), but Windows prints 2-characters day
179 184 # of month instead of one. Make them print the same thing.
180 185 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
181 186 fp.write('From %s %s\n' % (sender, date))
182 187 fp.write(msg)
183 188 fp.write('\n\n')
184 189 fp.close()
185 190
186 191 def connect(ui, mbox=None):
187 192 '''make a mail connection. return a function to send mail.
188 193 call as sendmail(sender, list-of-recipients, msg).'''
189 194 if mbox:
190 195 open(mbox, 'wb').close()
191 196 return lambda s, r, m: _mbox(mbox, s, r, m)
192 197 if ui.config('email', 'method', 'smtp') == 'smtp':
193 198 return _smtp(ui)
194 199 return lambda s, r, m: _sendmail(ui, s, r, m)
195 200
196 201 def sendmail(ui, sender, recipients, msg, mbox=None):
197 202 send = connect(ui, mbox=mbox)
198 203 return send(sender, recipients, msg)
199 204
200 205 def validateconfig(ui):
201 206 '''determine if we have enough config data to try sending email.'''
202 207 method = ui.config('email', 'method', 'smtp')
203 208 if method == 'smtp':
204 209 if not ui.config('smtp', 'host'):
205 210 raise error.Abort(_('smtp specified as email transport, '
206 211 'but no smtp host configured'))
207 212 else:
208 213 if not util.findexe(method):
209 214 raise error.Abort(_('%r specified as email transport, '
210 215 'but not in PATH') % method)
211 216
212 217 def mimetextpatch(s, subtype='plain', display=False):
213 218 '''Return MIME message suitable for a patch.
214 219 Charset will be detected as utf-8 or (possibly fake) us-ascii.
215 220 Transfer encodings will be used if necessary.'''
216 221
217 222 cs = 'us-ascii'
218 223 if not display:
219 224 try:
220 225 s.decode('us-ascii')
221 226 except UnicodeDecodeError:
222 227 try:
223 228 s.decode('utf-8')
224 229 cs = 'utf-8'
225 230 except UnicodeDecodeError:
226 231 # We'll go with us-ascii as a fallback.
227 232 pass
228 233
229 234 return mimetextqp(s, subtype, cs)
230 235
231 236 def mimetextqp(body, subtype, charset):
232 237 '''Return MIME message.
233 238 Quoted-printable transfer encoding will be used if necessary.
234 239 '''
235 240 enc = None
236 241 for line in body.splitlines():
237 242 if len(line) > 950:
238 243 body = quopri.encodestring(body)
239 244 enc = "quoted-printable"
240 245 break
241 246
242 247 msg = email.MIMEText.MIMEText(body, subtype, charset)
243 248 if enc:
244 249 del msg['Content-Transfer-Encoding']
245 250 msg['Content-Transfer-Encoding'] = enc
246 251 return msg
247 252
248 253 def _charsets(ui):
249 254 '''Obtains charsets to send mail parts not containing patches.'''
250 255 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
251 256 fallbacks = [encoding.fallbackencoding.lower(),
252 257 encoding.encoding.lower(), 'utf-8']
253 258 for cs in fallbacks: # find unique charsets while keeping order
254 259 if cs not in charsets:
255 260 charsets.append(cs)
256 261 return [cs for cs in charsets if not cs.endswith('ascii')]
257 262
258 263 def _encode(ui, s, charsets):
259 264 '''Returns (converted) string, charset tuple.
260 265 Finds out best charset by cycling through sendcharsets in descending
261 266 order. Tries both encoding and fallbackencoding for input. Only as
262 267 last resort send as is in fake ascii.
263 268 Caveat: Do not use for mail parts containing patches!'''
264 269 try:
265 270 s.decode('ascii')
266 271 except UnicodeDecodeError:
267 272 sendcharsets = charsets or _charsets(ui)
268 273 for ics in (encoding.encoding, encoding.fallbackencoding):
269 274 try:
270 275 u = s.decode(ics)
271 276 except UnicodeDecodeError:
272 277 continue
273 278 for ocs in sendcharsets:
274 279 try:
275 280 return u.encode(ocs), ocs
276 281 except UnicodeEncodeError:
277 282 pass
278 283 except LookupError:
279 284 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
280 285 # if ascii, or all conversion attempts fail, send (broken) ascii
281 286 return s, 'us-ascii'
282 287
283 288 def headencode(ui, s, charsets=None, display=False):
284 289 '''Returns RFC-2047 compliant header from given string.'''
285 290 if not display:
286 291 # split into words?
287 292 s, cs = _encode(ui, s, charsets)
288 293 return str(email.Header.Header(s, cs))
289 294 return s
290 295
291 296 def _addressencode(ui, name, addr, charsets=None):
292 297 name = headencode(ui, name, charsets)
293 298 try:
294 299 acc, dom = addr.split('@')
295 300 acc = acc.encode('ascii')
296 301 dom = dom.decode(encoding.encoding).encode('idna')
297 302 addr = '%s@%s' % (acc, dom)
298 303 except UnicodeDecodeError:
299 304 raise error.Abort(_('invalid email address: %s') % addr)
300 305 except ValueError:
301 306 try:
302 307 # too strict?
303 308 addr = addr.encode('ascii')
304 309 except UnicodeDecodeError:
305 310 raise error.Abort(_('invalid local address: %s') % addr)
306 311 return email.Utils.formataddr((name, addr))
307 312
308 313 def addressencode(ui, address, charsets=None, display=False):
309 314 '''Turns address into RFC-2047 compliant header.'''
310 315 if display or not address:
311 316 return address or ''
312 317 name, addr = email.Utils.parseaddr(address)
313 318 return _addressencode(ui, name, addr, charsets)
314 319
315 320 def addrlistencode(ui, addrs, charsets=None, display=False):
316 321 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
317 322 A single element of input list may contain multiple addresses, but output
318 323 always has one address per item'''
319 324 if display:
320 325 return [a.strip() for a in addrs if a.strip()]
321 326
322 327 result = []
323 328 for name, addr in email.Utils.getaddresses(addrs):
324 329 if name or addr:
325 330 result.append(_addressencode(ui, name, addr, charsets))
326 331 return result
327 332
328 333 def mimeencode(ui, s, charsets=None, display=False):
329 334 '''creates mime text object, encodes it if needed, and sets
330 335 charset and transfer-encoding accordingly.'''
331 336 cs = 'us-ascii'
332 337 if not display:
333 338 s, cs = _encode(ui, s, charsets)
334 339 return mimetextqp(s, 'plain', cs)
335 340
336 341 def headdecode(s):
337 342 '''Decodes RFC-2047 header'''
338 343 uparts = []
339 344 for part, charset in email.Header.decode_header(s):
340 345 if charset is not None:
341 346 try:
342 347 uparts.append(part.decode(charset))
343 348 continue
344 349 except UnicodeDecodeError:
345 350 pass
346 351 try:
347 352 uparts.append(part.decode('UTF-8'))
348 353 continue
349 354 except UnicodeDecodeError:
350 355 pass
351 356 uparts.append(part.decode('ISO-8859-1'))
352 357 return encoding.tolocal(u' '.join(uparts).encode('UTF-8'))
General Comments 0
You need to be logged in to leave comments. Login now