Skip to content

Commit d928ae7

Browse files
feat(sharebymail): add BeforeShare*MailSentEvent dispatched before each mail send
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<string,mixed>. 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 <kupriyanov@strato.de>
1 parent 934641f commit d928ae7

8 files changed

Lines changed: 491 additions & 4 deletions

apps/sharebymail/composer/composer/autoload_classmap.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
'OCA\\ShareByMail\\Activity' => $baseDir . '/../lib/Activity.php',
1111
'OCA\\ShareByMail\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
1212
'OCA\\ShareByMail\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
13+
'OCA\\ShareByMail\\Event\\AbstractBeforeShareMailSentEvent' => $baseDir . '/../lib/Event/AbstractBeforeShareMailSentEvent.php',
14+
'OCA\\ShareByMail\\Event\\BeforeShareMailSentEvent' => $baseDir . '/../lib/Event/BeforeShareMailSentEvent.php',
15+
'OCA\\ShareByMail\\Event\\BeforeShareNoteMailSentEvent' => $baseDir . '/../lib/Event/BeforeShareNoteMailSentEvent.php',
16+
'OCA\\ShareByMail\\Event\\BeforeSharePasswordMailSentEvent' => $baseDir . '/../lib/Event/BeforeSharePasswordMailSentEvent.php',
1317
'OCA\\ShareByMail\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
1418
'OCA\\ShareByMail\\Settings\\SettingsManager' => $baseDir . '/../lib/Settings/SettingsManager.php',
1519
'OCA\\ShareByMail\\ShareByMailProvider' => $baseDir . '/../lib/ShareByMailProvider.php',

