diff options
Diffstat (limited to 'apps')
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/IMipPlugin.php | 17 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php | 193 |
2 files changed, 206 insertions, 4 deletions
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 1f063540df6..2af6b162d8d 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -249,7 +249,6 @@ class IMipPlugin extends SabreIMipPlugin { // convert iTip Message to string $itip_msg = $iTipMessage->message->serialize(); - $user = null; $mailService = null; try { @@ -261,8 +260,14 @@ class IMipPlugin extends SabreIMipPlugin { $mailService = $this->mailManager->findServiceByAddress($user->getUID(), $sender); } } + + // The display name in Nextcloud can use utf-8. + // As the default charset for text/* is us-ascii, it's important to explicitly define it. + // See https://www.rfc-editor.org/rfc/rfc6047.html#section-2.4. + $contentType = 'text/calendar; method=' . $iTipMessage->method . '; charset="utf-8"'; + // evaluate if a mail service was found and has sending capabilities - if ($mailService !== null && $mailService instanceof IMessageSend) { + if ($mailService instanceof IMessageSend) { // construct mail message and set required parameters $message = $mailService->initiateMessage(); $message->setFrom( @@ -274,10 +279,12 @@ class IMipPlugin extends SabreIMipPlugin { $message->setSubject($template->renderSubject()); $message->setBodyPlain($template->renderText()); $message->setBodyHtml($template->renderHtml()); + // Adding name=event.ics is a trick to make the invitation also appear + // as a file attachment in mail clients like Thunderbird or Evolution. $message->setAttachments((new Attachment( $itip_msg, null, - 'text/calendar; name=event.ics; method=' . $iTipMessage->method, + $contentType . '; name=event.ics', true ))); // send message @@ -293,10 +300,12 @@ class IMipPlugin extends SabreIMipPlugin { (($senderName !== null) ? [$sender => $senderName] : [$sender]) ); $message->useTemplate($template); + // Using a different content type because Symfony Mailer/Mime will append the name to + // the content type header and attachInline does not allow null. $message->attachInline( $itip_msg, 'event.ics', - 'text/calendar; method=' . $iTipMessage->method + $contentType, ); $failed = $this->mailer->send($message); } diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php new file mode 100644 index 00000000000..fa52d5319c9 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php @@ -0,0 +1,193 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Tests\unit\CalDAV\Schedule; + +use OC\L10N\L10N; +use OC\URLGenerator; +use OCA\DAV\CalDAV\EventComparisonService; +use OCA\DAV\CalDAV\Schedule\IMipPlugin; +use OCA\DAV\CalDAV\Schedule\IMipService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Defaults; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserSession; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Mail\IMessage; +use OCP\Mail\Provider\IManager; +use OCP\Mail\Provider\IMessageSend; +use OCP\Mail\Provider\IService; +use OCP\Mail\Provider\Message as MailProviderMessage; +use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\ITip\Message; +use Sabre\VObject\Property\ICalendar\CalAddress; +use Symfony\Component\Mime\Email; +use Test\TestCase; + +class IMipPluginCharsetTest extends TestCase { + // Dependencies + private Defaults&MockObject $defaults; + private IAppConfig&MockObject $appConfig; + private IConfig&MockObject $config; + private IDBConnection&MockObject $db; + private IFactory $l10nFactory; + private IManager&MockObject $mailManager; + private IMailer&MockObject $mailer; + private ISecureRandom&MockObject $random; + private ITimeFactory&MockObject $timeFactory; + private IUrlGenerator&MockObject $urlGenerator; + private IUserSession&MockObject $userSession; + private LoggerInterface $logger; + + // Services + private EventComparisonService $eventComparisonService; + private IMipPlugin $imipPlugin; + private IMipService $imipService; + + // ITip Message + private Message $itipMessage; + + protected function setUp(): void { + // Used by IMipService and IMipPlugin + $today = new \DateTime('2025-06-15 14:30'); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory->method('getTime') + ->willReturn($today->getTimestamp()); + $this->timeFactory->method('getDateTime') + ->willReturn($today); + + // IMipService + $this->urlGenerator = $this->createMock(URLGenerator::class); + $this->config = $this->createMock(IConfig::class); + $this->db = $this->createMock(IDBConnection::class); + $this->random = $this->createMock(ISecureRandom::class); + $l10n = $this->createMock(L10N::class); + $this->l10nFactory = $this->createMock(IFactory::class); + $this->l10nFactory->method('findGenericLanguage') + ->willReturn('en'); + $this->l10nFactory->method('findLocale') + ->willReturn('en_US'); + $this->l10nFactory->method('get') + ->willReturn($l10n); + $this->imipService = new IMipService( + $this->urlGenerator, + $this->config, + $this->db, + $this->random, + $this->l10nFactory, + $this->timeFactory, + ); + + // EventComparisonService + $this->eventComparisonService = new EventComparisonService(); + + // IMipPlugin + $this->appConfig = $this->createMock(IAppConfig::class); + $message = new \OC\Mail\Message(new Email(), false); + $this->mailer = $this->createMock(IMailer::class); + $this->mailer->method('createMessage') + ->willReturn($message); + $this->mailer->method('validateMailAddress') + ->willReturn(true); + $this->logger = new NullLogger(); + $this->defaults = $this->createMock(Defaults::class); + $this->defaults->method('getName') + ->willReturn('Instance Name 123'); + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('luigi'); + $this->userSession = $this->createMock(IUserSession::class); + $this->userSession->method('getUser') + ->willReturn($user); + $this->mailManager = $this->createMock(IManager::class); + $this->imipPlugin = new IMipPlugin( + $this->appConfig, + $this->mailer, + $this->logger, + $this->timeFactory, + $this->defaults, + $this->userSession, + $this->imipService, + $this->eventComparisonService, + $this->mailManager, + ); + + // ITipMessage + $calendar = new VCalendar(); + $event = new VEvent($calendar, 'VEVENT'); + $event->UID = 'uid-1234'; + $event->SEQUENCE = 1; + $event->SUMMARY = 'Lunch'; + $event->DTSTART = new \DateTime('2025-06-20 12:30:00'); + $organizer = new CalAddress($calendar, 'ORGANIZER', 'mailto:luigi@example.org'); + $event->add($organizer); + $attendee = new CalAddress($calendar, 'ATTENDEE', 'mailto:jose@example.org', ['RSVP' => 'TRUE', 'CN' => 'José']); + $event->add($attendee); + $calendar->add($event); + $this->itipMessage = new Message(); + $this->itipMessage->method = 'REQUEST'; + $this->itipMessage->message = $calendar; + $this->itipMessage->sender = 'mailto:luigi@example.org'; + $this->itipMessage->senderName = 'Luigi'; + $this->itipMessage->recipient = 'mailto:' . 'jose@example.org'; + } + + public function testCharsetMailer(): void { + // Arrange + $symfonyEmail = null; + $this->mailer->expects(self::once()) + ->method('send') + ->willReturnCallback(function (IMessage $message) use (&$symfonyEmail): array { + if ($message instanceof \OC\Mail\Message) { + $symfonyEmail = $message->getSymfonyEmail(); + } + return []; + }); + + // Act + $this->imipPlugin->schedule($this->itipMessage); + + // Assert + $this->assertNotNull($symfonyEmail); + $body = $symfonyEmail->getBody()->toString(); + $this->assertStringContainsString('Content-Type: text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $body); + } + + public function testCharsetMailProvider(): void { + // Arrange + $this->appConfig->method('getValueBool') + ->with('core', 'mail_providers_enabled', true) + ->willReturn(true); + $mailMessage = new MailProviderMessage(); + $mailService = $this->createStubForIntersectionOfInterfaces([IService::class, IMessageSend::class]); + $mailService->method('initiateMessage') + ->willReturn($mailMessage); + $mailService->expects(self::once()) + ->method('sendMessage'); + $this->mailManager->method('findServiceByAddress') + ->willReturn($mailService); + + // Act + $this->imipPlugin->schedule($this->itipMessage); + + // Assert + $attachments = $mailMessage->getAttachments(); + $this->assertCount(1, $attachments); + $this->assertStringContainsString('text/calendar; method=REQUEST; charset="utf-8"; name=event.ics', $attachments[0]->getType()); + } +} |