From ad54e9c74d7859519bdde6600740b28afbb33667 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 3 Jun 2026 14:23:36 +0200 Subject: [PATCH 01/17] IONOS(ci): Pin workflow action versions Signed-off-by: Kai Henseler --- .github/workflows/hidrive-next-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/hidrive-next-build.yml b/.github/workflows/hidrive-next-build.yml index 4a072992352f6..c224c3d3e4248 100644 --- a/.github/workflows/hidrive-next-build.yml +++ b/.github/workflows/hidrive-next-build.yml @@ -664,7 +664,7 @@ jobs: echo "NC_VERSION=$NC_VERSION" >> $GITHUB_OUTPUT - name: Upload artifact result for job hidrive-next-build - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: retention-days: 30 name: hidrive_next_build_artifact @@ -732,12 +732,12 @@ jobs: fetch-depth: 1 - name: Download artifact zip - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: hidrive_next_build_artifact # This action sets up the JFrog CLI with the Artifactory URL and access token - - uses: jfrog/setup-jfrog-cli@v4 + - uses: jfrog/setup-jfrog-cli@1641575d87647fb969c0545f0b6a76873e328b7c # v5.0.0 env: JF_URL: ${{ secrets.JF_ARTIFACTORY_URL }} JF_USER: ${{ secrets.JF_ARTIFACTORY_USER }} @@ -871,7 +871,7 @@ jobs: steps: - name: Download artifact zip - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: hidrive_next_build_artifact From f89fe1712db721cfb1947027806c3922d58bb038 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Wed, 3 Jun 2026 14:28:37 +0200 Subject: [PATCH 02/17] IONOS(ci): Pin sbom workflow actions to commit SHAs Signed-off-by: Kai Henseler --- .github/workflows/sbom-matrix.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sbom-matrix.yaml b/.github/workflows/sbom-matrix.yaml index 365aedde5f9d7..eec493ecd2290 100644 --- a/.github/workflows/sbom-matrix.yaml +++ b/.github/workflows/sbom-matrix.yaml @@ -76,7 +76,7 @@ jobs: steps: - name: Set up PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # 2.31.1 with: php-version: '8.1' @@ -218,13 +218,13 @@ jobs: steps: - name: Checkout server - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: submodules: true - name: Setup PHP with PECL extension if: matrix.component.composer_output - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # 2.31.1 with: tools: composer:v2 extensions: gd, zip, curl, xml, xmlrpc, mbstring, sqlite, xdebug, pgsql, intl, imagick, gmp, apcu, bcmath, redis, soap, imap, opcache @@ -249,7 +249,7 @@ jobs: - name: Set up Node.js if: matrix.component.npm_output - uses: actions/setup-node@v4 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version-file: "${{ matrix.component.path }}/package.json" cache: 'npm' @@ -270,7 +270,7 @@ jobs: ls -la "${parent_dir}/${{ matrix.component.npm_output }}" - name: Upload component SBOM artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: sbom-${{ matrix.component.name }} path: | @@ -295,7 +295,7 @@ jobs: steps: - name: Download all SBOM artifacts - uses: actions/download-artifact@v5 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: pattern: sbom-* merge-multiple: true From 20da11fa305eec67c6043f562efb11e5695b715f Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Fri, 5 Jun 2026 12:59:27 +0200 Subject: [PATCH 03/17] IONOS(ci): fix pre-existing action pin issues in hidrive-next-build.yml - Pin docker/build-push-action to tagged release SHA v7.2.0 (was pinned to an untagged intermediate commit with no version label) - Fix shivammathur/setup-php version comment: the action uses no 'v' prefix in its tags, so '#v2.31.1' is wrong; correct to '# 2.31.1' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/hidrive-next-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hidrive-next-build.yml b/.github/workflows/hidrive-next-build.yml index c224c3d3e4248..4f942593f473d 100644 --- a/.github/workflows/hidrive-next-build.yml +++ b/.github/workflows/hidrive-next-build.yml @@ -621,7 +621,7 @@ jobs: run: make --version && node --version && npm --version - name: Setup PHP with PECL extension - uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 #v2.31.1 + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # 2.31.1 with: php-version: '8.3' tools: composer:v2 @@ -903,7 +903,7 @@ jobs: EOF - name: Build and push Docker image - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . push: true From 72abd53ef4ef86a3370acfb090dab6575ffae555 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 15 Jun 2026 11:35:51 +0200 Subject: [PATCH 04/17] IONOS(notify_push): add submodule at v1.3.3 Signed-off-by: Kai Henseler --- .gitmodules | 3 +++ apps-external/notify_push | 1 + 2 files changed, 4 insertions(+) create mode 160000 apps-external/notify_push diff --git a/.gitmodules b/.gitmodules index 762ee80885526..61413f430e467 100644 --- a/.gitmodules +++ b/.gitmodules @@ -40,3 +40,6 @@ [submodule "IONOS"] path = IONOS url = git@github.com:IONOS-Productivity/nc-config.git +[submodule "apps-external/notify_push"] + path = apps-external/notify_push + url = git@github.com:nextcloud/notify_push.git diff --git a/apps-external/notify_push b/apps-external/notify_push new file mode 160000 index 0000000000000..20eec4bdf060b --- /dev/null +++ b/apps-external/notify_push @@ -0,0 +1 @@ +Subproject commit 20eec4bdf060b240761a0a42f733c75cd236ec3a From b4865e2941529518250cb20bca21162b7fc10827 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 15 Jun 2026 11:41:53 +0200 Subject: [PATCH 05/17] IONOS(ci): Add notify_push app build step to HiDrive Next build workflow Signed-off-by: Kai Henseler --- .github/workflows/hidrive-next-build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/hidrive-next-build.yml b/.github/workflows/hidrive-next-build.yml index 4f942593f473d..69c7230285b3c 100644 --- a/.github/workflows/hidrive-next-build.yml +++ b/.github/workflows/hidrive-next-build.yml @@ -635,6 +635,9 @@ jobs: - name: Build Nextcloud (core only — apps restored from cache) run: make -f IONOS/Makefile build_nextcloud_only + - name: Install and build notify_push app + run: make -f IONOS/Makefile build_notify_push_app + - name: Add config partials run: make -f IONOS/Makefile add_config_partials From ca14cf875101243c93c71bace5b563102bfd8cde Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:29:13 +0200 Subject: [PATCH 06/17] 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 1d903f8f872ae..3541260210fb6 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -327,14 +327,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(); @@ -437,13 +438,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(); @@ -523,7 +525,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(); @@ -584,13 +590,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 eeb3cc5e6c37fc9e705179de8e58eb05712bd54b Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 16:34:23 +0200 Subject: [PATCH 07/17] 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 3541260210fb6..016ca5b325f09 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -10,6 +10,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; @@ -331,6 +334,7 @@ protected function sendEmail(IShare $share, array $emails): void { 'filename' => $filename, 'link' => $link, 'initiator' => $initiatorDisplayName, + 'senderUserId' => $initiator, 'expiration' => $expiration, 'shareWith' => $shareWith, 'note' => $note, @@ -401,6 +405,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)); @@ -498,10 +507,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); @@ -565,6 +578,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); } @@ -628,7 +646,11 @@ 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()) { + $this->mailer->send($message); + } $this->createPasswordSendActivity($share, $shareWith, true); From f664cd2f923993218753ee72c1475f09701603ef Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Thu, 25 Jun 2026 11:20:35 +0200 Subject: [PATCH 08/17] 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 2d70de1806b6a..b8e47cce9865f 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 beb7e3d2f964251fa846af98de79549c0c65aeb5 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Thu, 25 Jun 2026 14:32:17 +0200 Subject: [PATCH 09/17] 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 aaa608cc44645..5e30dae279aac 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -28,6 +28,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; @@ -343,9 +345,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(PasswordContext::SHARING)); + ->withConsecutive( + [new GenerateSecurePasswordEvent(PasswordContext::SHARING)], + [$this->isInstanceOf(BeforeShareMailSentEvent::class)], + [$this->isInstanceOf(BeforeSharePasswordMailSentEvent::class)], + ); $instance = $this->getInstance(['getSharedWith', 'createMailShare', 'getRawShare', 'createShareObject', 'createShareActivity', 'createPasswordSendActivity', 'sendPasswordToOwner']); @@ -375,6 +381,7 @@ public function testCreateSendPasswordByMailWithEnforcedPasswordProtectionWithPe 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '', @@ -457,6 +464,7 @@ public function testCreateSendPasswordByMailWithPasswordAndWithEnforcedPasswordP 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '', @@ -554,6 +562,7 @@ public function testCreateSendPasswordByTalkWithEnforcedPasswordProtectionWithPe 'filename' => 'filename', 'link' => 'https://example.com/file.txt', 'initiator' => 'owner', + 'senderUserId' => 'owner', 'expiration' => null, 'shareWith' => 'receiver@example.com', 'note' => '', From 4069c92d361f84dec392182bb07967c4767b4cb6 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Fri, 26 Jun 2026 12:27:46 +0200 Subject: [PATCH 10/17] 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 5e30dae279aac..e5c116da13e40 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -29,6 +29,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; @@ -1916,4 +1917,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 d112fcd61a02d05091c48d66fcb2ca51571d7270 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Tue, 30 Jun 2026 12:33:31 +0200 Subject: [PATCH 11/17] 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 016ca5b325f09..d39a98bc72b5c 100644 --- a/apps/sharebymail/lib/ShareByMailProvider.php +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -509,12 +509,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); @@ -648,10 +650,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 e5c116da13e40..a679a4a863d30 100644 --- a/apps/sharebymail/tests/ShareByMailProviderTest.php +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -1955,4 +1955,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 878b7ff44e298ef887b3fe3f1b6ca62753f9a9d7 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 15 Jun 2026 17:18:04 +0200 Subject: [PATCH 12/17] 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 170d1705a6c3b..9501a5ee7224a 160000 --- a/apps-custom/nc_ionos_processes +++ b/apps-custom/nc_ionos_processes @@ -1 +1 @@ -Subproject commit 170d1705a6c3b2f09d5270682aca6864497d1afc +Subproject commit 9501a5ee7224abf276913302e25e0684e54c560f From 96288ad5a9e5fd5aae3f44f32f590efdc8e1e1a4 Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Fri, 24 Jul 2026 15:40:01 +0200 Subject: [PATCH 13/17] IONOS(simplesettings): cherry-pick stable30 changes onto ionos-dev-v32.0.6 (Linux/Nautilus, files search, devtool fix, test fixes) --- apps-custom/simplesettings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-custom/simplesettings b/apps-custom/simplesettings index 373db5b384e72..824b77b194623 160000 --- a/apps-custom/simplesettings +++ b/apps-custom/simplesettings @@ -1 +1 @@ -Subproject commit 373db5b384e7200ff87867ffd254be9c2b064862 +Subproject commit 824b77b194623b716c69a2f54869cc3d6935f824 From 9e9a5238ddc5e2d65a3977d088c1cb83b82f411f Mon Sep 17 00:00:00 2001 From: Tatjana Kaschperko Lindt Date: Fri, 24 Jul 2026 15:48:15 +0200 Subject: [PATCH 14/17] IONOS(simplesettings): fix NC32 test reset in ionos-dev-v32.0.6 --- apps-custom/simplesettings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-custom/simplesettings b/apps-custom/simplesettings index 824b77b194623..479d691ebd6ea 160000 --- a/apps-custom/simplesettings +++ b/apps-custom/simplesettings @@ -1 +1 @@ -Subproject commit 824b77b194623b716c69a2f54869cc3d6935f824 +Subproject commit 479d691ebd6eace0ffeae3f8107b989e9db95234 From 062509e16a9e7042f538e6583f40f766fcd3e70b Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 27 Jul 2026 11:07:14 +0200 Subject: [PATCH 15/17] IONOS(config): update submodule (EU Markets changes) Signed-off-by: Kai Henseler --- IONOS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IONOS b/IONOS index ecde9e284b724..16f526f53deb9 160000 --- a/IONOS +++ b/IONOS @@ -1 +1 @@ -Subproject commit ecde9e284b724e290846f30e7d0dd1b1a0495648 +Subproject commit 16f526f53deb920c1a5444dda3ae0dd8d8da27c0 From ecb394c0b9f2047123ea2473ff303edd52f64c18 Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 27 Jul 2026 11:15:05 +0200 Subject: [PATCH 16/17] IONOS(ci): update to renamed make target for nextcloud build Signed-off-by: Kai Henseler --- .github/workflows/hidrive-next-build.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/hidrive-next-build.yml b/.github/workflows/hidrive-next-build.yml index 69c7230285b3c..59393db3830e5 100644 --- a/.github/workflows/hidrive-next-build.yml +++ b/.github/workflows/hidrive-next-build.yml @@ -633,10 +633,7 @@ jobs: run: php -i && php -m - name: Build Nextcloud (core only — apps restored from cache) - run: make -f IONOS/Makefile build_nextcloud_only - - - name: Install and build notify_push app - run: make -f IONOS/Makefile build_notify_push_app + run: make -f IONOS/Makefile build_nextcloud - name: Add config partials run: make -f IONOS/Makefile add_config_partials From 2f594dcdb20123eaad815d4ae79d0f59181a57cc Mon Sep 17 00:00:00 2001 From: Kai Henseler Date: Mon, 27 Jul 2026 11:23:32 +0200 Subject: [PATCH 17/17] IONOS(theming): update submodule Signed-off-by: Kai Henseler --- themes/nc-ionos-theme | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/nc-ionos-theme b/themes/nc-ionos-theme index 2463c233a1bb6..a5e6ec23f0e04 160000 --- a/themes/nc-ionos-theme +++ b/themes/nc-ionos-theme @@ -1 +1 @@ -Subproject commit 2463c233a1bb69d2d04f0db9fcbebe3bd5e6d120 +Subproject commit a5e6ec23f0e041699040f0de4cfb4fae4ab1eda7