Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Guests users can only access files shared to them and cannot create any files ou
<screenshot>https://raw.githubusercontent.com/nextcloud/guests/master/screenshots/settings.png</screenshot>
<screenshot>https://raw.githubusercontent.com/nextcloud/guests/master/screenshots/dropdown.png</screenshot>
<dependencies>
<nextcloud min-version="32" max-version="36" />
<nextcloud min-version="36" max-version="36" />

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should branch off before merging this

</dependencies>
<repair-steps>
<post-migration>
Expand Down
100 changes: 36 additions & 64 deletions lib/Mail.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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', [
Expand All @@ -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;
}
}
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
findUnusedIssueHandlerSuppression="false"
findUnusedBaselineEntry="false"
findUnusedCode="false"
phpVersion="8.1"
phpVersion="8.3"
resolveFromConfigFile="true"
totallyTyped="true"
>
Expand Down
130 changes: 130 additions & 0 deletions tests/unit/MailTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Guests\Test\Unit;

use OCA\Guests\Mail;
use OCP\Defaults;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use OCP\Server;
use OCP\Share\IShare;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

class MailTest extends TestCase {
private IMailer&MockObject $mailer;
private IUserManager&MockObject $userManager;
private ?IEMailTemplate $sentTemplate = null;
private Mail $mail;

protected function setUp(): void {
parent::setUp();

$this->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());
}
}
Loading