get_error_message(); } } /** * Send a message and report what happened. * * @param array|string $to Recipients. * @param string $subject Subject line. * @param string $message Body. * @param array $headers Headers. * @param array $context Trusted entry/job/purpose context, never sent as recipient-visible content. * @return array{sent: bool, error: string} */ public static function send( $to, $subject, $message, array $headers = array(), array $context = array() ) { self::$last_error = ''; $settings = Settings::get(); if ( ! empty( $settings['test_delivery'] ) ) { if ( ! is_email( $settings['test_recipient'] ) ) { return array( 'sent' => false, 'error' => 'Test recipient is not configured.', ); } $to = array( $settings['test_recipient'] ); $headers = array_values( array_filter( $headers, static function ( $header ) { return ! preg_match( '/^(?:to|cc|bcc|reply-to):/i', trim( $header ) ); } ) ); $subject = '[Test] ' . $subject; } $context['test'] = self::$debugging || ! empty( $settings['test_delivery'] ); $result = self::attempt( $to, $subject, $message, $headers, true, $context ); $routes = array( $result ); if ( ! $result['sent'] && ! $result['uncertain'] && true === Extensions::filter( 'accessible_forms_mail_fallback', false, $result ) ) { // One fallback only, using the same prepared message and WordPress's // normal configuration. Never redirect test mail or filter its body twice. $result = self::attempt( $to, $subject, $message, $headers, false, $context ); $routes[] = $result; } $result['routes'] = $routes; return $result; } /** * One WordPress handoff of an already prepared message, with no Pro API/SMTP * configuration or further fallback. Used by a durable, recipient-scoped retry. * The caller must preserve staging safety before supplying prepared content. */ public static function send_wordpress( $to, $subject, $message, array $headers = array() ) { $result = self::attempt( $to, $subject, $message, $headers, false ); $result['routes'] = array( $result ); return $result; } /** One scoped handoff. Add-ons may observe its outcome before a fallback is considered. */ private static function attempt( $to, $subject, $message, array $headers, $configure = true, array $context = array() ) { self::$last_error = ''; $transport = $configure ? self::transport() : __( 'WordPress default (wp_mail)', 'accessible-forms-by-accessible-org' ); $uncertain = false; $identity = wp_generate_uuid4(); // A dedicated API transport receives the final, staging-safe message. // This hook is scoped to Accessible Forms and omitted on the WP fallback. $delivery = $configure ? Extensions::filter( 'accessible_forms_mail_delivery', null, array( 'to' => $to, 'subject' => $subject, 'message' => $message, 'headers' => $headers ), array_merge( $context, array( 'debugging' => self::$debugging, 'attempt_id' => $identity ) ) ) : null; if ( null !== $delivery ) { if ( ! is_array( $delivery ) || ! isset( $delivery['sent'], $delivery['error'], $delivery['uncertain'] ) || ! is_bool( $delivery['sent'] ) || ! is_bool( $delivery['uncertain'] ) || ! is_string( $delivery['error'] ) ) { $delivery = array( 'sent' => false, 'error' => 'The mail transport did not report a valid outcome.', 'uncertain' => true, 'reason' => 'interrupted' ); } $delivery['transport'] = $transport; $delivery['attempt_id'] = $identity; if ( $delivery['sent'] ) { $delivery['error'] = ''; $delivery['uncertain'] = false; } return self::observed_result( $delivery, $configure ); } $observe = static function ( $mailer ) use ( $configure, $identity ) { Extensions::notify( 'accessible_forms_mailer_observe', $mailer, $configure, self::$debugging, $identity ); }; $previous_mailer = isset( $GLOBALS['phpmailer'] ) && is_object( $GLOBALS['phpmailer'] ) ? $GLOBALS['phpmailer'] : null; if ( $previous_mailer ) { $GLOBALS['phpmailer'] = clone $previous_mailer; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Work on a scoped copy, preserving the shared mailer and its identity. } add_action( 'wp_mail_failed', array( self::class, 'capture_failure' ) ); add_filter( 'wp_mail_from', array( self::class, 'filter_from' ), 20 ); add_filter( 'wp_mail_from_name', array( self::class, 'filter_from_name' ), 20 ); if ( $configure ) { add_action( 'phpmailer_init', array( self::class, 'configure' ) ); } add_action( 'phpmailer_init', $observe, PHP_INT_MAX ); try { add_action( 'phpmailer_init', array( self::class, 'plain_alternative' ) ); $sent = wp_mail( $to, $subject, $message, $headers ); } catch ( \Throwable $error ) { $sent = false; $uncertain = true; self::$last_error = 'timeout: transport interrupted; delivery is uncertain.'; } finally { remove_action( 'phpmailer_init', $observe, PHP_INT_MAX ); remove_action( 'phpmailer_init', array( self::class, 'plain_alternative' ) ); remove_action( 'wp_mail_failed', array( self::class, 'capture_failure' ) ); remove_filter( 'wp_mail_from', array( self::class, 'filter_from' ), 20 ); remove_filter( 'wp_mail_from_name', array( self::class, 'filter_from_name' ), 20 ); remove_action( 'phpmailer_init', array( self::class, 'configure' ) ); // Restore the shared mailer so this plugin's SMTP settings and // headers never leak into email sent later by other code. if ( $previous_mailer ) { $GLOBALS['phpmailer'] = $previous_mailer; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Restores the object that was there before this send. } else { unset( $GLOBALS['phpmailer'] ); } } $result = array( 'sent' => (bool) $sent, 'error' => $sent ? '' : ( self::$last_error ? self::$last_error : __( 'wp_mail() returned false without an error message.', 'accessible-forms-by-accessible-org' ) ), ); $result['transport'] = $transport; $result['attempt_id'] = $identity; $result['uncertain'] = ! $sent && ( $uncertain || (bool) preg_match( '/timed?\s*out|timeout/i', $result['error'] ) ); return self::observed_result( $result, $configure ); } /** Validate an observer without changing the transport's actual handoff. */ private static function observed_result( array $result, $configure ) { $observed = Extensions::filter( 'accessible_forms_mail_result', $result, $configure ); // A result observer must not hide a successful handoff or invent one. if ( is_array( $observed ) && isset( $observed['sent'], $observed['error'], $observed['uncertain'] ) && $observed['sent'] === $result['sent'] ) { $result = $observed; } return $result; } public static function plain_alternative( $mailer ) { if ( 'text/html' === $mailer->ContentType ) { // Keep each answer on its own line: "Label: value". $text = preg_replace( '#
#is', '', (string) $mailer->Body ); $text = str_replace( array( '', '', '' ), array( ': ', "\n", "\n\n" ), $text ); $mailer->AltBody = trim( wp_strip_all_tags( str_replace( array( '', '', '