Implementiere echtes sendmail für den Server. Lokal gibts kein mail und keine outbox mehr
This commit is contained in:
1 parent
6c16e16354
commit
b58d96cd5e
11 files changed
+109
-51
No files matched your search
@@ -55,10 +55,59 @@ function bookingProjectTitle(string $project): string
|
||||
return $project === 'produktionsschule' ? 'Produktionsschule' : 'PerspektivWerk';
|
||||
}
|
||||
|
||||
function bookingQueueMail(PDO $database, string $registrationId, string $kind, string $email, string $subject, string $body): void
|
||||
function bookingMailTransport(?string $configuredTransport = null): string
|
||||
{
|
||||
$statement = $database->prepare('INSERT IGNORE INTO sufw_booking_outbox (registration_id, kind, recipient_email, subject, body) VALUES (:registrationId, :kind, :email, :subject, :body)');
|
||||
$statement->execute(compact('registrationId', 'kind', 'email', 'subject', 'body'));
|
||||
$transport = $configuredTransport ?? getenv('BOOKING_MAIL_TRANSPORT');
|
||||
if (!is_string($transport) || $transport === '') return 'sendmail';
|
||||
return $transport;
|
||||
}
|
||||
|
||||
function bookingIcsCalendar(array $event): string
|
||||
{
|
||||
$timezone = new DateTimeZone('Europe/Berlin');
|
||||
$utc = new DateTimeZone('UTC');
|
||||
$start = new DateTimeImmutable((string) $event['starts_at'], $timezone);
|
||||
$end = new DateTimeImmutable((string) $event['ends_at'], $timezone);
|
||||
$lines = [
|
||||
'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//SUFW//Terminbuchung//DE', 'CALSCALE:GREGORIAN',
|
||||
'BEGIN:VEVENT',
|
||||
'UID:'.bookingIcsEscape((string) $event['id']).'@sufw-booking',
|
||||
'DTSTAMP:'.gmdate('Ymd\\THis\\Z'),
|
||||
'DTSTART:'.$start->setTimezone($utc)->format('Ymd\\THis\\Z'),
|
||||
'DTEND:'.$end->setTimezone($utc)->format('Ymd\\THis\\Z'),
|
||||
'SUMMARY:'.bookingIcsEscape((string) $event['title']),
|
||||
'DESCRIPTION:'.bookingIcsEscape(($event['format'] ?? '') === 'telefonisch' ? 'Telefonischer Termin' : 'Termin vor Ort'),
|
||||
'END:VEVENT', 'END:VCALENDAR',
|
||||
];
|
||||
return implode("\r\n", $lines)."\r\n";
|
||||
}
|
||||
|
||||
function bookingSendMail(string $email, string $subject, string $textBody, ?string $calendar = null): void
|
||||
{
|
||||
$transport = bookingMailTransport();
|
||||
if ($transport === 'disabled') return;
|
||||
if ($transport !== 'sendmail') throw new RuntimeException('Unsupported booking mail transport.');
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) throw new InvalidArgumentException('Invalid mail recipient.');
|
||||
|
||||
$sender = 'sufw-buchung@boogiedev.net';
|
||||
$headers = ['From' => $sender, 'Reply-To' => $sender, 'MIME-Version' => '1.0'];
|
||||
if ($calendar === null) {
|
||||
$headers['Content-Type'] = 'text/plain; charset=UTF-8';
|
||||
$message = $textBody;
|
||||
} else {
|
||||
$boundary = '=_sufw_'.bin2hex(random_bytes(16));
|
||||
$headers['Content-Type'] = 'multipart/mixed; boundary="'.$boundary.'"';
|
||||
$message = '--'.$boundary."\r\n"
|
||||
."Content-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n"
|
||||
.$textBody."\r\n--".$boundary."\r\n"
|
||||
."Content-Type: text/calendar; charset=UTF-8; method=PUBLISH; name=\"termin.ics\"\r\n"
|
||||
."Content-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename=\"termin.ics\"\r\n\r\n"
|
||||
.chunk_split(base64_encode($calendar), 76, "\r\n")
|
||||
.'--'.$boundary."--\r\n";
|
||||
}
|
||||
if (!mail($email, $subject, $message, $headers, '-f'.$sender)) {
|
||||
throw new RuntimeException('The confirmation email could not be handed to sendmail.');
|
||||
}
|
||||
}
|
||||
|
||||
function bookingNormaliseEvent(array $event): array
|
||||
@@ -132,8 +181,8 @@ function bookingCreateRegistration(PDO $database, array $payload): array
|
||||
$link = bookingBaseUrl().'/booking/confirm.php?token='.rawurlencode($token);
|
||||
$subject = 'Bitte bestätigen Sie Ihre Terminanfrage';
|
||||
$body = "Guten Tag $name,\n\nbitte bestätigen Sie Ihre Anfrage für \"{$event['title']}\" am {$event['starts_at']} innerhalb von 30 Minuten:\n$link\n\nKontakt für Rückfragen: {$event['contact_name']} ({$event['contact_email']}).";
|
||||
bookingQueueMail($database, $registrationId, 'confirmation_request', $email, $subject, $body);
|
||||
$database->commit();
|
||||
bookingSendMail($email, $subject, $body);
|
||||
return ['registrationId' => $registrationId, 'expiresAt' => gmdate('c', time() + 1800)];
|
||||
} catch (Throwable $exception) {
|
||||
if ($database->inTransaction()) $database->rollBack();
|
||||
@@ -147,7 +196,7 @@ function bookingConfirmRegistration(PDO $database, string $token): array
|
||||
$database->beginTransaction();
|
||||
try {
|
||||
$statement = $database->prepare(<<<'SQL'
|
||||
SELECT r.*, e.title, e.project, e.starts_at, e.ends_at, e.capacity, e.contact_name, e.contact_email
|
||||
SELECT r.*, e.title, e.project, e.starts_at, e.ends_at, e.capacity, e.format, e.contact_name, e.contact_email
|
||||
FROM sufw_booking_registrations r JOIN sufw_booking_events e ON e.id = r.event_id
|
||||
WHERE r.confirmation_token_hash = :tokenHash FOR UPDATE
|
||||
SQL);
|
||||
@@ -170,8 +219,8 @@ function bookingConfirmRegistration(PDO $database, string $token): array
|
||||
$history->execute(['email' => $registration['email'], 'name' => $registration['name']]);
|
||||
$appointments = implode("\n", array_map(static fn (array $row): string => "- {$row['title']} am {$row['starts_at']}", $history->fetchAll()));
|
||||
$body = "Guten Tag {$registration['name']},\n\nIhr Termin ist bestätigt:\n{$registration['title']} am {$registration['starts_at']} bis {$registration['ends_at']}.\n\nIhre weiteren bestätigten Termine:\n$appointments\n\nBei Stornierungswunsch wenden Sie sich an {$registration['contact_name']} ({$registration['contact_email']}).";
|
||||
bookingQueueMail($database, $registration['id'], 'confirmation', $registration['email'], 'Ihr Termin ist bestätigt', $body);
|
||||
$database->commit();
|
||||
bookingSendMail($registration['email'], 'Ihr Termin ist bestätigt', $body, bookingIcsCalendar($registration));
|
||||
return $registration;
|
||||
} catch (Throwable $exception) {
|
||||
if ($database->inTransaction()) $database->rollBack();
|
||||
@@ -179,21 +228,6 @@ function bookingConfirmRegistration(PDO $database, string $token): array
|
||||
}
|
||||
}
|
||||
|
||||
function bookingRunJobs(PDO $database): array
|
||||
{
|
||||
$expired = $database->exec("UPDATE sufw_booking_registrations SET status = 'expired' WHERE status = 'pending_confirmation' AND confirmation_expires_at < UTC_TIMESTAMP()");
|
||||
$reminders = $database->query(<<<'SQL'
|
||||
SELECT r.id, r.email, r.name, e.title, e.starts_at, e.contact_name, e.contact_email
|
||||
FROM sufw_booking_registrations r JOIN sufw_booking_events e ON e.id = r.event_id
|
||||
WHERE r.status = 'confirmed' AND DATE(e.starts_at) = DATE_ADD(UTC_DATE(), INTERVAL 2 DAY)
|
||||
SQL)->fetchAll();
|
||||
foreach ($reminders as $reminder) {
|
||||
$body = "Guten Tag {$reminder['name']},\n\ndies ist die Erinnerung an Ihren Termin \"{$reminder['title']}\" am {$reminder['starts_at']}.\n\nKontakt: {$reminder['contact_name']} ({$reminder['contact_email']}).";
|
||||
bookingQueueMail($database, $reminder['id'], 'reminder', $reminder['email'], 'Erinnerung an Ihren Termin', $body);
|
||||
}
|
||||
return ['expired' => $expired, 'remindersConsidered' => count($reminders)];
|
||||
}
|
||||
|
||||
function bookingIcsEscape(string $value): string
|
||||
{
|
||||
return str_replace(["\\", ";", ",", "\n"], ["\\\\", "\\;", "\\,", "\\n"], $value);
|
||||
|
||||
@@ -89,7 +89,7 @@ function openConfirmationDialog(container, event) {
|
||||
const dialog = document.createElement('dialog');
|
||||
dialog.className = 'sufw-booking__dialog';
|
||||
dialog.setAttribute('aria-labelledby', 'sufw-booking-confirmation-title');
|
||||
dialog.innerHTML = `<section class="sufw-booking__form"><header class="sufw-booking__form-header"><p class="sufw-booking__eyebrow">E-Mail bestätigen</p><h2 id="sufw-booking-confirmation-title">Fast geschafft</h2><p>${escapeHtml(bookingConfirmationMessage(event.title))}</p></header><p class="sufw-booking__appointment"><strong>${escapeHtml(event.project === 'perspektivwerk' ? 'PerspektivWerk' : 'Produktionsschule')}</strong><br>${escapeHtml(dateFormatter.format(parseDate(event.startsAt.slice(0, 10))))} · ${formatEventTime(event)} · ${escapeHtml(event.title)}</p><p>In der lokalen Demo liegt die Nachricht im Mailausgang des Verwaltungsbereichs bereit.</p><div class="sufw-booking__form-actions"><button type="button" data-close-confirmation>Schließen</button></div></section>`;
|
||||
dialog.innerHTML = `<section class="sufw-booking__form"><header class="sufw-booking__form-header"><p class="sufw-booking__eyebrow">E-Mail bestätigen</p><h2 id="sufw-booking-confirmation-title">Fast geschafft</h2><p>${escapeHtml(bookingConfirmationMessage(event.title))}</p></header><p class="sufw-booking__appointment"><strong>${escapeHtml(event.project === 'perspektivwerk' ? 'PerspektivWerk' : 'Produktionsschule')}</strong><br>${escapeHtml(dateFormatter.format(parseDate(event.startsAt.slice(0, 10))))} · ${formatEventTime(event)} · ${escapeHtml(event.title)}</p><p>Bitte prüfen Sie Ihr E-Mail-Postfach und bestätigen Sie dort Ihre Anfrage.</p><div class="sufw-booking__form-actions"><button type="button" data-close-confirmation>Schließen</button></div></section>`;
|
||||
dialog.querySelector('[data-close-confirmation]').addEventListener('click', () => dialog.close());
|
||||
dialog.addEventListener('close', () => dialog.remove());
|
||||
container.append(dialog);
|
||||
|
||||
Reference in new issue
Block a user