feat(mail): add SMTP mailer and order confirmation mail builder
This commit is contained in:
1 parent
2897d0ba89
commit
08929b48b2
2 files changed
+144
No files matched your search
@@ -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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user