From 0bfc2db32baeea9715fbf3a9872d8dd0afd07454 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Tue, 30 Jun 2026 17:28:11 +0530 Subject: [PATCH 1/2] fix(ssl): validate le-mail before letsencrypt registration --- src/helper/class-ee-site.php | 37 ++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/helper/class-ee-site.php b/src/helper/class-ee-site.php index 08ad268e..cf82bc04 100644 --- a/src/helper/class-ee-site.php +++ b/src/helper/class-ee-site.php @@ -1540,7 +1540,7 @@ protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or $this->site_data['site_fs_path'] = $site_fs_path; $this->site_data['site_ssl_wildcard'] = $wildcard; $client = new Site_Letsencrypt(); - $this->le_mail = \EE::get_runner()->config['le-mail'] ?? \EE::input( 'Enter your mail id: ' ); + $this->le_mail = $this->get_validated_le_mail( \EE::get_runner()->config['le-mail'] ?? null ); \EE::get_runner()->ensure_present_in_config( 'le-mail', $this->le_mail ); if ( ! $client->register( $this->le_mail ) ) { $this->site_data['site_ssl'] = null; @@ -1577,6 +1577,30 @@ protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or } } + /** + * Resolves and validates the Let's Encrypt account email. + * + * `??` only guards null/unset, so an empty/garbage le-mail (e.g. an empty STDIN + * read during non-interactive cron runs) used to slip through to ACME and fail + * with an opaque error. Validate here so callers always get a real email. + * + * @param string|null $config_mail Email resolved from config by the caller, if any. + * + * @return string A non-empty, valid email address. + */ + private function get_validated_le_mail( $config_mail = null ) { + $mail = $config_mail; + if ( empty( $mail ) || ! filter_var( $mail, FILTER_VALIDATE_EMAIL ) ) { + // One re-prompt for interactive users; on non-interactive runs STDIN is empty and this stays empty. + $mail = \EE::input( 'Enter your mail id: ' ); + } + if ( empty( $mail ) || ! filter_var( $mail, FILTER_VALIDATE_EMAIL ) ) { + \EE::error( 'A valid Let\'s Encrypt email is required. Set it with `ee config set le-mail `.' ); + } + + return $mail; + } + /** * Returns all domains required by cert * @@ -1679,7 +1703,7 @@ public function ssl_verify( $args = [], $assoc_args = [], $www_or_non_www = fals } if ( ! isset( $this->le_mail ) ) { - $this->le_mail = \EE::get_config( 'le-mail' ) ?? \EE::input( 'Enter your mail id: ' ); + $this->le_mail = $this->get_validated_le_mail( \EE::get_config( 'le-mail' ) ); } $force = \EE\Utils\get_flag_value( $assoc_args, 'force' ); @@ -1944,10 +1968,6 @@ public function ssl_renew( $args, $assoc_args ) { EE::log( 'Starting SSL cert renewal' ); - if ( ! isset( $this->le_mail ) ) { - $this->le_mail = EE::get_config( 'le-mail' ) ?? EE::input( 'Enter your mail id: ' ); - } - $force = get_flag_value( $assoc_args, 'force', false ); $all = get_flag_value( $assoc_args, 'all', false ); @@ -1999,6 +2019,11 @@ private function renew_ssl_cert( $args, $force ) { EE::error( 'Only Letsencrypt certificate renewal is supported.' ); } + // Resolve le-mail only after confirming the site is LE, so non-LE sites hit the site-type error first. + if ( ! isset( $this->le_mail ) ) { + $this->le_mail = $this->get_validated_le_mail( EE::get_config( 'le-mail' ) ); + } + $client = new Site_Letsencrypt(); $preferred_challenge = get_preferred_ssl_challenge( get_domains_of_site( $this->site_data['site_url'] ) ); From 6ca1026fe991527408563bb552156f61f1897a4d Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Thu, 24 Sep 2026 10:41:02 +0000 Subject: [PATCH 2/2] fix(ssl): let site create roll back on invalid le-mail get_validated_le_mail() called EE::error(), which exits the process. During `ee site create --ssl=le` this bypassed the create try/catch, so an empty or invalid email at the prompt left an orphaned site (containers, webroot and proxy config, but no DB entry; re-create fails with "Webroot directory ... already exists"). Before the helper, the bad email made register() throw and the site was rolled back. Throw an exception instead so create and `ee site update --ssl` handle it through their existing catch blocks, and convert it to EE::error() in the direct ssl-verify and ssl-renew entry points. init_le() also reuses an already resolved le_mail, so a renewal no longer prompts twice. --- src/helper/class-ee-site.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/helper/class-ee-site.php b/src/helper/class-ee-site.php index cf82bc04..a64c1d94 100644 --- a/src/helper/class-ee-site.php +++ b/src/helper/class-ee-site.php @@ -1540,7 +1540,10 @@ protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or $this->site_data['site_fs_path'] = $site_fs_path; $this->site_data['site_ssl_wildcard'] = $wildcard; $client = new Site_Letsencrypt(); - $this->le_mail = $this->get_validated_le_mail( \EE::get_runner()->config['le-mail'] ?? null ); + if ( ! isset( $this->le_mail ) ) { + // Throws instead of exiting so site create/update can roll back. + $this->le_mail = $this->get_validated_le_mail( \EE::get_runner()->config['le-mail'] ?? null ); + } \EE::get_runner()->ensure_present_in_config( 'le-mail', $this->le_mail ); if ( ! $client->register( $this->le_mail ) ) { $this->site_data['site_ssl'] = null; @@ -1587,6 +1590,7 @@ protected function init_le( $site_url, $site_fs_path, $wildcard = false, $www_or * @param string|null $config_mail Email resolved from config by the caller, if any. * * @return string A non-empty, valid email address. + * @throws \Exception If no valid email can be resolved. */ private function get_validated_le_mail( $config_mail = null ) { $mail = $config_mail; @@ -1595,7 +1599,7 @@ private function get_validated_le_mail( $config_mail = null ) { $mail = \EE::input( 'Enter your mail id: ' ); } if ( empty( $mail ) || ! filter_var( $mail, FILTER_VALIDATE_EMAIL ) ) { - \EE::error( 'A valid Let\'s Encrypt email is required. Set it with `ee config set le-mail `.' ); + throw new \Exception( 'A valid Let\'s Encrypt email is required. Set it with `ee config set le-mail `.' ); } return $mail; @@ -1703,7 +1707,11 @@ public function ssl_verify( $args = [], $assoc_args = [], $www_or_non_www = fals } if ( ! isset( $this->le_mail ) ) { - $this->le_mail = $this->get_validated_le_mail( \EE::get_config( 'le-mail' ) ); + try { + $this->le_mail = $this->get_validated_le_mail( \EE::get_config( 'le-mail' ) ); + } catch ( \Exception $e ) { + EE::error( $e->getMessage() ); + } } $force = \EE\Utils\get_flag_value( $assoc_args, 'force' ); @@ -2021,7 +2029,11 @@ private function renew_ssl_cert( $args, $force ) { // Resolve le-mail only after confirming the site is LE, so non-LE sites hit the site-type error first. if ( ! isset( $this->le_mail ) ) { - $this->le_mail = $this->get_validated_le_mail( EE::get_config( 'le-mail' ) ); + try { + $this->le_mail = $this->get_validated_le_mail( EE::get_config( 'le-mail' ) ); + } catch ( \Exception $e ) { + EE::error( $e->getMessage() ); + } } $client = new Site_Letsencrypt();