From 363e8aab1508728344f12b21b2470ce001d2213c Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:29:13 +0200 Subject: [PATCH 1/3] 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 090bea41e03ee0ed0b1f07b27fef50c075939607 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:34:23 +0200 Subject: [PATCH 2/3] 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..6fc93e6f935e0 --- /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 ccc0f83730795772f75c9e4ef87f84e8a8b409e8 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 17:18:04 +0200 Subject: [PATCH 3/3] IONOS(nc_ionos_processes): update submodule to 56d9e73 (typed event getters) Signed-off-by: Misha M.-Kupriyanov --- 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..56d9e73edebf4 160000 --- a/apps-custom/nc_ionos_processes +++ b/apps-custom/nc_ionos_processes @@ -1 +1 @@ -Subproject commit 5ee4b69ebed39714358f29e42f63dd76831682b3 +Subproject commit 56d9e73edebf4a8652b36ec508b495f0cfcea2f5