$links Plugin action links. */
public function action_links( array $links ): array {
array_unshift( $links, '' . esc_html__( 'Configure', 'mailbridgeapi-email-delivery' ) . '' );
return $links;
}
public function privacy_policy(): void {
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
return;
}
$content = '' . __( 'When MailBridgeAPI Email Delivery is connected, outgoing email recipient addresses, subjects, message bodies, headers and up to two attachments are sent to MailBridgeAPI for SMTP delivery. The plugin stores recipient address, subject, delivery state, provider response, message identifier and timestamp in the WordPress database for the configured retention period. The MailBridgeAPI dashboard email and password are used only during sign-in; the password is not stored. API credentials are encrypted locally using the WordPress authentication salt.', 'mailbridgeapi-email-delivery' ) . '
';
/* translators: %s: MailBridgeAPI privacy policy URL. */
$content .= '' . sprintf( wp_kses_post( __( 'Review the MailBridgeAPI privacy policy for service-side processing and retention.', 'mailbridgeapi-email-delivery' ) ), esc_url( 'https://mailbridgeapi.com/privacy-policy/' ) ) . '
';
wp_add_privacy_policy_content( 'MailBridgeAPI Email Delivery', wp_kses_post( $content ) );
}
public function connection_page(): void {
$this->guard();
$connection = Plugin::connection();
$pending = Plugin::pending();
$settings = Plugin::settings();
$this->page_start( __( 'Mailbox connection', 'mailbridgeapi-email-delivery' ), __( 'Route every WordPress email through one controlled MailBridgeAPI mailbox.', 'mailbridgeapi-email-delivery' ) );
$this->notice();
if ( $connection ) {
$this->render_connected( $connection, $settings );
} elseif ( $pending ) {
$this->render_mailbox_selection( $pending );
} else {
$this->render_login();
}
$this->page_end();
}
/** @param array $connection Connection values. @param array $settings Settings values. */
private function render_connected( array $connection, array $settings ): void {
$active = ! empty( $connection['subscription_active'] );
$conflicts = $this->health->conflicts();
?>
protocol_card( 'smtp', (string) ( $connection['smtp_host'] ?? '' ), $connection ); ?>
protocol_card( 'imap', (string) ( $connection['imap_host'] ?? '' ), $connection ); ?>
delivery_test_panel( $connection, $active ); ?>
$pending Pending values. */
private function render_mailbox_selection( array $pending ): void {
$mailboxes = is_array( $pending['mailboxes'] ?? null ) ? $pending['mailboxes'] : array();
?>
SMTPIMAP
test_button( 'smtp', (int) $mailbox['id'], empty( $mailbox['smtp_host'] ), 'pending' ); ?>test_button( 'imap', (int) $mailbox['id'], empty( $mailbox['imap_host'] ), 'pending' ); ?>
$connection Connection values. */
private function protocol_card( string $protocol, string $host, array $connection ): void {
$label = strtoupper( $protocol );
?>test_button( $protocol, (int) $connection['mailbox_id'], ! $host, 'connected' ); ?>test_result( $scope, $protocol, $mailbox_id );
$protocol_label = strtoupper( $protocol );
/* translators: %s: SMTP or IMAP protocol name. */
$button_label = sprintf( __( 'Test %s', 'mailbridgeapi-email-delivery' ), $protocol_label );
/* translators: %s: SMTP or IMAP protocol name. */
$success_label = sprintf( __( '%s connection successful', 'mailbridgeapi-email-delivery' ), $protocol_label );
/* translators: %s: SMTP or IMAP protocol name. */
$error_label = sprintf( __( '%s connection failed', 'mailbridgeapi-email-delivery' ), $protocol_label );
$tested_label = '';
if ( $result ) {
/* translators: %s: localized date and time of the connection test. */
$tested_label = sprintf( __( 'Last tested: %s', 'mailbridgeapi-email-delivery' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) $result['tested_at'] ) );
}
?> $connection */
private function delivery_test_panel( array $connection, bool $active ): void {
$user = wp_get_current_user();
$result = get_user_meta( get_current_user_id(), 'mbapi_ed_delivery_test_result', true );
$result = is_array( $result ) ? $result : array();
$ready = $active && ! empty( $connection['smtp_host'] );
$message_id_label = '';
$tested_label = '';
if ( $result ) {
if ( ! empty( $result['remote_id'] ) ) {
/* translators: %s: MailBridgeAPI remote message identifier. */
$message_id_label = sprintf( __( 'Message ID: %s', 'mailbridgeapi-email-delivery' ), (string) $result['remote_id'] );
}
/* translators: %s: localized date and time of the real test email. */
$tested_label = sprintf( __( 'Tested at: %s', 'mailbridgeapi-email-delivery' ), wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), (int) ( $result['tested_at'] ?? time() ) ) );
}
?>
guard();
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- These values only filter a read-only administrator table.
$status = isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : 'all';
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
$page = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$data = $this->logger->page( $page, $status, $search );
$this->page_start( __( 'Sent emails', 'mailbridgeapi-email-delivery' ), __( 'Delivery results generated by this WordPress site.', 'mailbridgeapi-email-delivery' ) );
$this->notice();
?>
pagination( $data, $status, $search ); ?>page_end();
}
public function stats_page(): void {
$this->guard();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This value only selects a read-only reporting period.
$period = isset( $_GET['period'] ) ? absint( $_GET['period'] ) : 30;
$period = in_array( $period, array( 7, 30, 90, 365 ), true ) ? $period : 30;
$stats = $this->logger->stats( $period );
$rate = $stats['total'] > 0 ? round( ( $stats['successful'] / $stats['total'] ) * 100, 1 ) : null;
$this->page_start( __( 'Email statistics', 'mailbridgeapi-email-delivery' ), __( 'A site-specific view of WordPress email delivery.', 'mailbridgeapi-email-delivery' ) );
?>
page_end();
}
public function diagnostics_page(): void {
$this->guard();
$checks = $this->health->checks();
$report = $this->health->report( $checks );
$this->page_start( __( 'Diagnostics', 'mailbridgeapi-email-delivery' ), __( 'Verify the complete WordPress-to-MailBridgeAPI delivery environment.', 'mailbridgeapi-email-delivery' ) );
?>
page_end();
}
public function help_page(): void {
$this->guard();
$this->page_start( __( 'Documentation', 'mailbridgeapi-email-delivery' ), __( 'How this WordPress transport works.', 'mailbridgeapi-email-delivery' ) );
?>page_end();
}
public function handle_login(): void {
$this->guard_post( 'mbapi_ed_login' );
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$password = isset( $_POST['password'] ) ? sanitize_text_field( wp_unslash( $_POST['password'] ) ) : '';
if ( ! is_email( $email ) || '' === $password || strlen( $password ) > 200 ) {
$this->redirect_notice( 'error', __( 'Enter a valid dashboard email address and password.', 'mailbridgeapi-email-delivery' ) );
}
$auth = $this->api->authenticate( $email, $password );
$password = '';
if ( is_wp_error( $auth ) ) {
$this->redirect_notice( 'error', $auth->get_error_message() );
}
if ( empty( $auth['token'] ) || empty( $auth['account']['api_key'] ) ) {
$this->redirect_notice( 'error', __( 'MailBridgeAPI authenticated the account but did not return the credentials required for email delivery.', 'mailbridgeapi-email-delivery' ) );
}
$mailboxes = $this->api->mailboxes( (string) $auth['token'] );
if ( is_wp_error( $mailboxes ) ) {
$this->api->logout( (string) $auth['token'] );
$this->redirect_notice( 'error', $mailboxes->get_error_message() );
}
Plugin::save_pending(
array(
'token' => (string) $auth['token'],
'token_expires_at' => time() + (int) ( $auth['expires_in'] ?? 2592000 ),
'account_key' => (string) ( $auth['account']['api_key'] ?? '' ),
'account_email' => (string) ( $auth['account']['email'] ?? $email ),
'subscription_active' => ! empty( $auth['account']['subscription_active'] ),
'subscription_end' => (string) ( $auth['account']['subscription_end'] ?? '' ),
'mailboxes' => $mailboxes,
)
);
$this->redirect_notice( 'success', __( 'Account verified. Select and test the mailbox reserved for this WordPress site.', 'mailbridgeapi-email-delivery' ) );
}
public function handle_select(): void {
$this->guard_post( 'mbapi_ed_select' );
$pending = Plugin::pending();
$id = isset( $_POST['mailbox_id'] ) ? absint( $_POST['mailbox_id'] ) : 0;
if ( ! $pending || ! $id ) {
$this->redirect_notice( 'error', __( 'The mailbox selection session expired. Sign in again.', 'mailbridgeapi-email-delivery' ) );
}
$selected = null;
foreach ( (array) $pending['mailboxes'] as $mailbox ) {
if ( (int) ( $mailbox['id'] ?? 0 ) === $id ) {
$selected = $mailbox;
break;
}
}
if ( ! is_array( $selected ) || empty( $selected['api_key'] ) ) {
$this->redirect_notice( 'error', __( 'This mailbox is not available to the authenticated account.', 'mailbridgeapi-email-delivery' ) );
}
if ( empty( $selected['smtp_host'] ) ) {
$this->redirect_notice( 'error', __( 'Configure SMTP for this mailbox before using it as the WordPress email transport.', 'mailbridgeapi-email-delivery' ) );
}
Plugin::save_connection(
array(
'token' => (string) $pending['token'],
'token_expires_at' => (int) $pending['token_expires_at'],
'account_key' => (string) $pending['account_key'],
'account_email' => (string) $pending['account_email'],
'subscription_active' => ! empty( $pending['subscription_active'] ),
'subscription_end' => (string) $pending['subscription_end'],
'mailbox_id' => $id,
'mailbox_key' => (string) $selected['api_key'],
'mailbox_name' => sanitize_text_field( (string) $selected['name'] ),
'mailbox_email' => sanitize_email( (string) $selected['email'] ),
'smtp_host' => sanitize_text_field( (string) ( $selected['smtp_host'] ?? '' ) ),
'imap_host' => sanitize_text_field( (string) ( $selected['imap_host'] ?? '' ) ),
'connected_at' => time(),
)
);
Plugin::clear_pending();
$this->redirect_notice( 'success', __( 'Mailbox connected. WordPress email now uses MailBridgeAPI.', 'mailbridgeapi-email-delivery' ) );
}
public function handle_test(): void {
$this->guard_post( 'mbapi_ed_test' );
$protocol = isset( $_POST['protocol'] ) ? sanitize_key( wp_unslash( $_POST['protocol'] ) ) : '';
$scope = isset( $_POST['scope'] ) ? sanitize_key( wp_unslash( $_POST['scope'] ) ) : '';
$id = isset( $_POST['mailbox_id'] ) ? absint( $_POST['mailbox_id'] ) : 0;
if ( ! in_array( $protocol, array( 'smtp', 'imap' ), true ) || ! $id ) {
$this->redirect_notice( 'error', __( 'Choose a valid mailbox test.', 'mailbridgeapi-email-delivery' ) );
}
if ( ! in_array( $scope, array( 'pending', 'connected' ), true ) ) {
$this->redirect_notice( 'error', __( 'Choose a valid mailbox test.', 'mailbridgeapi-email-delivery' ) );
}
$data = 'pending' === $scope ? Plugin::pending() : Plugin::connection();
if ( 'connected' === $scope && (int) ( $data['mailbox_id'] ?? 0 ) !== $id ) {
$this->redirect_notice( 'error', __( 'The requested mailbox is not connected to this WordPress site.', 'mailbridgeapi-email-delivery' ) );
}
if ( 'pending' === $scope ) {
$allowed = false;
foreach ( (array) ( $data['mailboxes'] ?? array() ) as $mailbox ) {
if ( (int) ( $mailbox['id'] ?? 0 ) === $id ) {
$allowed = true;
break;
}
}
if ( ! $allowed ) {
$this->redirect_notice( 'error', __( 'The requested mailbox is not available in this connection session.', 'mailbridgeapi-email-delivery' ) );
}
}
$token = (string) ( $data['token'] ?? '' );
if ( '' === $token ) {
$this->redirect_notice( 'error', __( 'The secure dashboard session expired. Disconnect and sign in again.', 'mailbridgeapi-email-delivery' ) );
}
$result = $this->api->test_mailbox( $token, $id, $protocol );
if ( is_wp_error( $result ) ) {
$message = sanitize_text_field( $result->get_error_message() );
$this->remember_test_result( $scope, $protocol, $id, false, $message );
$this->redirect_notice( 'error', $message );
}
$test = is_array( $result[ $protocol ] ?? null ) ? $result[ $protocol ] : array();
$ok = ! empty( $test['ok'] );
$message = sanitize_text_field( (string) ( $test['message'] ?? __( 'The connection test did not return a result.', 'mailbridgeapi-email-delivery' ) ) );
$this->remember_test_result( $scope, $protocol, $id, $ok, $message );
$this->redirect_notice( $ok ? 'success' : 'error', $message );
}
public function handle_disconnect(): void {
$this->guard_post( 'mbapi_ed_disconnect' );
$connection = Plugin::connection();
if ( ! empty( $connection['token'] ) ) {
$this->api->logout( (string) $connection['token'] );
}
Plugin::clear_connection();
Plugin::clear_pending();
delete_user_meta( get_current_user_id(), 'mbapi_ed_test_results' );
delete_user_meta( get_current_user_id(), 'mbapi_ed_delivery_test_result' );
$this->redirect_notice( 'success', __( 'MailBridgeAPI was disconnected. WordPress has returned to its original mail transport.', 'mailbridgeapi-email-delivery' ) );
}
public function handle_settings(): void {
$this->guard_post( 'mbapi_ed_settings' );
$days = isset( $_POST['retention_days'] ) ? absint( $_POST['retention_days'] ) : 90;
if ( ! in_array( $days, array( 30, 90, 180, 365 ), true ) ) {
$days = 90;
}
$delete_on_uninstall = isset( $_POST['delete_on_uninstall'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['delete_on_uninstall'] ) );
Plugin::save_settings( array( 'retention_days' => $days, 'delete_on_uninstall' => $delete_on_uninstall ) );
$this->logger->cleanup();
$this->redirect_notice( 'success', __( 'MailBridgeAPI settings saved.', 'mailbridgeapi-email-delivery' ) );
}
public function handle_language(): void {
$this->guard_post( 'mbapi_ed_language' );
$locale = isset( $_POST['interface_locale'] ) ? sanitize_text_field( wp_unslash( $_POST['interface_locale'] ) ) : 'en_US';
if ( ! Plugin::is_supported_locale( $locale ) ) {
$locale = 'en_US';
}
Plugin::save_language( $locale );
Plugin::load_textdomain();
$page = isset( $_POST['return_page'] ) ? sanitize_key( wp_unslash( $_POST['return_page'] ) ) : self::SLUG;
if ( self::SLUG !== $page && 0 !== strpos( $page, self::SLUG . '-' ) ) {
$page = self::SLUG;
}
$this->redirect_notice( 'success', __( 'Interface language saved.', 'mailbridgeapi-email-delivery' ), $page );
}
public function handle_send_test(): void {
$this->guard_post( 'mbapi_ed_send_test' );
$connection = Plugin::connection();
$recipient = isset( $_POST['recipient'] ) ? sanitize_email( wp_unslash( $_POST['recipient'] ) ) : '';
if ( ! is_email( $recipient ) ) {
$this->redirect_notice( 'error', __( 'Enter a valid destination email address.', 'mailbridgeapi-email-delivery' ) );
}
if ( ! $connection || empty( $connection['smtp_host'] ) ) {
$this->redirect_notice( 'error', __( 'Connect a mailbox with SMTP configured before sending a test email.', 'mailbridgeapi-email-delivery' ) );
}
if ( empty( $connection['subscription_active'] ) ) {
$this->redirect_notice( 'error', __( 'Renew the MailBridgeAPI subscription before sending a test email.', 'mailbridgeapi-email-delivery' ) );
}
/* translators: 1: website host name, 2: UTC date and time. */
$subject = sprintf( __( 'MailBridgeAPI delivery test · %1$s · %2$s UTC', 'mailbridgeapi-email-delivery' ), wp_parse_url( home_url( '/' ), PHP_URL_HOST ), gmdate( 'Y-m-d H:i:s' ) );
/* translators: %s: website home URL. */
$body = sprintf( __( "This is a real MailBridgeAPI delivery test from %s.\n\nWordPress submitted this message to MailBridgeAPI and the selected SMTP server accepted it.\n\nNo action is required.", 'mailbridgeapi-email-delivery' ), home_url( '/' ) );
$sent = wp_mail( $recipient, $subject, $body, array( 'Content-Type: text/plain; charset=UTF-8' ) );
$log = $this->logger->latest_for( $recipient, $subject );
$ok = $sent && 'success' === (string) ( $log['status'] ?? '' );
$message = $log ? (string) $log['result_message'] : __( 'No MailBridgeAPI delivery record was created. Another plugin may have intercepted wp_mail(). Review the Diagnostics page.', 'mailbridgeapi-email-delivery' );
update_user_meta(
get_current_user_id(),
'mbapi_ed_delivery_test_result',
array(
'ok' => $ok,
'message' => sanitize_text_field( $message ),
'remote_id' => sanitize_text_field( (string) ( $log['remote_id'] ?? '' ) ),
'tested_at' => time(),
)
);
$this->redirect_notice( $ok ? 'success' : 'error', $ok ? __( 'The real test email was accepted for delivery.', 'mailbridgeapi-email-delivery' ) : $message );
}
public function handle_clear_logs(): void {
$this->guard_post( 'mbapi_ed_clear_logs' );
$count = $this->logger->clear();
/* translators: %d: number of deleted local email delivery log rows. */
$this->redirect_notice( 'success', sprintf( _n( '%d local delivery log was deleted.', '%d local delivery logs were deleted.', $count, 'mailbridgeapi-email-delivery' ), $count ), self::SLUG . '-logs' );
}
public function handle_export_logs(): void {
$this->guard_post( 'mbapi_ed_export_logs' );
$status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'all';
$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';
nocache_headers();
header( 'Content-Type: text/csv; charset=UTF-8' );
header( 'Content-Disposition: attachment; filename="mailbridgeapi-email-log-' . gmdate( 'Y-m-d-His' ) . '.csv"' );
$output = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
if ( false === $output ) {
wp_die( esc_html__( 'The CSV export could not be created.', 'mailbridgeapi-email-delivery' ) );
}
fwrite( $output, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
fputcsv( $output, array( 'recipient', 'subject', 'status', 'result_code', 'result_message', 'message_id', 'created_at_utc' ) );
$offset = 0;
do {
$rows = $this->logger->export_rows( $status, $search, 1000, $offset );
foreach ( $rows as $row ) {
fputcsv( $output, array_map( array( $this, 'csv_cell' ), array( $row['recipient'], $row['subject'], $row['status'], $row['result_code'], $row['result_message'], $row['remote_id'], $row['created_at'] ) ) );
}
$offset += count( $rows );
} while ( 1000 === count( $rows ) );
fclose( $output ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
exit;
}
private function csv_cell( $value ): string {
$value = (string) $value;
return preg_match( '/^[=+\-@]/', $value ) ? "'" . $value : $value;
}
public function handle_cancel_pending(): void {
$this->guard_post( 'mbapi_ed_cancel_pending' );
$pending = Plugin::pending();
if ( ! empty( $pending['token'] ) ) {
$this->api->logout( (string) $pending['token'] );
}
Plugin::clear_pending();
delete_user_meta( get_current_user_id(), 'mbapi_ed_test_results' );
$this->redirect_notice( 'success', __( 'Mailbox selection cancelled. No mailbox was connected.', 'mailbridgeapi-email-delivery' ) );
}
private function test_result_key( string $scope, string $protocol, int $mailbox_id ): string {
return sanitize_key( $scope . '_' . $mailbox_id . '_' . $protocol );
}
/** @return array */
private function test_result( string $scope, string $protocol, int $mailbox_id ): array {
$results = get_user_meta( get_current_user_id(), 'mbapi_ed_test_results', true );
$key = $this->test_result_key( $scope, $protocol, $mailbox_id );
return is_array( $results ) && is_array( $results[ $key ] ?? null ) ? $results[ $key ] : array();
}
private function remember_test_result( string $scope, string $protocol, int $mailbox_id, bool $ok, string $message ): void {
$results = get_user_meta( get_current_user_id(), 'mbapi_ed_test_results', true );
$results = is_array( $results ) ? $results : array();
$results[ $this->test_result_key( $scope, $protocol, $mailbox_id ) ] = array(
'ok' => $ok,
'message' => $message,
'tested_at' => time(),
);
update_user_meta( get_current_user_id(), 'mbapi_ed_test_results', $results );
}
private function guard(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to manage MailBridgeAPI.', 'mailbridgeapi-email-delivery' ), esc_html__( 'Access denied', 'mailbridgeapi-email-delivery' ), array( 'response' => 403 ) );
}
}
private function guard_post( string $action ): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to manage MailBridgeAPI.', 'mailbridgeapi-email-delivery' ), esc_html__( 'Access denied', 'mailbridgeapi-email-delivery' ), array( 'response' => 403 ) );
}
$nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, $action ) ) {
wp_nonce_ays( $action );
}
}
private function redirect_notice( string $type, string $message, string $page = self::SLUG ): void {
set_transient( 'mbapi_ed_notice_' . get_current_user_id(), array( 'type' => 'success' === $type ? 'success' : 'error', 'message' => $message ), MINUTE_IN_SECONDS );
wp_safe_redirect( admin_url( 'admin.php?page=' . $page ) );
exit;
}
private function notice(): void {
$key = 'mbapi_ed_notice_' . get_current_user_id();
$notice = get_transient( $key );
delete_transient( $key );
if ( is_array( $notice ) ) {
?> $data Pagination data. */
private function pagination( array $data, string $status, string $search ): void {
if ( (int) $data['pages'] <= 1 ) {
return;
}
$base = add_query_arg( array( 'page' => self::SLUG . '-logs', 'status' => $status, 's' => $search, 'paged' => '%#%' ), admin_url( 'admin.php' ) );
$links = paginate_links( array( 'base' => $base, 'format' => '', 'current' => (int) $data['page'], 'total' => (int) $data['pages'] ) );
if ( is_string( $links ) ) {
echo '' . wp_kses_post( $links ) . '
';
}
}
}