From a6c9dfcb77b67ef178268e3ffae748d8f072932a Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:29:13 +0200 Subject: [PATCH 1/7] 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. --- 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 ee815b31ac02b..4c48c89532b16 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -319,14 +319,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(); @@ -429,13 +430,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(); @@ -515,7 +517,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(); @@ -576,13 +582,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 b434bbf887776601eb16d0539fbe4f99d3c8cc95 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:34:23 +0200 Subject: [PATCH 2/7] IONOS(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. Signed-off-by: Misha M.-Kupriyanov --- .../composer/composer/autoload_classmap.php | 4 ++ .../AbstractBeforeShareMailSentEvent.php | 62 ++++++++++++++++ .../lib/Event/BeforeShareMailSentEvent.php | 72 +++++++++++++++++++ .../Event/BeforeShareNoteMailSentEvent.php | 47 ++++++++++++ .../BeforeSharePasswordMailSentEvent.php | 66 +++++++++++++++++ apps/sharebymail/lib/ShareByMailProvider.php | 32 +++++++-- 6 files changed, 278 insertions(+), 5 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..eb1defce86867 100644 --- a/apps/sharebymail/composer/composer/autoload_classmap.php +++ b/apps/sharebymail/composer/composer/autoload_classmap.php @@ -8,6 +8,10 @@ return array( 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\ShareByMail\\Activity' => $baseDir . '/../lib/Activity.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\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', 'OCA\\ShareByMail\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\ShareByMail\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', diff --git a/apps/sharebymail/lib/Event/AbstractBeforeShareMailSentEvent.php b/apps/sharebymail/lib/Event/AbstractBeforeShareMailSentEvent.php new file mode 100644 index 0000000000000..739f2dbe52a43 --- /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 validated recipients + */ + 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 4c48c89532b16..2b7761a84aa31 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -9,6 +9,9 @@ use OC\Share20\Exception\InvalidShare; use OC\Share20\Share; use OC\User\NoUserException; +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; @@ -323,6 +326,7 @@ protected function sendEmail(IShare $share, array $emails): void { 'filename' => $filename, 'link' => $link, 'initiator' => $initiatorDisplayName, + 'senderUserId' => $initiator, 'expiration' => $expiration, 'shareWith' => $shareWith, 'note' => $note, @@ -393,6 +397,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)); @@ -490,10 +499,14 @@ protected function sendPassword(IShare $share, string $password, array $emails): } $message->useTemplate($emailTemplate); - $failedRecipients = $this->mailer->send($message); - if (!empty($failedRecipients)) { - $this->logger->error('Share password mail could not be sent to: ' . implode(', ', $failedRecipients)); - return false; + $event = new BeforeSharePasswordMailSentEvent($share, $emails, $message, $templateData); + $this->eventDispatcher->dispatchTyped($event); + if (!$event->isMailHandled()) { + $failedRecipients = $this->mailer->send($message); + if (!empty($failedRecipients)) { + $this->logger->error('Share password mail could not be sent to: ' . implode(', ', $failedRecipients)); + return false; + } } $this->createPasswordSendActivity($share, $shareWith, false); @@ -557,6 +570,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); } @@ -620,7 +638,11 @@ protected function sendPasswordToOwner(IShare $share, string $password): bool { $message->setFrom([\OCP\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()) { + $this->mailer->send($message); + } $this->createPasswordSendActivity($share, $shareWith, true); From 8ab47ed65f88d758e958a8d3e7c5e5baccc358fd Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Thu, 25 Jun 2026 11:20:35 +0200 Subject: [PATCH 3/7] IONOS(sharebymail): add Event classes to composer autoload_static classmap autoload_real.php sets setClassMapAuthoritative(true), which disables PSR-4 fallback entirely. Any class not listed in $classMap is never found, regardless of whether PSR-4 would resolve it. Signed-off-by: Tatjana Kaschperko Lindt --- apps/sharebymail/composer/composer/autoload_static.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/sharebymail/composer/composer/autoload_static.php b/apps/sharebymail/composer/composer/autoload_static.php index 16ce27764b6b7..ee32b6f1140db 100644 --- a/apps/sharebymail/composer/composer/autoload_static.php +++ b/apps/sharebymail/composer/composer/autoload_static.php @@ -24,6 +24,10 @@ class ComposerStaticInitShareByMail 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\ShareByMail\\Activity' => __DIR__ . '/..' . '/../lib/Activity.php', 'OCA\\ShareByMail\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.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\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\ShareByMail\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', 'OCA\\ShareByMail\\Settings\\SettingsManager' => __DIR__ . '/..' . '/../lib/Settings/SettingsManager.php', From 48b9e0739ba438367054ce00baf673d326710bd6 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Thu, 25 Jun 2026 14:32:17 +0200 Subject: [PATCH 4/7] IONOS(sharebymail): fix ShareByMailProviderTest broken by templateData and event refactors - Add senderUserId to the expected RecipientNotification templateData in 3 tests (added by the createEMailTemplate params refactor in 242b896) - Update dispatchTyped expectation from once(GenerateSecurePasswordEvent) to exactly(3) with withConsecutive for GenerateSecurePasswordEvent, BeforeShareMailSentEvent, and BeforeSharePasswordMailSentEvent (sendEmail() and sendPassword() now each dispatch a Before*Event) Signed-off-by: Tatjana Kaschperko Lindt --- apps/sharebymail/tests/ShareByMailProviderTest.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/sharebymail/tests/ShareByMailProviderTest.php b/apps/sharebymail/tests/ShareByMailProviderTest.php index 18ca9574d6f05..d87a850027d9f 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -22,6 +22,8 @@ use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Mail\IMessage; +use OCA\ShareByMail\Event\BeforeShareMailSentEvent; +use OCA\ShareByMail\Event\BeforeSharePasswordMailSentEvent; use OCP\Security\Events\GenerateSecurePasswordEvent; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; @@ -359,9 +361,13 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtectionWithPe ->method('generate') ->with(8, ISecureRandom::CHAR_HUMAN_READABLE) ->willReturn('autogeneratedPassword'); - $this->eventDispatcher->expects($this->once()) + $this->eventDispatcher->expects($this->exactly(3)) ->method('dispatchTyped') - ->with(new GenerateSecurePasswordEvent()); + ->withConsecutive( + [new GenerateSecurePasswordEvent()], + [$this->isInstanceOf(BeforeShareMailSentEvent::class)], + [$this->isInstanceOf(BeforeSharePasswordMailSentEvent::class)], + ); // Assume the mail address is valid. $this->mailer->expects($this->any())->method('validateMailAddress')->willReturn(true); @@ -393,6 +399,7 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtectionWithPe 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '' @@ -467,6 +474,7 @@ public function testCreateSendPasswordByMailWithPasswordAndWithEnforcedPasswordP 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '' @@ -546,6 +554,7 @@ public function testCreateSendPasswordByTalkWithEnforcedPasswordProtectionWithPe 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '' From ba01f57caa2240afe3d02754b397e51af92b8077 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Fri, 26 Jun 2026 12:27:46 +0200 Subject: [PATCH 5/7] IONOS(sharebymail): add test coverage for BeforeShareNoteMailSentEvent dispatch Signed-off-by: Tatjana Kaschperko Lindt --- .../tests/ShareByMailProviderTest.php | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/sharebymail/tests/ShareByMailProviderTest.php b/apps/sharebymail/tests/ShareByMailProviderTest.php index d87a850027d9f..25aca7a85029e 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -23,6 +23,7 @@ use OCP\Mail\IMailer; use OCP\Mail\IMessage; use OCA\ShareByMail\Event\BeforeShareMailSentEvent; +use OCA\ShareByMail\Event\BeforeShareNoteMailSentEvent; use OCA\ShareByMail\Event\BeforeSharePasswordMailSentEvent; use OCP\Security\Events\GenerateSecurePasswordEvent; use OCP\Security\IHasher; @@ -1931,4 +1932,42 @@ public function testSendMailNotificationWithDifferentUserAndNoUserEmailAndReplyT [$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]); + } } From 0f82ec414a929f12712b10d08746fb78ba27b165 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Tue, 30 Jun 2026 12:33:31 +0200 Subject: [PATCH 6/7] IONOS(sharebymail): fix createPasswordSendActivity firing when mail is suppressed sendPassword() and sendPasswordToOwner() called createPasswordSendActivity() unconditionally outside the isMailHandled() guard. A listener intercepting the BeforeSharePasswordMailSentEvent would suppress the SMTP send but still cause a false activity-log entry for a mail that was never delivered. Apply the same early-return pattern used by sendEmail(): return immediately when isMailHandled() is true so no side effects run after the flag is set. Signed-off-by: Tatjana Kaschperko Lindt --- apps/sharebymail/lib/ShareByMailProvider.php | 19 +++-- .../tests/ShareByMailProviderTest.php | 74 +++++++++++++++++++ 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php index 2b7761a84aa31..9033024c5f3d3 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -501,12 +501,14 @@ 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()) { - $failedRecipients = $this->mailer->send($message); - if (!empty($failedRecipients)) { - $this->logger->error('Share password mail could not be sent to: ' . implode(', ', $failedRecipients)); - return false; - } + 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)); + return false; } $this->createPasswordSendActivity($share, $shareWith, false); @@ -640,10 +642,11 @@ protected function sendPasswordToOwner(IShare $share, string $password): bool { $message->useTemplate($emailTemplate); $event = new BeforeSharePasswordMailSentEvent($share, [$initiatorEMailAddress], $message, $templateData); $this->eventDispatcher->dispatchTyped($event); - if (!$event->isMailHandled()) { - $this->mailer->send($message); + 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 25aca7a85029e..8995a7500c403 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -1970,4 +1970,78 @@ public function testSendNoteDispatchesBeforeShareNoteMailSentEvent(): void { 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']); + } } From e97f70647f7b798bb8185049cffe1c9a03246b66 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 17:18:04 +0200 Subject: [PATCH 7/7] IONOS(nc_ionos_processes): update submodule to 9501a5e (send email for "file request") https://github.com/IONOS-Productivity/nc-ionos-processes/releases/tag/1.0.0-9501a5e Signed-off-by: Misha M.-Kupriyanov Co-authored-by: Tatjana Kaschperko Lindt --- apps-custom/nc_ionos_processes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-custom/nc_ionos_processes b/apps-custom/nc_ionos_processes index 5ee4b69ebed39..9501a5ee7224a 160000 --- a/apps-custom/nc_ionos_processes +++ b/apps-custom/nc_ionos_processes @@ -1 +1 @@ -Subproject commit 5ee4b69ebed39714358f29e42f63dd76831682b3 +Subproject commit 9501a5ee7224abf276913302e25e0684e54c560f