feat(mail): add SMTP mailer and order confirmation mail builder

This commit is contained in:
dualfroz committed 2026-08-23 21:49:10 +02:00
1 parent 2897d0ba89
commit 08929b48b2
2 files changed
+144

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
<?php
namespace Bazaar\Mail;
final class OrderMailer
{
public function __construct(
private readonly Mailer $mailer,
private readonly string $currency = 'usd',
private readonly string $appName = 'Bazaar'
) {
}
public function orderConfirmation(string $recipient, array $order, array $items): void
{
$this->mailer->send(new Email(
$recipient,
sprintf('%s order #%d confirmed', $this->appName, (int) $order['id']),
$this->confirmationBody($order, $items)
));
}
public function statusUpdate(string $recipient, array $order): void
{
$this->mailer->send(new Email(
$recipient,
sprintf('%s order #%d is now %s', $this->appName, (int) $order['id'], $order['status']),
$this->statusBody($order)
));
}
private function confirmationBody(array $order, array $items): string
{
$lines = [
'Thank you for your order.',
'',
'Order #' . (int) $order['id'],
'',
];
foreach ($items as $item) {
$label = $item['productName'];
if (($item['variantLabel'] ?? '') !== '') {
$label .= ' (' . $item['variantLabel'] . ')';
}
$lines[] = sprintf(
' %d x %s - %s',
(int) $item['quantity'],
$label,
formatMoney((int) $item['unitPriceCents'] * (int) $item['quantity'], $order['currency'])
);
}
$lines[] = '';
if ((int) ($order['discountCents'] ?? 0) > 0) {
$lines[] = 'Discount (' . ($order['couponCode'] ?? '') . '): -'
. formatMoney((int) $order['discountCents'], $order['currency']);
}
$lines[] = 'Total: ' . formatMoney((int) $order['totalCents'], $order['currency']);
return implode("\n", $lines);
}
private function statusBody(array $order): string
{
return implode("\n", [
'Your order status has changed.',
'',
'Order #' . (int) $order['id'],
'New status: ' . $order['status'],
]);
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace Bazaar\Mail;
final class SmtpMailer implements Mailer
{
private array $sent = [];
public function __construct(
private readonly string $host,
private readonly int $port = 587,
private readonly ?string $username = null,
private readonly ?string $password = null,
private readonly int $timeout = 10
) {
}
public function send(Email $email): void
{
$socket = fsockopen($this->host, $this->port, $errno, $error, $this->timeout);
if ($socket === false) {
throw new \RuntimeException("SMTP connection failed: {$error} ({$errno})");
}
$this->expect($socket, 220);
$this->command($socket, 'EHLO bazaar', 250);
if ($this->username !== null && $this->password !== null) {
$this->command($socket, 'AUTH LOGIN', 334);
$this->command($socket, base64_encode($this->username), 334);
$this->command($socket, base64_encode($this->password), 235);
}
$this->command($socket, 'MAIL FROM:<' . $email->from . '>', 250);
$this->command($socket, 'RCPT TO:<' . $email->to . '>', 250);
$this->command($socket, 'DATA', 354);
$headers = implode("\r\n", [
'From: ' . $email->from,
'To: ' . $email->to,
'Subject: ' . $email->subject,
'MIME-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
]);
fwrite($socket, $headers . "\r\n\r\n" . $email->body . "\r\n.\r\n");
$this->expect($socket, 250);
$this->command($socket, 'QUIT', 221);
fclose($socket);
$this->sent[] = $email;
}
public function sent(): array
{
return $this->sent;
}
private function command($socket, string $line, int $expected): void
{
fwrite($socket, $line . "\r\n");
$this->expect($socket, $expected);
}
private function expect($socket, int $expected): void
{
$response = (string) fgets($socket, 512);
$code = (int) substr($response, 0, 3);
if ($code !== $expected) {
throw new \RuntimeException("SMTP expected {$expected}, got: " . trim($response));
}
}
}