Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 5 additions & 59 deletions apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ public function schedule(Message $iTipMessage) {
$vEvent = $primaryInstance['new'];
/** @var VEvent|null $oldVevent */
$oldVevent = $primaryInstance['old'];
$isModified = $oldVevent !== null;

// we might not have an old event as this could be a new invitation,
// or a new recurrence exception
Expand Down Expand Up @@ -248,68 +247,15 @@ public function schedule(Message $iTipMessage) {
$senderName = $this->getSenderNameFor($sender);
}

$replyingAttendee = null;
switch ($method) {
case self::METHOD_REPLY:
$data = $this->imipService->buildReplyBodyData($vEvent);
$replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage);
break;
case self::METHOD_CANCEL:
$data = $this->imipService->buildCancelledBodyData($vEvent);
break;
default:
$data = $this->imipService->buildBodyData($vEvent, $oldVevent);
break;
}

$data['attendee_name'] = ($recipientName ?: $recipient);
$data['invitee_name'] = ($senderName ?: $sender);
$template = match ($method) {
self::METHOD_REPLY => $this->imipService->buildReplyEmail($iTipMessage, $vEvent, $recipient, $recipientName, $sender, $senderName),
self::METHOD_CANCEL => $this->imipService->buildCancellationEmail($vEvent, $recipient, $recipientName, $sender, $senderName),
default => $this->imipService->buildRequestEmail($iTipMessage, $vEvent, $oldVevent, $attendee, $recipient, $recipientName, $sender, $senderName, $lastOccurrence),
};

$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
$fromName = $this->imipService->getFrom($senderName, $this->defaults->getName());

$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
$template->setLanguage($this->imipService->getLanguageCode());
$template->addHeader();
$template->addBodySender($data['invitee_name'], $senderName !== null ? $sender : '');

$this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified, $replyingAttendee);
$this->imipService->addEventDetails($template, $vEvent, $data);

// Only add response buttons to invitation requests: Fix Issue #11230
if (strcasecmp($method, self::METHOD_REQUEST) === 0 && $this->imipService->getAttendeeRsvpOrReqForParticipant($attendee)) {

/*
** Only offer invitation accept/reject buttons, which link back to the
** nextcloud server, to recipients who can access the nextcloud server via
** their internet/intranet. Issue #12156
**
** The app setting is stored in the appconfig database table.
**
** For nextcloud servers accessible to the public internet, the default
** "invitation_link_recipients" value "yes" (all recipients) is appropriate.
**
** When the nextcloud server is restricted behind a firewall, accessible
** only via an internal network or via vpn, you can set "dav.invitation_link_recipients"
** to the email address or email domain, or comma separated list of addresses or domains,
** of recipients who can access the server.
**
** To always deliver URLs, set invitation_link_recipients to "yes".
** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
*/

$recipientDomain = substr(strrchr($recipient, '@'), 1);
$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getValueString('dav', 'invitation_link_recipients', 'yes'))));

if (strcmp('yes', $invitationLinkRecipients[0]) === 0
|| in_array(strtolower($recipient), $invitationLinkRecipients, true)
|| in_array(strtolower($recipientDomain), $invitationLinkRecipients, true)) {
$token = $this->imipService->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence);
$this->imipService->addResponseButtons($template, $token);
}
}

$template->addFooter();
// convert iTip Message to string
$itip_msg = $iTipMessage->message->serialize();

Expand Down
113 changes: 113 additions & 0 deletions apps/dav/lib/CalDAV/Schedule/IMipService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\EMailDetails;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Security\ISecureRandom;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
Expand Down Expand Up @@ -77,6 +78,7 @@ public function __construct(
private readonly IUserManager $userManager,
private readonly IUserConfig $userConfig,
private readonly IAppConfig $appConfig,
private readonly IMailer $mailer,
) {
$language = $this->l10nFactory->findGenericLanguage();
$locale = $this->l10nFactory->findLocale($language);
Expand Down Expand Up @@ -150,6 +152,117 @@ public function diffInstance(VEvent $newEvent, VEvent $oldEvent): array {
return $changed;
}

/**
* Builds the full REQUEST invitation/update email, including the
* old-vs-new diff and, when the recipient is eligible, the Accept/Decline
* response buttons.
*/
public function buildRequestEmail(
Message $iTipMessage,
VEvent $vEvent,
?VEvent $oldVevent,
Property $attendee,
string $recipient,
?string $recipientName,
string $sender,
?string $senderName,
int $lastOccurrence,
): IEMailTemplate {
$data = $this->withRecipientNames($this->buildBodyData($vEvent, $oldVevent), $recipient, $recipientName, $sender, $senderName);

$template = $this->newTemplate(IMipPlugin::METHOD_REQUEST, $data, $senderName !== null ? $sender : '');
$this->addSubjectAndHeading($template, IMipPlugin::METHOD_REQUEST, $data['invitee_name'], $data['meeting_title'], $oldVevent !== null);
$this->addEventDetails($template, $vEvent, $data);

// Only add response buttons to invitation requests: Fix Issue #11230
if ($this->getAttendeeRsvpOrReqForParticipant($attendee)) {
/*
** Only offer invitation accept/reject buttons, which link back to the
** nextcloud server, to recipients who can access the nextcloud server via
** their internet/intranet. Issue #12156
**
** The app setting is stored in the appconfig database table.
**
** For nextcloud servers accessible to the public internet, the default
** "invitation_link_recipients" value "yes" (all recipients) is appropriate.
**
** When the nextcloud server is restricted behind a firewall, accessible
** only via an internal network or via vpn, you can set "dav.invitation_link_recipients"
** to the email address or email domain, or comma separated list of addresses or domains,
** of recipients who can access the server.
**
** To always deliver URLs, set invitation_link_recipients to "yes".
** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
*/
$recipientDomain = substr(strrchr($recipient, '@'), 1);
$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->appConfig->getValueString('dav', 'invitation_link_recipients', 'yes'))));

