diff --git a/apps/sharebymail/composer/composer/autoload_classmap.php b/apps/sharebymail/composer/composer/autoload_classmap.php index 38fec4de2788d..d356740f53ec2 100644 --- a/apps/sharebymail/composer/composer/autoload_classmap.php +++ b/apps/sharebymail/composer/composer/autoload_classmap.php @@ -10,6 +10,10 @@ 'OCA\\ShareByMail\\Activity' => $baseDir . '/../lib/Activity.php', 'OCA\\ShareByMail\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\ShareByMail\\Capabilities' => $baseDir . '/../lib/Capabilities.php', + 'OCA\\ShareByMail\\Event\\AbstractBeforeShareMailSentEvent' => $baseDir . '/../lib/Event/AbstractBeforeShareMailSentEvent.php', + 'OCA\\ShareByMail\\Event\\BeforeShareMailSentEvent' => $baseDir . '/../lib/Event/BeforeShareMailSentEvent.php', + 'OCA\\ShareByMail\\Event\\BeforeShareNoteMailSentEvent' => $baseDir . '/../lib/Event/BeforeShareNoteMailSentEvent.php', + 'OCA\\ShareByMail\\Event\\BeforeSharePasswordMailSentEvent' => $baseDir . '/../lib/Event/BeforeSharePasswordMailSentEvent.php', 'OCA\\ShareByMail\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', 'OCA\\ShareByMail\\Settings\\SettingsManager' => $baseDir . '/../lib/Settings/SettingsManager.php', 'OCA\\ShareByMail\\ShareByMailProvider' => $baseDir . '/../lib/ShareByMailProvider.php', diff --git a/apps/sharebymail/composer/composer/autoload_static.php b/apps/sharebymail/composer/composer/autoload_static.php index 2d70de1806b6a..cca28cd940e20 100644 --- a/apps/sharebymail/composer/composer/autoload_static.php +++ b/apps/sharebymail/composer/composer/autoload_static.php @@ -25,6 +25,10 @@ class ComposerStaticInitShareByMail 'OCA\\ShareByMail\\Activity' => __DIR__ . '/..' . '/../lib/Activity.php', 'OCA\\ShareByMail\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', 'OCA\\ShareByMail\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', + 'OCA\\ShareByMail\\Event\\AbstractBeforeShareMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/AbstractBeforeShareMailSentEvent.php', + 'OCA\\ShareByMail\\Event\\BeforeShareMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/BeforeShareMailSentEvent.php', + 'OCA\\ShareByMail\\Event\\BeforeShareNoteMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/BeforeShareNoteMailSentEvent.php', + 'OCA\\ShareByMail\\Event\\BeforeSharePasswordMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/BeforeSharePasswordMailSentEvent.php', 'OCA\\ShareByMail\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', 'OCA\\ShareByMail\\Settings\\SettingsManager' => __DIR__ . '/..' . '/../lib/Settings/SettingsManager.php', 'OCA\\ShareByMail\\ShareByMailProvider' => __DIR__ . '/..' . '/../lib/ShareByMailProvider.php', diff --git a/apps/sharebymail/lib/Event/AbstractBeforeShareMailSentEvent.php b/apps/sharebymail/lib/Event/AbstractBeforeShareMailSentEvent.php new file mode 100644 index 0000000000000..ed4868ce88650 --- /dev/null +++ b/apps/sharebymail/lib/Event/AbstractBeforeShareMailSentEvent.php @@ -0,0 +1,62 @@ +send(). + */ +abstract class AbstractBeforeShareMailSentEvent extends Event { + private bool $mailHandled = false; + + /** + * @param string[] $resolvedEmails resolved recipient addresses (not guaranteed to be validated) + */ + public function __construct( + private readonly IShare $share, + private readonly array $resolvedEmails, + private readonly IMessage $message, + ) { + parent::__construct(); + } + + public function getShare(): IShare { + return $this->share; + } + + /** @return string[] */ + public function getResolvedEmails(): array { + return $this->resolvedEmails; + } + + public function getMessage(): IMessage { + return $this->message; + } + + /** + * Call to suppress the native mailer->send() for this message. + * Must be called before any send attempt — if the listener's own send + * throws, the exception propagates and the native send is also skipped. + */ + public function markMailHandled(): void { + $this->mailHandled = true; + } + + public function isMailHandled(): bool { + return $this->mailHandled; + } +} diff --git a/apps/sharebymail/lib/Event/BeforeShareMailSentEvent.php b/apps/sharebymail/lib/Event/BeforeShareMailSentEvent.php new file mode 100644 index 0000000000000..657429d2ac772 --- /dev/null +++ b/apps/sharebymail/lib/Event/BeforeShareMailSentEvent.php @@ -0,0 +1,72 @@ +send() call for share-link notifications to recipients. + * + * @psalm-type TemplateData = array{ + * senderUserId: string, + * filename: string, + * link: string, + * initiator: string, + * expiration: \DateTime|null, + * shareWith: string, + * note: string, + * } + * + * @psalm-api + */ +class BeforeShareMailSentEvent extends AbstractBeforeShareMailSentEvent { + /** + * @param string[] $resolvedEmails + * @param TemplateData $templateData + */ + public function __construct( + IShare $share, + array $resolvedEmails, + IMessage $message, + private readonly array $templateData, + ) { + parent::__construct($share, $resolvedEmails, $message); + } + + public function getSenderUserId(): string { + return $this->templateData['senderUserId']; + } + + public function getFileName(): string { + return $this->templateData['filename']; + } + + public function getResourceUrl(): string { + return $this->templateData['link']; + } + + public function getNote(): string { + return $this->templateData['note']; + } + + public function getShareWith(): string { + return $this->templateData['shareWith']; + } + + public function getInitiatorDisplayName(): string { + return $this->templateData['initiator']; + } + + public function getExpiration(): ?\DateTime { + return $this->templateData['expiration']; + } +} diff --git a/apps/sharebymail/lib/Event/BeforeShareNoteMailSentEvent.php b/apps/sharebymail/lib/Event/BeforeShareNoteMailSentEvent.php new file mode 100644 index 0000000000000..bbf1ec2dbe661 --- /dev/null +++ b/apps/sharebymail/lib/Event/BeforeShareNoteMailSentEvent.php @@ -0,0 +1,47 @@ +send() call for note-update notifications to recipients. + * + * @psalm-type TemplateData = array{ + * filename: string, + * note: string, + * } + * + * @psalm-api + */ +class BeforeShareNoteMailSentEvent extends AbstractBeforeShareMailSentEvent { + /** + * @param string[] $resolvedEmails + * @param TemplateData $templateData + */ + public function __construct( + IShare $share, + array $resolvedEmails, + IMessage $message, + private readonly array $templateData, + ) { + parent::__construct($share, $resolvedEmails, $message); + } + + public function getFileName(): string { + return $this->templateData['filename']; + } + + public function getNote(): string { + return $this->templateData['note']; + } +} diff --git a/apps/sharebymail/lib/Event/BeforeSharePasswordMailSentEvent.php b/apps/sharebymail/lib/Event/BeforeSharePasswordMailSentEvent.php new file mode 100644 index 0000000000000..cbfe0b831658c --- /dev/null +++ b/apps/sharebymail/lib/Event/BeforeSharePasswordMailSentEvent.php @@ -0,0 +1,66 @@ +send() call for password emails. + * + * For sendPassword(), initiatorEmail may be null when the initiator has no + * email address configured. For sendPasswordToOwner() it is always a non-null + * string (the call site throws earlier if the owner has no email address). + * + * @psalm-type TemplateData = array{ + * filename: string, + * password: string, + * initiator: string, + * initiatorEmail: string|null, + * shareWith: string, + * } + * + * @psalm-api + */ +class BeforeSharePasswordMailSentEvent extends AbstractBeforeShareMailSentEvent { + /** + * @param string[] $resolvedEmails + * @param TemplateData $templateData + */ + public function __construct( + IShare $share, + array $resolvedEmails, + IMessage $message, + private readonly array $templateData, + ) { + parent::__construct($share, $resolvedEmails, $message); + } + + public function getFileName(): string { + return $this->templateData['filename']; + } + + public function getPassword(): string { + return $this->templateData['password']; + } + + public function getInitiatorDisplayName(): string { + return $this->templateData['initiator']; + } + + public function getInitiatorEmail(): ?string { + return $this->templateData['initiatorEmail']; + } + + public function getShareWith(): string { + return $this->templateData['shareWith']; + } +} diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index 1f9ee46ebc7ec..85887479f1942 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -10,6 +10,9 @@ use OC\Share20\DefaultShareProvider; use OC\Share20\Exception\InvalidShare; use OC\Share20\Share; +use OCA\ShareByMail\Event\BeforeShareMailSentEvent; +use OCA\ShareByMail\Event\BeforeShareNoteMailSentEvent; +use OCA\ShareByMail\Event\BeforeSharePasswordMailSentEvent; use OCA\ShareByMail\Settings\SettingsManager; use OCP\Activity\IManager; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -330,14 +333,16 @@ protected function sendEmail(IShare $share, array $emails): void { $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; $message = $this->mailer->createMessage(); - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [ + $templateData = [ 'filename' => $filename, 'link' => $link, 'initiator' => $initiatorDisplayName, + 'senderUserId' => $initiator, 'expiration' => $expiration, 'shareWith' => $shareWith, - 'note' => $note - ]); + 'note' => $note, + ]; + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', $templateData); $emailTemplate->setSubject($this->l->t('%1$s shared %2$s with you', [$initiatorDisplayName, $filename])); $emailTemplate->addHeader(); @@ -403,6 +408,11 @@ protected function sendEmail(IShare $share, array $emails): void { } $message->useTemplate($emailTemplate); + $event = new BeforeShareMailSentEvent($share, $emails, $message, $templateData); + $this->eventDispatcher->dispatchTyped($event); + if ($event->isMailHandled()) { + return; + } $failedRecipients = $this->mailer->send($message); if (!empty($failedRecipients)) { $this->logger->error('Share notification mail could not be sent to: ' . implode(', ', $failedRecipients)); @@ -440,13 +450,14 @@ protected function sendPassword(IShare $share, string $password, array $emails): $message = $this->mailer->createMessage(); - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [ + $templateData = [ 'filename' => $filename, 'password' => $password, 'initiator' => $initiatorDisplayName, 'initiatorEmail' => $initiatorEmailAddress, 'shareWith' => $shareWith, - ]); + ]; + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', $templateData); $emailTemplate->setSubject($this->l->t('Password to access %1$s shared to you by %2$s', [$filename, $initiatorDisplayName])); $emailTemplate->addHeader(); @@ -499,6 +510,12 @@ protected function sendPassword(IShare $share, string $password, array $emails): } $message->useTemplate($emailTemplate); + $event = new BeforeSharePasswordMailSentEvent($share, $emails, $message, $templateData); + $this->eventDispatcher->dispatchTyped($event); + if ($event->isMailHandled()) { + return true; + } + $failedRecipients = $this->mailer->send($message); if (!empty($failedRecipients)) { $this->logger->error('Share password mail could not be sent to: ' . implode(', ', $failedRecipients)); @@ -525,7 +542,11 @@ protected function sendNote(IShare $share): void { $message = $this->mailer->createMessage(); - $emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote'); + $templateData = [ + 'filename' => $filename, + 'note' => $note, + ]; + $emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote', $templateData); $emailTemplate->setSubject($this->l->t('%s added a note to a file shared with you', [$initiatorDisplayName])); $emailTemplate->addHeader(); @@ -561,6 +582,11 @@ protected function sendNote(IShare $share): void { $message->setTo([$recipient]); $message->useTemplate($emailTemplate); + $event = new BeforeShareNoteMailSentEvent($share, [$recipient], $message, $templateData); + $this->eventDispatcher->dispatchTyped($event); + if ($event->isMailHandled()) { + return; + } $this->mailer->send($message); } @@ -586,13 +612,14 @@ protected function sendPasswordToOwner(IShare $share, string $password): bool { $bodyPart = $this->l->t('You just shared %1$s with %2$s. The share was already sent to the recipient. Due to the security policies defined by the administrator of %3$s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.', [$filename, $shareWith, $this->defaults->getName()]); $message = $this->mailer->createMessage(); - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [ + $templateData = [ 'filename' => $filename, 'password' => $password, 'initiator' => $initiatorDisplayName, 'initiatorEmail' => $initiatorEMailAddress, 'shareWith' => $shareWith, - ]); + ]; + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', $templateData); $emailTemplate->setSubject($this->l->t('Password to access %1$s shared by you with %2$s', [$filename, $shareWith])); $emailTemplate->addHeader(); @@ -623,8 +650,13 @@ protected function sendPasswordToOwner(IShare $share, string $password): bool { $message->setFrom([Util::getDefaultEmailAddress($instanceName) => $senderName]); $message->setTo([$initiatorEMailAddress => $initiatorDisplayName]); $message->useTemplate($emailTemplate); - $this->mailer->send($message); + $event = new BeforeSharePasswordMailSentEvent($share, [$initiatorEMailAddress], $message, $templateData); + $this->eventDispatcher->dispatchTyped($event); + if ($event->isMailHandled()) { + return true; + } + $this->mailer->send($message); $this->createPasswordSendActivity($share, $shareWith, true); return true; diff --git a/apps/sharebymail/tests/ShareByMailProviderTest.php b/apps/sharebymail/tests/ShareByMailProviderTest.php index 300bf7c94d0ec..b0f68846e5509 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -11,6 +11,9 @@ use DateTime; use OC\Mail\Message; use OC\Share20\Share; +use OCA\ShareByMail\Event\BeforeShareMailSentEvent; +use OCA\ShareByMail\Event\BeforeShareNoteMailSentEvent; +use OCA\ShareByMail\Event\BeforeSharePasswordMailSentEvent; use OCA\ShareByMail\Settings\SettingsManager; use OCA\ShareByMail\ShareByMailProvider; use OCP\Activity\IManager as IActivityManager; @@ -32,7 +35,6 @@ use OCP\Security\Events\GenerateSecurePasswordEvent; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; -use OCP\Security\PasswordContext; use OCP\Server; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IAttributes; @@ -344,9 +346,19 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtectionWithPe ->method('generate') ->with(8, ISecureRandom::CHAR_HUMAN_READABLE) ->willReturn('autogeneratedPassword'); - $this->eventDispatcher->expects($this->once()) + $expectedDispatchedEvents = [ + GenerateSecurePasswordEvent::class, + BeforeShareMailSentEvent::class, + BeforeSharePasswordMailSentEvent::class, + ]; + $this->eventDispatcher->expects($this->exactly(3)) ->method('dispatchTyped') - ->with(new GenerateSecurePasswordEvent(PasswordContext::SHARING)); + ->willReturnCallback(function ($event) use (&$expectedDispatchedEvents): void { + $this->assertInstanceOf(array_shift($expectedDispatchedEvents), $event); + }); + + // Assume the mail address is valid. + $this->mailer->expects($this->any())->method('validateMailAddress')->willReturn(true); $instance = $this->getInstance(['getSharedWith', 'createMailShare', 'getRawShare', 'createShareObject', 'createShareActivity', 'createPasswordSendActivity', 'sendPasswordToOwner']); @@ -376,6 +388,7 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtectionWithPe 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '', @@ -458,6 +471,7 @@ public function testCreateSendPasswordByMailWithPasswordAndWithEnforcedPasswordP 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '', @@ -555,6 +569,7 @@ public function testCreateSendPasswordByTalkWithEnforcedPasswordProtectionWithPe 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '', @@ -1897,4 +1912,196 @@ public function testSendMailNotificationWithDifferentUserAndNoUserEmailAndReplyT [$share] ); } + + public function testSendEmailDoesNotSendWhenMailHandledByListener(): void { + $provider = $this->getInstance(); + $user = $this->createMock(IUser::class); + $user->method('getDisplayName')->willReturn('Mrs. Owner User'); + $user->method('getEMailAddress')->willReturn('owner@example.com'); + + $this->settingsManager->method('replyToInitiator')->willReturn(true); + $this->userManager->method('get')->with('OwnerUser')->willReturn($user); + + $message = $this->createMock(Message::class); + $this->mailer->method('createMessage')->willReturn($message); + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $this->urlGenerator->method('linkToRouteAbsolute') + ->with('files_sharing.sharecontroller.showShare', ['token' => 'token']) + ->willReturn('https://example.com/file.txt'); + $this->defaults->method('getName')->willReturn('UnitTestCloud'); + $this->defaults->method('getSlogan')->willReturn('Testing like 1990'); + + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->willReturnCallback(function (BeforeShareMailSentEvent $event): void { + $event->markMailHandled(); + }); + $this->mailer->expects($this->never())->method('send'); + + $node = $this->createMock(File::class); + $node->method('getName')->willReturn('file.txt'); + + $share = $this->createMock(IShare::class); + $share->method('getSharedBy')->willReturn('OwnerUser'); + $share->method('getSharedWith')->willReturn('john@doe.com'); + $share->method('getNode')->willReturn($node); + $share->method('getNote')->willReturn(''); + $share->method('getToken')->willReturn('token'); + + self::invokePrivate($provider, 'sendEmail', [$share, ['john@doe.com']]); + } + + public function testSendNoteDoesNotSendWhenMailHandledByListener(): void { + $provider = $this->getInstance(); + $user = $this->createMock(IUser::class); + $user->method('getDisplayName')->willReturn('Mrs. Owner User'); + $user->method('getEMailAddress')->willReturn('owner@example.com'); + + $this->settingsManager->method('replyToInitiator')->willReturn(true); + $this->userManager->method('get')->with('OwnerUser')->willReturn($user); + + $message = $this->createMock(Message::class); + $this->mailer->method('createMessage')->willReturn($message); + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $this->urlGenerator->method('linkToRouteAbsolute') + ->with('files_sharing.sharecontroller.showShare', ['token' => 'token']) + ->willReturn('https://example.com/file.txt'); + $this->defaults->method('getName')->willReturn('UnitTestCloud'); + $this->defaults->method('getSlogan')->willReturn('Testing like 1990'); + + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->willReturnCallback(function (BeforeShareNoteMailSentEvent $event): void { + $event->markMailHandled(); + }); + $this->mailer->expects($this->never())->method('send'); + + $node = $this->getMockBuilder(File::class)->getMock(); + $node->method('getName')->willReturn('file.txt'); + + $share = $this->getMockBuilder(IShare::class)->getMock(); + $share->method('getSharedWith')->willReturn('john@doe.com'); + $share->method('getSharedBy')->willReturn('OwnerUser'); + $share->method('getNode')->willReturn($node); + $share->method('getNote')->willReturn('This is a note to the recipient'); + $share->method('getToken')->willReturn('token'); + + self::invokePrivate($provider, 'sendNote', [$share]); + } + + public function testSendNoteDispatchesBeforeShareNoteMailSentEvent(): void { + $provider = $this->getInstance(); + $user = $this->createMock(IUser::class); + $user->method('getDisplayName')->willReturn('Mrs. Owner User'); + $user->method('getEMailAddress')->willReturn('owner@example.com'); + + $this->settingsManager->method('replyToInitiator')->willReturn(true); + $this->userManager->method('get')->with('OwnerUser')->willReturn($user); + + $message = $this->createMock(Message::class); + $this->mailer->method('createMessage')->willReturn($message); + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $this->urlGenerator->method('linkToRouteAbsolute') + ->with('files_sharing.sharecontroller.showShare', ['token' => 'token']) + ->willReturn('https://example.com/file.txt'); + $this->defaults->method('getName')->willReturn('UnitTestCloud'); + $this->defaults->method('getSlogan')->willReturn('Testing like 1990'); + + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with($this->isInstanceOf(BeforeShareNoteMailSentEvent::class)); + $this->mailer->expects($this->once())->method('send')->with($message); + + $node = $this->getMockBuilder(File::class)->getMock(); + $node->method('getName')->willReturn('file.txt'); + + $share = $this->getMockBuilder(IShare::class)->getMock(); + $share->method('getSharedWith')->willReturn('john@doe.com'); + $share->method('getSharedBy')->willReturn('OwnerUser'); + $share->method('getNode')->willReturn($node); + $share->method('getNote')->willReturn('This is a note to the recipient'); + $share->method('getToken')->willReturn('token'); + + self::invokePrivate($provider, 'sendNote', [$share]); + } + + public function testSendPasswordDoesNotCreateActivityWhenMailHandledByListener(): void { + $provider = $this->getInstance(['createPasswordSendActivity']); + + $node = $this->createMock(File::class); + $node->method('getName')->willReturn('file.txt'); + + $share = $this->createMock(IShare::class); + $share->method('getSharedWith')->willReturn('recipient@example.com'); + $share->method('getSharedBy')->willReturn('sender'); + $share->method('getNode')->willReturn($node); + $share->method('getSendPasswordByTalk')->willReturn(false); + + $this->settingsManager->method('sendPasswordByMail')->willReturn(true); + $this->defaults->method('getName')->willReturn('TestCloud'); + $this->defaults->method('getSlogan')->willReturn(''); + + $message = $this->createMock(Message::class); + $this->mailer->method('createMessage')->willReturn($message); + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->willReturnCallback(function ($event): void { + if ($event instanceof BeforeSharePasswordMailSentEvent) { + $event->markMailHandled(); + } + }); + + $provider->expects($this->never())->method('createPasswordSendActivity'); + $this->mailer->expects($this->never())->method('send'); + + self::invokePrivate($provider, 'sendPassword', [$share, 'secret', ['recipient@example.com']]); + } + + public function testSendPasswordToOwnerDoesNotCreateActivityWhenMailHandledByListener(): void { + $provider = $this->getInstance(['createPasswordSendActivity']); + + $node = $this->createMock(File::class); + $node->method('getName')->willReturn('file.txt'); + + $initiatorUser = $this->createMock(IUser::class); + $initiatorUser->method('getDisplayName')->willReturn('Sender User'); + $initiatorUser->method('getEMailAddress')->willReturn('sender@example.com'); + + $share = $this->createMock(IShare::class); + $share->method('getSharedWith')->willReturn('recipient@example.com'); + $share->method('getSharedBy')->willReturn('sender'); + $share->method('getNode')->willReturn($node); + + $this->userManager->method('get')->with('sender')->willReturn($initiatorUser); + $this->defaults->method('getName')->willReturn('TestCloud'); + $this->defaults->method('getSlogan')->willReturn(''); + $this->settingsManager->method('replyToInitiator')->willReturn(false); + + $message = $this->createMock(Message::class); + $this->mailer->method('createMessage')->willReturn($message); + $template = $this->createMock(IEMailTemplate::class); + $this->mailer->method('createEMailTemplate')->willReturn($template); + + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->willReturnCallback(function ($event): void { + if ($event instanceof BeforeSharePasswordMailSentEvent) { + $event->markMailHandled(); + } + }); + + $provider->expects($this->never())->method('createPasswordSendActivity'); + $this->mailer->expects($this->never())->method('send'); + + self::invokePrivate($provider, 'sendPasswordToOwner', [$share, 'secret']); + } }