is_sendable() ) { return false; } /** * Filters a notification right before it is handed to the mailer. * * @since 1.0.0 * * @param Notification $notification Notification about to be sent. */ $notification = apply_filters( 'formpilot_notification', $notification ); $this->last_error = null; $this->text = $notification->text(); add_action( 'wp_mail_failed', array( $this, 'capture_error' ) ); add_action( 'phpmailer_init', array( $this, 'add_text_part' ) ); $sent = wp_mail( $notification->to(), $notification->subject(), $notification->message(), $notification->headers(), $this->readable_attachments( $notification->attachments() ) ); remove_action( 'phpmailer_init', array( $this, 'add_text_part' ) ); remove_action( 'wp_mail_failed', array( $this, 'capture_error' ) ); $this->text = ''; /** * Fires after a notification has been handed to the mailer. * * @since 1.0.0 * * @param bool $sent Whether the mailer accepted the message. * @param Notification $notification Notification that was sent. * @param WP_Error|null $error Failure reported by the mailer, if any. */ do_action( 'formpilot_notification_sent', $sent, $notification, $this->last_error ); return (bool) $sent; } /** * Stores the error reported by `wp_mail()`. * * @param WP_Error $error Failure reported by the mailer. */ public function capture_error( WP_Error $error ): void { $this->last_error = $error; } /** * Returns the error reported by the last failed send. * * @return WP_Error|null The error, or null when the last send succeeded. */ public function last_error(): ?WP_Error { return $this->last_error; } /** * Drops attachment paths that no longer exist. * * A missing file makes PHPMailer throw, which would lose the whole message * over a single deleted upload. * * @param array $paths Absolute file paths. * * @return array The readable paths. */ private function readable_attachments( array $paths ): array { /** * Filters the total size attachments may reach, in bytes. * * Mail servers reject an oversized message outright, so a large upload * would cost the notification rather than just the attachment. * * @since 1.0.0 * * @param int $limit Total attachment budget in bytes. */ $budget = (int) apply_filters( 'formpilot_attachment_size_limit', 10 * MB_IN_BYTES ); $attachments = array(); $total = 0; foreach ( $paths as $path ) { /* * Containment is asserted here, at the sink, not only where the list was * built. * * `is_readable()` was the only test, and it is as happy with * `wp-config.php` as with an upload. The builder screens its own list — * but a Notification also arrives from `Notification::from_array()` off * the cron queue, and from anything filtering `formpilot_notifications`, so the * list reaching this method is not one the builder necessarily made. */ if ( '' === $path || ! UploadManager::owns( $path ) || ! is_readable( $path ) ) { continue; } $size = filesize( $path ); /* * A file that cannot be measured does not get a free ride. `filesize()` * answers false on failure, and `(int) false` is 0 — so an unstattable * file consumed none of the budget and was attached regardless of how * large it actually was. */ if ( false === $size ) { /** This action is documented below. */ do_action( 'formpilot_attachment_skipped', $path, 0 ); continue; } $size = (int) $size; if ( $budget > 0 && ( $total + $size ) > $budget ) { /** * Fires when an attachment is left off an oversized message. * * The entry still holds the file, so a handler can link to it * instead of attaching it. * * @since 1.0.0 * * @param string $path Absolute path of the skipped file. * @param int $size Size of the skipped file in bytes. */ do_action( 'formpilot_attachment_skipped', $path, $size ); continue; } $total += $size; $attachments[] = $path; } return $attachments; } /** * Adds the plain text half of the message. * * `wp_mail()` carries one body, so the text alternative is attached to the * mailer directly. A message with both parts renders in clients that cannot * show HTML and scores better with spam filters, which treat HTML-only mail * as a signal. * * @param object $phpmailer Mailer about to send, passed by reference by WordPress. */ public function add_text_part( object $phpmailer ): void { if ( '' === $this->text ) { return; } // Only meaningful when the message itself is HTML. if ( ! isset( $phpmailer->ContentType ) || 'text/html' !== $phpmailer->ContentType ) { return; } $phpmailer->AltBody = $this->text; } }