diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 76909bba26ab7..b12dd5464c8f3 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -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 @@ -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(); diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php index e86363ba0e4db..0897023ec7107 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -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; @@ -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); @@ -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 diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php index 1e69ef14af96a..f19ed706be523 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php @@ -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, @@ -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') diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php index 03b93878c08c4..45a0d8a9d7e2f 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php @@ -41,6 +41,7 @@ use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; use Sabre\VObject\ITip\Message; +use Sabre\VObject\Property; use Test\TestCase; use Test\Traits\EmailValidatorTrait; use function array_merge; @@ -108,6 +109,10 @@ protected function setUp(): void { // the actual pairing/diff algorithm instead of re-describing its outcome // per test. eventInstances() stays mocked per test below, since it reads // a VCalendar's children and these fixtures build VEvents standalone. + // buildRequestEmail()/buildReplyEmail()/buildCancellationEmail() own the + // full content-building sequence internally now (see IMipServiceTest for + // coverage of that sequencing); these tests only verify schedule() calls + // the right one with the right arguments. $l10nFactory = $this->createMock(L10NFactory::class); $l10nFactory->method('findGenericLanguage')->willReturn('en'); $l10nFactory->method('findLocale')->willReturn('en_US'); @@ -121,6 +126,7 @@ protected function setUp(): void { $this->createMock(IUserManager::class), $this->createMock(IUserConfig::class), $this->createMock(IAppConfig::class), + $this->createMock(IMailer::class), ); $this->service->method('instanceKey') ->willReturnCallback(fn (VEvent $event) => $realImipService->instanceKey($event)); @@ -201,10 +207,6 @@ public function testParsingSingle(): void { $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); $oldVCalendar->add($oldVEvent); - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting without (!) Boromir', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $newVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -240,10 +242,6 @@ public function testParsingSingle(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($newVevent, $oldVEvent) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -258,33 +256,10 @@ public function testParsingSingle(): void { ->willReturn($this->user); $this->service->expects(self::once()) ->method('getFrom'); - $this->service->method('getLanguageCode')->willReturn('ar'); - $this->emailTemplate->expects(self::once()) - ->method('setLanguage') - ->with('ar'); - $this->emailTemplate->expects(self::once()) - ->method('addBodySender') - ->with('Mr. Wizard', 'gandalf@wiz.ard'); - $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true); $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newVevent, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $newVevent, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); + ->method('buildRequestEmail') + ->with($message, $newVevent, $oldVEvent, $atnd, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->mailer->expects(self::once()) ->method('send') ->willReturn([]); @@ -320,10 +295,6 @@ public function testAttendeeIsResource(): void { $oldVEvent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']); $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); $oldVCalendar->add($oldVEvent); - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting without (!) Boromir', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $newVevent->select('ATTENDEE'); $room = ''; foreach ($attendees as $attendee) { @@ -355,8 +326,6 @@ public function testAttendeeIsResource(): void { ->willReturn(true); $this->service->expects(self::never()) ->method('isCircle'); - $this->service->expects(self::never()) - ->method('buildBodyData'); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -369,17 +338,11 @@ public function testAttendeeIsResource(): void { $this->service->expects(self::never()) ->method('getFrom'); $this->service->expects(self::never()) - ->method('addSubjectAndHeading'); + ->method('buildRequestEmail'); $this->service->expects(self::never()) - ->method('addEventDetails'); + ->method('buildReplyEmail'); $this->service->expects(self::never()) - ->method('getAttendeeRsvpOrReqForParticipant'); - $this->config->expects(self::never()) - ->method('getValueString'); - $this->service->expects(self::never()) - ->method('createInvitationToken'); - $this->service->expects(self::never()) - ->method('addResponseButtons'); + ->method('buildCancellationEmail'); $this->mailer->expects(self::never()) ->method('send'); $this->plugin->schedule($message); @@ -434,8 +397,6 @@ public function testAttendeeIsCircle(): void { ->method('isCircle') ->with($circle) ->willReturn(true); - $this->service->expects(self::never()) - ->method('buildBodyData'); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -448,17 +409,11 @@ public function testAttendeeIsCircle(): void { $this->service->expects(self::never()) ->method('getFrom'); $this->service->expects(self::never()) - ->method('addSubjectAndHeading'); - $this->service->expects(self::never()) - ->method('addEventDetails'); - $this->service->expects(self::never()) - ->method('getAttendeeRsvpOrReqForParticipant'); - $this->config->expects(self::never()) - ->method('getValueString'); + ->method('buildRequestEmail'); $this->service->expects(self::never()) - ->method('createInvitationToken'); + ->method('buildReplyEmail'); $this->service->expects(self::never()) - ->method('addResponseButtons'); + ->method('buildCancellationEmail'); $this->mailer->expects(self::never()) ->method('send'); $this->plugin->schedule($message); @@ -503,10 +458,6 @@ public function testParsingRecurrence(): void { ]); $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Elevenses', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $newVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -545,10 +496,6 @@ public function testParsingRecurrence(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($newvEvent2, null) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -564,25 +511,9 @@ public function testParsingRecurrence(): void { $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Elevenses', false); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newvEvent2, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $newvEvent2, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); + ->method('buildRequestEmail') + ->with($message, $newvEvent2, null, $atnd, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->mailer->expects(self::once()) ->method('send') ->willReturn([]); @@ -623,10 +554,6 @@ public function testMultipleModifiedInstancesUsesMasterAndLogsTheRest(): void { $message->sender = 'mailto:gandalf@wiz.ard'; $message->senderName = 'Mr. Wizard'; $message->recipient = 'mailto:' . 'frodo@hobb.it'; - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $masterVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -663,10 +590,6 @@ public function testMultipleModifiedInstancesUsesMasterAndLogsTheRest(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($masterVevent, null) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -682,25 +605,9 @@ public function testMultipleModifiedInstancesUsesMasterAndLogsTheRest(): void { $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $masterVevent, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $masterVevent, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); + ->method('buildRequestEmail') + ->with($message, $masterVevent, null, $atnd, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->mailer->expects(self::once()) ->method('send') ->willReturn([]); @@ -733,10 +640,6 @@ public function testCancelUsesAllInstancesWithoutDiffing(): void { $message->sender = 'mailto:gandalf@wiz.ard'; $message->senderName = 'Mr. Wizard'; $message->recipient = 'mailto:' . 'boromir@tra.it.or'; - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting', - 'attendee_name' => 'boromir@tra.it.or' - ]; $attendees = $newVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -769,10 +672,6 @@ public function testCancelUsesAllInstancesWithoutDiffing(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildCancelledBodyData') - ->with($newVevent) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -788,18 +687,13 @@ public function testCancelUsesAllInstancesWithoutDiffing(): void { $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'cancel', 'Mr. Wizard', 'Fellowship meeting', false); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newVevent, $data); - // no RSVP/response-button handling for CANCEL messages - $this->service->expects(self::never()) - ->method('getAttendeeRsvpOrReqForParticipant'); + ->method('buildCancellationEmail') + ->with($newVevent, 'boromir@tra.it.or', null, 'gandalf@wiz.ard', 'Mr. Wizard') + ->willReturn($this->emailTemplate); $this->service->expects(self::never()) - ->method('createInvitationToken'); + ->method('buildRequestEmail'); $this->service->expects(self::never()) - ->method('addResponseButtons'); + ->method('buildReplyEmail'); $this->mailer->expects(self::once()) ->method('send') ->willReturn([]); @@ -847,10 +741,6 @@ public function testFailedDelivery(): void { $message->sender = 'mailto:gandalf@wiz.ard'; $message->senderName = 'Mr. Wizard'; $message->recipient = 'mailto:' . 'frodo@hobb.it'; - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting without (!) Boromir', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $newVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -877,10 +767,6 @@ public function testFailedDelivery(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($newVevent, null) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -896,28 +782,9 @@ public function testFailedDelivery(): void { $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newVevent, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $newVevent, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); - $this->mailer->expects(self::once()) - ->method('send') - ->willReturn([]); + ->method('buildRequestEmail') + ->with($message, $newVevent, null, $atnd, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->mailer ->method('send') ->willThrowException(new \Exception()); @@ -950,16 +817,6 @@ public function testMailProviderSend(): void { $attendee = $entry; } } - // construct body data return - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting without (!) Boromir', - 'attendee_name' => 'frodo@hobb.it' - ]; - // construct system config mock returns - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); // construct user mock returns $this->user->expects(self::any()) ->method('getUID') @@ -993,28 +850,12 @@ public function testMailProviderSend(): void { ->method('isCircle') ->with($attendee) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($event, null) - ->willReturn($data); $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', false); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $event, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $event, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); + ->method('buildRequestEmail') + ->with($message, $event, null, $attendee, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->service->expects(self::once()) ->method('eventInstances') ->with($calendar) @@ -1063,10 +904,6 @@ public function testMailProviderDisabled(): void { $oldVEvent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); $oldVEvent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE']); $oldVCalendar->add($oldVEvent); - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting without (!) Boromir', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $newVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -1096,10 +933,6 @@ public function testMailProviderDisabled(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($newVevent, $oldVEvent) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -1115,31 +948,15 @@ public function testMailProviderDisabled(): void { $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting without (!) Boromir', true); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newVevent, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); + ->method('buildRequestEmail') + ->with($message, $newVevent, $oldVEvent, $atnd, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->config->expects(self::exactly(2)) ->method('getValueBool') ->willReturnMap([ ['dav', 'caldav_external_attendees_disabled', false, false], ['core', 'mail_providers_enabled', true, false], ]); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $newVevent, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); $this->mailer->expects(self::once()) ->method('send') ->willReturn([]); @@ -1163,10 +980,6 @@ public function testNoOldEvent(): void { $message->sender = 'mailto:gandalf@wiz.ard'; $message->senderName = 'Mr. Wizard'; $message->recipient = 'mailto:' . 'frodo@hobb.it'; - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $newVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -1199,10 +1012,6 @@ public function testNoOldEvent(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($newVevent, null) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -1218,127 +1027,12 @@ public function testNoOldEvent(): void { $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newVevent, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $newVevent, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); + ->method('buildRequestEmail') + ->with($message, $newVevent, null, $atnd, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->mailer->expects(self::once()) ->method('send') ->willReturn([]); - $this->mailer - ->method('send') - ->willReturn([]); - $this->plugin->schedule($message); - $this->assertEquals('1.1', $message->getScheduleStatus()); - } - - public function testNoButtons(): void { - $message = new Message(); - $message->method = 'REQUEST'; - $newVCalendar = new VCalendar(); - $newVevent = new VEvent($newVCalendar, 'VEVENT', array_merge([ - 'UID' => 'uid-1234', - 'SEQUENCE' => 1, - 'SUMMARY' => 'Fellowship meeting', - 'DTSTART' => new \DateTime('2016-01-01 00:00:00') - ], [])); - $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); - $newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); - $message->message = $newVCalendar; - $message->sender = 'mailto:gandalf@wiz.ard'; - $message->recipient = 'mailto:' . 'frodo@hobb.it'; - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting', - 'attendee_name' => 'frodo@hobb.it' - ]; - $attendees = $newVevent->select('ATTENDEE'); - $atnd = ''; - foreach ($attendees as $attendee) { - if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { - $atnd = $attendee; - } - } - $this->service->expects(self::once()) - ->method('getLastOccurrence') - ->willReturn(1496912700); - $this->config->expects(self::exactly(2)) - ->method('getValueBool') - ->willReturnMap([ - ['dav', 'caldav_external_attendees_disabled', false, false], - ['core', 'mail_providers_enabled', true, false], - ]); - $this->service->expects(self::once()) - ->method('eventInstances') - ->with($newVCalendar) - ->willReturn([$newVevent]); - $this->service->expects(self::once()) - ->method('getCurrentAttendee') - ->with($message) - ->willReturn($atnd); - $this->service->expects(self::once()) - ->method('isRoomOrResource') - ->with($atnd) - ->willReturn(false); - $this->service->expects(self::once()) - ->method('isCircle') - ->with($atnd) - ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($newVevent, null) - ->willReturn($data); - $this->user->expects(self::any()) - ->method('getUID') - ->willReturn('user1'); - $this->user->expects(self::any()) - ->method('getDisplayName') - ->willReturn('Mr. Wizard'); - $this->user->expects(self::any()) - ->method('getEMailAddress') - ->willReturn('gandalf@wiz.ard'); - $this->userSession->expects(self::any()) - ->method('getUser') - ->willReturn($this->user); - $this->service->expects(self::once()) - ->method('getFrom'); - $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', false); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newVevent, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('no'); - $this->service->expects(self::never()) - ->method('createInvitationToken'); - $this->service->expects(self::never()) - ->method('addResponseButtons'); - $this->mailer->expects(self::once()) - ->method('send') - ->willReturn([]); - $this->mailer - ->method('send') - ->willReturn([]); $this->plugin->schedule($message); $this->assertEquals('1.1', $message->getScheduleStatus()); } @@ -1410,10 +1104,6 @@ public function testExternalAttendeesDisabledForSystemUser(): void { $oldVEvent->add('ATTENDEE', 'mailto:frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); $oldVCalendar->add($oldVEvent); - $data = ['invitee_name' => 'Mr. Wizard', - 'meeting_title' => 'Fellowship meeting', - 'attendee_name' => 'frodo@hobb.it' - ]; $attendees = $newVevent->select('ATTENDEE'); $atnd = ''; foreach ($attendees as $attendee) { @@ -1453,10 +1143,6 @@ public function testExternalAttendeesDisabledForSystemUser(): void { ->method('isCircle') ->with($atnd) ->willReturn(false); - $this->service->expects(self::once()) - ->method('buildBodyData') - ->with($newVevent, $oldVEvent) - ->willReturn($data); $this->user->expects(self::any()) ->method('getUID') ->willReturn('user1'); @@ -1472,25 +1158,9 @@ public function testExternalAttendeesDisabledForSystemUser(): void { $this->service->expects(self::once()) ->method('getFrom'); $this->service->expects(self::once()) - ->method('addSubjectAndHeading') - ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', true); - $this->service->expects(self::once()) - ->method('addEventDetails') - ->with($this->emailTemplate, $newVevent, $data); - $this->service->expects(self::once()) - ->method('getAttendeeRsvpOrReqForParticipant') - ->willReturn(true); - $this->config->expects(self::once()) - ->method('getValueString') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); - $this->service->expects(self::once()) - ->method('createInvitationToken') - ->with($message, $newVevent, 1496912700) - ->willReturn('token'); - $this->service->expects(self::once()) - ->method('addResponseButtons') - ->with($this->emailTemplate, 'token'); + ->method('buildRequestEmail') + ->with($message, $newVevent, $oldVEvent, $atnd, 'frodo@hobb.it', null, 'gandalf@wiz.ard', 'Mr. Wizard', 1496912700) + ->willReturn($this->emailTemplate); $this->mailer->expects(self::once()) ->method('send') ->willReturn([]); @@ -1501,9 +1171,10 @@ public function testExternalAttendeesDisabledForSystemUser(): void { /** * Runs schedule() for an organizer sourced REQUEST whose iTip message * carries no sender name and captures the resulting From and Reply-To - * headers, sent either via the system or the user's mail account. + * headers and the sender name handed to the mail builder, sent either + * via the system or the user's mail account. * - * @return array{from: ?array, replyTo: ?array} + * @return array{from: ?array, replyTo: ?array, senderName: ?string} */ private function scheduleWithoutSenderName(string $organizer, string $recipient, bool $viaMailProvider = false): array { $vCalendar = new VCalendar(); @@ -1525,11 +1196,7 @@ private function scheduleWithoutSenderName(string $organizer, string $recipient, $capturedFrom = null; $capturedReplyTo = null; - $capturedSender = null; - $this->emailTemplate->method('addBodySender') - ->willReturnCallback(function (string $displayName, string $subline) use (&$capturedSender): void { - $capturedSender = [$displayName, $subline]; - }); + $capturedSenderName = null; $mailMessage = $this->createMock(IMessage::class); $mailMessage->method('setTo')->willReturn($mailMessage); $mailMessage->method('setFrom') @@ -1565,12 +1232,11 @@ private function scheduleWithoutSenderName(string $organizer, string $recipient, $this->service->method('getCurrentAttendee')->willReturn($vEvent->select('ATTENDEE')[0]); $this->service->method('isRoomOrResource')->willReturn(false); $this->service->method('isCircle')->willReturn(false); - $this->service->method('getAttendeeRsvpOrReqForParticipant')->willReturn(false); - $this->service->method('buildBodyData')->willReturn([ - 'meeting_title' => 'Meeting', - 'invitee_name' => '', - 'attendee_name' => $recipient, - ]); + $this->service->method('buildRequestEmail') + ->willReturnCallback(function (Message $iTipMessage, VEvent $vEvent, ?VEvent $oldVevent, Property $attendee, string $recipient, ?string $recipientName, string $sender, ?string $senderName) use (&$capturedSenderName): IEMailTemplate { + $capturedSenderName = $senderName; + return $this->emailTemplate; + }); // Mirrors the real IMipService::getFrom() so assertions read like the // actual mail header. $this->service->method('getFrom') @@ -1600,7 +1266,7 @@ private function scheduleWithoutSenderName(string $organizer, string $recipient, $plugin->schedule($message); self::assertSame('1.1', $message->getScheduleStatus()); - return ['from' => $capturedFrom, 'replyTo' => $capturedReplyTo, 'sender' => $capturedSender]; + return ['from' => $capturedFrom, 'replyTo' => $capturedReplyTo, 'senderName' => $capturedSenderName]; } /** @@ -1617,7 +1283,7 @@ public function testSenderNameIsNotTakenFromAnUnrelatedSessionUser(bool $viaMail $result = $this->scheduleWithoutSenderName('a@example.com', 'frodo@hobb.it', $viaMailProvider); self::assertSame(['Instance Name 123'], array_values($result['from'])); - self::assertSame(['a@example.com', ''], $result['sender']); + self::assertNull($result['senderName']); if (!$viaMailProvider) { self::assertSame(['a@example.com'], $result['replyTo']); } @@ -1632,7 +1298,7 @@ public function testSenderNameFallsBackToSessionUserWhenTheyAreTheSender(bool $v $result = $this->scheduleWithoutSenderName('gandalf@wiz.ard', 'frodo@hobb.it', $viaMailProvider); self::assertSame(['Mr. Wizard via Instance Name 123'], array_values($result['from'])); - self::assertSame(['Mr. Wizard', 'gandalf@wiz.ard'], $result['sender']); + self::assertSame('Mr. Wizard', $result['senderName']); if (!$viaMailProvider) { self::assertSame(['gandalf@wiz.ard' => 'Mr. Wizard'], $result['replyTo']); } @@ -1661,7 +1327,7 @@ public function testSenderNameUsesSessionUserForTheirAliasAddress(bool $viaMailP $result = $this->scheduleWithoutSenderName('shared@corp.example', 'frodo@hobb.it', $viaMailProvider); self::assertSame(['Carl Session via Instance Name 123'], array_values($result['from'])); - self::assertSame(['Carl Session', 'shared@corp.example'], $result['sender']); + self::assertSame('Carl Session', $result['senderName']); if (!$viaMailProvider) { self::assertSame(['shared@corp.example' => 'Carl Session'], $result['replyTo']); } @@ -1675,7 +1341,7 @@ public function testSenderNameStaysNeutralForBlankDisplayNames(bool $viaMailProv $result = $this->scheduleWithoutSenderName('gandalf@wiz.ard', 'frodo@hobb.it', $viaMailProvider); self::assertSame(['Instance Name 123'], array_values($result['from'])); - self::assertSame(['gandalf@wiz.ard', ''], $result['sender']); + self::assertNull($result['senderName']); if (!$viaMailProvider) { self::assertSame(['gandalf@wiz.ard'], $result['replyTo']); } @@ -1690,7 +1356,7 @@ public function testSenderNameStaysNeutralWithoutSessionUser(): void { $result = $this->scheduleWithoutSenderName('a@example.com', 'frodo@hobb.it'); self::assertSame(['Instance Name 123'], array_values($result['from'])); - self::assertSame(['a@example.com', ''], $result['sender']); + self::assertNull($result['senderName']); self::assertSame(['a@example.com'], $result['replyTo']); } diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php index 49eaa5efe7463..b4ab5f5dc8786 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php @@ -14,6 +14,7 @@ use OCA\DAV\CalDAV\Schedule\IMipService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Config\IUserConfig; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IAppConfig; use OCP\IDBConnection; use OCP\IL10N; @@ -23,9 +24,12 @@ use OCP\Mail\EMailDetails; use OCP\Mail\EMailDetailsRow; use OCP\Mail\IEMailTemplate; +use OCP\Mail\IMailer; use OCP\Security\ISecureRandom; use PHPUnit\Framework\MockObject\MockObject; use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\ITip\Message; use Sabre\VObject\Property\ICalendar\DateTime; use Test\TestCase; @@ -40,6 +44,7 @@ class IMipServiceTest extends TestCase { private ITimeFactory&MockObject $timeFactory; private IMipService $service; private IUserManager&MockObject $userManager; + private IMailer&MockObject $mailer; private VCalendar $vCalendar1a; private VCalendar $vCalendar1b; @@ -60,6 +65,7 @@ protected function setUp(): void { $this->l10n = $this->createMock(IL10N::class); $this->timeFactory = $this->createMock(ITimeFactory::class); $this->userManager = $this->createMock(IUserManager::class); + $this->mailer = $this->createMock(IMailer::class); $this->l10nFactory->expects(self::once()) ->method('findGenericLanguage') ->willReturn('en'); @@ -76,6 +82,7 @@ protected function setUp(): void { $this->userManager, $this->userConfig, $this->appConfig, + $this->mailer, ); // construct calendar with a 1 hour event and same start/end time zones @@ -2847,4 +2854,208 @@ public function testEventInstancesReturnsOnlyVeventComponentsInOrder(): void { $this->assertSame([$vEvent1, $vEvent2], $this->service->eventInstances($vCalendar)); } + + private function stubNow(): void { + $this->timeFactory->method('getDateTime')->willReturnCallback( + function ($v1, $v2) { + return match (true) { + $v1 === 'now' && $v2 === null => new \DateTime('2016-01-01 00:00:00'), + }; + } + ); + } + + private function mockInvitationTokenStorage(): void { + $this->random->method('generate')->willReturn('sometoken'); + $queryBuilder = $this->createMock(IQueryBuilder::class); + $queryBuilder->method('insert')->willReturnSelf(); + $queryBuilder->method('values')->willReturnSelf(); + $queryBuilder->method('createNamedParameter')->willReturnArgument(0); + $queryBuilder->method('executeStatement')->willReturn(1); + $this->db->method('getQueryBuilder')->willReturn($queryBuilder); + } + + /** + * Recreates the eligibility/config gate that used to be asserted at the + * IMipPlugin level (schedule() no longer sees it at all - it's fully + * internal to buildRequestEmail() now). + */ + public function testBuildRequestEmailAddsResponseButtonsWhenEligibleAndAllowed(): void { + $this->stubNow(); + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $attendee = $vEvent->add('ATTENDEE', 'mailto:frodo@hobb.it', ['RSVP' => 'TRUE']); + + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + $this->appConfig->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + $this->mockInvitationTokenStorage(); + + $iTipMessage = new Message(); + $iTipMessage->recipient = 'mailto:frodo@hobb.it'; + $iTipMessage->sender = 'mailto:gandalf@wiz.ard'; + $iTipMessage->sequence = 1; + + $this->l10n->method('getLanguageCode')->willReturn('ar'); + $template->expects(self::once())->method('setLanguage')->with('ar'); + $template->expects(self::once())->method('addBodySender')->with('Mr. Wizard', 'gandalf@wiz.ard'); + $template->expects(self::once())->method('addBodyButtons'); + $template->expects(self::once())->method('addFooter'); + + $result = $this->service->buildRequestEmail( + $iTipMessage, + $vEvent, + null, + $attendee, + 'frodo@hobb.it', + null, + 'gandalf@wiz.ard', + 'Mr. Wizard', + 1735689600, + ); + + $this->assertSame($template, $result); + } + + public function testBuildRequestEmailOmitsResponseButtonsWhenLinkRecipientsDisallow(): void { + $this->stubNow(); + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $attendee = $vEvent->add('ATTENDEE', 'mailto:frodo@hobb.it', ['RSVP' => 'TRUE']); + + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + $this->appConfig->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('no'); + + $iTipMessage = new Message(); + $iTipMessage->recipient = 'mailto:frodo@hobb.it'; + $iTipMessage->sender = 'mailto:gandalf@wiz.ard'; + $iTipMessage->sequence = 1; + + $template->expects(self::never())->method('addBodyButtons'); + + $this->service->buildRequestEmail( + $iTipMessage, + $vEvent, + null, + $attendee, + 'frodo@hobb.it', + null, + 'gandalf@wiz.ard', + 'Mr. Wizard', + 1735689600, + ); + } + + public function testBuildRequestEmailOmitsResponseButtonsWhenAttendeeNotEligible(): void { + $this->stubNow(); + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $attendee = $vEvent->add('ATTENDEE', 'mailto:frodo@hobb.it', ['RSVP' => 'FALSE', 'ROLE' => 'NON-PARTICIPANT']); + + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $iTipMessage = new Message(); + $iTipMessage->recipient = 'mailto:frodo@hobb.it'; + $iTipMessage->sender = 'mailto:gandalf@wiz.ard'; + $iTipMessage->sequence = 1; + + // eligibility is checked before the link-recipients config at all + $this->appConfig->expects(self::never())->method('getValueString'); + $template->expects(self::never())->method('addBodyButtons'); + + $this->service->buildRequestEmail( + $iTipMessage, + $vEvent, + null, + $attendee, + 'frodo@hobb.it', + null, + 'gandalf@wiz.ard', + 'Mr. Wizard', + 1735689600, + ); + } + + /** + * REPLY never offers Accept/Decline buttons (those belong on the + * REQUEST the reply is answering, not on the reply notification the + * organizer receives). + */ + public function testBuildReplyEmailNeverAddsResponseButtons(): void { + $this->stubNow(); + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + $vEvent->add('ATTENDEE', 'mailto:frodo@hobb.it', ['PARTSTAT' => 'ACCEPTED']); + + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $iTipMessage = new Message(); + $iTipMessage->method = 'REPLY'; + $iTipMessage->message = $vCalendar; + $iTipMessage->sender = 'mailto:frodo@hobb.it'; + + $template->expects(self::once())->method('addBodySender')->with('frodo@hobb.it', ''); + $template->expects(self::never())->method('addBodyButtons'); + $template->expects(self::once())->method('addFooter'); + + $result = $this->service->buildReplyEmail( + $iTipMessage, + $vEvent, + 'gandalf@wiz.ard', + 'Mr. Wizard', + 'frodo@hobb.it', + null, + ); + + $this->assertSame($template, $result); + } + + public function testBuildCancellationEmailNeverAddsResponseButtons(): void { + $this->stubNow(); + $vCalendar = new VCalendar(); + $vEvent = $vCalendar->add('VEVENT', [ + 'UID' => 'uid-1234', + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00'), + ]); + + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $template->expects(self::never())->method('addBodyButtons'); + $template->expects(self::once())->method('addFooter'); + + $result = $this->service->buildCancellationEmail( + $vEvent, + 'frodo@hobb.it', + null, + 'gandalf@wiz.ard', + 'Mr. Wizard', + ); + + $this->assertSame($template, $result); + } } diff --git a/apps/dav/tests/unit/Listener/CalendarDelegateActionListenerTest.php b/apps/dav/tests/unit/Listener/CalendarDelegateActionListenerTest.php index 7ebbf745b88f2..af852cb89d2a0 100644 --- a/apps/dav/tests/unit/Listener/CalendarDelegateActionListenerTest.php +++ b/apps/dav/tests/unit/Listener/CalendarDelegateActionListenerTest.php @@ -66,6 +66,7 @@ protected function setUp(): void { $this->createMock(IUserManager::class), $this->createMock(IUserConfig::class), $this->createMock(IAppConfig::class), + $this->createMock(IMailer::class), ); $proxy = new Proxy();