From fa20f20afaba07dda4c7b0ea9543899b8b9c10ec Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:29:13 +0200 Subject: [PATCH 1/2] refactor(sharebymail): extract createEMailTemplate params into $templateData variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract the inline array literals passed to each createEMailTemplate() call into named $templateData variables. For sendNote(), which previously passed no data array, add a minimal array so all four send methods are consistent. Zero behaviour change — $templateData is passed straight through to createEMailTemplate() with identical contents. Signed-off-by: Misha M.-Kupriyanov --- apps/sharebymail/lib/ShareByMailProvider.php | 23 +++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index 1f9ee46ebc7ec..3474859703541 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -330,14 +330,15 @@ 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, '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(); @@ -440,13 +441,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(); @@ -525,7 +527,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(); @@ -586,13 +592,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(); From edc6c9c3b4400de98e909f339e9f5891da1cebce Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:34:23 +0200 Subject: [PATCH 2/2] feat(sharebymail): add BeforeShare*MailSentEvent dispatched before each mail send MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dispatch a typed PSR-14 event immediately before every mailer->send() call in ShareByMailProvider so that external listeners can intercept and replace Nextcloud's native SMTP delivery. Event hierarchy: AbstractBeforeShareMailSentEvent (base: share, resolvedEmails, message, markMailHandled / isMailHandled) ├── BeforeShareMailSentEvent – sendEmail() ├── BeforeSharePasswordMailSentEvent – sendPassword() + sendPasswordToOwner() └── BeforeShareNoteMailSentEvent – sendNote() Each concrete class holds its own typed $templateData (psalm array-shape) and exposes named getters (getSenderUserId(), getFileName(), …) instead of a generic getMailData(): array. This avoids defensive is_string() / null guards in listeners. $templateData reuses the array already passed to createEMailTemplate(), with one extra key added for sendEmail(): senderUserId (the raw user ID, distinct from the display name stored under 'initiator'). The native mailer->send() is skipped when a listener calls markMailHandled(). If the listener's own send throws, the exception propagates and the native send is also skipped — no silent SMTP fallback. sendEmail() and sendPassword() are flattened from nested if (!isMailHandled()) { ... } pyramids to early-return style. createPasswordSendActivity() in sendPassword() and sendPasswordToOwner() now runs only when the mail was actually sent: it moved inside the isMailHandled() early return, so a listener that suppresses the SMTP send no longer produces a false password-share activity entry. Signed-off-by: Misha M.-Kupriyanov --- .../composer/composer/autoload_classmap.php | 4 + .../composer/composer/autoload_static.php | 4 + .../AbstractBeforeShareMailSentEvent.php | 62 +++++ .../lib/Event/BeforeShareMailSentEvent.php | 72 ++++++ .../Event/BeforeShareNoteMailSentEvent.php | 47 ++++ .../BeforeSharePasswordMailSentEvent.php | 66 ++++++ apps/sharebymail/lib/ShareByMailProvider.php | 27 ++- .../tests/ShareByMailProviderTest.php | 213 +++++++++++++++++- 8 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 apps/sharebymail/lib/Event/AbstractBeforeShareMailSentEvent.php create mode 100644 apps/sharebymail/lib/Event/BeforeShareMailSentEvent.php create mode 100644 apps/sharebymail/lib/Event/BeforeShareNoteMailSentEvent.php create mode 100644 apps/sharebymail/lib/Event/BeforeSharePasswordMailSentEvent.php 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 3474859703541..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; @@ -334,6 +337,7 @@ protected function sendEmail(IShare $share, array $emails): void { 'filename' => $filename, 'link' => $link, 'initiator' => $initiatorDisplayName, + 'senderUserId' => $initiator, 'expiration' => $expiration, 'shareWith' => $shareWith, 'note' => $note, @@ -404,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)); @@ -501,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)); @@ -567,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); } @@ -630,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']); + } }