Remove warnings about empty email. (#10214)

We strongly encouraged providing an email address because we wanted
people to get expiration notices to ensure that even if their Certbot
install broke, they could fix it before their site goes down.

Now that Let's Encrypt is getting rid of expiration notices
(https://letsencrypt.org/2025/01/22/ending-expiration-emails/), we can
remove some of the encouragement, providing a smoother user experience.
This commit is contained in:
Jacob Hoffman-Andrews
2025-03-06 14:38:52 -08:00
committed by GitHub
parent 2a92e22332
commit c323af7be9
8 changed files with 23 additions and 68 deletions
+2
View File
@@ -10,6 +10,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
### Changed ### Changed
* The --register-unsafely-without-email flag is no longer needed in non-interactive mode.
* In interactive mode, pressing Enter at the email prompt will register without an email.
* deprecated `acme.crypto_util.dump_pyopenssl_chain` * deprecated `acme.crypto_util.dump_pyopenssl_chain`
* deprecated `acme.crypto_util._pyopenssl_cert_or_req_all_names` * deprecated `acme.crypto_util._pyopenssl_cert_or_req_all_names`
* deprecated `acme.crypto_util._pyopenssl_cert_or_req_san` * deprecated `acme.crypto_util._pyopenssl_cert_or_req_san`
+1 -5
View File
@@ -171,11 +171,7 @@ def prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: List[st
helpful.add( helpful.add(
["register", "automation"], "--register-unsafely-without-email", action="store_true", ["register", "automation"], "--register-unsafely-without-email", action="store_true",
default=flag_default("register_unsafely_without_email"), default=flag_default("register_unsafely_without_email"),
help="Specifying this flag enables registering an account with no " help=argparse.SUPPRESS)
"email address. This is strongly discouraged, because you will be "
"unable to receive notice about impending expiration or "
"revocation of your certificates or problems with your Certbot "
"installation that will lead to failure to renew.")
helpful.add( helpful.add(
["register", "update_account", "unregister", "automation"], "-m", "--email", ["register", "update_account", "unregister", "automation"], "-m", "--email",
default=flag_default("email"), default=flag_default("email"),
+2 -8
View File
@@ -186,15 +186,9 @@ def register(config: configuration.NamespaceConfig, account_storage: AccountStor
# Log non-standard actions, potentially wrong API calls # Log non-standard actions, potentially wrong API calls
if account_storage.find_all(): if account_storage.find_all():
logger.info("There are already existing accounts for %s", config.server) 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.error(msg)
raise errors.Error(msg)
if not config.dry_run:
logger.debug("Registering without email!")
if config.email == "":
config.email = None
# If --dry-run is used, and there is no staging account, create one with no email. # If --dry-run is used, and there is no staging account, create one with no email.
if config.dry_run: if config.dry_run:
config.email = None config.email = None
+1 -1
View File
@@ -932,7 +932,7 @@ def update_account(config: configuration.NamespaceConfig,
if not accounts: if not accounts:
return f"Could not find an existing account for server {config.server}." return f"Could not find an existing account for server {config.server}."
if config.email is None and not config.register_unsafely_without_email: if config.email is None and not config.register_unsafely_without_email:
config.email = display_ops.get_email(optional=False) config.email = display_ops.get_email()
acc, acme = _determine_account(config) acc, acme = _determine_account(config)
cb_client = client.Client(config, acc, None, None, acme=acme) cb_client = client.Client(config, acc, None, None, acme=acme)
@@ -158,9 +158,10 @@ class RegisterTest(test_util.ConfigTestCase):
with pytest.raises(errors.Error): with pytest.raises(errors.Error):
self._call() self._call()
def test_needs_email(self): def test_no_email_is_chill(self):
self.config.email = None self.config.email = None
with pytest.raises(errors.Error): with self._patched_acme_client() as mock_client:
mock_client().external_account_required.side_effect = self._false_mock
self._call() self._call()
@mock.patch("certbot._internal.client.logger") @mock.patch("certbot._internal.client.logger")
@@ -172,7 +173,6 @@ class RegisterTest(test_util.ConfigTestCase):
self.config.register_unsafely_without_email = True self.config.register_unsafely_without_email = True
self.config.dry_run = False self.config.dry_run = False
self._call() self._call()
mock_logger.debug.assert_called_once_with(mock.ANY)
assert mock_prepare.called is True assert mock_prepare.called is True
@mock.patch("certbot._internal.client.display_ops.get_email") @mock.patch("certbot._internal.client.display_ops.get_email")
@@ -35,7 +35,7 @@ class GetEmailTest(unittest.TestCase):
with pytest.raises(errors.Error): with pytest.raises(errors.Error):
self._call() self._call()
with pytest.raises(errors.Error): with pytest.raises(errors.Error):
self._call(optional=False) self._call()
@test_util.patch_display_util() @test_util.patch_display_util()
def test_ok_safe(self, mock_get_utility): def test_ok_safe(self, mock_get_utility):
@@ -55,7 +55,7 @@ class GetEmailTest(unittest.TestCase):
@test_util.patch_display_util() @test_util.patch_display_util()
def test_invalid_flag(self, mock_get_utility): def test_invalid_flag(self, mock_get_utility):
invalid_txt = "There seem to be problems" invalid_txt = "The server reported a problem"
mock_input = mock_get_utility().input mock_input = mock_get_utility().input
mock_input.return_value = (display_util.OK, "foo@bar.baz") mock_input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
@@ -65,19 +65,9 @@ class GetEmailTest(unittest.TestCase):
self._call(invalid=True) self._call(invalid=True)
assert invalid_txt in mock_input.call_args[0][0] assert invalid_txt in mock_input.call_args[0][0]
@test_util.patch_display_util()
def test_optional_flag(self, mock_get_utility):
mock_input = mock_get_utility().input
mock_input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
mock_safe_email.side_effect = [False, True]
self._call(optional=False)
for call in mock_input.call_args_list:
assert "--register-unsafely-without-email" not in call[0][0]
@test_util.patch_display_util() @test_util.patch_display_util()
def test_optional_invalid_unsafe(self, mock_get_utility): def test_optional_invalid_unsafe(self, mock_get_utility):
invalid_txt = "There seem to be problems" invalid_txt = "There is a problem"
mock_input = mock_get_utility().input mock_input = mock_get_utility().input
mock_input.return_value = (display_util.OK, "foo@bar.baz") mock_input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email: with mock.patch("certbot.display.ops.util.safe_email") as mock_safe_email:
@@ -1062,8 +1062,6 @@ class MainTest(test_util.ConfigTestCase):
def test_noninteractive(self, _): def test_noninteractive(self, _):
args = ['-n', 'certonly'] args = ['-n', 'certonly']
self._cli_missing_flag(args, "specify a plugin") self._cli_missing_flag(args, "specify a plugin")
args.extend(['--standalone', '-d', 'eg.is'])
self._cli_missing_flag(args, "register before running")
@mock.patch('certbot._internal.eff.handle_subscription') @mock.patch('certbot._internal.eff.handle_subscription')
@mock.patch('certbot._internal.log.post_arg_parse_setup') @mock.patch('certbot._internal.log.post_arg_parse_setup')
+11 -36
View File
@@ -19,12 +19,10 @@ from certbot.display import util as display_util
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_email(invalid: bool = False, optional: bool = True) -> str: def get_email(invalid: bool = False, **kwargs: Any) -> str:
"""Prompt for valid email address. """Prompt for valid email address.
:param bool invalid: True if an invalid address was provided by the user :param bool invalid: True if an invalid address was provided by the user
:param bool optional: True if the user can use
--register-unsafely-without-email to avoid providing an e-mail
:returns: e-mail address :returns: e-mail address
:rtype: str :rtype: str
@@ -32,45 +30,22 @@ def get_email(invalid: bool = False, optional: bool = True) -> str:
:raises errors.Error: if the user cancels :raises errors.Error: if the user cancels
""" """
invalid_prefix = "There seem to be problems with that address. " # pylint: disable=unused-argument
msg = "Enter email address (used for urgent renewal and security notices)\n" invalid_prefix = ""
unsafe_suggestion = ("\n\nIf you really want to skip this, you can run " if invalid:
"the client with --register-unsafely-without-email " invalid_prefix = "The server reported a problem with your email address. "
"but you will then be unable to receive notice about " msg = "Enter email address or hit Enter to skip.\n"
"impending expiration or revocation of your "
"certificates or problems with your Certbot "
"installation that will lead to failure to renew.\n\n")
if optional:
if invalid:
msg += unsafe_suggestion
suggest_unsafe = False
else:
suggest_unsafe = True
else:
suggest_unsafe = False
while True: while True:
try: code, email = display_util.input_text(invalid_prefix + msg, default="")
code, email = display_util.input_text(invalid_prefix + msg if invalid else msg,
force_interactive=True)
except errors.MissingCommandlineFlag:
msg = ("You should register before running non-interactively, "
"or provide --agree-tos and --email <email_address> flags.")
raise errors.MissingCommandlineFlag(msg)
if code != display_util.OK: if code != display_util.OK:
if optional: raise errors.Error("Error getting email address.")
raise errors.Error( if email == "":
"An e-mail address or " return ""
"--register-unsafely-without-email must be provided.")
raise errors.Error("An e-mail address must be provided.")
if util.safe_email(email): if util.safe_email(email):
return email return email
if suggest_unsafe: invalid_prefix = "There is a problem with your email address. "
msg = unsafe_suggestion + msg
suggest_unsafe = False # add this message at most once
invalid = bool(email)
def choose_account(accounts: List[account.Account]) -> Optional[account.Account]: def choose_account(accounts: List[account.Account]) -> Optional[account.Account]: