stored field, for text fields. */ const TEXT_FLAGS = array( 'host' => 'host', 'port' => 'port', 'encryption' => 'encryption', 'username' => 'username', 'from-email' => 'from_email', 'from-name' => 'from_name', ); /** * Flag => stored field, for switches. WP-CLI turns --no- into false. */ const BOOL_FLAGS = array( 'auth' => 'auth', 'force-from' => 'force_from', 'force-name' => 'force_name', ); /** * The three ways to say something about the password. */ const PASSWORD_FLAGS = array( 'password', 'password-stdin', 'clear-password' ); /** * Terminal. * * @var Io */ private $io; /** * Constructor. * * @param Io $io Terminal. */ public function __construct( Io $io ) { $this->io = $io; } /** * The ADMINKEEP_SMTP_* constants in force. Its own method so a test can supply some; PHP * would never let a test undefine a real one. * * @return array Field key => value. */ protected function constants() { return SmtpConfig::defined_constants(); } /** * The configuration in force. * * @return SmtpConfig */ private function config() { return new SmtpConfig( SmtpConfig::stored(), $this->constants(), SecretBox::from_salts() ); } /** * Shows the SMTP settings and where each value comes from. * * The password is never shown, only whether one is set. * * ## OPTIONS * * [--format=] * : Output format. * --- * default: table * options: * - table * - json * - csv * - yaml * --- * * ## EXAMPLES * * $ wp adminkeep smtp get * * @param array $args Positional arguments. * @param array $assoc_args Flags. * @return void */ public function get( $args, $assoc_args ) { $config = $this->config(); $stored = SmtpConfig::stored(); $rows = array(); foreach ( array_keys( SmtpConfig::defaults() ) as $key ) { if ( 'password' === $key ) { $value = $config->has_password() ? '(set)' : '(not set)'; } else { $value = $config->get( $key ); $value = is_bool( $value ) ? ( $value ? 'yes' : 'no' ) : (string) $value; } if ( $config->is_locked( $key ) ) { $source = 'constant'; } else { $source = array_key_exists( $key, $stored ) ? 'stored' : 'default'; } $rows[] = array( 'field' => $key, 'value' => $value, 'source' => $source, ); } $this->io->items( isset( $assoc_args['format'] ) ? (string) $assoc_args['format'] : 'table', $rows, array( 'field', 'value', 'source' ) ); } /** * Changes the SMTP settings. Only what you name changes; everything else is kept. * * A setting fixed by an ADMINKEEP_SMTP_* constant in wp-config.php cannot be changed here. * * ## OPTIONS * * [--host=] * : SMTP server, e.g. smtp.example.com. * * [--port=] * : Port, 1 to 65535. Usually 587 (tls) or 465 (ssl). * * [--encryption=] * : none, ssl or tls. * * [--auth] * : The server needs a username and password. --no-auth switches this off. * * [--username=] * : SMTP username. * * [--password-stdin] * : Read the password from standard input. The safe way to set it. * * [--password=] * : The password. Avoid: it is saved in your shell history and * visible in the process list while the command runs. Prefer * --password-stdin. * * [--clear-password] * : Remove the saved password. * * [--from-email=] * : Address email is sent from. An empty value clears it. * * [--from-name=] * : Name email is sent from. * * [--force-from] * : Use the From address even when a plugin sets its own. --no-force-from switches this off. * * [--force-name] * : Use the From name even when a plugin sets its own. --no-force-name switches this off. * * ## EXAMPLES * * $ wp adminkeep smtp set --host=smtp.example.com --port=587 * $ wp adminkeep smtp set --encryption=tls --username=apikey * * # Set the password without it reaching your shell history. * $ printf %s "$SMTP_PASSWORD" | wp adminkeep smtp set --password-stdin * * @param array $args Positional arguments. * @param array $assoc_args Flags. * @return void */ public function set( $args, $assoc_args ) { $constants = $this->constants(); $password_flags = array_values( array_intersect( self::PASSWORD_FLAGS, array_keys( array_filter( $assoc_args, array( $this, 'is_given' ) ) ) ) ); if ( count( $password_flags ) > 1 ) { $this->io->error( 'Use only one of --password, --password-stdin and --clear-password.' ); } // What is stored now, in full: SmtpConfig::sanitize() replaces the whole option, so a // field left out of its input would be reset. The password is deliberately absent — // sanitize() keeps the sealed one when the key is missing, and keeps every locked field. $input = array_merge( SmtpConfig::defaults(), array_intersect_key( SmtpConfig::stored(), SmtpConfig::defaults() ) ); unset( $input['password'] ); $changed = array(); foreach ( self::TEXT_FLAGS as $flag => $key ) { if ( ! array_key_exists( $flag, $assoc_args ) ) { continue; } // WP-CLI turns a value-less --no- into false. Cast to (string) that would // silently become '' and clear the field; an explicit empty value (--from-email=) // is still a string and reaches here fine. if ( ! is_string( $assoc_args[ $flag ] ) && ! is_numeric( $assoc_args[ $flag ] ) ) { $this->io->error( sprintf( '--%s needs a value.', $flag ) ); } $this->refuse_if_locked( $key, $constants ); $input[ $key ] = (string) $assoc_args[ $flag ]; $changed[] = $key; } foreach ( self::BOOL_FLAGS as $flag => $key ) { if ( ! array_key_exists( $flag, $assoc_args ) ) { continue; } $this->refuse_if_locked( $key, $constants ); $input[ $key ] = (bool) $assoc_args[ $flag ]; $changed[] = $key; } if ( $password_flags ) { $this->refuse_if_locked( 'password', $constants ); $input['password'] = $this->new_password( $password_flags[0], $assoc_args ); $changed[] = 'password'; } if ( empty( $changed ) ) { $this->io->error( 'Nothing to change. See: wp help adminkeep smtp set' ); } $this->validate( $input, $changed ); // sanitize() keeps the stored value of every constant-locked field, so a locked field // this call never named is kept too. $sanitized = SmtpConfig::sanitize( $input, SmtpConfig::stored(), $constants, SecretBox::from_salts() ); update_option( SmtpConfig::OPTION, $sanitized, false ); $message = sprintf( 'SMTP settings updated: %s.', implode( ', ', $changed ) ); if ( ! Settings::is_enabled( 'smtp' ) ) { $message .= ' SMTP is off; turn it on with: wp adminkeep feature enable smtp'; } $this->io->success( $message ); } /** * Sends a test email through the configured server. * * On failure, prints the server's side of the conversation with the * username and password removed, and exits with an error. * * ## OPTIONS * * * : Address to send the test email to. * * [--log] * : Print the SMTP conversation even when the email is sent. * * ## EXAMPLES * * $ wp adminkeep smtp test you@example.com * Success: Test email sent to you@example.com. * * @param array $args Recipient. * @param array $assoc_args Flags. * @return void */ public function test( $args, $assoc_args ) { if ( ! Settings::is_enabled( 'smtp' ) ) { $this->io->error( 'SMTP is off, so a test would go through the default mailer and prove nothing. Turn it on with: wp adminkeep feature enable smtp' ); } $config = $this->config(); // Same false proof as the feature being off: with the feature on but no host named, // Smtp::configure_mailer() bails and the send goes out through the default mailer. if ( ! $config->is_active() ) { $this->io->error( 'No SMTP host is set, so a test would go through the default mailer and prove nothing. Set one with: wp adminkeep smtp set --host=' ); } $result = SmtpTester::send( isset( $args[0] ) ? (string) $args[0] : '', $config ); if ( $result['sent'] ) { if ( ! empty( $assoc_args['log'] ) && '' !== $result['log'] ) { $this->io->line( $result['log'] ); } $this->io->success( sprintf( 'Test email sent to %s.', $result['to'] ) ); return; } if ( '' !== $result['log'] ) { $this->io->line( $result['log'] ); } $this->io->error( '' !== $result['error'] ? $result['error'] : 'The test email could not be sent.' ); } /** * Copies the SMTP connection settings from another SMTP plugin. * * Copies the server, port, encryption, authentication, username, * password, From address and name, and whether each is forced. Your * current Adminkeep SMTP settings are replaced, except values held by * ADMINKEEP_SMTP_* constants in wp-config.php, which are kept. The * other plugin's settings are only read. * * Only an SMTP connection can be copied: a plugin set to send through * a provider's API (SendLayer, Amazon SES API, Gmail sign-in and the * like) has no server and password to bring across. The password is * not printed, only whether it was copied. * * ## OPTIONS * * * : Where to copy from. wp-mail-smtp: the WP Mail SMTP plugin. * * [--dry-run] * : Show what would be copied without changing anything. * * ## EXAMPLES * * $ wp adminkeep smtp import wp-mail-smtp --dry-run * $ wp adminkeep smtp import wp-mail-smtp * * @param array $args Positional arguments. * @param array $assoc_args Flags. * @return void */ public function import( $args, $assoc_args ) { $sources = SmtpImporter::sources(); $name = isset( $args[0] ) ? (string) $args[0] : ''; if ( ! isset( $sources[ $name ] ) ) { $this->io->error( 'Unknown source. Use one of: ' . implode( ', ', array_keys( $sources ) ) . '.' ); return; } $source = $sources[ $name ]; $dry_run = ! empty( $assoc_args['dry-run'] ); $importer = new SmtpImporter( $this->constants() ); $result = $dry_run ? $importer->preview( $source ) : $importer->run( $source ); if ( is_wp_error( $result ) ) { $this->io->error( $result->get_error_message() ); return; } $this->io->line( sprintf( '%s from %s: %s.', $dry_run ? 'Would copy' : 'Copied', $result['source'], implode( ', ', $result['written'] ) ) ); if ( $result['kept'] ) { $this->io->line( sprintf( 'Kept from wp-config.php: %s.', implode( ', ', $result['kept'] ) ) ); } $this->io->line( 'Password: ' . $this->password_outcome( $result, $dry_run ) ); $off = Settings::is_enabled( 'smtp' ) ? '' : ' SMTP is off; turn it on with: wp adminkeep feature enable smtp'; if ( $dry_run ) { $this->io->success( 'Dry run: nothing was changed.' . $off ); return; } $this->io->success( sprintf( 'SMTP settings copied from %s.', $result['source'] ) . $off ); $this->io->line( sprintf( 'Send a test email with: wp adminkeep smtp test
. Then deactivate %s if it is still active, so the two plugins do not both configure the mailer.', $result['source'] ) ); } /** * The password outcome in words, without the password. * * @param array $result The importer's report. * @param bool $dry_run Whether nothing is written. * @return string */ private function password_outcome( array $result, $dry_run ) { switch ( $result['password'] ) { case SmtpImporter::PASSWORD_COPIED: return $dry_run ? 'would be copied, and stored encrypted.' : 'copied, and stored encrypted.'; case SmtpImporter::PASSWORD_UNREADABLE: return sprintf( '%s\'s saved password could not be read, so the Adminkeep password %s. Set it with: wp adminkeep smtp set --password-stdin', $result['source'], $dry_run ? 'would be left as it is' : 'was left as it was' ); case SmtpImporter::PASSWORD_LOCKED: return sprintf( 'set in wp-config.php (%s), so it %s.', SmtpConfig::CONSTANTS['password'], $dry_run ? 'would be kept' : 'was kept' ); default: return sprintf( '%s has none saved, so the Adminkeep password %s.', $result['source'], $dry_run ? 'would be left as it is' : 'was left as it was' ); } } /** * Whether a flag was really passed. `--no-clear-password` arrives as false and means nothing. * * Private, not just an implementation detail: WP-CLI builds a subcommand from every public * method of a registered command object, and this one has no synopsis of its own. * * @param mixed $value Flag value. * @return bool */ private function is_given( $value ) { return false !== $value && null !== $value; } /** * Stop if wp-config.php owns this field. * * @param string $key Field key. * @param array $constants Constants in force. * @return void */ private function refuse_if_locked( $key, array $constants ) { if ( array_key_exists( $key, $constants ) ) { $this->io->error( sprintf( '%s is fixed by the %s constant in wp-config.php and cannot be changed here.', $key, SmtpConfig::CONSTANTS[ $key ] ) ); } } /** * The password a flag asks for: '' removes the saved one. * * @param string $flag Which of the three flags. * @param array $assoc_args Flags. * @return string */ private function new_password( $flag, array $assoc_args ) { if ( 'clear-password' === $flag ) { return ''; } // Only the line ending a shell adds is dropped: a password may legitimately end in a space. $password = 'password-stdin' === $flag ? rtrim( $this->io->stdin(), "\r\n" ) : (string) $assoc_args['password']; if ( '' === $password ) { $this->io->error( 'The password is empty. To remove the saved password use --clear-password.' ); } return $password; } /** * Refuse what SmtpConfig would otherwise quietly replace with a default. * * @param array $input Full input. * @param string[] $changed Keys this call changes. * @return void */ private function validate( array $input, array $changed ) { if ( in_array( 'port', $changed, true ) ) { $port = $input['port']; if ( ! preg_match( '/^\d+$/', $port ) || (int) $port < 1 || (int) $port > 65535 ) { $this->io->error( sprintf( '"%s" is not a port. Use a number from 1 to 65535.', $port ) ); } } if ( in_array( 'encryption', $changed, true ) && ! in_array( $input['encryption'], array( 'none', 'ssl', 'tls' ), true ) ) { $this->io->error( sprintf( '"%s" is not an encryption. Choose one of: none, ssl, tls.', $input['encryption'] ) ); } if ( in_array( 'from_email', $changed, true ) && '' !== $input['from_email'] && ! is_email( $input['from_email'] ) ) { $this->io->error( sprintf( '"%s" is not a valid email address.', $input['from_email'] ) ); } } }