Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/sharebymail/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
62 changes: 62 additions & 0 deletions apps/sharebymail/lib/Event/AbstractBeforeShareMailSentEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\ShareByMail\Event;

use OCP\EventDispatcher\Event;
use OCP\Mail\IMessage;
use OCP\Share\IShare;

/**
* Base class for all BeforeShare*MailSentEvent types.
*
* Carries the fully-prepared IMessage (already rendered), the resolved
* recipient list, and the raw template data used to build the message.
* Listeners call markMailHandled() to suppress the native mailer->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;
}
}
72 changes: 72 additions & 0 deletions apps/sharebymail/lib/Event/BeforeShareMailSentEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\ShareByMail\Event;

use OCP\Mail\IMessage;
use OCP\Share\IShare;

/**
* Fired by ShareByMailProvider::sendEmail() immediately before the native
* mailer->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'];
}
}
47 changes: 47 additions & 0 deletions apps/sharebymail/lib/Event/BeforeShareNoteMailSentEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\ShareByMail\Event;

use OCP\Mail\IMessage;
use OCP\Share\IShare;

/**
* Fired by ShareByMailProvider::sendNote() immediately before the native
* mailer->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'];
}
}
66 changes: 66 additions & 0 deletions apps/sharebymail/lib/Event/BeforeSharePasswordMailSentEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\ShareByMail\Event;

use OCP\Mail\IMessage;
use OCP\Share\IShare;

/**
* Fired by ShareByMailProvider::sendPassword() and sendPasswordToOwner()
* immediately before the native mailer->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'];
}
}
55 changes: 42 additions & 13 deletions apps/sharebymail/lib/ShareByMailProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -319,14 +322,16 @@ 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,
'senderUserId' => $initiator,
'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();
Expand Down Expand Up @@ -392,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));
Expand Down Expand Up @@ -429,13 +439,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();
Expand Down Expand Up @@ -488,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);
Expand All @@ -515,7 +530,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();
Expand Down Expand Up @@ -551,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);
}

Expand All @@ -576,13 +600,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();
Expand Down Expand Up @@ -613,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);

Expand Down
Loading