recipients = new RecipientResolver(); $this->renderer = new Renderer(); $this->ics = new Ics(); $this->logs = new DeliveryLogRepository(); } /** * @param mixed[] $args * @return mixed[] */ public function send(NotificationEntity $notification, BookingEntity $booking, string $context, array $args = []): array { $template = isset($args['template']) && is_array($args['template']) ? $args['template'] : $notification->template($context); $source = $this->source($args); $parent_log_id = absint($args['retry_of_log_id'] ?? 0); $attempt = $this->attempt($parent_log_id); $force = !empty($args['force']); if (!$force && empty($template['active'])) { $log_id = $this->log($notification, $booking, $context, [], $template, DeliveryLog::STATUS_SKIPPED, $source, $attempt, $parent_log_id, esc_html__('Notification recipient is inactive.', 'daytidy')); return [ 'sent' => 0, 'failed' => 0, 'skipped' => 1, 'messages' => [esc_html__('Notification recipient is inactive.', 'daytidy')], 'log_ids' => [$log_id], ]; } $recipients = isset($args['recipient']) && is_array($args['recipient']) ? [$args['recipient']] : (isset($args['test_to']) ? $this->testRecipients($args['test_to'], $context) : $this->recipients->resolve($booking, $context, $template)); if (!count($recipients)) { $log_id = $this->log($notification, $booking, $context, [], $template, DeliveryLog::STATUS_SKIPPED, $source, $attempt, $parent_log_id, esc_html__('No recipients were found.', 'daytidy')); return [ 'sent' => 0, 'failed' => 0, 'skipped' => 1, 'messages' => [esc_html__('No recipients were found.', 'daytidy')], 'log_ids' => [$log_id], ]; } $suppress_extra = !empty($args['suppress_extra_recipients']); $placeholders = isset($args['placeholders']) && is_array($args['placeholders']) ? $args['placeholders'] : []; $headers = $this->headers($template, $suppress_extra); $temp_files = []; $attachments = $this->attachments($template, $booking, $temp_files); $headers = (array) apply_filters( 'DayTidy/F__DayTidy/Notifications__headers/headers', $headers, $notification, $booking, $context, $template ); $attachments = (array) apply_filters( 'DayTidy/F__DayTidy/Notifications__attachments/attachments', $attachments, $notification, $booking, $context, $template ); $results = [ 'sent' => 0, 'failed' => 0, 'skipped' => 0, 'messages' => [], 'log_ids' => [], ]; foreach ($recipients as $recipient) { $subject = wp_strip_all_tags($this->renderer->render($template, $booking, $recipient, 'subject', $placeholders)); $message = $this->renderer->render($template, $booking, $recipient, 'content', $placeholders); if (apply_filters('DayTidy/F__DayTidy/Notifications__run_shortcodes/enabled', true, $template, $notification, $booking, $context)) { $message = do_shortcode($message); } $message = wpautop($message); $subject = (string) apply_filters( 'DayTidy/F__DayTidy/Notifications__subject/subject', $subject, $notification, $booking, $context, $recipient, $template ); $message = (string) apply_filters( 'DayTidy/F__DayTidy/Notifications__message/message', $message, $notification, $booking, $context, $recipient, $template ); do_action( 'DayTidy/A__DayTidy/Notifications__before_send', $notification, $booking, $context, $recipient, $subject, $message, $headers, $attachments ); $sent = false; $mail_errors = []; $mail_failed = function ($error) use (&$mail_errors): void { if ($error instanceof \WP_Error) $mail_errors[] = $error->get_error_message(); }; add_action('wp_mail_failed', $mail_failed); try { $sent = wp_mail( // Keep display names with commas intact; wp_mail() only comma-splits string recipients. [$this->mailbox($recipient)], $subject, $message, $headers, $attachments ); } catch (\Throwable $e) { $results['messages'][] = $e->getMessage(); $mail_errors[] = $e->getMessage(); } finally { remove_action('wp_mail_failed', $mail_failed); } if ($sent) { $results['sent']++; $log_id = $this->log($notification, $booking, $context, $recipient, $template, DeliveryLog::STATUS_SENT, $source, $attempt, $parent_log_id, '', $subject, $message); $results['log_ids'][] = $log_id; } else { $error = $this->mailError($mail_errors); if ($error !== '') $results['messages'][] = $error; $results['failed']++; $log_id = $this->log($notification, $booking, $context, $recipient, $template, DeliveryLog::STATUS_FAILED, $source, $attempt, $parent_log_id, $error, $subject, $message); $results['log_ids'][] = $log_id; do_action( 'DayTidy/A__DayTidy/Notifications__send_failed', $notification, $booking, $context, $recipient, $subject, $message, $headers, $attachments, $error ); } do_action( 'DayTidy/A__DayTidy/Notifications__after_send', $notification, $booking, $context, $recipient, $sent, $subject, $message, $headers, $attachments, $log_id ); } $this->cleanup($temp_files); return $results; } /** * @param mixed $to * @return mixed[] */ private function testRecipients($to, string $context): array { $emails = is_array($to) ? $to : [$to]; $recipients = []; foreach ($emails as $email) { $email = sanitize_email((string) $email); if (!is_email($email)) continue; $recipients[] = [ 'email' => $email, 'name' => '', 'context' => $context, 'data' => [], ]; } return $recipients; } /** * @param mixed[] $template * @return string[] */ private function headers(array $template, bool $suppress_extra): array { $headers = ['Content-Type: text/html; charset=UTF-8']; if ($suppress_extra) return $headers; foreach (['cc' => 'Cc', 'bcc' => 'Bcc'] as $field => $header) { $enabled_field = $field . '_enabled'; if (array_key_exists($enabled_field, $template) && empty($template[$enabled_field])) continue; foreach ($this->recipients->picker((array) ($template[$field] ?? [])) as $recipient) { $headers[] = $header . ': ' . $recipient['email']; } } return $headers; } /** * @param mixed[] $template * @param string[] $temp_files * @return string[] */ private function attachments(array $template, BookingEntity $booking, array &$temp_files): array { $attachments = []; foreach ((array) ($template['attachments'] ?? []) as $attachment_id) { $path = get_attached_file((int) $attachment_id); if ($path && file_exists($path)) $attachments[] = $path; } if (!empty($template['attach_ical'])) { $path = $this->ics->create($booking); if ($path) { $attachments[] = $path; $temp_files[] = $path; } } return $attachments; } /** * @param mixed[] $recipient */ private function mailbox(array $recipient): string { $email = (string) ($recipient['email'] ?? ''); $name = trim((string) ($recipient['name'] ?? '')); return $name ? $name . ' <' . $email . '>' : $email; } /** * @param string[] $paths */ private function cleanup(array $paths): void { foreach ($paths as $path) { if (is_string($path) && file_exists($path)) wp_delete_file($path); } } /** * @param mixed[] $args */ private function source(array $args): string { $source = sanitize_key((string) ($args['source'] ?? DeliveryLog::SOURCE_EVENT)); return in_array($source, DeliveryLog::sources(), true) ? $source : DeliveryLog::SOURCE_EVENT; } private function attempt(int $parent_log_id): int { if (!$parent_log_id) return 1; $parent = $this->logs->find($parent_log_id); return $parent ? max(1, (int) ($parent->attempt ?? 1)) + 1 : 1; } /** * @param mixed[] $recipient * @param mixed[] $template */ private function log( NotificationEntity $notification, BookingEntity $booking, string $context, array $recipient, array $template, string $status, string $source, int $attempt, int $parent_log_id, string $error, string $subject = '', string $body = '' ): int { if (!apply_filters('DayTidy/F__DayTidy/Notifications/EmailSender__logging/enabled', false, $notification, $booking, $context, $status, $source)) { return 0; } [$logged_subject, $logged_body] = $this->loggedContent($notification, $template, $subject, $body); return $this->logs->insert([ 'notification_id' => $notification->id(), 'booking_id' => $booking->id(), 'trigger_key' => $notification->trigger(), 'recipient_context' => $context, 'recipient_email' => (string) ($recipient['email'] ?? ''), 'recipient_name' => (string) ($recipient['name'] ?? ''), 'recipient_snapshot' => $recipient, 'subject' => $logged_subject, 'body' => $logged_body, 'status' => $status, 'source' => $source, 'attempt' => $attempt, 'parent_log_id' => $parent_log_id, 'error_message' => $error, ]); } /** * @param mixed[] $template * @return array{0: string, 1: string} */ private function loggedContent(NotificationEntity $notification, array $template, string $subject, string $body): array { if ($notification->trigger() === Registry::TRIGGER_CUSTOMER_PORTAL_VERIFICATION_CODE) { return [ (string) ($template['subject'] ?? ''), (string) ($template['content'] ?? ''), ]; } return [ $subject !== '' ? $subject : (string) ($template['subject'] ?? ''), $body, ]; } /** * @param string[] $errors */ private function mailError(array $errors): string { $errors = array_values(array_filter(array_map('strval', $errors))); if (count($errors)) return implode("\n", array_unique($errors)); return esc_html__('wp_mail() returned false.', 'daytidy'); } }