config(); if ( ! $config->is_active() ) { return __( 'Not set up', 'adminkeep' ); } if ( array() !== self::last_error() ) { return __( 'Last send failed', 'adminkeep' ); } if ( $config->is_locked( 'host' ) ) { // A constant's value is not echoed: wp-config.php is where it is kept private, and the // settings panel is no more private than the screen that already refuses to show it. return __( 'Sending via the server in wp-config.php', 'adminkeep' ); } /* translators: %s: SMTP host name. */ return sprintf( __( 'Sending via %s', 'adminkeep' ), $config->get( 'host' ) ); } /** * Settings schema. Everything is edited on the feature's own screen. * * @return array */ public function settings_schema() { return array(); } /** * No fields, so no labels. * * @param string $key Field key. * @return string */ public function field_label( $key ) { return ''; } /** * What switching this on actually does. * * @return string[] */ public function how_it_works() { return array( __( 'Adds Settings → SMTP. Until you name a server there, WordPress sends email exactly as it did before.', 'adminkeep' ), __( 'Works with any SMTP server: your host\'s, or a service such as Amazon SES, Brevo, Mailgun, Postmark or Zoho. Presets fill in the server details for you.', 'adminkeep' ), __( 'Gmail and Google Workspace work with an app password. Microsoft 365 and Outlook.com now require signing in through Microsoft (OAuth) for SMTP, which is not supported.', 'adminkeep' ), __( 'The password is stored encrypted, which protects it in database backups. For the strongest setup, put the settings in wp-config.php instead: values set there are not saved to the database, and the settings form does not display them.', 'adminkeep' ), __( 'If the server refuses an email, that email fails and Settings → SMTP shows the server\'s own error. Nothing is quietly re-sent another way.', 'adminkeep' ), __( 'Switching this off returns email to the server\'s default. Your settings are kept until you uninstall.', 'adminkeep' ), ); } /** * Where the settings panel sends you to do the actual work. * * @return array{url?: string, label?: string} Empty when this user could not open the screen. */ public function panel_link() { if ( ! current_user_can( SmtpScreen::CAPABILITY ) ) { return array(); } return array( 'url' => SmtpScreen::url(), 'label' => __( 'SMTP settings', 'adminkeep' ), ); } /** * The settings screen. * * @return SmtpScreen */ public function screen() { if ( null === $this->screen ) { $this->screen = new SmtpScreen(); } return $this->screen; } /** * Register hooks. * * @param array $settings Resolved settings. Unused — this feature has none. * @return void */ public function boot( array $settings ) { add_action( 'phpmailer_init', array( $this, 'configure_mailer' ), self::MAILER_PRIORITY ); add_filter( 'wp_mail_from', array( $this, 'from_email' ), self::MAILER_PRIORITY ); add_filter( 'wp_mail_from_name', array( $this, 'from_name' ), self::MAILER_PRIORITY ); add_action( 'wp_mail_failed', array( $this, 'record_failure' ) ); add_action( 'wp_mail_succeeded', array( $this, 'clear_failure' ), 10, 0 ); if ( is_admin() ) { $this->screen()->boot(); } } /** * The configuration in force. Read fresh each time: mail is rare, and a test must be able to * change the option between calls. * * @return SmtpConfig */ protected function config() { return SmtpConfig::from_environment(); } /** * Point PHPMailer at the SMTP server. * * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer The mailer, by reference. * @return void */ public function configure_mailer( $phpmailer ) { $config = $this->config(); if ( ! $config->is_active() ) { return; } $encryption = $config->get( 'encryption' ); $phpmailer->isSMTP(); // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHPMailer's own property names. $phpmailer->Host = $config->get( 'host' ); $phpmailer->Port = $config->get( 'port' ); $phpmailer->SMTPSecure = 'none' === $encryption ? '' : $encryption; $phpmailer->Timeout = self::TIMEOUT; $phpmailer->SMTPAuth = (bool) $config->get( 'auth' ); if ( $phpmailer->SMTPAuth ) { $password = $config->password(); $phpmailer->Username = $config->get( 'username' ); $phpmailer->Password = false === $password ? '' : $password; } else { $phpmailer->Username = ''; $phpmailer->Password = ''; } // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } /** * The from address. * * Ours replaces WordPress's placeholder default, and everything when forced. A plugin that * chose its own sender is otherwise left alone. * * @param string $current Address chosen so far. * @return string */ public function from_email( $current ) { $config = $this->config(); $ours = $config->is_active() ? $config->get( 'from_email' ) : ''; if ( '' === $ours ) { return $current; } if ( $config->get( 'force_from' ) || strtolower( (string) $current ) === $this->default_from_email() ) { return $ours; } return $current; } /** * The from name, by the same rules. WordPress's default is the literal "WordPress". * * @param string $current Name chosen so far. * @return string */ public function from_name( $current ) { $config = $this->config(); $ours = $config->is_active() ? $config->get( 'from_name' ) : ''; if ( '' === $ours ) { return $current; } if ( $config->get( 'force_name' ) || 'WordPress' === $current ) { return $ours; } return $current; } /** * The address wp_mail() makes up when nobody set one. * * @return string */ private function default_from_email() { $host = strtolower( (string) wp_parse_url( network_home_url(), PHP_URL_HOST ) ); if ( 0 === strpos( $host, 'www.' ) ) { $host = substr( $host, 4 ); } return 'wordpress@' . $host; } /** * Remember why a send failed. * * @param \WP_Error $error What wp_mail() reported. * @return void */ public function record_failure( $error ) { $config = $this->config(); if ( ! $config->is_active() ) { return; } $message = $error instanceof \WP_Error ? $error->get_error_message() : ''; // The option outlives this request, and a rejection often quotes the account it came // from: Postfix answers "Sender address rejected: not owned by user ". $password = $config->password(); $message = SmtpScrubber::scrub( $message, (string) $config->get( 'username' ), false === $password ? '' : $password ); // After the site's salts change the stored password cannot be opened. The server then // reports a failed login, which is true and useless: say what to do instead. if ( $config->get( 'auth' ) && $config->has_password() && false === $config->password() ) { $message = __( 'The saved SMTP password can no longer be read, which happens when the security keys in wp-config.php change. Open Settings → SMTP and enter it again.', 'adminkeep' ); } update_option( self::LAST_ERROR_OPTION, array( 'time' => time(), 'message' => $message, ), false ); } /** * Forget the last failure once mail is flowing again. * * @return void */ public function clear_failure() { if ( array() !== self::last_error() ) { delete_option( self::LAST_ERROR_OPTION ); } } /** * The last recorded failure. * * @return array Empty, or array( 'time' => int, 'message' => string ). */ public static function last_error() { $error = get_option( self::LAST_ERROR_OPTION, array() ); if ( ! is_array( $error ) || ! isset( $error['time'], $error['message'] ) ) { return array(); } return array( 'time' => (int) $error['time'], 'message' => (string) $error['message'], ); } }