##// END OF EJS Templates
fix smtp with mercurial tip...
Dan Villiom Podlaski Christiansen -
r228:d2baaf23 default
parent child Browse files
Show More
@@ -1,805 +1,814 b''
1 1 # -*- coding: utf-8 -*-
2 2 #
3 3 # mercurial_keyring: save passwords in password database
4 4 #
5 5 # Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 6 # All rights reserved.
7 7 #
8 8 # Redistribution and use in source and binary forms, with or without
9 9 # modification, are permitted provided that the following conditions
10 10 # are met:
11 11 # 1. Redistributions of source code must retain the above copyright
12 12 # notice, this list of conditions and the following disclaimer.
13 13 # 2. Redistributions in binary form must reproduce the above copyright
14 14 # notice, this list of conditions and the following disclaimer in the
15 15 # documentation and/or other materials provided with the distribution.
16 16 # 3. The name of the author may not be used to endorse or promote products
17 17 # derived from this software without specific prior written permission.
18 18 #
19 19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 29 #
30 30 # See README.txt for more details.
31 31
32 32 '''securely save HTTP and SMTP passwords to encrypted storage
33 33
34 34 mercurial_keyring securely saves HTTP and SMTP passwords in password
35 35 databases (Gnome Keyring, KDE KWallet, OSXKeyChain, Win32 crypto
36 36 services).
37 37
38 38 The process is automatic. Whenever bare Mercurial just prompts for
39 39 the password, Mercurial with mercurial_keyring enabled checks whether
40 40 saved password is available first. If so, it is used. If not, you
41 41 will be prompted for the password, but entered password will be
42 42 saved for the future use.
43 43
44 44 In case saved password turns out to be invalid (HTTP or SMTP login
45 45 fails) it is dropped, and you are asked for current password.
46 46
47 47 Actual password storage is implemented by Python keyring library, this
48 48 extension glues those services to Mercurial. Consult keyring
49 49 documentation for information how to configure actual password
50 50 backend (by default keyring guesses, usually correctly, for example
51 51 you get KDE Wallet under KDE, and Gnome Keyring under Gnome or Unity).
52 52 '''
53 53
54 54 import urllib2
55 55 import smtplib
56 56 import socket
57 57 import os
58 58 import sys
59 59 import re
60 60
61 61 from mercurial import util, sslutil
62 62 from mercurial.i18n import _
63 63 from mercurial.url import passwordmgr
64 64 from mercurial import mail
65 65 from mercurial.mail import SMTPS, STARTTLS
66 66 from mercurial import encoding
67 67
68 68 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
69 69
70 70 ###########################################################################
71 71 # Specific import trickery
72 72 ###########################################################################
73 73
74 74
75 75 def import_meu():
76 76 """
77 77 Convoluted import of mercurial_extension_utils, which helps
78 78 TortoiseHg/Win setups. This routine and it's use below
79 79 performs equivalent of
80 80 from mercurial_extension_utils import monkeypatch_method
81 81 but looks for some non-path directories.
82 82 """
83 83 try:
84 84 import mercurial_extension_utils
85 85 except ImportError:
86 86 my_dir = os.path.dirname(__file__)
87 87 sys.path.extend([
88 88 # In the same dir (manual or site-packages after pip)
89 89 my_dir,
90 90 # Developer clone
91 91 os.path.join(os.path.dirname(my_dir), "extension_utils"),
92 92 # Side clone
93 93 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
94 94 ])
95 95 try:
96 96 import mercurial_extension_utils
97 97 except ImportError:
98 98 raise util.Abort(_("""Can not import mercurial_extension_utils.
99 99 Please install this module in Python path.
100 100 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
101 101 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
102 102 return mercurial_extension_utils
103 103
104 104 meu = import_meu()
105 105 monkeypatch_method = meu.monkeypatch_method
106 106
107 107
108 108 def import_keyring():
109 109 """
110 110 Importing keyring happens to be costly if wallet is slow, so we delay it
111 111 until it is really needed. The routine below also works around various
112 112 demandimport-related problems.
113 113 """
114 114 # mercurial.demandimport incompatibility workaround.
115 115 # various keyring backends fail as they can't properly import helper
116 116 # modules (as demandimport modifies python import behaviour).
117 117 # If you get import errors with demandimport in backtrace, try
118 118 # guessing what to block and extending the list below.
119 119 mod, was_imported_now = meu.direct_import_ext(
120 120 "keyring", [
121 121 "gobject._gobject",
122 122 "configparser",
123 123 "json",
124 124 "abc",
125 125 "io",
126 126 "keyring",
127 127 "gdata.docs.service",
128 128 "gdata.service",
129 129 "types",
130 130 "atom.http",
131 131 "atom.http_interface",
132 132 "atom.service",
133 133 "atom.token_store",
134 134 "ctypes",
135 135 "secretstorage.exceptions",
136 136 "fs.opener",
137 137 ])
138 138 if was_imported_now:
139 139 # Shut up warning about uninitialized logging by keyring
140 140 meu.disable_logging("keyring")
141 141 return mod
142 142
143 143 #################################################################
144 144 # Actual implementation
145 145 #################################################################
146 146
147 147 KEYRING_SERVICE = "Mercurial"
148 148
149 149
150 150 class PasswordStore(object):
151 151 """
152 152 Helper object handling keyring usage (password save&restore,
153 153 the way passwords are keyed in the keyring).
154 154 """
155 155 def __init__(self):
156 156 self.cache = dict()
157 157
158 158 def get_http_password(self, url, username):
159 159 """
160 160 Checks whether password of username for url is available,
161 161 returns it or None
162 162 """
163 163 return self._read_password_from_keyring(
164 164 self._format_http_key(url, username))
165 165
166 166 def set_http_password(self, url, username, password):
167 167 """Saves password to keyring"""
168 168 self._save_password_to_keyring(
169 169 self._format_http_key(url, username),
170 170 password)
171 171
172 172 def clear_http_password(self, url, username):
173 173 """Drops saved password"""
174 174 self.set_http_password(url, username, "")
175 175
176 176 @staticmethod
177 177 def _format_http_key(url, username):
178 178 """Construct actual key for password identification"""
179 179 return "%s@@%s" % (username, url)
180 180
181 181 def get_smtp_password(self, machine, port, username):
182 182 """Checks for SMTP password in keyring, returns
183 183 password or None"""
184 184 return self._read_password_from_keyring(
185 185 self._format_smtp_key(machine, port, username))
186 186
187 187 def set_smtp_password(self, machine, port, username, password):
188 188 """Saves SMTP password to keyring"""
189 189 self._save_password_to_keyring(
190 190 self._format_smtp_key(machine, port, username),
191 191 password)
192 192
193 193 def clear_smtp_password(self, machine, port, username):
194 194 """Drops saved SMTP password"""
195 195 self.set_smtp_password(machine, port, username, "")
196 196
197 197 @staticmethod
198 198 def _format_smtp_key(machine, port, username):
199 199 """Construct key for SMTP password identification"""
200 200 return "%s@@%s:%s" % (username, machine, str(port))
201 201
202 202 @staticmethod
203 203 def _read_password_from_keyring(pwdkey):
204 204 """Physically read from keyring"""
205 205 keyring = import_keyring()
206 206 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
207 207 # Reverse recoding from next routine
208 208 if isinstance(password, unicode):
209 209 return encoding.tolocal(password.encode('utf-8'))
210 210 return password
211 211
212 212 @staticmethod
213 213 def _save_password_to_keyring(pwdkey, password):
214 214 """Physically write to keyring"""
215 215 keyring = import_keyring()
216 216 # keyring in general expects unicode.
217 217 # Mercurial provides "local" encoding. See #33
218 218 password = encoding.fromlocal(password).decode('utf-8')
219 219 keyring.set_password(
220 220 KEYRING_SERVICE, pwdkey, password)
221 221
222 222 password_store = PasswordStore()
223 223
224 224
225 225 ############################################################
226 226 # Various utils
227 227 ############################################################
228 228
229 229 def _debug(ui, msg):
230 230 """Generic debug message"""
231 231 ui.debug("keyring: " + msg + "\n")
232 232
233 233
234 234 class PwdCache(object):
235 235 """Short term cache, used to preserve passwords
236 236 if they are used twice during a command"""
237 237 def __init__(self):
238 238 self._cache = {}
239 239
240 240 def store(self, realm, url, user, pwd):
241 241 """Saves password"""
242 242 cache_key = (realm, url, user)
243 243 self._cache[cache_key] = pwd
244 244
245 245 def check(self, realm, url, user):
246 246 """Checks for cached password"""
247 247 cache_key = (realm, url, user)
248 248 return self._cache.get(cache_key)
249 249
250 250
251 251 _re_http_url = re.compile(r'^https?://')
252 252
253 253 def is_http_path(url):
254 254 return bool(_re_http_url.search(url))
255 255
256 256 ############################################################
257 257 # HTTP password management
258 258 ############################################################
259 259
260 260
261 261 class HTTPPasswordHandler(object):
262 262 """
263 263 Actual implementation of password handling (user prompting,
264 264 configuration file searching, keyring save&restore).
265 265
266 266 Object of this class is bound as passwordmgr attribute.
267 267 """
268 268 def __init__(self):
269 269 self.pwd_cache = PwdCache()
270 270 self.last_reply = None
271 271
272 272 # Markers and also names used in debug notes. Password source
273 273 SRC_URL = "repository URL"
274 274 SRC_CFGAUTH = "hgrc"
275 275 SRC_MEMCACHE = "temporary cache"
276 276 SRC_URLCACHE = "urllib temporary cache"
277 277 SRC_KEYRING = "keyring"
278 278
279 279 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
280 280 """
281 281 Looks up for user credentials in various places, returns them
282 282 and information about their source.
283 283
284 284 Used internally inside find_auth and inside informative
285 285 commands (thiis method doesn't cache, doesn't detect bad
286 286 passwords etc, doesn't prompt interactively, doesn't store
287 287 password in keyring).
288 288
289 289 Returns: user, password, SRC_*, actual_url
290 290
291 291 If not found, password and SRC is None, user can be given or
292 292 not, url is always set
293 293 """
294 294 ui = pwmgr.ui
295 295
296 296 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
297 297 base_url = str(parsed_url)
298 298 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
299 299 (base_url, url_user or '', url_passwd and '******' or ''))
300 300
301 301 # Extract username (or password) stored directly in url
302 302 if url_user and url_passwd:
303 303 return url_user, url_passwd, self.SRC_URL, base_url
304 304
305 305 # Extract data from urllib (in case it was already stored)
306 306 urllib_user, urllib_pwd \
307 307 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
308 308 pwmgr, realm, authuri)
309 309 if urllib_user and urllib_pwd:
310 310 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
311 311
312 312 actual_user = url_user or urllib_user
313 313
314 314 # Consult configuration to normalize url to prefix, and find username
315 315 # (and maybe password)
316 316 auth_user, auth_pwd, keyring_url = self.get_url_config(
317 317 ui, parsed_url, actual_user)
318 318 if auth_user and actual_user and (actual_user != auth_user):
319 319 raise util.Abort(_('keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, actual_user, auth_user)))
320 320 if auth_user and auth_pwd:
321 321 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
322 322
323 323 actual_user = actual_user or auth_user
324 324
325 325 if skip_caches:
326 326 return actual_user, None, None, keyring_url
327 327
328 328 # Check memory cache (reuse )
329 329 # Checking the memory cache (there may be many http calls per command)
330 330 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
331 331 if cached_pwd:
332 332 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
333 333
334 334 # Load from keyring.
335 335 if actual_user:
336 336 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (actual_user, keyring_url))
337 337 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
338 338 if keyring_pwd:
339 339 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
340 340
341 341 return actual_user, None, None, keyring_url
342 342
343 343 @staticmethod
344 344 def prompt_interactively(ui, user, realm, url):
345 345 """Actual interactive prompt"""
346 346 if not ui.interactive():
347 347 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
348 348
349 349 if not user:
350 350 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
351 351
352 352 ui.write(_("http authorization required\n"))
353 353 ui.status(_("realm: %s\n") % realm)
354 354 ui.status(_("url: %s\n") % url)
355 355 if user:
356 356 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
357 357 else:
358 358 user = ui.prompt(_("user:"), default=None)
359 359 pwd = ui.getpass(_("password: "))
360 360 return user, pwd
361 361
362 362 def find_auth(self, pwmgr, realm, authuri, req):
363 363 """
364 364 Actual implementation of find_user_password - different
365 365 ways of obtaining the username and password.
366 366
367 367 Returns pair username, password
368 368 """
369 369 ui = pwmgr.ui
370 370 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
371 371
372 372 # Look in url, cache, etc
373 373 user, pwd, src, final_url = self.get_credentials(
374 374 pwmgr, realm, authuri, skip_caches=after_bad_auth)
375 375 if pwd:
376 376 if src != self.SRC_MEMCACHE:
377 377 self.pwd_cache.store(realm, final_url, user, pwd)
378 378 self._note_last_reply(realm, authuri, user, req)
379 379 _debug(ui, _("Password found in " + src))
380 380 return user, pwd
381 381
382 382 # Last resort: interactive prompt
383 383 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
384 384
385 385 if user:
386 386 # Saving password to the keyring.
387 387 # It is done only if username is permanently set.
388 388 # Otherwise we won't be able to find the password so it
389 389 # does not make much sense to preserve it
390 390 _debug(ui, _("Saving password for %s to keyring") % user)
391 391 try:
392 392 password_store.set_http_password(final_url, user, pwd)
393 393 except keyring.errors.PasswordSetError, e:
394 394 ui.traceback()
395 395 ui.warn(_("warning: failed to save password in keyring\n"))
396 396
397 397 # Saving password to the memory cache
398 398 self.pwd_cache.store(realm, final_url, user, pwd)
399 399 self._note_last_reply(realm, authuri, user, req)
400 400 _debug(ui, _("Manually entered password"))
401 401 return user, pwd
402 402
403 403 def get_url_config(self, ui, parsed_url, user):
404 404 """
405 405 Checks configuration to decide whether/which username, prefix,
406 406 and password are configured for given url. Consults [auth] section.
407 407
408 408 Returns tuple (username, password, prefix) containing elements
409 409 found. username and password can be None (if unset), if prefix
410 410 is not found, url itself is returned.
411 411 """
412 412 base_url = str(parsed_url)
413 413
414 414 from mercurial.httpconnection import readauthforuri
415 415 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
416 416 res = readauthforuri(ui, base_url, user)
417 417 # If it user-less version not work, let's try with added username to handle
418 418 # both config conventions
419 419 if (not res) and user:
420 420 parsed_url.user = user
421 421 res = readauthforuri(ui, str(parsed_url), user)
422 422 parsed_url.user = None
423 423 if res:
424 424 group, auth_token = res
425 425 else:
426 426 auth_token = None
427 427
428 428 if auth_token:
429 429 username = auth_token.get('username')
430 430 password = auth_token.get('password')
431 431 prefix = auth_token.get('prefix')
432 432 else:
433 433 username = user
434 434 password = None
435 435 prefix = None
436 436
437 437 password_url = self.password_url(base_url, prefix)
438 438
439 439 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
440 440 password_url, username, '********' if password else '', prefix))
441 441
442 442 return username, password, password_url
443 443
444 444 def _note_last_reply(self, realm, authuri, user, req):
445 445 """
446 446 Internal helper. Saves info about auth-data obtained,
447 447 preserves them in last_reply, and returns pair user, pwd
448 448 """
449 449 self.last_reply = dict(realm=realm, authuri=authuri,
450 450 user=user, req=req)
451 451
452 452 def _after_bad_auth(self, ui, realm, authuri, req):
453 453 """
454 454 If we are called again just after identical previous
455 455 request, then the previously returned auth must have been
456 456 wrong. So we note this to force password prompt (and avoid
457 457 reusing bad password indefinitely).
458 458
459 459 This routine checks for this condition.
460 460 """
461 461 if self.last_reply:
462 462 if (self.last_reply['realm'] == realm) \
463 463 and (self.last_reply['authuri'] == authuri) \
464 464 and (self.last_reply['req'] == req):
465 465 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
466 466 return True
467 467 return False
468 468
469 469 @staticmethod
470 470 def password_url(base_url, prefix):
471 471 """Calculates actual url identifying the password. Takes
472 472 configured prefix under consideration (so can be shorter
473 473 than repo url)"""
474 474 if not prefix or prefix == '*':
475 475 return base_url
476 476 scheme, hostpath = base_url.split('://', 1)
477 477 p = prefix.split('://', 1)
478 478 if len(p) > 1:
479 479 prefix_host_path = p[1]
480 480 else:
481 481 prefix_host_path = prefix
482 482 password_url = scheme + '://' + prefix_host_path
483 483 return password_url
484 484
485 485 @staticmethod
486 486 def unpack_url(authuri):
487 487 """
488 488 Takes original url for which authentication is attempted and:
489 489
490 490 - Strips query params from url. Used to convert urls like
491 491 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
492 492 to
493 493 https://repo.machine.com/repos/apps/module
494 494
495 495 - Extracts username and password, if present, and removes them from url
496 496 (so prefix matching works properly)
497 497
498 498 Returns url, user, password
499 499 where url is mercurial.util.url object already stripped of all those
500 500 params.
501 501 """
502 502 # mercurial.util.url, rather handy url parser
503 503 parsed_url = util.url(authuri)
504 504 parsed_url.query = ''
505 505 parsed_url.fragment = None
506 506 # Strip arguments to get actual remote repository url.
507 507 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
508 508 # parsed_url.path)
509 509 user = parsed_url.user
510 510 passwd = parsed_url.passwd
511 511 parsed_url.user = None
512 512 parsed_url.passwd = None
513 513
514 514 return parsed_url, user, passwd
515 515
516 516
517 517 ############################################################
518 518 # Mercurial monkey-patching
519 519 ############################################################
520 520
521 521
522 522 @monkeypatch_method(passwordmgr)
523 523 def find_user_password(self, realm, authuri):
524 524 """
525 525 keyring-based implementation of username/password query
526 526 for HTTP(S) connections
527 527
528 528 Passwords are saved in gnome keyring, OSX/Chain or other platform
529 529 specific storage and keyed by the repository url
530 530 """
531 531 # Extend object attributes
532 532 if not hasattr(self, '_pwd_handler'):
533 533 self._pwd_handler = HTTPPasswordHandler()
534 534
535 535 if hasattr(self, '_http_req'):
536 536 req = self._http_req
537 537 else:
538 538 req = None
539 539
540 540 return self._pwd_handler.find_auth(self, realm, authuri, req)
541 541
542 542
543 543 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
544 544 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
545 545 """Preserves current HTTP request so it can be consulted
546 546 in find_user_password above"""
547 547 self.passwd._http_req = req
548 548 try:
549 549 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
550 550 finally:
551 551 self.passwd._http_req = None
552 552
553 553
554 554 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
555 555 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
556 556 """Preserves current HTTP request so it can be consulted
557 557 in find_user_password above"""
558 558 self.passwd._http_req = req
559 559 try:
560 560 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
561 561 finally:
562 562 self.passwd._http_req = None
563 563
564 564 ############################################################
565 565 # SMTP support
566 566 ############################################################
567 567
568 568
569 569 def try_smtp_login(ui, smtp_obj, username, password):
570 570 """
571 571 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
572 572 password.
573 573
574 574 Returns:
575 575 - True if login succeeded
576 576 - False if login failed due to the wrong credentials
577 577
578 578 Throws Abort exception if login failed for any other reason.
579 579
580 580 Immediately returns False if password is empty
581 581 """
582 582 if not password:
583 583 return False
584 584 try:
585 585 ui.note(_('(authenticating to mail server as %s)\n') %
586 586 (username))
587 587 smtp_obj.login(username, password)
588 588 return True
589 589 except smtplib.SMTPException, inst:
590 590 if inst.smtp_code == 535:
591 591 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
592 592 return False
593 593 else:
594 594 raise util.Abort(inst)
595 595
596 596
597 597 def keyring_supported_smtp(ui, username):
598 598 """
599 599 keyring-integrated replacement for mercurial.mail._smtp
600 600 Used only when configuration file contains username, but
601 601 does not contain the password.
602 602
603 603 Most of the routine below is copied as-is from
604 604 mercurial.mail._smtp. The only changed part is
605 605 marked with # >>>>> and # <<<<< markers
606 606 """
607 607 local_hostname = ui.config('smtp', 'local_hostname')
608 608 tls = ui.config('smtp', 'tls', 'none')
609 609 # backward compatible: when tls = true, we use starttls.
610 610 starttls = tls == 'starttls' or util.parsebool(tls)
611 611 smtps = tls == 'smtps'
612 612 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
613 613 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
614 614 mailhost = ui.config('smtp', 'host')
615 615 if not mailhost:
616 616 raise util.Abort(_('smtp.host not configured - cannot send mail'))
617 617 verifycert = ui.config('smtp', 'verifycert', 'strict')
618 618 if verifycert not in ['strict', 'loose']:
619 619 if util.parsebool(verifycert) is not False:
620 620 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
621 621 % (verifycert))
622 622 verifycert = False
623 if (starttls or smtps) and verifycert:
623 if getattr(sslutil, 'sslkwargs', None) is None:
624 sslkwargs = None
625 elif (starttls or smtps) and verifycert:
624 626 sslkwargs = sslutil.sslkwargs(ui, mailhost)
625 627 else:
626 628 sslkwargs = {}
627 629 if smtps:
628 630 ui.note(_('(using smtps)\n'))
629 631
630 632 # mercurial 3.8 added a mandatory host arg
631 if 'host' in SMTPS.__init__.__code__.co_varnames:
633 if not sslkwargs:
634 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
635 elif 'host' in SMTPS.__init__.__code__.co_varnames:
632 636 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
633 637 else:
634 638 s = SMTPS(sslkwargs, local_hostname=local_hostname)
635 639 elif starttls:
636 if 'host' in STARTTLS.__init__.__code__.co_varnames:
640 if not sslkwargs:
641 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
642 elif 'host' in STARTTLS.__init__.__code__.co_varnames:
637 643 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
638 644 else:
639 645 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
640 646 else:
641 647 s = smtplib.SMTP(local_hostname=local_hostname)
642 648 if smtps:
643 649 defaultport = 465
644 650 else:
645 651 defaultport = 25
646 652 mailport = util.getport(ui.config('smtp', 'port', defaultport))
647 653 ui.note(_('sending mail: smtp host %s, port %s\n') %
648 654 (mailhost, mailport))
649 655 s.connect(host=mailhost, port=mailport)
650 656 if starttls:
651 657 ui.note(_('(using starttls)\n'))
652 658 s.ehlo()
653 659 s.starttls()
654 660 s.ehlo()
655 661 if (starttls or smtps) and verifycert:
656 662 ui.note(_('(verifying remote certificate)\n'))
657 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
663 if getattr(sslutil, 'validatesocket', None):
664 sslutil.validatesocket(s.sock)
665 else:
666 validator(ui, mailhost)(s.sock, verifycert == 'strict')
658 667
659 668 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
660 669 stored = password = password_store.get_smtp_password(
661 670 mailhost, mailport, username)
662 671 # No need to check whether password was found as try_smtp_login
663 672 # just returns False if it is absent.
664 673 while not try_smtp_login(ui, s, username, password):
665 674 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
666 675
667 676 if stored != password:
668 677 password_store.set_smtp_password(
669 678 mailhost, mailport, username, password)
670 679 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
671 680
672 681 def send(sender, recipients, msg):
673 682 try:
674 683 return s.sendmail(sender, recipients, msg)
675 684 except smtplib.SMTPRecipientsRefused, inst:
676 685 recipients = [r[1] for r in inst.recipients.values()]
677 686 raise util.Abort('\n' + '\n'.join(recipients))
678 687 except smtplib.SMTPException, inst:
679 688 raise util.Abort(inst)
680 689
681 690 return send
682 691
683 692 ############################################################
684 693 # SMTP monkeypatching
685 694 ############################################################
686 695
687 696
688 697 @monkeypatch_method(mail)
689 698 def _smtp(ui):
690 699 """
691 700 build an smtp connection and return a function to send email
692 701
693 702 This is the monkeypatched version of _smtp(ui) function from
694 703 mercurial/mail.py. It calls the original unless username
695 704 without password is given in the configuration.
696 705 """
697 706 username = ui.config('smtp', 'username')
698 707 password = ui.config('smtp', 'password')
699 708
700 709 if username and not password:
701 710 return keyring_supported_smtp(ui, username)
702 711 else:
703 712 return _smtp.orig(ui)
704 713
705 714
706 715 ############################################################
707 716 # Custom commands
708 717 ############################################################
709 718
710 719 cmdtable = {}
711 720 command = meu.command(cmdtable)
712 721
713 722
714 723 @command('keyring_check',
715 724 [],
716 725 _("keyring_check [PATH]"),
717 726 optionalrepo=True)
718 727 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
719 728 """
720 729 Prints basic info (whether password is currently saved, and how is
721 730 it identified) for given path.
722 731
723 732 Can be run without parameters to show status for all (current repository) paths which
724 733 are HTTP-like.
725 734 """
726 735 defined_paths = [(name, url)
727 736 for name, url in ui.configitems('paths')]
728 737 if path_args:
729 738 # Maybe parameter is an alias
730 739 defined_paths_dic = dict(defined_paths)
731 740 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
732 741 for path_arg in path_args]
733 742 else:
734 743 if not repo:
735 744 ui.status(_("Url to check not specified. Either run ``hg keyring_check https://...``, or run the command inside some repository (to test all defined paths).\n"))
736 745 return
737 746 paths = [(name, url) for name, url in defined_paths]
738 747
739 748 if not paths:
740 749 ui.status(_("keyring_check: no paths defined\n"))
741 750 return
742 751
743 752 handler = HTTPPasswordHandler()
744 753
745 754 ui.status(_("keyring password save status:\n"))
746 755 for name, url in paths:
747 756 if not is_http_path(url):
748 757 if path_args:
749 758 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
750 759 continue
751 760 user, pwd, source, final_url = handler.get_credentials(
752 761 passwordmgr(ui), name, url)
753 762 if pwd:
754 763 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
755 764 name, source, user, final_url))
756 765 elif user:
757 766 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
758 767 name, user, final_url))
759 768 else:
760 769 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
761 770 name, final_url))
762 771
763 772
764 773 @command('keyring_clear',
765 774 [],
766 775 _('hg keyring_clear PATH-OR-ALIAS'),
767 776 optionalrepo=True)
768 777 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
769 778 """
770 779 Drops password bound to given path (if any is saved).
771 780
772 781 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
773 782 of path alias (``bitbucket``).
774 783 """
775 784 path_url = path
776 785 for name, url in ui.configitems('paths'):
777 786 if name == path:
778 787 path_url = url
779 788 break
780 789 if not is_http_path(path_url):
781 790 ui.status(_("%s is not a http path (and %s can't be resolved as path alias)\n") % (path, path_url))
782 791 return
783 792
784 793 handler = HTTPPasswordHandler()
785 794
786 795 user, pwd, source, final_url = handler.get_credentials(
787 796 passwordmgr(ui), path, path_url)
788 797 if not user:
789 798 ui.status(_("Username not configured for url %s\n") % final_url)
790 799 return
791 800 if not pwd:
792 801 ui.status(_("No password is saved for user %s, url %s\n") % (
793 802 user, final_url))
794 803 return
795 804
796 805 if source != handler.SRC_KEYRING:
797 806 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
798 807 user, final_url, source))
799 808
800 809 password_store.clear_http_password(final_url, user)
801 810 ui.status(_("Password removed for user %s, url %s\n") % (
802 811 user, final_url))
803 812
804 813
805 814 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
General Comments 0
You need to be logged in to leave comments. Login now