if (strcmp('yes', $invitationLinkRecipients[0]) === 0
|| in_array(strtolower($recipient), $invitationLinkRecipients, true)
|| in_array(strtolower($recipientDomain), $invitationLinkRecipients, true)) {
$token = $this->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence);
$this->addResponseButtons($template, $token);
}
}

$template->addFooter();
return $template;
}

/**
* Builds the REPLY email an organizer receives when an attendee responds.
*/
public function buildReplyEmail(
Message $iTipMessage,
VEvent $vEvent,
string $recipient,
?string $recipientName,
string $sender,
?string $senderName,
): IEMailTemplate {
$data = $this->withRecipientNames($this->buildReplyBodyData($vEvent), $recipient, $recipientName, $sender, $senderName);

$template = $this->newTemplate(IMipPlugin::METHOD_REPLY, $data, $senderName !== null ? $sender : '');
$replyingAttendee = $this->getReplyingAttendee($iTipMessage);
$this->addSubjectAndHeading($template, IMipPlugin::METHOD_REPLY, $data['invitee_name'], $data['meeting_title'], false, $replyingAttendee);
$this->addEventDetails($template, $vEvent, $data);
$template->addFooter();
return $template;
}

/**
* Builds the CANCEL email an attendee receives when dropped from an event.
*/
public function buildCancellationEmail(
VEvent $vEvent,
string $recipient,
?string $recipientName,
string $sender,
?string $senderName,
): IEMailTemplate {
$data = $this->withRecipientNames($this->buildCancelledBodyData($vEvent), $recipient, $recipientName, $sender, $senderName);

$template = $this->newTemplate(IMipPlugin::METHOD_CANCEL, $data, $senderName !== null ? $sender : '');
$this->addSubjectAndHeading($template, IMipPlugin::METHOD_CANCEL, $data['invitee_name'], $data['meeting_title'], false);
$this->addEventDetails($template, $vEvent, $data);
$template->addFooter();
return $template;
}

private function withRecipientNames(array $data, string $recipient, ?string $recipientName, string $sender, ?string $senderName): array {
$data['attendee_name'] = $recipientName ?: $recipient;
$data['invitee_name'] = $senderName ?: $sender;
return $data;
}

private function newTemplate(string $method, array $data, string $senderEmail): IEMailTemplate {
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
$template->setLanguage($this->getLanguageCode());
$template->addHeader();
$template->addBodySender($data['invitee_name'], $senderEmail);
return $template;
}

/**
* @param VEvent $vEvent
* @param VEvent|null $oldVEvent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ protected function setUp(): void {
->willReturn($l10n);
$this->userManager = $this->createMock(IUserManager::class);
$this->userManager->method('getByEmail')->willReturn([]);
$message = new \OC\Mail\Message(new Email(), false);
$this->mailer = $this->createMock(IMailer::class);
$this->mailer->method('createMessage')
->willReturn($message);
$this->imipService = new IMipService(
$this->urlGenerator,
$this->db,
Expand All @@ -99,13 +103,10 @@ protected function setUp(): void {
$this->userManager,
$this->userConfig,
$this->appConfig,
$this->mailer,
);

// IMipPlugin
$message = new \OC\Mail\Message(new Email(), false);
$this->mailer = $this->createMock(IMailer::class);
$this->mailer->method('createMessage')
->willReturn($message);
$this->logger = new NullLogger();
$this->defaults = $this->createMock(Defaults::class);
$this->defaults->method('getName')
Expand Down
Loading
Loading