diff --git a/appinfo/info.xml b/appinfo/info.xml index 596abe233..fb7adfe3c 100755 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -31,7 +31,7 @@ Guests users can only access files shared to them and cannot create any files ou https://raw.githubusercontent.com/nextcloud/guests/master/screenshots/settings.png https://raw.githubusercontent.com/nextcloud/guests/master/screenshots/dropdown.png - + diff --git a/lib/Mail.php b/lib/Mail.php index ead0be5b3..190720f00 100644 --- a/lib/Mail.php +++ b/lib/Mail.php @@ -16,6 +16,7 @@ use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\Mail\EMailDetails; use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Share\IShare; @@ -61,17 +62,15 @@ public function sendGuestInviteMail(string $uid, string $guest, string $token, s $senderDisplayName = $this->userSession->getUser()->getDisplayName(); if (!$share instanceof IShare) { - [ $subject, $emailTemplate ] = $this->composeInviteMessage($senderDisplayName, $guestEmail, $passwordLink, $l10n); + $emailTemplate = $this->composeInviteMessage($senderDisplayName, $replyTo, $guestEmail, $passwordLink, $l10n); } else { - [ $subject, $emailTemplate ] = $this->composeShareMessage($share, $senderDisplayName, $guestEmail, $passwordLink, $l10n); + $emailTemplate = $this->composeShareMessage($share, $senderDisplayName, $replyTo, $guestEmail, $passwordLink, $l10n); } try { $message = $this->mailer->createMessage(); $message->setTo([$guestEmail => $targetUser->getDisplayName()]); - $message->setSubject($subject); - $message->setHtmlBody($emailTemplate->renderHtml()); - $message->setPlainBody($emailTemplate->renderText()); + $message->useTemplate($emailTemplate); $message->setFrom([ Util::getDefaultEmailAddress('sharing-noreply') => $l10n->t('%s via %s', [ @@ -92,87 +91,60 @@ public function sendGuestInviteMail(string $uid, string $guest, string $token, s } } - /** - * @return array{string, IEMailTemplate} - */ - private function composeShareMessage(IShare $share, string $senderDisplayName, string $guestEmail, string $passwordLink, IL10N $l10n): array { + private function composeShareMessage(IShare $share, string $senderDisplayName, ?string $senderEmail, string $guestEmail, string $passwordLink, IL10N $l10n): IEMailTemplate { $filename = trim($share->getTarget(), '/'); - $subject = $l10n->t('%s shared a file with you', [$senderDisplayName]); $expiration = $share->getExpirationDate(); + $note = $share->getNote(); $link = $this->urlGenerator->linkToRouteAbsolute( 'files.viewcontroller.showFile', ['fileid' => $share->getNodeId(), 'direct' => 1] ); $emailTemplate = $this->mailer->createEMailTemplate('guest.share'); + $emailTemplate->setLanguage($l10n->getLanguageCode()); + $emailTemplate->setSubject($l10n->t('%1$s shared %2$s with you', [$senderDisplayName, $filename])); $emailTemplate->addHeader(); - $emailTemplate->addHeading($l10n->t('%s shared a file with you', [$senderDisplayName])); - - $emailTemplate->addBodyText( - $l10n->t('Hey there,') - ); - - $emailTemplate->addBodyText( - $l10n->t('%1$s just invited you and shared »%2$s« with you.', [$senderDisplayName, $filename]) - ); + $emailTemplate->addBodySender($senderDisplayName, $senderEmail ?? ''); + $emailTemplate->addHeading($l10n->t('%1$s shared %2$s with you', [$senderDisplayName, $filename])); - $emailTemplate->addBodyText( - $l10n->t('You can access the shared file by activating your guest account.') - ); - $emailTemplate->addBodyText( - $l10n->t('After your account is activated you can view the share by logging in with %s.', [$guestEmail]) - ); - - if ($expiration) { - $formattedDate = $l10n->l('date', $expiration); - $emailTemplate->addBodyText( - $l10n->t('The share will expire at %s.', [$formattedDate]) - ); + if ($note !== '') { + $emailTemplate->addBodyNote($note, $l10n->t('Note')); } - $emailTemplate->addBodyButtonGroup( - $l10n->t('Activate account'), - $passwordLink, - $l10n->t('View share'), - $link - ); + $details = new EMailDetails($filename); + if ($expiration !== null) { + $details->addRow($l10n->t('Valid until'))->text((string)$l10n->l('date', $expiration, ['width' => 'medium'])); + } + $details->addRow($l10n->t('Your login'))->text($guestEmail); + $emailTemplate->addBodyDetails($details); + + $emailTemplate->addBodyText($l10n->t('Activate your guest account to open it.')); + $emailTemplate->addBodyButtons([ + ['text' => $l10n->t('Activate account'), 'url' => $passwordLink], + ['text' => $l10n->t('View share'), 'url' => $link], + ]); $emailTemplate->addFooter(); - return [ $subject, $emailTemplate ]; + return $emailTemplate; } - /** - * @return array{string, IEMailTemplate} - */ - private function composeInviteMessage(string $senderDisplayName, string $guestEmail, string $passwordLink, IL10N $l10n): array { - $subject = $l10n->t('%s invited you as a guest', [$senderDisplayName]); - + private function composeInviteMessage(string $senderDisplayName, ?string $senderEmail, string $guestEmail, string $passwordLink, IL10N $l10n): IEMailTemplate { $emailTemplate = $this->mailer->createEMailTemplate('guest.invite'); + $emailTemplate->setLanguage($l10n->getLanguageCode()); + $emailTemplate->setSubject($l10n->t('%s invited you as a guest', [$senderDisplayName])); $emailTemplate->addHeader(); - $emailTemplate->addHeading($l10n->t('You have been invited')); - - $emailTemplate->addBodyText( - $l10n->t('Hey there,') - ); + $emailTemplate->addBodySender($senderDisplayName, $senderEmail ?? ''); + $emailTemplate->addHeading($l10n->t('%s invited you as a guest', [$senderDisplayName])); - $emailTemplate->addBodyText( - $l10n->t('%s just invited you.', [$senderDisplayName]) - ); + $details = new EMailDetails($this->defaults->getName()); + $details->addRow($l10n->t('Your login'))->text($guestEmail); + $emailTemplate->addBodyDetails($details); - $emailTemplate->addBodyText( - $l10n->t('You can activate your guest account with the button below.') - ); - $emailTemplate->addBodyText( - $l10n->t('After your account is activated you can log in with %s.', [$guestEmail]) - ); - - $emailTemplate->addBodyButton( - $l10n->t('Activate account'), - $passwordLink - ); + $emailTemplate->addBodyText($l10n->t('Activate your guest account to get started.')); + $emailTemplate->addBodyButton($l10n->t('Activate account'), $passwordLink); $emailTemplate->addFooter(); - return [ $subject, $emailTemplate ]; + return $emailTemplate; } } diff --git a/psalm.xml b/psalm.xml index 16f7e79cb..e6a608e56 100644 --- a/psalm.xml +++ b/psalm.xml @@ -12,7 +12,7 @@ findUnusedIssueHandlerSuppression="false" findUnusedBaselineEntry="false" findUnusedCode="false" - phpVersion="8.1" + phpVersion="8.3" resolveFromConfigFile="true" totallyTyped="true" > diff --git a/tests/unit/MailTest.php b/tests/unit/MailTest.php new file mode 100644 index 000000000..fb5f1bb9e --- /dev/null +++ b/tests/unit/MailTest.php @@ -0,0 +1,130 @@ +mailer = $this->createMock(IMailer::class); + $this->mailer->method('createEMailTemplate') + ->willReturnCallback(fn (string $emailId) => Server::get(IMailer::class)->createEMailTemplate($emailId)); + $message = $this->createMock(IMessage::class); + $message->method('useTemplate') + ->willReturnCallback(function (IEMailTemplate $template) use ($message) { + $this->sentTemplate = $template; + return $message; + }); + $this->mailer->method('createMessage')->willReturn($message); + + $sender = $this->createMock(IUser::class); + $sender->method('getDisplayName')->willReturn('Alice Martin'); + $sender->method('getEMailAddress')->willReturn('alice@example.com'); + $guest = $this->createMock(IUser::class); + $guest->method('getDisplayName')->willReturn('Jordan'); + $guest->method('getEMailAddress')->willReturn('jordan@partner.example'); + + $this->userManager = $this->createMock(IUserManager::class); + $this->userManager->method('get')->willReturnMap([ + ['alice', $sender], + ['jordan@partner.example', $guest], + ]); + $userSession = $this->createMock(IUserSession::class); + $userSession->method('getUser')->willReturn($sender); + + $defaults = $this->createMock(Defaults::class); + $defaults->method('getName')->willReturn('Nextcloud'); + + $urlGenerator = $this->createMock(IURLGenerator::class); + $urlGenerator->method('linkToRouteAbsolute')->willReturnMap([ + ['core.lost.resetform', ['userId' => 'jordan@partner.example', 'token' => 'token'], 'https://cloud.example.com/reset'], + ['files.viewcontroller.showFile', ['fileid' => 42, 'direct' => 1], 'https://cloud.example.com/f/42'], + ]); + + $this->mail = new Mail( + $userSession, + $this->mailer, + $defaults, + Server::get(IFactory::class), + $this->userManager, + $urlGenerator, + ); + } + + private function createShare(string $note, ?\DateTime $expiration): IShare&MockObject { + $share = $this->createMock(IShare::class); + $share->method('getTarget')->willReturn('/Budget.xlsx'); + $share->method('getNodeId')->willReturn(42); + $share->method('getNote')->willReturn($note); + $share->method('getExpirationDate')->willReturn($expiration); + return $share; + } + + public function testShareMail(): void { + $share = $this->createShare("Can you check\nthe travel line?", new \DateTime('2026-11-15')); + $this->mail->sendGuestInviteMail('alice', 'jordan@partner.example', 'token', 'en', $share); + + $this->assertSame('Alice Martin shared Budget.xlsx with you', $this->sentTemplate->renderSubject()); + $text = $this->sentTemplate->renderText(); + $this->assertStringContainsString('Alice Martin (alice@example.com)', $text); + $this->assertStringContainsString("> Can you check\n> the travel line?", $text); + $this->assertStringContainsString('Valid until: Nov 15, 2026', $text); + $this->assertStringContainsString('Your login: jordan@partner.example', $text); + $this->assertStringContainsString('Activate account: https://cloud.example.com/reset', $text); + $this->assertStringContainsString('View share: https://cloud.example.com/f/42', $text); + } + + public function testShareMailWithoutNoteAndExpiration(): void { + $share = $this->createShare('', null); + $this->mail->sendGuestInviteMail('alice', 'jordan@partner.example', 'token', 'en', $share); + + $text = $this->sentTemplate->renderText(); + $this->assertStringNotContainsString('Note', $text); + $this->assertStringNotContainsString('Valid until', $text); + $this->assertStringContainsString('Your login: jordan@partner.example', $text); + } + + public function testInviteMail(): void { + $this->mail->sendGuestInviteMail('alice', 'jordan@partner.example', 'token', 'en'); + + $this->assertSame('Alice Martin invited you as a guest', $this->sentTemplate->renderSubject()); + $text = $this->sentTemplate->renderText(); + $this->assertStringContainsString('Alice Martin (alice@example.com)', $text); + $this->assertStringContainsString('Your login: jordan@partner.example', $text); + $this->assertStringContainsString('Activate account: https://cloud.example.com/reset', $text); + $this->assertStringNotContainsString('View share', $text); + } + + public function testRightToLeftLanguage(): void { + $this->mail->sendGuestInviteMail('alice', 'jordan@partner.example', 'token', 'ar'); + + $this->assertStringContainsString('lang="ar" xml:lang="ar" dir="rtl"', $this->sentTemplate->renderHtml()); + } +}