From 36842b7bbbd52f0086a706264f92b7ba6519ded7 Mon Sep 17 00:00:00 2001 From: Miquel Ruiz Date: Sat, 24 Oct 2015 13:25:04 +0100 Subject: [PATCH 01/16] Ask for email unless --allow-unsafe-registration Add new option that explicitly allows to not provide an email. Fixes #414 --- letsencrypt/cli.py | 14 ++++++++++++-- letsencrypt/display/ops.py | 4 +++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index e9cb31a21..175354f5d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -139,9 +139,9 @@ def _determine_account(args, config): elif len(accounts) == 1: acc = accounts[0] else: # no account registered yet - if args.email is None: + if args.email is None and not args.allow_unsafe_registration: args.email = display_ops.get_email() - if not args.email: # get_email might return "" + else: args.email = None def _tos_cb(regr): @@ -842,6 +842,16 @@ def prepare_and_parse_args(plugins, args): helpful.add( None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") + helpful.add( + None, "--allow-unsafe-registration", action="store_true", + help="Specifying this flag enables registering an account with no " + "email address. This is strongly discouraged, because in the " + "event of key loss or account compromise you will irrevocably " + "lose access to your account. You will also be unable to receive " + "notice about impending expiration of revocation of your " + "certificates. Updates to the Subscriber Agreement will still " + "affect you, and will be effective N days after posting an " + "update to the web site.") helpful.add(None, "-m", "--email", help=config_help("email")) # positional arg shadows --domains, instead of appending, and # --domains is useful, because it can be stored in config diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 37ce66b62..31913e708 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -124,7 +124,9 @@ def get_email(): """ while True: code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address (used for urgent notices and lost key recovery)") + "Enter email address (mandatory since no " + "--allow-unsafe-registration was provided)" + "(used for urgent notices and lost key recovery)") if code == display_util.OK: if le_util.safe_email(email): From 99f9f1b106cc8594feda849d78bdc52f221a9f82 Mon Sep 17 00:00:00 2001 From: Miquel Ruiz Date: Sun, 25 Oct 2015 10:17:25 +0000 Subject: [PATCH 02/16] Rename option and fix displayed info --- letsencrypt/cli.py | 4 ++-- letsencrypt/display/ops.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 175354f5d..a36a2ff4b 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -139,7 +139,7 @@ def _determine_account(args, config): elif len(accounts) == 1: acc = accounts[0] else: # no account registered yet - if args.email is None and not args.allow_unsafe_registration: + if args.email is None and not args.register_unsafely_without_email: args.email = display_ops.get_email() else: args.email = None @@ -843,7 +843,7 @@ def prepare_and_parse_args(plugins, args): None, "-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") helpful.add( - None, "--allow-unsafe-registration", action="store_true", + None, "--register-unsafely-without-email", action="store_true", help="Specifying this flag enables registering an account with no " "email address. This is strongly discouraged, because in the " "event of key loss or account compromise you will irrevocably " diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 31913e708..37e18e6d8 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -124,9 +124,10 @@ def get_email(): """ while True: code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address (mandatory since no " - "--allow-unsafe-registration was provided)" - "(used for urgent notices and lost key recovery)") + "Enter email address " + "(used for urgent notices and lost key recovery)\n\n" + "If you really want to skip this, run the client with " + "--register-unsafely-without-email") if code == display_util.OK: if le_util.safe_email(email): From 37089b9eff18929ffbc99be0769a2916357baa35 Mon Sep 17 00:00:00 2001 From: Miquel Ruiz Date: Sun, 25 Oct 2015 10:18:06 +0000 Subject: [PATCH 03/16] Ensure cancelling without password exits --- letsencrypt/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 8e053e926..959eb9917 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -100,6 +100,11 @@ def register(config, account_storage, tos_cb=None): if account_storage.find_all(): logger.info("There are already existing accounts for %s", config.server) if config.email is None: + if not config.register_unsafely_without_email: + msg = ("No email was provided and " + "--register-unsafely-without-email was not present.") + logger.warn(msg) + raise errors.Error(msg) logger.warn("Registering without email!") # Each new registration shall use a fresh new key From 77f2a29bfe8fa71fe3531c05cdb4296f66397631 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Sun, 15 Nov 2015 23:16:05 -0800 Subject: [PATCH 04/16] Show the message about unsafe registration only conditionally - If the user enters a blank email, or one that doesn't check out --- letsencrypt/display/ops.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 37e18e6d8..5a51647a8 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -114,26 +114,36 @@ def pick_configurator( config, default, plugins, question, (interfaces.IAuthenticator, interfaces.IInstaller)) - -def get_email(): +def get_email(more=False, invalid=False): """Prompt for valid email address. + :param bool more: -- explain why the email is strongly advisable, but how to + skip it + "param bool invalid: -- true if the user just typed something, but it wasn't + a valid-looking email + :returns: Email or ``None`` if cancelled by user. :rtype: str """ - while True: - code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address " - "(used for urgent notices and lost key recovery)\n\n" - "If you really want to skip this, run the client with " - "--register-unsafely-without-email") + msg = "Enter email address (used for urgent notices and lost key recovery)" + if invalid: + msg = "There seem to be problems with that address. " + msg + if more: + msg += ('\n\nIf you really want to skip this, you can run the client with ' + '--register-unsafely-without-email but make sure you backup your ' + 'account key from /etc/letsencrypt/accounts\n\n') + code, email = zope.component.getUtility(interfaces.IDisplay).input(msg) - if code == display_util.OK: - if le_util.safe_email(email): - return email + if code == display_util.OK: + if le_util.safe_email(email): + return email else: - return None + # TODO catch the server's ACME invalid email address error, and + # make a similar call when that happens + return get_email(more=True, invalid=(email != "")) + else: + return None def choose_account(accounts): From 371e57fc51eb74d385575b7d3701067e617eaccd Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 11:17:41 -0800 Subject: [PATCH 05/16] If the server rejects an email address, ask again rather than erroring This is essentially symmetrical with cases where the client itself can tell that what the user entered isn't an email address. --- letsencrypt/client.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 959eb9917..236a15a34 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -115,7 +115,7 @@ def register(config, account_storage, tos_cb=None): backend=default_backend()))) acme = acme_from_config_key(config, key) # TODO: add phone? - regr = acme.register(messages.NewRegistration.from_data(email=config.email)) + regr = perform_registration(acme, config) if regr.terms_of_service is not None: if tos_cb is not None and not tos_cb(regr): @@ -130,6 +130,21 @@ def register(config, account_storage, tos_cb=None): return acc, acme +def perform_registration(acme, config): + """ + Actually register new account, trying repeatedly if there are email + problems + + :returns: the same value as acme.register + """ + try: + regr = acme.register(messages.NewRegistration.from_data(email=config.email)) + except messages.Error, e: + if "MX record" in repr(e): + config.namespace.email = display_ops.get_email(more=True, invalid=True) + return perform_registration(acme, config) + else: + raise class Client(object): """ACME protocol client. From 2d07c017b2b922e65e78d1ea6ba8063de5575c7e Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 12:29:03 -0800 Subject: [PATCH 06/16] Test cases for get_email --- letsencrypt/tests/display/ops_test.py | 32 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 9d4a3a933..d06d24d4f 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -168,9 +168,9 @@ class GetEmailTest(unittest.TestCase): zope.component.provideUtility(mock_display, interfaces.IDisplay) @classmethod - def _call(cls): + def _call(cls, **kwargs): from letsencrypt.display.ops import get_email - return get_email() + return get_email(**kwargs) def test_cancel_none(self): self.input.return_value = (display_util.CANCEL, "foo@bar.baz") @@ -178,18 +178,38 @@ class GetEmailTest(unittest.TestCase): def test_ok_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("letsencrypt.display.ops.le_util" - ".safe_email") as mock_safe_email: + with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.return_value = True self.assertTrue(self._call() is "foo@bar.baz") def test_ok_not_safe(self): self.input.return_value = (display_util.OK, "foo@bar.baz") - with mock.patch("letsencrypt.display.ops.le_util" - ".safe_email") as mock_safe_email: + with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: mock_safe_email.side_effect = [False, True] self.assertTrue(self._call() is "foo@bar.baz") + def test_more_and_invalid_flags(self): + more_txt = "--register-unsafely-without-email" + invalid_txt = "There seem to be problems" + base_txt = "Enter email" + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("letsencrypt.display.ops.le_util.safe_email") as mock_safe_email: + mock_safe_email.return_value = True + self._call() + msg = self.input.call_args[0][0] + self.assertTrue(more_txt not in msg) + self.assertTrue(invalid_txt not in msg) + self.assertTrue(base_txt in msg) + self._call(more=True) + msg = self.input.call_args[0][0] + self.assertTrue(more_txt in msg) + self.assertTrue(invalid_txt not in msg) + self._call(more=True, invalid=True) + msg = self.input.call_args[0][0] + self.assertTrue(more_txt in msg) + self.assertTrue(invalid_txt in msg) + self.assertTrue(base_txt in msg) + class ChooseAccountTest(unittest.TestCase): """Tests for letsencrypt.display.ops.choose_account.""" From c265fb5fb93c294f907a4a074d6aecc25f445463 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 12:46:26 -0800 Subject: [PATCH 07/16] Fix bugs and test cases --- letsencrypt/cli.py | 2 -- letsencrypt/client.py | 2 +- letsencrypt/tests/cli_test.py | 3 ++- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index a36a2ff4b..f9de4d72f 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -141,8 +141,6 @@ def _determine_account(args, config): else: # no account registered yet if args.email is None and not args.register_unsafely_without_email: args.email = display_ops.get_email() - else: - args.email = None def _tos_cb(regr): if args.tos: diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 236a15a34..0ae0e26dd 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -138,7 +138,7 @@ def perform_registration(acme, config): :returns: the same value as acme.register """ try: - regr = acme.register(messages.NewRegistration.from_data(email=config.email)) + return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error, e: if "MX record" in repr(e): config.namespace.email = display_ops.get_email(more=True, invalid=True) diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 500ff074e..df4f67928 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -488,7 +488,8 @@ class DetermineAccountTest(unittest.TestCase): """Tests for letsencrypt.cli._determine_account.""" def setUp(self): - self.args = mock.MagicMock(account=None, email=None) + self.args = mock.MagicMock(account=None, email=None, + register_unsafely_without_email=False) self.config = configuration.NamespaceConfig(self.args) self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')] self.account_storage = account.AccountMemoryStorage() From c6bb119d43379f713cafff72b164073b97b66355 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 17:50:42 -0800 Subject: [PATCH 08/16] Test perform_registration recursion --- letsencrypt/tests/client_test.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index d396e25bc..dec9a0950 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -47,10 +47,20 @@ class RegisterTest(unittest.TestCase): def test_it(self): with mock.patch("letsencrypt.client.acme_client.Client"): - with mock.patch("letsencrypt.account." - "report_new_account"): + with mock.patch("letsencrypt.account.report_new_account"): self._call() + @mock.patch("letsencrypt.account.report_new_account") + @mock.patch("letsencrypt.client.display_ops.get_email") + def test_email_retry(self, _rep, mock_get_email): + from acme import messages + msg = "No MX record for domain ofijfoisjfs.com" + mx_err = messages.Error(detail=msg, typ="malformed", title="title") + with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: + mock_client.register.side_effect = mx_err + self._call() + self.assertEqual(mock_get_email.call_count, 1) + class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" From f74da52320c6d01f6a39670e7a2887285dad64a5 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Mon, 16 Nov 2015 18:31:23 -0800 Subject: [PATCH 09/16] Avoid hacky --email "" case for integration tests --- tests/integration/_common.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index dbc473728..71d745d93 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -23,7 +23,7 @@ letsencrypt_test () { --no-redirect \ --agree-dev-preview \ --agree-tos \ - --email "" \ + --register-unsafely-without-email \ --renew-by-default \ --debug \ -vvvvvvv \ From 6d497b8076a26830acc139117fcda789b9927df4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 12:50:18 -0800 Subject: [PATCH 10/16] Track recent boulder error change --- letsencrypt/client.py | 3 ++- letsencrypt/tests/client_test.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 0ae0e26dd..57ca8d3dd 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -140,7 +140,8 @@ def perform_registration(acme, config): try: return acme.register(messages.NewRegistration.from_data(email=config.email)) except messages.Error, e: - if "MX record" in repr(e): + err = repr(e) + if "MX record" in err or "Validation of contact mailto" in err: config.namespace.email = display_ops.get_email(more=True, invalid=True) return perform_registration(acme, config) else: diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index dec9a0950..45b27f989 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -54,7 +54,7 @@ class RegisterTest(unittest.TestCase): @mock.patch("letsencrypt.client.display_ops.get_email") def test_email_retry(self, _rep, mock_get_email): from acme import messages - msg = "No MX record for domain ofijfoisjfs.com" + msg = "Validation of contact mailto:sousaphone@improbablylongggstring.tld failed" mx_err = messages.Error(detail=msg, typ="malformed", title="title") with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: mock_client.register.side_effect = mx_err From 981b9dd3bc13d756f83bf373434a1876e1d705ad Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 13:43:49 -0800 Subject: [PATCH 11/16] Add test case for trying to register without email --- letsencrypt/tests/client_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 45b27f989..0a7d64a84 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -24,7 +24,7 @@ class RegisterTest(unittest.TestCase): """Tests for letsencrypt.client.register.""" def setUp(self): - self.config = mock.MagicMock(rsa_key_size=1024) + self.config = mock.MagicMock(rsa_key_size=1024, register_unsafely_without_email=False) self.account_storage = account.AccountMemoryStorage() self.tos_cb = mock.MagicMock() @@ -61,6 +61,9 @@ class RegisterTest(unittest.TestCase): self._call() self.assertEqual(mock_get_email.call_count, 1) + def test_needs_email(self): + self.config.email = None + self.assertRaises(errors.Error, self._call) class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" From c35c4f3fbebd8cd487d14a7b9f589f76f5129ed8 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Tue, 17 Nov 2015 16:01:53 -0800 Subject: [PATCH 12/16] Extra docstring --- letsencrypt/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 57ca8d3dd..486eef198 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -135,7 +135,13 @@ def perform_registration(acme, config): Actually register new account, trying repeatedly if there are email problems - :returns: the same value as acme.register + :param .IConfig config: Client configuration. + :param acme.client.Client client: ACME client object. + + :returns: Registration Resource. + :rtype: `acme.messages.RegistrationResource` + + :raises .UnexpectedUpdate: """ try: return acme.register(messages.NewRegistration.from_data(email=config.email)) From ec267cf215152091f2c3d7dd4966f9c5da1b704a Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 20 Nov 2015 14:58:37 -0800 Subject: [PATCH 13/16] "Compute" the minimum height needed to reasonably display input --- letsencrypt/display/util.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 0e9c76e38..7107bfb3b 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -114,7 +114,11 @@ class NcursesDisplay(object): `string` - input entered by the user """ - return self.dialog.inputbox(message, width=self.width) + sections = message.split("\n") + # each section takes at least one line, plus extras if it's longer than self.width + wordlines = [1+(len(section)/self.width) for section in sections] + height = 6+ sum(wordlines) + len(sections) + return self.dialog.inputbox(message, width=self.width, height=height) def yesno(self, message, yes_label="Yes", no_label="No"): """Display a Yes/No dialog box. From 1a4d7c144529c4c0986c4620167cde4aa2f3d4eb Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 20 Nov 2015 15:12:05 -0800 Subject: [PATCH 14/16] Lintmonster --- letsencrypt/client.py | 2 ++ letsencrypt/display/util.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 486eef198..3dbf9d337 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -130,6 +130,7 @@ def register(config, account_storage, tos_cb=None): return acc, acme + def perform_registration(acme, config): """ Actually register new account, trying repeatedly if there are email @@ -153,6 +154,7 @@ def perform_registration(acme, config): else: raise + class Client(object): """ACME protocol client. diff --git a/letsencrypt/display/util.py b/letsencrypt/display/util.py index 7107bfb3b..01a8cbc92 100644 --- a/letsencrypt/display/util.py +++ b/letsencrypt/display/util.py @@ -104,6 +104,7 @@ class NcursesDisplay(object): return code, int(tag) - 1 + def input(self, message): """Display an input box to the user. @@ -116,10 +117,11 @@ class NcursesDisplay(object): """ sections = message.split("\n") # each section takes at least one line, plus extras if it's longer than self.width - wordlines = [1+(len(section)/self.width) for section in sections] - height = 6+ sum(wordlines) + len(sections) + wordlines = [1 + (len(section)/self.width) for section in sections] + height = 6 + sum(wordlines) + len(sections) return self.dialog.inputbox(message, width=self.width, height=height) + def yesno(self, message, yes_label="Yes", no_label="No"): """Display a Yes/No dialog box. From 3fec57d8548d4cd200dcae69a5ea8ef2ec3c50f5 Mon Sep 17 00:00:00 2001 From: Brad Warren Date: Fri, 20 Nov 2015 17:26:29 -0800 Subject: [PATCH 15/16] Fixed test --- letsencrypt/tests/client_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 0a7d64a84..160dd55c1 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -57,7 +57,7 @@ class RegisterTest(unittest.TestCase): msg = "Validation of contact mailto:sousaphone@improbablylongggstring.tld failed" mx_err = messages.Error(detail=msg, typ="malformed", title="title") with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: - mock_client.register.side_effect = mx_err + mock_client().register.side_effect = [mx_err, mock.MagicMock()] self._call() self.assertEqual(mock_get_email.call_count, 1) From c3e2c58272fd53fada865d5f0a708dc156c506a4 Mon Sep 17 00:00:00 2001 From: Peter Eckersley Date: Fri, 20 Nov 2015 18:57:57 -0800 Subject: [PATCH 16/16] Fix comment nits --- letsencrypt/display/ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 5a51647a8..33a69b2a3 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -117,9 +117,9 @@ def pick_configurator( def get_email(more=False, invalid=False): """Prompt for valid email address. - :param bool more: -- explain why the email is strongly advisable, but how to + :param bool more: explain why the email is strongly advisable, but how to skip it - "param bool invalid: -- true if the user just typed something, but it wasn't + :param bool invalid: true if the user just typed something, but it wasn't a valid-looking email :returns: Email or ``None`` if cancelled by user.