Files

1704 lines
61 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
use OC\URLGenerator;
use OCA\DAV\CalDAV\Schedule\IMipPlugin;
use OCA\DAV\CalDAV\Schedule\IMipService;
use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\Accounts\IAccountPropertyCollection;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Config\IUserConfig;
use OCP\Defaults;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\IAttachment;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use OCP\Mail\Provider\Address;
use OCP\Mail\Provider\IManager as IMailManager;
use OCP\Mail\Provider\IMessage as IMailMessageNew;
use OCP\Mail\Provider\IMessageSend as IMailMessageSend;
use OCP\Mail\Provider\IService as IMailService;
use OCP\Security\ISecureRandom;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\ITip\Message;
use Test\TestCase;
use Test\Traits\EmailValidatorTrait;
use function array_merge;
interface IMailServiceMock extends IMailService, IMailMessageSend {
// workaround for creating mock class with multiple interfaces
// TODO: remove after phpUnit 10 is supported.
}
class IMipPluginTest extends TestCase {
use EmailValidatorTrait;
private IMessage&MockObject $mailMessage;
private IMailer&MockObject $mailer;
private IEMailTemplate&MockObject $emailTemplate;
private IAttachment&MockObject $emailAttachment;
private ITimeFactory&MockObject $timeFactory;
private IAppConfig&MockObject $config;
private IUserSession&MockObject $userSession;
private IUser&MockObject $user;
private IMipPlugin $plugin;
private IMipService&MockObject $service;
private Defaults&MockObject $defaults;
private LoggerInterface&MockObject $logger;
private IMailManager&MockObject $mailManager;
private IMailServiceMock&MockObject $mailService;
private IMailMessageNew&MockObject $mailMessageNew;
private IAccountManager&MockObject $accountManager;
protected function setUp(): void {
$this->mailMessage = $this->createMock(IMessage::class);
$this->mailMessage->method('setFrom')->willReturn($this->mailMessage);
$this->mailMessage->method('setReplyTo')->willReturn($this->mailMessage);
$this->mailMessage->method('setTo')->willReturn($this->mailMessage);
$this->mailer = $this->createMock(IMailer::class);
$this->mailer->method('createMessage')->willReturn($this->mailMessage);
$this->emailTemplate = $this->createMock(IEMailTemplate::class);
$this->mailer->method('createEMailTemplate')->willReturn($this->emailTemplate);
$this->emailAttachment = $this->createMock(IAttachment::class);
$this->mailer->method('createAttachment')->willReturn($this->emailAttachment);
$this->logger = $this->createMock(LoggerInterface::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->timeFactory->method('getTime')->willReturn(1496912528); // 2017-01-01
$this->config = $this->createMock(IAppConfig::class);
$this->user = $this->createMock(IUser::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->userSession->method('getUser')
->willReturn($this->user);
$this->defaults = $this->createMock(Defaults::class);
$this->defaults->method('getName')
->willReturn('Instance Name 123');
$this->service = $this->createMock(IMipService::class);
// instanceKey()/diffInstance() are pure comparisons over a VEvent's own
// properties - delegate to a real IMipService so REQUEST tests exercise
// 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.
$l10nFactory = $this->createMock(L10NFactory::class);
$l10nFactory->method('findGenericLanguage')->willReturn('en');
$l10nFactory->method('findLocale')->willReturn('en_US');
$l10nFactory->method('get')->willReturn($this->createMock(IL10N::class));
$realImipService = new IMipService(
$this->createMock(URLGenerator::class),
$this->createMock(IDBConnection::class),
$this->createMock(ISecureRandom::class),
$l10nFactory,
$this->createMock(ITimeFactory::class),
$this->createMock(IUserManager::class),
$this->createMock(IUserConfig::class),
$this->createMock(IAppConfig::class),
);
$this->service->method('instanceKey')
->willReturnCallback(fn (VEvent $event) => $realImipService->instanceKey($event));
$this->service->method('diffInstance')
->willReturnCallback(fn (VEvent $new, VEvent $old) => $realImipService->diffInstance($new, $old));
$this->mailManager = $this->createMock(IMailManager::class);
$this->mailService = $this->createMock(IMailServiceMock::class);
$this->mailMessageNew = $this->createMock(IMailMessageNew::class);
$this->accountManager = $this->createMock(IAccountManager::class);
$this->plugin = new IMipPlugin(
$this->config,
$this->mailer,
$this->logger,
$this->timeFactory,
$this->defaults,
$this->userSession,
$this->service,
$this->mailManager,
$this->getEmailValidatorWithStrictEmailCheck(),
$this->accountManager,
);
}
public function testDeliveryNoSignificantChange(): void {
$message = new Message();
$message->method = 'REQUEST';
$message->message = new VCalendar();
$message->message->add('VEVENT', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 0,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
], []));
$message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']);
$message->sender = 'mailto:gandalf@wiz.ard';
$message->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:' . 'frodo@hobb.it';
$message->significantChange = false;
$this->config->expects(self::never())
->method('getValueBool');
$this->plugin->schedule($message);
$this->assertEquals('1.0', $message->getScheduleStatus());
}
public function testParsingSingle(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Fellowship meeting without (!) Boromir',
'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->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:' . 'frodo@hobb.it';
// save the old copy in the plugin
$oldVCalendar = new VCalendar();
$oldVEvent = new VEvent($oldVCalendar, 'one', [
'UID' => 'uid-1234',
'SEQUENCE' => 0,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
]);
$oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$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) {
if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
$atnd = $attendee;
}
}
$this->plugin->setVCalendar($oldVCalendar);
$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::exactly(2))
->method('eventInstances')
->willReturnMap([
[$newVCalendar, [$newVevent]],
[$oldVCalendar, [$oldVEvent]],
]);
$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, $oldVEvent)
->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->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');
$this->mailer->expects(self::once())
->method('send')
->willReturn([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
public function testAttendeeIsResource(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Fellowship meeting without (!) Boromir',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
], []));
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$newVevent->add('ATTENDEE', 'mailto:' . 'the-shire@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'The Shire', 'CUTYPE' => 'ROOM']);
$message->message = $newVCalendar;
$message->sender = 'mailto:gandalf@wiz.ard';
$message->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:' . 'the-shire@hobb.it';
// save the old copy in the plugin
$oldVCalendar = new VCalendar();
$oldVEvent = new VEvent($oldVCalendar, 'one', [
'UID' => 'uid-1234',
'SEQUENCE' => 0,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
]);
$oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$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) {
if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
$room = $attendee;
}
}
$this->plugin->setVCalendar($oldVCalendar);
$this->service->expects(self::once())
->method('getLastOccurrence')
->willReturn(1496912700);
$this->config->expects(self::once())
->method('getValueBool')
->with('dav', 'caldav_external_attendees_disabled', false)
->willReturn(false);
$this->service->expects(self::exactly(2))
->method('eventInstances')
->willReturnMap([
[$newVCalendar, [$newVevent]],
[$oldVCalendar, [$oldVEvent]],
]);
$this->service->expects(self::once())
->method('getCurrentAttendee')
->with($message)
->willReturn($room);
$this->service->expects(self::once())
->method('isRoomOrResource')
->with($room)
->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');
$this->user->expects(self::any())
->method('getDisplayName')
->willReturn('Mr. Wizard');
$this->userSession->expects(self::any())
->method('getUser')
->willReturn($this->user);
$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');
$this->service->expects(self::never())
->method('createInvitationToken');
$this->service->expects(self::never())
->method('addResponseButtons');
$this->mailer->expects(self::never())
->method('send');
$this->plugin->schedule($message);
$this->assertEquals('1.0', $message->getScheduleStatus());
}
public function testAttendeeIsCircle(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Fellowship meeting without (!) Boromir',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
], []));
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$newVevent->add('ATTENDEE', 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth', ['RSVP' => 'TRUE', 'CN' => 'The Fellowship', 'CUTYPE' => 'GROUP']);
$newVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE', 'MEMBER' => 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth']);
$message->message = $newVCalendar;
$message->sender = 'mailto:gandalf@wiz.ard';
$message->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth';
$attendees = $newVevent->select('ATTENDEE');
$circle = '';
foreach ($attendees as $attendee) {
if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
$circle = $attendee;
}
}
$this->assertNotEmpty($circle, 'Failed to find attendee belonging to the circle');
$this->service->expects(self::once())
->method('getLastOccurrence')
->willReturn(1496912700);
$this->config->expects(self::once())
->method('getValueBool')
->with('dav', 'caldav_external_attendees_disabled', false)
->willReturn(false);
$this->service->expects(self::once())
->method('eventInstances')
->with($newVCalendar)
->willReturn([$newVevent]);
$this->service->expects(self::once())
->method('getCurrentAttendee')
->with($message)
->willReturn($circle);
$this->service->expects(self::once())
->method('isRoomOrResource')
->with($circle)
->willReturn(false);
$this->service->expects(self::once())
->method('isCircle')
->with($circle)
->willReturn(true);
$this->service->expects(self::never())
->method('buildBodyData');
$this->user->expects(self::any())
->method('getUID')
->willReturn('user1');
$this->user->expects(self::any())
->method('getDisplayName')
->willReturn('Mr. Wizard');
$this->userSession->expects(self::any())
->method('getUser')
->willReturn($this->user);
$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');
$this->service->expects(self::never())
->method('createInvitationToken');
$this->service->expects(self::never())
->method('addResponseButtons');
$this->mailer->expects(self::never())
->method('send');
$this->plugin->schedule($message);
$this->assertEquals('1.0', $message->getScheduleStatus());
}
public function testParsingRecurrence(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', [
'UID' => 'uid-1234',
'LAST-MODIFIED' => 123456,
'SEQUENCE' => 2,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z'
]);
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$newVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
$newvEvent2 = new VEvent($newVCalendar, 'two', [
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Elevenses',
'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
'RECURRENCE-ID' => new \DateTime('2016-01-01 00:00:00')
]);
$newvEvent2->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$newvEvent2->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';
// save the old copy in the plugin
$oldVCalendar = new VCalendar();
$oldVEvent = new VEvent($oldVCalendar, 'one', [
'UID' => 'uid-1234',
'LAST-MODIFIED' => 123456,
'SEQUENCE' => 2,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z'
]);
$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) {
if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
$atnd = $attendee;
}
}
$this->plugin->setVCalendar($oldVCalendar);
$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],
]);
// the master is identical to its old version (filtered out as
// unchanged); the override has no old counterpart at all, so it's
// the one selected as the primary instance for this email
$this->service->expects(self::exactly(2))
->method('eventInstances')
->willReturnMap([
[$newVCalendar, [$newVevent, $newvEvent2]],
[$oldVCalendar, [$oldVEvent]],
]);
$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($newvEvent2, 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', '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');
$this->mailer->expects(self::once())
->method('send')
->willReturn([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
/**
* A message can bundle several instances (e.g. a recurring event's
* master plus one of its overrides edited together). The master is used
* to build the email regardless of its position in the list returned by
* eventInstances(), and the fact that other instances were dropped from
* the email is logged.
*/
public function testMultipleModifiedInstancesUsesMasterAndLogsTheRest(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$masterVevent = new VEvent($newVCalendar, 'one', [
'UID' => 'uid-1234',
'SEQUENCE' => 2,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
'RRULE' => 'FREQ=DAILY;INTERVAL=1;UNTIL=20160201T000000Z',
]);
$masterVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$masterVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
$overrideVevent = new VEvent($newVCalendar, 'two', [
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Elevenses',
'DTSTART' => new \DateTime('2016-01-02 00:00:00'),
'RECURRENCE-ID' => new \DateTime('2016-01-02 00:00:00'),
]);
$overrideVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$overrideVevent->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
$message->message = $newVCalendar;
$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) {
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],
]);
// the override is listed before the master on purpose: the master must still win
$this->service->expects(self::once())
->method('eventInstances')
->with($newVCalendar)
->willReturn([$overrideVevent, $masterVevent]);
$this->logger->expects(self::once())
->method('debug')
->with(self::stringContains('multiple instances'), self::anything());
$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($masterVevent, 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, $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');
$this->mailer->expects(self::once())
->method('send')
->willReturn([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
/**
* buildCancelledBodyData() only ever consumes the new instance, so a
* CANCEL message must never be routed through old/new diffing at all -
* that comparison could legitimately find nothing "changed" (e.g.
* removing an attendee doesn't necessarily touch any of the compared
* properties), which would incorrectly look like there was nothing to
* send even though iTip's own significantChange flag says this message
* matters. CANCEL goes straight to eventInstances() instead.
*/
public function testCancelUsesAllInstancesWithoutDiffing(): void {
$message = new Message();
$message->method = 'CANCEL';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', [
'UID' => 'uid-1234',
'SEQUENCE' => 3,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00'),
]);
$newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$newVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE', 'CN' => 'Boromir']);
$message->message = $newVCalendar;
$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) {
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('buildCancelledBodyData')
->with($newVevent)
->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, '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');
$this->service->expects(self::never())
->method('createInvitationToken');
$this->service->expects(self::never())
->method('addResponseButtons');
$this->mailer->expects(self::once())
->method('send')
->willReturn([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
public function testEmailValidationFailed(): void {
$message = new Message();
$message->method = 'REQUEST';
$message->message = new VCalendar();
$message->message->add('VEVENT', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 0,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
], []));
$message->message->VEVENT->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$message->message->VEVENT->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE']);
$message->sender = 'mailto:gandalf@wiz.ard';
$message->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:' . 'frodo@@hobb.it';
$this->service->expects(self::once())
->method('getLastOccurrence')
->willReturn(1496912700);
$this->plugin->schedule($message);
$this->assertEquals('5.0', $message->getScheduleStatus());
}
public function testFailedDelivery(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVcalendar = new VCalendar();
$newVevent = new VEvent($newVcalendar, 'one', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Fellowship meeting without (!) Boromir',
'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->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) {
if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
$atnd = $attendee;
}
}
$this->service->expects(self::once())
->method('getLastOccurrence')
->willReturn(1496912700);
$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 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([]);
$this->mailer
->method('send')
->willThrowException(new \Exception());
$this->logger->expects(self::once())
->method('error');
$this->plugin->schedule($message);
$this->assertEquals('5.0', $message->getScheduleStatus());
}
public function testMailProviderSend(): void {
// construct iTip message with event and attendees
$message = new Message();
$message->method = 'REQUEST';
$calendar = new VCalendar();
$event = new VEvent($calendar, 'one', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Fellowship meeting without (!) Boromir',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
], []));
$event->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$event->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
$message->message = $calendar;
$message->sender = 'mailto:gandalf@wiz.ard';
$message->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:' . 'frodo@hobb.it';
// construct
foreach ($event->select('ATTENDEE') as $entry) {
if (strcasecmp($entry->getValue(), $message->recipient) === 0) {
$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')
->willReturn('user1');
$this->user->expects(self::any())
->method('getDisplayName')
->willReturn('Mr. Wizard');
// construct user session mock returns
$this->userSession->expects(self::any())
->method('getUser')
->willReturn($this->user);
// construct service mock returns
$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, true],
]);
$this->service->expects(self::once())
->method('getCurrentAttendee')
->with($message)
->willReturn($attendee);
$this->service->expects(self::once())
->method('isRoomOrResource')
->with($attendee)
->willReturn(false);
$this->service->expects(self::once())
->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');
$this->service->expects(self::once())
->method('eventInstances')
->with($calendar)
->willReturn([$event]);
// construct mail provider mock returns
$this->mailService
->method('initiateMessage')
->willReturn($this->mailMessageNew);
$this->mailService
->method('sendMessage')
->with($this->mailMessageNew);
$this->mailManager
->method('findServiceByAddress')
->with('user1', 'gandalf@wiz.ard')
->willReturn($this->mailService);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
public function testMailProviderDisabled(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', array_merge([
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Fellowship meeting without (!) Boromir',
'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->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:' . 'frodo@hobb.it';
// save the old copy in the plugin
$oldVCalendar = new VCalendar();
$oldVEvent = new VEvent($oldVCalendar, 'one', [
'UID' => 'uid-1234',
'SEQUENCE' => 0,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
]);
$oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$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) {
if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
$atnd = $attendee;
}
}
$this->plugin->setVCalendar($oldVCalendar);
$this->service->expects(self::once())
->method('getLastOccurrence')
->willReturn(1496912700);
$this->service->expects(self::exactly(2))
->method('eventInstances')
->willReturnMap([
[$newVCalendar, [$newVevent]],
[$oldVCalendar, [$oldVEvent]],
]);
$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, $oldVEvent)
->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 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->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([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
public function testNoOldEvent(): 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->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) {
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('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([]);
$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());
}
public function testExternalAttendeesDisabledForExternalUser(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', 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:external@example.com', ['RSVP' => 'TRUE', 'CN' => 'External User']);
$message->message = $newVCalendar;
$message->sender = 'mailto:gandalf@wiz.ard';
$message->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:external@example.com';
$this->service->expects(self::once())
->method('getLastOccurrence')
->willReturn(1496912700);
$this->config->expects(self::once())
->method('getValueBool')
->with('dav', 'caldav_external_attendees_disabled', false)
->willReturn(true);
$this->service->expects(self::once())
->method('isSystemUser')
->with('external@example.com')
->willReturn(false);
$this->service->expects(self::never())
->method('eventInstances');
$this->service->expects(self::never())
->method('getCurrentAttendee');
$this->mailer->expects(self::never())
->method('send');
$this->plugin->schedule($message);
$this->assertEquals('5.0', $message->getScheduleStatus());
}
public function testExternalAttendeesDisabledForSystemUser(): void {
$message = new Message();
$message->method = 'REQUEST';
$newVCalendar = new VCalendar();
$newVevent = new VEvent($newVCalendar, 'one', 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->senderName = 'Mr. Wizard';
$message->recipient = 'mailto:frodo@hobb.it';
$oldVCalendar = new VCalendar();
$oldVEvent = new VEvent($oldVCalendar, 'one', [
'UID' => 'uid-1234',
'SEQUENCE' => 0,
'SUMMARY' => 'Fellowship meeting',
'DTSTART' => new \DateTime('2016-01-01 00:00:00')
]);
$oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
$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) {
if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
$atnd = $attendee;
}
}
$this->plugin->setVCalendar($oldVCalendar);
$this->service->expects(self::once())
->method('getLastOccurrence')
->willReturn(1496912700);
$this->config->expects(self::exactly(2))
->method('getValueBool')
->willReturnMap([
['dav', 'caldav_external_attendees_disabled', false, true],
['core', 'mail_providers_enabled', true, false],
]);
$this->service->expects(self::once())
->method('isSystemUser')
->with('frodo@hobb.it')
->willReturn(true);
$this->service->expects(self::exactly(2))
->method('eventInstances')
->willReturnMap([
[$newVCalendar, [$newVevent]],
[$oldVCalendar, [$oldVEvent]],
]);
$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, $oldVEvent)
->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', 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');
$this->mailer->expects(self::once())
->method('send')
->willReturn([]);
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
/**
* 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.
*
* @return array{from: ?array, replyTo: ?array}
*/
private function scheduleWithoutSenderName(string $organizer, string $recipient, bool $viaMailProvider = false): array {
$vCalendar = new VCalendar();
$vEvent = new VEvent($vCalendar, 'VEVENT', [
'UID' => 'uid-1234',
'SEQUENCE' => 1,
'SUMMARY' => 'Meeting',
'DTSTART' => new \DateTime('2017-01-01 00:00:00'),
]);
$vEvent->add('ORGANIZER', 'mailto:' . $organizer);
$vEvent->add('ATTENDEE', 'mailto:' . $recipient, ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
$message = new Message();
$message->method = 'REQUEST';
$message->message = $vCalendar;
$message->sender = 'mailto:' . $organizer;
$message->senderName = null;
$message->recipient = 'mailto:' . $recipient;
$capturedFrom = null;
$capturedReplyTo = null;
$capturedSender = null;
$this->emailTemplate->method('addBodySender')
->willReturnCallback(function (string $displayName, string $subline) use (&$capturedSender): void {
$capturedSender = [$displayName, $subline];
});
$mailMessage = $this->createMock(IMessage::class);
$mailMessage->method('setTo')->willReturn($mailMessage);
$mailMessage->method('setFrom')
->willReturnCallback(function (array $from) use (&$capturedFrom, $mailMessage) {
$capturedFrom = $from;
return $mailMessage;
});
$mailMessage->method('setReplyTo')
->willReturnCallback(function (array $replyTo) use (&$capturedReplyTo, $mailMessage) {
$capturedReplyTo = $replyTo;
return $mailMessage;
});
$mailer = $this->createMock(IMailer::class);
$mailer->method('createMessage')->willReturn($mailMessage);
$mailer->method('createEMailTemplate')->willReturn($this->emailTemplate);
$mailer->method('send')->willReturn([]);
if ($viaMailProvider) {
$this->mailMessageNew->method('setFrom')
->willReturnCallback(function (Address $from) use (&$capturedFrom) {
$capturedFrom = [$from->getAddress() => (string)$from->getLabel()];
return $this->mailMessageNew;
});
$this->mailService->method('initiateMessage')->willReturn($this->mailMessageNew);
$this->mailService->expects(self::once())
->method('sendMessage')
->with($this->mailMessageNew);
$this->mailManager->method('findServiceByAddress')->willReturn($this->mailService);
}
$this->service->method('getLastOccurrence')->willReturn(1496912700);
$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,
]);
// Mirrors the real IMipService::getFrom() so assertions read like the
// actual mail header.
$this->service->method('getFrom')
->willReturnCallback(static fn (?string $senderName, string $default): string
=> ($senderName === null || $senderName === '') ? $default : $senderName . ' via ' . $default);
$this->config->method('getValueBool')->willReturnMap([
['dav', 'caldav_external_attendees_disabled', false, false],
['core', 'mail_providers_enabled', true, $viaMailProvider],
]);
$this->service->method('eventInstances')
->with($vCalendar)
->willReturn([$vEvent]);
$plugin = new IMipPlugin(
$this->config,
$mailer,
$this->logger,
$this->timeFactory,
$this->defaults,
$this->userSession,
$this->service,
$this->mailManager,
$this->getEmailValidatorWithStrictEmailCheck(),
$this->accountManager,
);
$plugin->schedule($message);
self::assertSame('1.1', $message->getScheduleStatus());
return ['from' => $capturedFrom, 'replyTo' => $capturedReplyTo, 'sender' => $capturedSender];
}
/**
* Messages are regularly brokered on behalf of somebody else, so headers
* must not fall back to the session user's name when the sender address
* is not theirs.
*/
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'transportProvider')]
public function testSenderNameIsNotTakenFromAnUnrelatedSessionUser(bool $viaMailProvider): void {
$this->user->method('getUID')->willReturn('bilbo');
$this->user->method('getDisplayName')->willReturn('Bilbo Baggins');
$this->user->method('getEMailAddress')->willReturn('bilbo@hobb.it');
$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']);
if (!$viaMailProvider) {
self::assertSame(['a@example.com'], $result['replyTo']);
}
}
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'transportProvider')]
public function testSenderNameFallsBackToSessionUserWhenTheyAreTheSender(bool $viaMailProvider): void {
$this->user->method('getUID')->willReturn('gandalf');
$this->user->method('getDisplayName')->willReturn('Mr. Wizard');
$this->user->method('getEMailAddress')->willReturn('gandalf@wiz.ard');
$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']);
if (!$viaMailProvider) {
self::assertSame(['gandalf@wiz.ard' => 'Mr. Wizard'], $result['replyTo']);
}
}
/**
* The session user must also be recognized as the sender when sending
* under one of their profile email aliases.
*/
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'transportProvider')]
public function testSenderNameUsesSessionUserForTheirAliasAddress(bool $viaMailProvider): void {
$this->user->method('getUID')->willReturn('carl');
$this->user->method('getDisplayName')->willReturn('Carl Session');
$this->user->method('getEMailAddress')->willReturn('carl@example.com');
$aliasProperty = $this->createMock(IAccountProperty::class);
$aliasProperty->method('getValue')->willReturn('Shared@Corp.example');
$emailCollection = $this->createMock(IAccountPropertyCollection::class);
$emailCollection->method('getProperties')->willReturn([$aliasProperty]);
$account = $this->createMock(IAccount::class);
$account->method('getPropertyCollection')
->with(IAccountManager::COLLECTION_EMAIL)
->willReturn($emailCollection);
$this->accountManager->method('getAccount')->with($this->user)->willReturn($account);
$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']);
if (!$viaMailProvider) {
self::assertSame(['shared@corp.example' => 'Carl Session'], $result['replyTo']);
}
}
#[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'transportProvider')]
public function testSenderNameStaysNeutralForBlankDisplayNames(bool $viaMailProvider): void {
$this->user->method('getDisplayName')->willReturn(' ');
$this->user->method('getEMailAddress')->willReturn('gandalf@wiz.ard');
$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']);
if (!$viaMailProvider) {
self::assertSame(['gandalf@wiz.ard'], $result['replyTo']);
}
}
/**
* Sessionless contexts: invitation link responses and background jobs.
*/
public function testSenderNameStaysNeutralWithoutSessionUser(): void {
$this->userSession = $this->createMock(IUserSession::class);
$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::assertSame(['a@example.com'], $result['replyTo']);
}
public static function transportProvider(): array {
return [
'system email account' => [false],
'user email account' => [true],
];
}
}