Notifications (admin/class-tools-page.php) * gives an admin somewhere to actually see delivery status instead of * having to dig through Activity Log or a host's mail log. * * Deliberately NOT a generic outgoing-mail queue for every wp_mail() call * ChargeForms makes (double-optin emails, Email Summaries) — those have * different retry semantics worth designing separately; this is scoped * to the one notification type the audit specifically flagged. */ class Notifications { const RETRY_HOOK = 'chargeforms_retry_notification'; const MAX_ATTEMPTS = 3; const RETRY_DELAY_SECONDS = 300; // 5 minutes — long enough that a transient host mail hiccup has usually cleared, short enough an admin isn't left wondering for hours. public static function boot() { add_action( self::RETRY_HOOK, [ __CLASS__, 'handle_retry' ], 10, 7 ); } /** * Sends one notification email. Returns the same true/false wp_mail() * itself would have, so SubmissionHandler's existing call site didn't * need to change how it treats the return value — only what happens * behind it. * * @param string[] $headers Raw wp_mail()-style header lines (e.g. * 'Cc: a@x.com, b@x.com') — built by the * caller so this class stays unopinionated * about where Cc/Bcc/From come from. * @param string[] $attachments Absolute file paths, passed straight * through to wp_mail(). */ public static function send( $form_id, $entry_id, $to, $subject, $body, array $headers = [], array $attachments = [] ) { $log_id = Notification_Log::create( $form_id, $entry_id, $to, $subject ); return self::attempt( $log_id, $form_id, $entry_id, $to, $subject, $body, $headers, $attachments ); } /** WP-Cron callback — re-attempts a specific previously-logged send. */ public static function handle_retry( $log_id, $form_id, $entry_id, $to, $subject, $body, $headers = [], $attachments = [] ) { self::attempt( $log_id, $form_id, $entry_id, $to, $subject, $body, $headers, $attachments ); } private static function attempt( $log_id, $form_id, $entry_id, $to, $subject, $body, array $headers = [], array $attachments = [] ) { $error_message = ''; // A temporary, request-scoped hook just to capture THIS attempt's // failure reason — Activity_Log::log_mail_failure() (already // hooked in SubmissionHandler's constructor) still fires too and // keeps doing its own thing; this doesn't replace it. $capture_error = function ( $wp_error ) use ( &$error_message ) { $error_message = $wp_error->get_error_message(); }; add_action( 'wp_mail_failed', $capture_error ); // Tags this one wp_mail() call for Email\Email_Log's generic // wp_mail_succeeded/failed hooks — see docs/SMTP_ARCHITECTURE.md // §8. Purely additive: Notification_Log (above) is unaffected, // this only enriches the separate, broader SMTP delivery log. \ChargeForms\Includes\Email\Email_Log::set_next_context( [ 'email_type' => 'notification', 'form_id' => $form_id, 'entry_id' => $entry_id, ] ); $sent = wp_mail( $to, $subject, $body, $headers, $attachments ); remove_action( 'wp_mail_failed', $capture_error ); if ( $sent ) { Notification_Log::mark_sent( $log_id ); return true; } $row = Notification_Log::get( $log_id ); $attempts_so_far = (int) ( $row['attempts'] ?? 0 ); $will_retry = ( $attempts_so_far + 1 ) < self::MAX_ATTEMPTS; Notification_Log::mark_failed( $log_id, '' !== $error_message ? $error_message : __( 'wp_mail() returned false.', 'chargeforms' ), $will_retry ); if ( $will_retry ) { wp_schedule_single_event( time() + self::RETRY_DELAY_SECONDS, self::RETRY_HOOK, [ $log_id, $form_id, $entry_id, $to, $subject, $body, $headers, $attachments ] ); } return false; } }