f7cloud_client/apps/calendar/lib/Service/Appointments/MailService.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

370 lines
13 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2021 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Calendar\Service\Appointments;
use Exception;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\Booking;
use OCA\Calendar\Exception\ServiceException;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Notification\IManager;
use Psr\Log\LoggerInterface;
use function htmlspecialchars;
use function implode;
class MailService {
/** @var IUserManager */
private $userManager;
/** @var IMailer */
private $mailer;
/** @var IL10N */
private $l10n;
/** @var Defaults */
private $defaults;
/** @var LoggerInterface */
private $logger;
/** @var IURLGenerator */
private $urlGenerator;
/** @var IDateTimeFormatter */
private $dateFormatter;
/** @var IFactory */
private $lFactory;
private IManager $notificationManager;
public function __construct(
IMailer $mailer,
IUserManager $userManager,
IL10N $l10n,
Defaults $defaults,
LoggerInterface $logger,
IURLGenerator $urlGenerator,
IDateTimeFormatter $dateFormatter,
IFactory $lFactory,
IManager $notificationManager,
private IConfig $userConfig,
) {
$this->userManager = $userManager;
$this->mailer = $mailer;
$this->l10n = $l10n;
$this->defaults = $defaults;
$this->logger = $logger;
$this->urlGenerator = $urlGenerator;
$this->dateFormatter = $dateFormatter;
$this->lFactory = $lFactory;
$this->notificationManager = $notificationManager;
}
/**
* @param Booking $booking
* @param AppointmentConfig $config
* @throws ServiceException
*/
public function sendConfirmationEmail(Booking $booking, AppointmentConfig $config): void {
$user = $this->userManager->get($config->getUserId());
if ($user === null) {
throw new ServiceException('Could not find organizer');
}
$fromEmail = $user->getEMailAddress();
if ($fromEmail === null) {
throw new ServiceException('Organizer has no email set');
}
$fromName = $user->getDisplayName();
$sys = $this->getSysEmail();
$message = $this->mailer->createMessage()
->setFrom([$sys => $fromName])
->setTo([$booking->getEmail() => $booking->getDisplayName()])
->setReplyTo([$fromEmail => $fromName]);
$template = $this->mailer->createEMailTemplate('calendar.confirmAppointment');
$template->addHeader();
//Subject
$subject = $this->l10n->t('Your appointment "%s" with %s needs confirmation', [$config->getName(), $user->getDisplayName()]);
$template->setSubject($subject);
// Heading
$summary = $this->l10n->t('Dear %s, please confirm your booking', [$booking->getDisplayName()]);
$template->addHeading($summary);
$bookingUrl = $this->urlGenerator->linkToRouteAbsolute('calendar.booking.confirmBooking', ['token' => $booking->getToken()]);
$template->addBodyButton($this->l10n->t('Confirm'), $bookingUrl);
$template->addBodyListItem($user->getDisplayName(), $this->l10n->t('Appointment with:'));
if (!empty($config->getDescription())) {
$template->addBodyListItem($config->getDescription(), $this->l10n->t('Description:'));
}
// Create Booking overview
$this->addBulletList($template, $this->l10n, $booking, $config, false);
$bodyText = $this->l10n->t('This confirmation link expires in %s hours.', [(BookingService::EXPIRY / 3600)]);
$template->addBodyText($bodyText);
$bodyText = $this->l10n->t('If you wish to cancel the appointment after all, please contact your organizer by replying to this email or by visiting their profile page.');
$template->addBodyText($bodyText);
$template->addFooter();
$message->useTemplate($template);
try {
$failed = $this->mailer->send($message);
if (count($failed) > 0) {
$this->logger->warning('Mail delivery failed for some recipients.', ['app' => 'calendar-appointments']);
foreach ($failed as $fail) {
$this->logger->debug('Failed to deliver email to ' . $fail, ['app' => 'calendar-appointments']);
}
throw new ServiceException('Could not send mail for recipient(s) ' . implode(', ', $failed));
}
} catch (Exception $ex) {
$this->logger->error($ex->getMessage(), ['exception' => $ex, 'app' => 'calendar-appointments']);
throw new ServiceException('Could not send mail: ' . $ex->getMessage(), $ex->getCode(), $ex);
}
}
/**
* @param Booking $booking
* @param AppointmentConfig $config
* @throws ServiceException
*/
public function sendBookingInformationEmail(Booking $booking, AppointmentConfig $config, string $calendar): void {
$user = $this->userManager->get($config->getUserId());
if ($user === null) {
throw new ServiceException('Could not find organizer');
}
$fromEmail = $user->getEMailAddress();
if ($fromEmail === null) {
throw new ServiceException('Organizer has no email set');
}
$fromName = $user->getDisplayName();
$sys = $this->getSysEmail();
$message = $this->mailer->createMessage()
->setFrom([$sys => $fromName])
->setTo([$booking->getEmail() => $booking->getDisplayName()])
->setReplyTo([$fromEmail => $fromName]);
$template = $this->mailer->createEMailTemplate('calendar.confirmAppointment');
$template->addHeader();
// Subject
$subject = $this->l10n->t('Your appointment "%s" with %s has been accepted', [$config->getName(), $user->getDisplayName()]);
$template->setSubject($subject);
// Heading
$summary = $this->l10n->t('Dear %s, your booking has been accepted.', [$booking->getDisplayName()]);
$template->addHeading($summary);
$template->addBodyListItem($user->getDisplayName(), $this->l10n->t('Appointment with:'));
if (!empty($config->getDescription())) {
$template->addBodyListItem($config->getDescription(), $this->l10n->t('Description:'));
}
// Create Booking overview
$this->addBulletList($template, $this->l10n, $booking, $config, false);
$bodyText = $this->l10n->t('If you wish to cancel the appointment after all, please contact your organizer by replying to this email or by visiting their profile page.');
$template->addBodyText($bodyText);
$template->addFooter();
$attachment = $this->mailer->createAttachment($calendar, 'appointment.ics', 'text/calendar');
$message->attach($attachment);
$message->useTemplate($template);
try {
$failed = $this->mailer->send($message);
if (count($failed) > 0) {
$this->logger->warning('Mail delivery failed for some recipients.', ['app' => 'calendar-appointments']);
foreach ($failed as $fail) {
$this->logger->debug('Failed to deliver email to ' . $fail, ['app' => 'calendar-appointments']);
}
throw new ServiceException('Could not send mail for recipient(s) ' . implode(', ', $failed));
}
} catch (Exception $ex) {
$this->logger->error($ex->getMessage(), ['exception' => $ex, 'app' => 'calendar-appointments']);
throw new ServiceException('Could not send mail: ' . $ex->getMessage(), $ex->getCode(), $ex);
}
}
private function addBulletList(IEMailTemplate $template,
IL10N $l10n,
Booking $booking,
AppointmentConfig $config,
bool $recipient): void {
$template->addBodyListItem($booking->getDisplayName(), $l10n->t('Appointment for:'));
// determine timezone depending on who is getting the message (Requestee/Requester)
$tzid = ($recipient) ? $config->getAvailabilityAsArray()['timezoneId'] : $booking->getTimezone();
$dtstart = new \DateTime('now', new \DateTimeZone($booking->getTimezone())); // generate DateTime with booking time zone
$dtstart->setTimestamp($booking->getStart()); // set booking time stamp
$l = $this->lFactory->findGenericLanguage();
$relativeTimezone = new \DateTimeZone($tzid);
$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
$dtstart,
'long',
'short',
$relativeTimezone,
$this->lFactory->get('calendar', $l)
);
$timestring = $relativeDateTime . ' (' . $relativeTimezone->getName() . ')';
$template->addBodyListItem($timestring, $l10n->t('Date:'));
if (!$booking->isConfirmed() && $config->getCreateTalkRoom()) {
$template->addBodyListItem($l10n->t('You will receive a link with the confirmation email'), $l10n->t('Where:'));
} elseif (!$booking->isConfirmed() && !empty($config->getLocation())) {
$template->addBodyListItem($config->getLocation(), $l10n->t('Where:'));
} elseif ($booking->isConfirmed() && $booking->getTalkUrl() !== null) {
$template->addBodyListItem(
'<a href="' . htmlspecialchars($booking->getTalkUrl()) . '">' . $booking->getTalkUrl() . '</a>',
$l10n->t('Where:'),
'',
$booking->getTalkUrl(),
);
} elseif ($booking->isConfirmed() && !empty($config->getLocation())) {
$template->addBodyListItem($config->getLocation(), $l10n->t('Where:'));
}
if (!empty($booking->getDescription())) {
$template->addBodyListItem($booking->getDescription(), $l10n->t('Comment:'));
}
}
/**
* @return string
*/
private function getSysEmail(): string {
$instanceName = $this->defaults->getName();
return \OCP\Util::getDefaultEmailAddress('appointments-noreply');
}
public function sendOrganizerBookingInformationEmail(Booking $booking, AppointmentConfig $config, string $calendar): void {
/** @var IUser $user */
$user = $this->userManager->get($config->getUserId());
if ($user === null) {
throw new ServiceException('Could not find organizer');
}
$toEmail = $user->getEMailAddress();
if ($toEmail === null) {
throw new ServiceException('Organizer has no email set');
}
$toName = $user->getDisplayName();
$sys = $this->getSysEmail();
$message = $this->mailer->createMessage()
->setFrom([$sys => $booking->getDisplayName()])
->setTo([$toEmail => $toName]);
$template = $this->mailer->createEMailTemplate('calendar.confirmOrganizer');
$template->addHeader();
$lang = $this->userConfig->getUserValue($user->getUID(), 'core', 'lang', null);
$l10n = $this->lFactory->get('calendar', $lang);
// Subject
$subject = $l10n->t('You have a new appointment booking "%s" from %s', [$config->getName(), $booking->getDisplayName()]);
$template->setSubject($subject);
// Heading
$summary = $l10n->t('Dear %s, %s (%s) booked an appointment with you.', [$user->getDisplayName(), $booking->getDisplayName(), $booking->getEmail()]);
$template->addHeading($summary);
$template->addBodyListItem($booking->getDisplayName() . ' (' . $booking->getEmail() . ')', $l10n->t('Appointment with:'));
if (!empty($config->getDescription())) {
$template->addBodyListItem($config->getDescription(), $l10n->t('Description:'));
}
// Create Booking overview
$this->addBulletList($template, $l10n, $booking, $config, true);
$template->addFooter();
$attachment = $this->mailer->createAttachment($calendar, 'appointment.ics', 'text/calendar');
$message->attach($attachment);
$message->useTemplate($template);
try {
$failed = $this->mailer->send($message);
if (count($failed) > 0) {
$this->logger->warning('Mail delivery failed for some recipients.', ['app' => 'calendar-appointments']);
foreach ($failed as $fail) {
$this->logger->debug('Failed to deliver email to ' . $fail, ['app' => 'calendar-appointments']);
}
throw new ServiceException('Could not send mail for recipient(s) ' . implode(', ', $failed));
}
} catch (Exception $ex) {
$this->logger->error('Could not send appointment organizer email: ' . $ex->getMessage(), ['exception' => $ex, 'app' => 'calendar-appointments']);
throw new ServiceException('Could not send mail: ' . $ex->getMessage(), $ex->getCode(), $ex);
}
}
public function sendOrganizerBookingInformationNotification(Booking $booking, AppointmentConfig $config): void {
$tzid = $config->getAvailabilityAsArray()['timezoneId']; // extract time zone from appointment configuration
$dtstart = new \DateTime('now', new \DateTimeZone($booking->getTimezone())); // generate DateTime with booking time zone
$dtstart->setTimestamp($booking->getStart()); // set booking time stamp
$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
$dtstart,
'long',
'short',
new \DateTimeZone($tzid),
$this->lFactory->get('calendar')
);
$notification = $this->notificationManager->createNotification();
$notification
->setApp('calendar')
->setUser($config->getUserId())
->setObject('booking', (string)$booking->getId())
->setSubject('booking_accepted',
[
'type' => 'highlight',
'id' => $booking->getId(),
'name' => $config->getName(),
'link' => $config->getPrincipalUri()
])
->setDateTime(new \DateTime())
->setMessage('booking_accepted_message',
[
'type' => 'highlight',
'id' => $booking->getId(),
'display_name' => $booking->getDisplayName(),
'config_display_name' => $config->getName(),
'link' => $config->getPrincipalUri(),
'email' => $booking->getEmail(),
'date_time' => $relativeDateTime
]
);
$this->notificationManager->notify($notification);
}
}