Files
api/services/nginx/app/classes/email.php
T

624 lines
23 KiB
PHP

<?php
namespace classes;
require_once WD . '/modules/email/email_c.php';
require_once WD . '/modules/email/helpers/email_template.php';
require_once WD . '/modules/email/templates/email_template_header.php';
require_once WD . '/modules/email/templates/email_template_footer.php';
require_once WD . '/modules/email/templates/email_template_head.php';
require_once WD . '/modules/email/templates/email_template_stripe_invoice.php';
require_once WD . '/modules/email/templates/email_template_booking_confirmation.php';
require_once WD . '/modules/email/templates/email_template_booking_notification.php';
require_once WD . '/modules/email/templates/email_template_wash_certificate.php';
require_once WD . '/modules/email/templates/email_template_new_customer.php';
use AllowDynamicProperties;
use email\email_c;
use email\templates\email_template_booking_confirmation;
use email\templates\email_template_booking_notification;
use email\templates\email_template_footer;
use email\templates\email_template_head;
use email\templates\email_template_header;
use email\templates\email_template_stripe_invoice;
use email\templates\email_template_wash_certificate;
use email\templates\email_template_new_customer;
use Exception;
use interfaces\email_i;
use JsonException;
use MailerSend\Exceptions\MailerSendAssertException;
use MailerSend\Exceptions\MailerSendException;
use MailerSend\Helpers\Builder\Attachment;
use MailerSend\Helpers\Builder\EmailParams;
use MailerSend\Helpers\Builder\Header;
use MailerSend\Helpers\Builder\Recipient;
use MailerSend\MailerSend;
use objects\bookings_o;
use objects\departments_o;
use objects\users_o;
use Psr\Http\Client\ClientExceptionInterface;
#[AllowDynamicProperties] class email implements email_i
{
public static array $fake_deliveries = [];
/**
* Configuration for the email service
* @var email_c
*/
public email_c $config;
public function __construct()
{
$this->config = new email_c();
}
public function testConfigRequest(string $test_recipient): string
{
if ($test_recipient) {
try {
$this->sendEmail($test_recipient, 'Test recipient', 'Test email', 'This is a test email');
return 'Email sent successfully to ' . $test_recipient;
} catch (Exception $e) {
return 'Email error: ' . $e->getMessage();
}
} else {
return 'No test recipient provided';
}
}
/**
* Send an email using the preferred email service
* @param string $to Email address to send the email to
* @param string $recipient_name Name of the recipient
* @param string $subject Subject of the email
* @param string $message Message of the email
* @param string|null $references Optional references header value
* @param array $attachments Array of attachments, where each attachment is an array with the first element being the file content and the second element being the filename
* @throws Exception If an error occurs while sending the email
*/
public function sendEmail(string $to, string $recipient_name, string $subject, string $message, string $references = null, array $attachments = []): void
{
if ($this->config->mailersend_enabled->getVariableValue()) {
try {
// Generate HTML email
$html = self::generateHtmlHeader();
// Add email content
$html .= $message;
// Add email footer
$html .= self::generateHtmlFooter();
$this->sendEmailMailerSend($to, $recipient_name, $subject, $message, $html, $references, $attachments);
} catch (JsonException|MailerSendException|ClientExceptionInterface $e) {
throw new Exception('Error sending email: ' . $e->getMessage());
}
} else {
$this->sendEmailDefault($to, $subject, $message);
}
}
private static function generateHtmlHeader(): false|string
{
$email_template_head = (new email_template_head())->generate_html();
$email_template_header = (new email_template_header())->generate_html();
return "
<!DOCTYPE html>
<html lang='da'>
<head>
$email_template_head
</head>
<body>
$email_template_header";
}
private static function generateHtmlFooter(): string
{
$email_template_footer = (new email_template_footer())->generate_html();
return "
$email_template_footer
</body>
</html>";
}
/**
* @throws ClientExceptionInterface
* @throws JsonException
* @throws MailerSendAssertException
* @throws MailerSendException
*/
private function sendEmailMailerSend(string $to, string $recipient_name, string $subject, string $message, string $html = null, string $references = null, array $attachments = []): void
{
if (self::isFakeDeliveryEnabled()) {
self::recordFakeDelivery([
'to' => $to,
'recipient_name' => $recipient_name,
'subject' => $subject,
'message' => $message,
'html' => $html,
]);
return;
}
// Check if the email is blacklisted
$blacklisted_emails = [
'invoice.dk@freja.com', // TODO: Make this dynamic.
];
// If the email is blacklisted, return without sending the email
if (in_array($to, $blacklisted_emails)) {
return;
}
// Send POST request to email service
$mailersend = new MailerSend(['api_key' => $this->config->mailersend_api_key->getVariableValue()]);
$recipients = [
new Recipient($to, $recipient_name)
];
$emailParams = (new EmailParams())
->setFrom($this->config->smtp_from->getVariableValue())
->setFromName($this->config->smtp_from_name->getVariableValue())
->setRecipients($recipients)
->setSubject($subject)
->setHtml($html ?? $message)
->setReplyTo($this->config->smtp_reply_to->getVariableValue())
->setReplyToName($this->config->smtp_reply_to_name->getVariableValue());
if ($attachments) {
$attachments = array_map(function ($attachment) {
// Read the data from the path (Attachment[0]) and set the filename (Attachment[1])
$attachment[0] = file_get_contents($attachment[0]);
if ($attachment[0] === false) {
throw new Exception('Failed to read file: ' . $attachment[0]);
}
if (empty($attachment[1])) {
throw new Exception('Filename is empty');
}
return new Attachment($attachment[0], $attachment[1]);
}, $attachments);
}
if ($attachments) {
$emailParams->setAttachments($attachments);
}
if ($references) {
$emailParams->setHeaders([
new Header('References', $references)
]);
}
$mailersend->email->send($emailParams);
}
/**
* Send an email using the default SMTP service
* @param string $to Email address to send the email to
* @param string $subject Subject of the email
* @param string $message Message of the email
* @throws Exception If an error occurs while sending the email
*/
private function sendEmailDefault(string $to, string $subject, string $message): void
{
// Send email using default SMTP service
throw new Exception('Default SMTP service not implemented');
}
/**
* @throws MailerSendException
* @throws ClientExceptionInterface
* @throws JsonException
* @throws MailerSendAssertException
*/
public function sendStripeInvoiceEmail(string $to, string|null $recipient_name, string $payment_link, $order_id): void
{
$recipient_name = $recipient_name ?? 'Kunde';
$recipient_name = ucfirst($recipient_name);
// Generate HTML email
$html = self::generateHtmlHeader();
// Add email content
$html .= (new email_template_stripe_invoice(
$order_id,
$payment_link,
$recipient_name
))->generate_html();
// Add email footer
$html .= self::generateHtmlFooter();
$this->sendEmailMailerSend($to, $recipient_name, 'Betalingslink for bestilling #' . $order_id, '', $html);
}
public static function resetFakeDeliveries(): void
{
self::$fake_deliveries = [];
$path = self::getFakeDeliveriesPath();
if ($path !== null && is_file($path)) {
unlink($path);
}
}
public static function syncFakeDeliveries(): void
{
$path = self::getFakeDeliveriesPath();
if ($path === null || !is_file($path)) {
self::$fake_deliveries = [];
return;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
self::$fake_deliveries = [];
return;
}
$deliveries = [];
foreach ($lines as $line) {
$delivery = json_decode($line, true);
if (is_array($delivery)) {
$deliveries[] = $delivery;
}
}
self::$fake_deliveries = $deliveries;
}
private static function recordFakeDelivery(array $delivery): void
{
self::$fake_deliveries[] = $delivery;
$path = self::getFakeDeliveriesPath();
if ($path === null) {
return;
}
$directory = dirname($path);
if (!is_dir($directory)) {
mkdir($directory, 0777, true);
}
file_put_contents($path, json_encode($delivery, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL, FILE_APPEND | LOCK_EX);
}
private static function getFakeDeliveriesPath(): ?string
{
if (!self::isFakeDeliveryEnabled()) {
return null;
}
$configuredPath = trim((string)(getenv('EMAIL_FAKE_DELIVERIES_PATH') ?: ''));
if ($configuredPath !== '') {
return $configuredPath;
}
if (getenv('RUN_API_TESTS') !== '1') {
return null;
}
return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR
. 'truckwash-email-fake-deliveries-' . md5((string)getcwd()) . '.jsonl';
}
private static function isFakeDeliveryEnabled(): bool
{
return getenv('EMAIL_FAKE_MODE') === '1';
}
/**
* Send a booking confirmation email
* @throws MailerSendException
* @throws ClientExceptionInterface
* @throws JsonException
* @throws MailerSendAssertException
* @throws Exception
*/
public function sendBookingConfirmationEmail(
int $booking_id,
): void
{
// Get the booking details from the database
$booking = (new bookings_o())->select((int)$booking_id);
$customer = (new users_o())->getUserByCustomerNumber((int)$booking->customer_number->value());
$department = (new departments_o())->select((int)$booking->department->value());
// Generate HTML email
$html = self::generateHtmlHeader();
// Add email content
$html .= (new email_template_booking_confirmation(
$booking->id,
$customer->getCustomerName((int)$customer->customer_number->value()),
$customer->customer_number->value(),
$booking->contact_email->value(),
$booking->reference_number->value(),
$booking->regNrTraekker->value(),
$booking->regNrTrailer->value(),
($booking->washCertificateStatus->value() === 'cancelled' ? 'Nej' : 'Ja'),
(!empty($booking->washCertificateEmail->value()) ? (string)$booking->washCertificateEmail->value() : ''),
$booking->date->value(),
(string)$department->getDepartmentName((int)$department->id),
(string)$department->getBranding()->address->value(),
$booking->pickup_bool->value() === 'yes' ? 'Ja' : 'Nej',
$booking->notes->value(),
))->generate_html();
// Add email footer
$html .= self::generateHtmlFooter();
// Send email
$this->sendEmailMailerSend(
$booking->contact_email->value(),
$customer->getCustomerName((int)$customer->customer_number->value()),
'Truck Wash Booking ( ID: ' . $booking_id . ', REF: ' . $booking->reference_number->value() . ' )',
'',
$html
);
}
/**
* Send a wash certificate email
* @throws MailerSendException
* @throws ClientExceptionInterface
* @throws JsonException
* @throws MailerSendAssertException
*/
public function sendWashCertificateEmail(
$booking_id,
$company_name,
$contact_email,
$reference_number,
$wash_certificate_url,
$wash_certificate_email,
): void
{
// Generate HTML email
$html = self::generateHtmlHeader();
// Add email content
$html .= (new email_template_wash_certificate(
$booking_id,
$company_name,
))->generate_html();
// Add email footer
$html .= self::generateHtmlFooter();
// Attach the wash certificate
$pdf = (new pdf_store())->download($wash_certificate_url);
// Send email
$this->sendEmailMailerSend(
$wash_certificate_email,
$company_name,
'Truck Wash Booking ( ID: ' . $booking_id . ', REF: ' . $reference_number . ' )',
'',
$html,
null,
[
[
$pdf,
'wash_certificate_' . $booking_id . '_' . $reference_number . '.pdf'
]
]
);
}
/**
* @throws Exception
* @throws ClientExceptionInterface
*/
public function sendBookingNotification(int $booking_id): void
{
// Get the booking details from the database
$booking = (new bookings_o())->select((int)$booking_id);
$customer = (new users_o())->getUserByCustomerNumber((int)$booking->customer_number->value());
$department = (new departments_o())->select((int)$booking->department->value());
// Generate HTML email
$html = self::generateHtmlHeader();
$html .= (new email_template_booking_notification(
$booking->id,
$customer->getCustomerName((int)$customer->customer_number->value()),
$customer->customer_number->value(),
$booking->contact_email->value(),
$booking->reference_number->value(),
$booking->regNrTraekker->value(),
$booking->regNrTrailer->value(),
($booking->washCertificateStatus->value() === 'cancelled' ? 'Nej' : 'Ja'),
(!empty($booking->washCertificateEmail->value()) ? (string)$booking->washCertificateEmail->value() : ''),
$booking->date->value(),
(string)$department->getDepartmentName((int)$department->id),
(string)$department->getBranding()->address->value(),
$booking->pickup_bool->value() === 'yes' ? 'Ja' : 'Nej',
$booking->notes->value(),
))->generate_html();
// Add email footer
$html .= self::generateHtmlFooter();
// Send email
$this->sendEmailMailerSend(
$department->getBranding()->email->value(),
$department->getDepartmentName((int)$department->id),
'Truck Wash Booking ( ID: ' . $booking_id . ', REF: ' . $booking->reference_number->value() . ' )',
'',
$html
);
}
/**
* @throws JsonException
* @throws Exception
*/
public function sendOrderBookingConfirmationEmail(\objects\order_bookings_o $order_booking): void
{
// Validate the booking object
$order_booking->requireSelected();
// Get customer details
$customer = (new users_o())->getUserByCustomerNumber((int)$order_booking->customer_number->value());
$customer->requireSelected();
$recipient_name = $customer->getCustomerName((int)$customer->customer_number->value());
$recipient_email = $customer->email->value();
$customer_contact_email = ($recipient_email ?: '');
// Check if the customer has a booking-specific email provided
if (!empty($customer->wash_certificate_email->value())) {
$recipient_email = (string)$customer->wash_certificate_email->value();
}
// Get the department details
$department = (new departments_o())->select((int)$order_booking->department->value());
$department->requireSelected();
// Generate HTML email
$html = self::generateHtmlHeader();
// Add email content
$html .= (new email_template_booking_confirmation(
$order_booking->id,
$recipient_name,
$customer->customer_number->value(),
$customer_contact_email,
$order_booking->reference->value(),
$order_booking->reg_1->value(),
$order_booking->reg_2->value(),
($order_booking->containsWashCertificateItem() ? 'Ja' : 'Nej'),
$recipient_email,
$order_booking->datetime->value(),
(string)$department->getDepartmentName((int)$department->id),
(string)$department->getBranding()->address->value(),
!!$order_booking->pickup->value() ? 'Ja' : 'Nej',
$order_booking->note->value()
))->generate_html();
// Add email footer
$html .= self::generateHtmlFooter();
// Send email
$this->sendEmailMailerSend(
$recipient_email,
$department->getDepartmentName((int)$department->id),
'Truck Wash Booking ( ID: ' . $order_booking->id . ', REF: ' . $order_booking->reference->value() . ' )',
'',
$html
);
}
/**
* @throws JsonException
* @throws Exception
*/
public function sendWashCertificateEmailToCustomer(\objects\order_bookings_o $order_booking): void
{
// Validate the booking object
$order_booking->requireSelected();
if (!$order_booking->hasTransaction()) return; // Only send a wash certificate if the order has been created.
// Get the order details
$order = $order_booking->getOrder();
// Get customer details
$customer = (new users_o())->getUserByCustomerNumber((int)$order_booking->customer_number->value());
$customer->requireSelected();
$recipient_name = $customer->getCustomerName((int)$customer->customer_number->value());
$recipient_email = $customer->email->value();
// Check if the customer has a booking-specific email provided
if (!empty($customer->wash_certificate_email->value())) {
$recipient_email = (string)$customer->wash_certificate_email->value();
}
// Generate HTML email
$html = self::generateHtmlHeader();
// Add email content
$html .= (new email_template_wash_certificate(
$order_booking->id,
$recipient_name,
))->generate_html();
// Add email footer
$html .= self::generateHtmlFooter();
// Attach the wash certificate
$attachments = $order->listAttachments();
$pdf = null;
foreach ($attachments as $attachment) {
if ($attachment->isWashCertificate()) {
$pdf = (new pdf_store())->download($attachment->content->document);
break;
}
}
if (!$pdf) {
throw new Exception('Wash certificate PDF not found for order booking ID: ' . $order_booking->id);
}
// Send email
$this->sendEmailMailerSend(
$recipient_email,
$recipient_name,
'Truck Wash Booking ( ID: ' . $order_booking->id . ', REF: ' . $order_booking->reference->value() . ' )',
'',
$html,
null,
[
[
$pdf,
'wash_certificate_' . $order_booking->id . '_' . $order_booking->reference->value() . '.pdf'
]
]
);
}
/**
* @throws Exception
*/
public function sendWelcomeEmailToCustomer(int $customer_number, string $email): void
{
$customer = (new users_o())->getUserByCustomerNumber($customer_number);
if (!$customer->exists()) {
throw new Exception('Customer not found with customer number: ' . $customer_number);
}
$password_reset_link = $customer->generatePasswordResetLink();
$company_name = $customer->getCustomerName((int)$customer->customer_number->value());
// Generate HTML email using the new customer template
$html = self::generateHtmlHeader();
$html .= (new email_template_new_customer(
(int)$customer->customer_number->value(),
$password_reset_link
))->generate_html();
$html .= self::generateHtmlFooter();
// Prepare attachment (departments flyer)
$this->attachments = [
/**
* [
* 'https://www.truckwash.dk/wp-content/uploads/2024/10/Truck-Wash_Flyer_A5.pdf',
* 'Truck Wash Afdelinger.pdf'
* ]
*/
];
// Send the email via MailerSend
$this->sendEmailMailerSend(
$email,
$company_name,
'Velkommen til Truck Wash',
'',
$html,
null,
$this->attachments
);
}
/**
* @throws Exception
*/
public function sendNewCustomerRegistrationNotifications(int $customer_number): void
{
$customer = (new users_o())->getUserByCustomerNumber($customer_number);
if (!$customer->exists()) {
throw new Exception('Customer not found with customer number: ' . $customer_number);
}
$customerName = $customer->getCustomerName((int)$customer->customer_number->value()) ?: 'Unknown customer';
$safeCustomerName = htmlspecialchars($customerName, ENT_QUOTES, 'UTF-8');
$safeCustomerNumber = (int)$customer->customer_number->value();
$customerUrl = 'https://truckwash.io/superuser/users?search=' . $safeCustomerNumber;
$message = "
<p>A new customer has registered on truckwash.io.</p>
<p>
<strong>Customer number:</strong> $safeCustomerNumber<br>
<strong>Customer name:</strong> $safeCustomerName
</p>
<p><a href='$customerUrl'>Open customer in Superuser</a></p>
";
foreach ((new users_o())->getSuperuserNewCustomerEmailNotificationRecipients() as $recipient) {
$recipientEmail = trim((string)($recipient['email'] ?? ''));
if ($recipientEmail === '') {
continue;
}
$recipientName = trim((string)($recipient['display_name'] ?? ''));
if ($recipientName === '') {
$recipientName = $recipientEmail;
}
$this->sendEmail(
$recipientEmail,
$recipientName,
'New customer registered on Truck Wash',
$message,
);
}
}
}