##// END OF EJS Templates
keyring_check handles a few args
Marcin Kasperski -
r188:44833bd3 default
parent child Browse files
Show More
@@ -1,729 +1,728 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 from mercurial import util, sslutil
55 55 from mercurial.i18n import _
56 56 from mercurial.url import passwordmgr
57 57 from mercurial import mail
58 58 from mercurial.mail import SMTPS, STARTTLS
59 59 from mercurial import encoding
60 60
61 61 from urlparse import urlparse
62 62 import urllib2
63 63 import smtplib
64 64 import socket
65 65 import os
66 66 import sys
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-dynamic_username/ 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 if 'keyring' in sys.modules:
115 115 return sys.modules['keyring']
116 116 # mercurial.demandimport incompatibility workaround.
117 117 # various keyring backends fail as they can't properly import helper
118 118 # modules (as demandimport modifies python import behaviour).
119 119 # If you get import errors with demandimport in backtrace, try
120 120 # guessing what to block and extending the list below.
121 121 from mercurial import demandimport
122 122 for blocked_module in [
123 123 "gobject._gobject",
124 124 "configparser",
125 125 "json",
126 126 "abc",
127 127 "io",
128 128 "keyring",
129 129 "gdata.docs.service",
130 130 "gdata.service",
131 131 "types",
132 132 "atom.http",
133 133 "atom.http_interface",
134 134 "atom.service",
135 135 "atom.token_store",
136 136 "ctypes",
137 137 "secretstorage.exceptions",
138 138 "fs.opener",
139 139 ]:
140 140 if blocked_module not in demandimport.ignore:
141 141 demandimport.ignore.append(blocked_module)
142 142
143 143 # Various attempts to define is_demandimport_enabled
144 144 try:
145 145 # Since Mercurial 2.9.1
146 146 is_demandimport_enabled = demandimport.isenabled
147 147 except AttributeError:
148 148 def is_demandimport_enabled():
149 149 """Checks whether demandimport is enabled at the moment"""
150 150 return __import__ == demandimport._demandimport
151 151
152 152 # Shut up warning about uninitialized logging for new keyring versions.
153 153 # But beware 2.6…
154 154 try:
155 155 import logging
156 156 logging.getLogger("keyring").addHandler(logging.NullHandler())
157 157 except: # pylint: disable=bare-except
158 158 pass
159 159
160 160 # Temporarily disable demandimport to make the need of extending
161 161 # the list above less likely.
162 162 if is_demandimport_enabled():
163 163 demandimport.disable()
164 164 try:
165 165 import keyring
166 166 finally:
167 167 demandimport.enable()
168 168 else:
169 169 import keyring
170 170 return keyring
171 171
172 172 #################################################################
173 173 # Actual implementation
174 174 #################################################################
175 175
176 176 KEYRING_SERVICE = "Mercurial"
177 177
178 178
179 179 class PasswordStore(object):
180 180 """
181 181 Helper object handling keyring usage (password save&restore,
182 182 the way passwords are keyed in the keyring).
183 183 """
184 184 def __init__(self):
185 185 self.cache = dict()
186 186
187 187 def get_http_password(self, url, username):
188 188 """
189 189 Checks whether password of username for url is available,
190 190 returns it or None
191 191 """
192 192 return self._read_password_from_keyring(
193 193 self._format_http_key(url, username))
194 194
195 195 def set_http_password(self, url, username, password):
196 196 """Saves password to keyring"""
197 197 self._save_password_to_keyring(
198 198 self._format_http_key(url, username),
199 199 password)
200 200
201 201 def clear_http_password(self, url, username):
202 202 """Drops saved password"""
203 203 self.set_http_password(url, username, "")
204 204
205 205 @staticmethod
206 206 def _format_http_key(url, username):
207 207 """Construct actual key for password identification"""
208 208 return "%s@@%s" % (username, url)
209 209
210 210 def get_smtp_password(self, machine, port, username):
211 211 """Checks for SMTP password in keyring, returns
212 212 password or None"""
213 213 return self._read_password_from_keyring(
214 214 self._format_smtp_key(machine, port, username))
215 215
216 216 def set_smtp_password(self, machine, port, username, password):
217 217 """Saves SMTP password to keyring"""
218 218 self._save_password_to_keyring(
219 219 self._format_smtp_key(machine, port, username),
220 220 password)
221 221
222 222 def clear_smtp_password(self, machine, port, username):
223 223 """Drops saved SMTP password"""
224 224 self.set_smtp_password(machine, port, username, "")
225 225
226 226 @staticmethod
227 227 def _format_smtp_key(machine, port, username):
228 228 """Construct key for SMTP password identification"""
229 229 return "%s@@%s:%s" % (username, machine, str(port))
230 230
231 231 @staticmethod
232 232 def _read_password_from_keyring(pwdkey):
233 233 """Physically read from keyring"""
234 234 keyring = import_keyring()
235 235 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
236 236 # Reverse recoding from next routine
237 237 if isinstance(password, unicode):
238 238 return encoding.tolocal(password.encode('utf-8'))
239 239 return password
240 240
241 241 @staticmethod
242 242 def _save_password_to_keyring(pwdkey, password):
243 243 """Physically write to keyring"""
244 244 keyring = import_keyring()
245 245 # keyring in general expects unicode.
246 246 # Mercurial provides "local" encoding. See #33
247 247 password = encoding.fromlocal(password).decode('utf-8')
248 248 keyring.set_password(
249 249 KEYRING_SERVICE, pwdkey, password)
250 250
251 251 password_store = PasswordStore()
252 252
253 253
254 254 ############################################################
255 255 # Various utils
256 256 ############################################################
257 257
258 258 def _debug(ui, msg):
259 259 """Generic debug message"""
260 260 ui.debug("keyring: " + msg + "\n")
261 261
262 262
263 263 class PwdCache(object):
264 264 """Short term cache, used to preserve passwords
265 265 if they are used twice during a command"""
266 266 def __init__(self):
267 267 self._cache = {}
268 268
269 269 def store(self, realm, url, user, pwd):
270 270 """Saves password"""
271 271 cache_key = (realm, url, user)
272 272 self._cache[cache_key] = pwd
273 273
274 274 def check(self, realm, url, user):
275 275 """Checks for cached password"""
276 276 cache_key = (realm, url, user)
277 277 return self._cache.get(cache_key)
278 278
279 279
280 280 ############################################################
281 281 # HTTP password management
282 282 ############################################################
283 283
284 284
285 285 class HTTPPasswordHandler(object):
286 286 """
287 287 Actual implementation of password handling (user prompting,
288 288 configuration file searching, keyring save&restore).
289 289
290 290 Object of this class is bound as passwordmgr attribute.
291 291 """
292 292 def __init__(self):
293 293 self.pwd_cache = PwdCache()
294 294 self.last_reply = None
295 295
296 296 # Markers and also names used in debug notes. Password source
297 297 SRC_URL = "repository URL"
298 298 SRC_CFGAUTH = "hgrc"
299 299 SRC_MEMCACHE = "temporary cache"
300 300 SRC_KEYRING = "keyring"
301 301
302 302 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
303 303 """
304 304 Looks up for user credentials in various places, returns them
305 305 and information about their source.
306 306
307 307 Used internally inside find_auth and inside informative
308 308 commands (thiis method doesn't cache, doesn't detect bad
309 309 passwords etc, doesn't prompt interactively, doesn't store
310 310 password in keyring).
311 311
312 312 Returns: user, password, SRC_*, actual_url
313 313
314 314 If not found, password and SRC is None, user can be given or
315 315 not, url is always set
316 316 """
317 317 ui = pwmgr.ui
318 318
319 319 # Strip arguments to get actual remote repository url.
320 320 base_url = self.canonical_url(authuri)
321 321
322 322 # Extract username (or password) stored directly in url
323 323 url_user, url_pwd \
324 324 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
325 325 pwmgr, realm, authuri)
326 326 if url_user and url_pwd:
327 327 return url_user, url_pwd, self.SRC_URL, base_url
328 328
329 329 # Consult configuration to normalize url to prefix, and find username
330 330 # (and maybe password)
331 331 auth_user, auth_pwd, keyring_url = self.get_url_config(
332 332 ui, base_url, url_user)
333 333 if auth_user and url_user and (url_user != auth_user):
334 334 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, url_user, auth_user)))
335 335 if auth_user and auth_pwd:
336 336 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
337 337
338 338 if skip_caches:
339 339 return auth_user, None, None, keyring_url
340 340
341 341 # Check memory cache (reuse )
342 342 # Checking the memory cache (there may be many http calls per command)
343 343 cached_pwd = self.pwd_cache.check(realm, keyring_url, auth_user)
344 344 if cached_pwd:
345 345 return auth_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
346 346
347 347 # Load from keyring.
348 348 if auth_user:
349 349 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (auth_user, keyring_url))
350 350 keyring_pwd = password_store.get_http_password(keyring_url, auth_user)
351 351 if keyring_pwd:
352 352 return auth_user, keyring_pwd, self.SRC_KEYRING, keyring_url
353 353
354 354 return auth_user, None, None, keyring_url
355 355
356 356
357 357 @staticmethod
358 358 def prompt_interactively(ui, user, realm, url):
359 359 """Actual interactive prompt"""
360 360 if not ui.interactive():
361 361 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
362 362
363 363 if not user:
364 364 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
365 365
366 366 ui.write(_("http authorization required\n"))
367 367 ui.status(_("realm: %s\n") % realm)
368 368 ui.status(_("url: %s\n") % url)
369 369 if user:
370 370 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
371 371 else:
372 372 user = ui.prompt(_("user:"), default=None)
373 373 pwd = ui.getpass(_("password: "))
374 374 return user, pwd
375 375
376 376 def find_auth(self, pwmgr, realm, authuri, req):
377 377 """
378 378 Actual implementation of find_user_password - different
379 379 ways of obtaining the username and password.
380 380
381 381 Returns pair username, password
382 382 """
383 383 ui = pwmgr.ui
384 384 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
385 385
386 386 # Look in url, cache, etc
387 387 user, pwd, src, final_url = self.get_credentials(
388 388 pwmgr, realm, authuri, skip_caches=after_bad_auth)
389 389 if pwd:
390 390 if src != self.SRC_MEMCACHE:
391 391 self.pwd_cache.store(realm, final_url, user, pwd)
392 392 self._note_last_reply(realm, authuri, user, req)
393 393 _debug(ui, _("Password found in " + src))
394 394 return user, pwd
395 395
396 396 # Last resort: interactive prompt
397 397 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
398 398
399 399 if user:
400 400 # Saving password to the keyring.
401 401 # It is done only if username is permanently set.
402 402 # Otherwise we won't be able to find the password so it
403 403 # does not make much sense to preserve it
404 404 _debug(ui, _("Saving password for %s to keyring") % user)
405 405 password_store.set_http_password(final_url, user, pwd)
406 406
407 407 # Saving password to the memory cache
408 408 self.pwd_cache.store(realm, final_url, user, pwd)
409 409 self._note_last_reply(realm, authuri, user, req)
410 410 _debug(ui, _("Manually entered password"))
411 411 return user, pwd
412 412
413 413 def get_url_config(self, ui, base_url, user):
414 414 """
415 415 Checks configuration to decide whether/which username, prefix,
416 416 and password are configured for given url. Consults [auth] section.
417 417
418 418 Returns tuple (username, password, prefix) containing elements
419 419 found. username and password can be None (if unset), if prefix
420 420 is not found, url itself is returned.
421 421 """
422 422 from mercurial.httpconnection import readauthforuri
423 423 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
424 424 res = readauthforuri(ui, base_url, user)
425 425 if res:
426 426 group, auth_token = res
427 427 else:
428 428 auth_token = None
429 429
430 430 if auth_token:
431 431 username = auth_token.get('username')
432 432 password = auth_token.get('password')
433 433 prefix = auth_token.get('prefix')
434 434 else:
435 435 username = None
436 436 password = None
437 437 prefix = None
438 438
439 439 password_url = self.password_url(base_url, prefix)
440 440 return username, password, password_url
441 441
442 442 def _note_last_reply(self, realm, authuri, user, req):
443 443 """
444 444 Internal helper. Saves info about auth-data obtained,
445 445 preserves them in last_reply, and returns pair user, pwd
446 446 """
447 447 self.last_reply = dict(realm=realm, authuri=authuri,
448 448 user=user, req=req)
449 449
450 450 def _after_bad_auth(self, ui, realm, authuri, req):
451 451 """
452 452 If we are called again just after identical previous
453 453 request, then the previously returned auth must have been
454 454 wrong. So we note this to force password prompt (and avoid
455 455 reusing bad password indefinitely).
456 456
457 457 This routine checks for this condition.
458 458 """
459 459 if self.last_reply:
460 460 if (self.last_reply['realm'] == realm) \
461 461 and (self.last_reply['authuri'] == authuri) \
462 462 and (self.last_reply['req'] == req):
463 463 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
464 464 return True
465 465 return False
466 466
467 467 @staticmethod
468 468 def password_url(base_url, prefix):
469 469 """Calculates actual url identifying the password. Takes
470 470 configured prefix under consideration (so can be shorter
471 471 than repo url)"""
472 472 if not prefix or prefix == '*':
473 473 return base_url
474 474 scheme, hostpath = base_url.split('://', 1)
475 475 p = prefix.split('://', 1)
476 476 if len(p) > 1:
477 477 prefix_host_path = p[1]
478 478 else:
479 479 prefix_host_path = prefix
480 480 password_url = scheme + '://' + prefix_host_path
481 481 return password_url
482 482
483 483 @staticmethod
484 484 def canonical_url(authuri):
485 485 """
486 486 Strips query params from url. Used to convert urls like
487 487 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
488 488 to
489 489 https://repo.machine.com/repos/apps/module
490 490 """
491 491 parsed_url = urlparse(authuri)
492 492 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
493 493 parsed_url.path)
494 494
495 495 ############################################################
496 496 # Mercurial monkey-patching
497 497 ############################################################
498 498
499 499
500 500 @monkeypatch_method(passwordmgr)
501 501 def find_user_password(self, realm, authuri):
502 502 """
503 503 keyring-based implementation of username/password query
504 504 for HTTP(S) connections
505 505
506 506 Passwords are saved in gnome keyring, OSX/Chain or other platform
507 507 specific storage and keyed by the repository url
508 508 """
509 509 # Extend object attributes
510 510 if not hasattr(self, '_pwd_handler'):
511 511 self._pwd_handler = HTTPPasswordHandler()
512 512
513 513 if hasattr(self, '_http_req'):
514 514 req = self._http_req
515 515 else:
516 516 req = None
517 517
518 518 return self._pwd_handler.find_auth(self, realm, authuri, req)
519 519
520 520
521 521 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
522 522 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
523 523 """Preserves current HTTP request so it can be consulted
524 524 in find_user_password above"""
525 525 self.passwd._http_req = req
526 526 try:
527 527 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
528 528 finally:
529 529 self.passwd._http_req = None
530 530
531 531
532 532 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
533 533 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
534 534 """Preserves current HTTP request so it can be consulted
535 535 in find_user_password above"""
536 536 self.passwd._http_req = req
537 537 try:
538 538 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
539 539 finally:
540 540 self.passwd._http_req = None
541 541
542 542 ############################################################
543 543 # SMTP support
544 544 ############################################################
545 545
546 546
547 547 def try_smtp_login(ui, smtp_obj, username, password):
548 548 """
549 549 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
550 550 password.
551 551
552 552 Returns:
553 553 - True if login succeeded
554 554 - False if login failed due to the wrong credentials
555 555
556 556 Throws Abort exception if login failed for any other reason.
557 557
558 558 Immediately returns False if password is empty
559 559 """
560 560 if not password:
561 561 return False
562 562 try:
563 563 ui.note(_('(authenticating to mail server as %s)\n') %
564 564 (username))
565 565 smtp_obj.login(username, password)
566 566 return True
567 567 except smtplib.SMTPException, inst:
568 568 if inst.smtp_code == 535:
569 569 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
570 570 return False
571 571 else:
572 572 raise util.Abort(inst)
573 573
574 574
575 575 def keyring_supported_smtp(ui, username):
576 576 """
577 577 keyring-integrated replacement for mercurial.mail._smtp
578 578 Used only when configuration file contains username, but
579 579 does not contain the password.
580 580
581 581 Most of the routine below is copied as-is from
582 582 mercurial.mail._smtp. The only changed part is
583 583 marked with # >>>>> and # <<<<< markers
584 584 """
585 585 local_hostname = ui.config('smtp', 'local_hostname')
586 586 tls = ui.config('smtp', 'tls', 'none')
587 587 # backward compatible: when tls = true, we use starttls.
588 588 starttls = tls == 'starttls' or util.parsebool(tls)
589 589 smtps = tls == 'smtps'
590 590 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
591 591 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
592 592 mailhost = ui.config('smtp', 'host')
593 593 if not mailhost:
594 594 raise util.Abort(_('smtp.host not configured - cannot send mail'))
595 595 verifycert = ui.config('smtp', 'verifycert', 'strict')
596 596 if verifycert not in ['strict', 'loose']:
597 597 if util.parsebool(verifycert) is not False:
598 598 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
599 599 % (verifycert))
600 600 verifycert = False
601 601 if (starttls or smtps) and verifycert:
602 602 sslkwargs = sslutil.sslkwargs(ui, mailhost)
603 603 else:
604 604 sslkwargs = {}
605 605 if smtps:
606 606 ui.note(_('(using smtps)\n'))
607 607 s = SMTPS(sslkwargs, local_hostname=local_hostname)
608 608 elif starttls:
609 609 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
610 610 else:
611 611 s = smtplib.SMTP(local_hostname=local_hostname)
612 612 if smtps:
613 613 defaultport = 465
614 614 else:
615 615 defaultport = 25
616 616 mailport = util.getport(ui.config('smtp', 'port', defaultport))
617 617 ui.note(_('sending mail: smtp host %s, port %s\n') %
618 618 (mailhost, mailport))
619 619 s.connect(host=mailhost, port=mailport)
620 620 if starttls:
621 621 ui.note(_('(using starttls)\n'))
622 622 s.ehlo()
623 623 s.starttls()
624 624 s.ehlo()
625 625 if (starttls or smtps) and verifycert:
626 626 ui.note(_('(verifying remote certificate)\n'))
627 627 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
628 628
629 629 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
630 630 stored = password = password_store.get_smtp_password(
631 631 mailhost, mailport, username)
632 632 # No need to check whether password was found as try_smtp_login
633 633 # just returns False if it is absent.
634 634 while not try_smtp_login(ui, s, username, password):
635 635 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
636 636
637 637 if stored != password:
638 638 password_store.set_smtp_password(
639 639 mailhost, mailport, username, password)
640 640 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
641 641
642 642 def send(sender, recipients, msg):
643 643 try:
644 644 return s.sendmail(sender, recipients, msg)
645 645 except smtplib.SMTPRecipientsRefused, inst:
646 646 recipients = [r[1] for r in inst.recipients.values()]
647 647 raise util.Abort('\n' + '\n'.join(recipients))
648 648 except smtplib.SMTPException, inst:
649 649 raise util.Abort(inst)
650 650
651 651 return send
652 652
653 653 ############################################################
654 654 # SMTP monkeypatching
655 655 ############################################################
656 656
657 657
658 658 @monkeypatch_method(mail)
659 659 def _smtp(ui):
660 660 """
661 661 build an smtp connection and return a function to send email
662 662
663 663 This is the monkeypatched version of _smtp(ui) function from
664 664 mercurial/mail.py. It calls the original unless username
665 665 without password is given in the configuration.
666 666 """
667 667 username = ui.config('smtp', 'username')
668 668 password = ui.config('smtp', 'password')
669 669
670 670 if username and not password:
671 671 return keyring_supported_smtp(ui, username)
672 672 else:
673 673 return _smtp.orig(ui)
674 674
675 675
676 676 ############################################################
677 677 # Custom commands
678 678 ############################################################
679 679
680 def cmd_keyring_check(ui, repo, path=None, **opts):
680 def cmd_keyring_check(ui, repo, *path_args, **opts):
681 681 """
682 682 Prints basic info (whether password is currently saved, and how is
683 683 it identified) for given path or all defined repo paths.
684 684 """
685 685 import re
686 686
687 687 re_http_url = re.compile(r'^https?://')
688 688 defined_paths = [(name, url)
689 689 for name, url in ui.configitems('paths')]
690 if path:
690 if path_args:
691 691 # Maybe parameter is an alias
692 692 defined_paths_dic = dict(defined_paths)
693 resolved = defined_paths_dic.get(path, path)
694 if not re_http_url.search(resolved):
695 ui.status(_("keyring: %s is not http path (%s)\n") % (path, resolved))
696 return
697 paths = [(path, resolved)]
693 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
694 for path_arg in path_args]
698 695 else:
699 paths = [(name, url) for name, url in defined_paths
700 if re_http_url.search(url)]
696 paths = [(name, url) for name, url in defined_paths]
701 697
702 698 if not paths:
703 ui.status(_("keyring: no http path aliases defined\n"))
704 return
699 ui.status(_("keyring_check: no paths defined"))
705 700
706 701 handler = HTTPPasswordHandler()
707 702
708 703 ui.status(_("keyring password save status:\n"))
709 704 for name, url in paths:
705 if not re_http_url.search(url):
706 if path_args:
707 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
708 continue
710 709 user, pwd, source, final_url = handler.get_credentials(passwordmgr(ui), name, url)
711 710 if pwd:
712 711 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
713 712 name, source, user, final_url))
714 713 elif user:
715 714 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
716 715 name, user, final_url))
717 716 else:
718 717 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
719 718 name, final_url))
720 719
721 720
722 721
723 722 cmdtable = {
724 723 "keyring_check": (
725 724 cmd_keyring_check,
726 725 [],
727 726 "keyring_check PATH",
728 727 ),
729 728 }
General Comments 0
You need to be logged in to leave comments. Login now