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());
+ }
+}