apps/sharebymail/composer/composer/autoload_static.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ class ComposerStaticInitShareByMail
2525
'OCA\\ShareByMail\\Activity' => __DIR__ . '/..' . '/../lib/Activity.php',
2626
'OCA\\ShareByMail\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
2727
'OCA\\ShareByMail\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
28+
'OCA\\ShareByMail\\Event\\AbstractBeforeShareMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/AbstractBeforeShareMailSentEvent.php',
29+
'OCA\\ShareByMail\\Event\\BeforeShareMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/BeforeShareMailSentEvent.php',
30+
'OCA\\ShareByMail\\Event\\BeforeShareNoteMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/BeforeShareNoteMailSentEvent.php',
31+
'OCA\\ShareByMail\\Event\\BeforeSharePasswordMailSentEvent' => __DIR__ . '/..' . '/../lib/Event/BeforeSharePasswordMailSentEvent.php',
2832
'OCA\\ShareByMail\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
2933
'OCA\\ShareByMail\\Settings\\SettingsManager' => __DIR__ . '/..' . '/../lib/Settings/SettingsManager.php',
3034
'OCA\\ShareByMail\\ShareByMailProvider' => __DIR__ . '/..' . '/../lib/ShareByMailProvider.php',
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\ShareByMail\Event;
11+
12+
use OCP\EventDispatcher\Event;
13+
use OCP\Mail\IMessage;
14+
use OCP\Share\IShare;
15+
16+
/**
17+
* Base class for all BeforeShare*MailSentEvent types.
18+
*
19+
* Carries the fully-prepared IMessage (already rendered) and the resolved
20+
* recipient list. Listeners call markMailHandled() to suppress the native
21+
* mailer->send().
22+
*/
23+
abstract class AbstractBeforeShareMailSentEvent extends Event {
24+
private bool $mailHandled = false;
25+
26+
/**
27+
* @param string[] $resolvedEmails resolved recipient addresses (not guaranteed to be validated)
28+
*/
29+
public function __construct(
30+
private readonly IShare $share,
31+
private readonly array $resolvedEmails,
32+
private readonly IMessage $message,
33+
) {
34+
parent::__construct();
35+
}
36+
37+
public function getShare(): IShare {
38+
return $this->share;
39+
}
40+
41+
/** @return string[] */
42+
public function getResolvedEmails(): array {
43+
return $this->resolvedEmails;
44+
}
45+
46+
public function getMessage(): IMessage {
47+
return $this->message;
48+
}
49+
50+
/**
51+
* Call to suppress the native mailer->send() for this message.
52+
* Must be called before any send attempt — if the listener's own send
53+
* throws, the exception propagates and the native send is also skipped.
54+
*/
55+
public function markMailHandled(): void {
56+
$this->mailHandled = true;
57+
}
58+
59+
public function isMailHandled(): bool {
60+
return $this->mailHandled;
61+
}
62+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\ShareByMail\Event;
11+
12+
use OCP\Mail\IMessage;
13+
use OCP\Share\IShare;
14+
15+
/**
16+
* Fired by ShareByMailProvider::sendEmail() immediately before the native
17+
* mailer->send() call for share-link notifications to recipients.
18+
*
19+
* @psalm-type TemplateData = array{
20+
* senderUserId: string,
21+
* filename: string,
22+
* link: string,
23+
* initiator: string,
24+
* expiration: \DateTime|null,
25+
* shareWith: string,
26+
* note: string,
27+
* }
28+
*
29+
* @psalm-api
30+
*/
31+
class BeforeShareMailSentEvent extends AbstractBeforeShareMailSentEvent {
32+
/**
33+
* @param string[] $resolvedEmails
34+
* @param TemplateData $templateData
35+
*/
36+
public function __construct(
37+
IShare $share,
38+
array $resolvedEmails,
39+
IMessage $message,
40+
private readonly array $templateData,
41+
) {
42+
parent::__construct($share, $resolvedEmails, $message);
43+
}
44+
45+
public function getSenderUserId(): string {
46+
return $this->templateData['senderUserId'];
47+
}
48+
49+
public function getFileName(): string {
50+
return $this->templateData['filename'];
51+
}
52+
53+
public function getResourceUrl(): string {
54+
return $this->templateData['link'];
55+
}
56+
57+
public function getNote(): string {
58+
return $this->templateData['note'];
59+
}
60+
61+
public function getShareWith(): string {
62+
return $this->templateData['shareWith'];
63+
}
64+
65+
public function getInitiatorDisplayName(): string {
66+
return $this->templateData['initiator'];
67+
}
68+
69+
public function getExpiration(): ?\DateTime {
70+
return $this->templateData['expiration'];
71+
}
72+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\ShareByMail\Event;
11+
12+
use OCP\Mail\IMessage;
13+
use OCP\Share\IShare;
14+
15+
/**
16+
* Fired by ShareByMailProvider::sendNote() immediately before the native
17+
* mailer->send() call for note-update notifications to recipients.
18+
*
19+
* @psalm-type TemplateData = array{
20+
* filename: string,
21+
* note: string,
22+
* }
23+
*
24+
* @psalm-api
25+
*/
26+
class BeforeShareNoteMailSentEvent extends AbstractBeforeShareMailSentEvent {
27+
/**
28+
* @param string[] $resolvedEmails
29+
* @param TemplateData $templateData
30+
*/
31+
public function __construct(
32+
IShare $share,
33+
array $resolvedEmails,
34+
IMessage $message,
35+
private readonly array $templateData,
36+
) {
37+
parent::__construct($share, $resolvedEmails, $message);
38+
}
39+
40+
public function getFileName(): string {
41+
return $this->templateData['filename'];
42+
}
43+
44+
public function getNote(): string {
45+
return $this->templateData['note'];
46+
}
47+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\ShareByMail\Event;
11+
12+
use OCP\Mail\IMessage;
13+
use OCP\Share\IShare;
14+
15+
/**
16+
* Fired by ShareByMailProvider::sendPassword() and sendPasswordToOwner()
17+
* immediately before the native mailer->send() call for password emails.
18+
*
19+
* For sendPassword(), initiatorEmail may be null when the initiator has no
20+
* email address configured. For sendPasswordToOwner() it is always a non-null
21+
* string (the call site throws earlier if the owner has no email address).
22+
*
23+
* @psalm-type TemplateData = array{
24+
* filename: string,
25+
* password: string,
26+
* initiator: string,
27+
* initiatorEmail: string|null,
28+
* shareWith: string,
29+
* }
30+
*
31+
* @psalm-api
32+
*/
33+
class BeforeSharePasswordMailSentEvent extends AbstractBeforeShareMailSentEvent {
34+
/**
35+
* @param string[] $resolvedEmails
36+
* @param TemplateData $templateData
37+
*/
38+
public function __construct(
39+
IShare $share,
40+
array $resolvedEmails,
41+
IMessage $message,
42+
private readonly array $templateData,
43+
) {
44+
parent::__construct($share, $resolvedEmails, $message);
45+
}
46+
47+
public function getFileName(): string {
48+
return $this->templateData['filename'];
49+
}
50+
51+
public function getPassword(): string {
52+
return $this->templateData['password'];
53+
}
54+
55+
public function getInitiatorDisplayName(): string {
56+
return $this->templateData['initiator'];
57+
}
58+
59+
public function getInitiatorEmail(): ?string {
60+
return $this->templateData['initiatorEmail'];
61+
}
62+
63+
public function getShareWith(): string {
64+
return $this->templateData['shareWith'];
65+
}
66+
}

apps/sharebymail/lib/ShareByMailProvider.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use OC\Share20\DefaultShareProvider;
1111
use OC\Share20\Exception\InvalidShare;
1212
use OC\Share20\Share;
13+
use OCA\ShareByMail\Event\BeforeShareMailSentEvent;
14+
use OCA\ShareByMail\Event\BeforeShareNoteMailSentEvent;
15+
use OCA\ShareByMail\Event\BeforeSharePasswordMailSentEvent;
1316
use OCA\ShareByMail\Settings\SettingsManager;
1417
use OCP\Activity\IManager;
1518
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -334,6 +337,7 @@ protected function sendEmail(IShare $share, array $emails): void {
334337
'filename' => $filename,
335338
'link' => $link,
336339
'initiator' => $initiatorDisplayName,
340+
'senderUserId' => $initiator,
337341
'expiration' => $expiration,
338342
'shareWith' => $shareWith,
339343
'note' => $note,
@@ -404,6 +408,11 @@ protected function sendEmail(IShare $share, array $emails): void {
404408
}
405409

406410
$message->useTemplate($emailTemplate);
411+
$event = new BeforeShareMailSentEvent($share, $emails, $message, $templateData);
412+
$this->eventDispatcher->dispatchTyped($event);
413+
if ($event->isMailHandled()) {
414+
return;
415+
}
407416
$failedRecipients = $this->mailer->send($message);
408417
if (!empty($failedRecipients)) {
409418
$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):
501510
}
502511

503512
$message->useTemplate($emailTemplate);
513+
$event = new BeforeSharePasswordMailSentEvent($share, $emails, $message, $templateData);
514+
$this->eventDispatcher->dispatchTyped($event);
515+
if ($event->isMailHandled()) {
516+
return true;
517+
}
518+
504519
$failedRecipients = $this->mailer->send($message);
505520
if (!empty($failedRecipients)) {
506521
$this->logger->error('Share password mail could not be sent to: ' . implode(', ', $failedRecipients));
@@ -567,6 +582,11 @@ protected function sendNote(IShare $share): void {
567582

568583
$message->setTo([$recipient]);
569584
$message->useTemplate($emailTemplate);
585+
$event = new BeforeShareNoteMailSentEvent($share, [$recipient], $message, $templateData);
586+
$this->eventDispatcher->dispatchTyped($event);
587+
if ($event->isMailHandled()) {
588+
return;
589+
}
570590
$this->mailer->send($message);
571591
}
572592

@@ -630,8 +650,13 @@ protected function sendPasswordToOwner(IShare $share, string $password): bool {
630650
$message->setFrom([Util::getDefaultEmailAddress($instanceName) => $senderName]);
631651
$message->setTo([$initiatorEMailAddress => $initiatorDisplayName]);
632652
$message->useTemplate($emailTemplate);
633-
$this->mailer->send($message);
653+
$event = new BeforeSharePasswordMailSentEvent($share, [$initiatorEMailAddress], $message, $templateData);
654+
$this->eventDispatcher->dispatchTyped($event);
655+
if ($event->isMailHandled()) {
656+
return true;
657+
}
634658

659+
$this->mailer->send($message);
635660
$this->createPasswordSendActivity($share, $shareWith, true);
636661

637662
return true;

0 commit comments

Comments
 (0)