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