Skip to content

Commit 1ee80b7

Browse files
committed
Enhance sharebymail: Allow user-level sender addresses via Mail Provider API
Resolves #56904 This commit updates the ShareByMailProvider to support sending share emails using the user's personal email address via the Mail Provider API, mirroring the functionality available for Calendar invitations (IMipPlugin). - Added an admin setting ('useUserEmail') to enable/disable user-level senders. - Modified ShareByMailProvider to try to find an appropriate Mail Provider service. - If a Mail Provider is available and user-level sender is enabled, emails are sent from the sharing user's address. - If no provider is available, or an error occurs during sending, it cleanly falls back to the existing system mailer logic. - Updated relevant UI components and unit tests. Signed-off-by: Divyam <divyam@divyam.dev>
1 parent 27bb57b commit 1ee80b7

13 files changed

Lines changed: 685 additions & 87 deletions

‎.htaccess‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,7 @@
196196

197197
AddDefaultCharset utf-8
198198
Options -Indexes
199+
#### DO NOT CHANGE ANYTHING ABOVE THIS LINE ####
200+
201+
ErrorDocument 403 /index.php/error/403
202+
ErrorDocument 404 /index.php/error/404

‎apps/sharebymail/lib/Settings/Admin.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function __construct(
3030
public function getForm() {
3131
$this->initialState->provideInitialState('sendPasswordMail', $this->settingsManager->sendPasswordByMail());
3232
$this->initialState->provideInitialState('replyToInitiator', $this->settingsManager->replyToInitiator());
33+
$this->initialState->provideInitialState('useUserEmail', $this->settingsManager->useUserEmail());
3334

3435
Util::addStyle('sharebymail', 'admin-settings');
3536
Util::addScript('sharebymail', 'admin-settings');
@@ -67,6 +68,7 @@ public function getAuthorizedAppConfig(): array {
6768
'sharebymail' => [
6869
'sendpasswordmail',
6970
'replyToInitiator',
71+
'useUserEmail',
7072
],
7173
];
7274
}

‎apps/sharebymail/lib/Settings/SettingsManager.php‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class SettingsManager {
1717

1818
private $replyToInitiatorDefault = 'yes';
1919

20+
private $useUserEmailDefault = 'no';
21+
2022
public function __construct(
2123
private IConfig $config,
2224
) {
@@ -41,4 +43,19 @@ public function replyToInitiator(): bool {
4143
$replyToInitiator = $this->config->getAppValue('sharebymail', 'replyToInitiator', $this->replyToInitiatorDefault);
4244
return $replyToInitiator === 'yes';
4345
}
46+
47+
/**
48+
* Should share-by-mail emails be sent using the user's personal email
49+
* address (via Mail Provider) instead of the global system email address.
50+
*
51+
* When enabled and a Mail Provider is available for the user, share
52+
* notification emails will be sent from the user's own address, similar
53+
* to how calendar invitations work.
54+
*
55+
* @return bool
56+
*/
57+
public function useUserEmail(): bool {
58+
$useUserEmail = $this->config->getAppValue('sharebymail', 'useUserEmail', $this->useUserEmailDefault);
59+
return $useUserEmail === 'yes';
60+
}
4461
}

‎apps/sharebymail/lib/ShareByMailProvider.php‎

Lines changed: 205 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use OCP\Files\IRootFolder;
2020
use OCP\Files\Node;
2121
use OCP\HintException;
22+
use OCP\IAppConfig;
2223
use OCP\IConfig;
2324
use OCP\IDBConnection;
2425
use OCP\IL10N;
@@ -27,6 +28,9 @@
2728
use OCP\IUserManager;
2829
use OCP\Mail\IEmailValidator;
2930
use OCP\Mail\IMailer;
31+
use OCP\Mail\Provider\Address;
32+
use OCP\Mail\Provider\IManager as IMailManager;
33+
use OCP\Mail\Provider\IMessageSend;
3034
use OCP\Security\Events\GenerateSecurePasswordEvent;
3135
use OCP\Security\IHasher;
3236
use OCP\Security\ISecureRandom;
@@ -74,6 +78,8 @@ public function __construct(
7478
private IEventDispatcher $eventDispatcher,
7579
private IShareManager $shareManager,
7680
private IEmailValidator $emailValidator,
81+
private IMailManager $mailManager,
82+
private IAppConfig $appConfig,
7783
) {
7884
}
7985

@@ -328,6 +334,78 @@ private function trySendPasswordToOwner(IShare $share): void {
328334
'exception' => $e,
329335
]);
330336
}
337+
338+
}
339+
340+
/**
341+
* Try to find a Mail Provider service for the given user that can send mail.
342+
*
343+
* This follows the same pattern as IMipPlugin for calendar invitations:
344+
* if mail providers are enabled globally and the admin has enabled
345+
* user-level sending for share emails, look up the user's mail service.
346+
*
347+
* @param string $userId The user ID of the share initiator
348+
* @return IMessageSend|null A mail service that can send, or null to fall back to the system mailer
349+
*/
350+
protected function findMailService(string $userId): ?IMessageSend {
351+
if (!$this->settingsManager->useUserEmail()) {
352+
return null;
353+
}
354+
355+
if (!$this->appConfig->getValueBool('core', 'mail_providers_enabled', true)) {
356+
return null;
357+
}
358+
359+
$user = $this->userManager->get($userId);
360+
if ($user === null) {
361+
return null;
362+
}
363+
364+
$userEmail = $user->getEMailAddress();
365+
if ($userEmail === null) {
366+
return null;
367+
}
368+
369+
$mailService = $this->mailManager->findServiceByAddress($userId, $userEmail);
370+
if ($mailService instanceof IMessageSend) {
371+
return $mailService;
372+
}
373+
374+
return null;
375+
}
376+
377+
/**
378+
* Send the share notification email via Mail Provider if available,
379+
* otherwise fall back to the system mailer.
380+
*
381+
* @param IMessageSend $mailService The mail provider service
382+
* @param string $senderEmail The sender's email address
383+
* @param string $senderName The sender's display name
384+
* @param array $recipientEmails The recipient email addresses
385+
* @param \OCP\Mail\IEMailTemplate $emailTemplate The email template
386+
*/
387+
protected function sendViaMailProvider(
388+
IMessageSend $mailService,
389+
string $senderEmail,
390+
string $senderName,
391+
array $recipientEmails,
392+
\OCP\Mail\IEMailTemplate $emailTemplate,
393+
): void {
394+
$message = $mailService->initiateMessage();
395+
$message->setFrom(new Address($senderEmail, $senderName));
396+
397+
$recipients = array_map(fn (string $email) => new Address($email), $recipientEmails);
398+
if (count($recipients) > 1) {
399+
$message->setBcc(...$recipients);
400+
} else {
401+
$message->setTo(...$recipients);
402+
}
403+
404+
$message->setSubject($emailTemplate->renderSubject());
405+
$message->setBodyPlain($emailTemplate->renderText());
406+
$message->setBodyHtml($emailTemplate->renderHtml());
407+
408+
$mailService->sendMessage($message);
331409
}
332410

333411
/**
@@ -347,7 +425,6 @@ protected function sendEmail(IShare $share, array $emails): void {
347425

348426
$initiatorUser = $this->userManager->get($initiator);
349427
$initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
350-
$message = $this->mailer->createMessage();
351428

352429
$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [
353430
'filename' => $filename,
@@ -385,6 +462,62 @@ protected function sendEmail(IShare $share, array $emails): void {
385462
$link
386463
);
387464

465+
$instanceName = $this->defaults->getName();
466+
467+
// Try to send via the user's Mail Provider
468+
$mailService = $this->findMailService($initiator);
469+
if ($mailService !== null && $initiatorUser instanceof IUser) {
470+
$initiatorEmail = $initiatorUser->getEMailAddress();
471+
if ($initiatorEmail !== null) {
472+
$emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
473+
try {
474+
$this->sendViaMailProvider($mailService, $initiatorEmail, $initiatorDisplayName, $emails, $emailTemplate);
475+
return;
476+
} catch (\Exception $e) {
477+
$this->logger->warning('Failed to send share email via Mail Provider, falling back to system mailer.', [
478+
'app' => 'sharebymail',
479+
'exception' => $e,
480+
]);
481+
// Fall through to system mailer
482+
// Re-create template since footer was already added
483+
$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [
484+
'filename' => $filename,
485+
'link' => $link,
486+
'initiator' => $initiatorDisplayName,
487+
'expiration' => $expiration,
488+
'shareWith' => $shareWith,
489+
'note' => $note
490+
]);
491+
$emailTemplate->setSubject($this->l->t('%1$s shared %2$s with you', [$initiatorDisplayName, $filename]));
492+
$emailTemplate->addHeader();
493+
$emailTemplate->addHeading($this->l->t('%1$s shared %2$s with you', [$initiatorDisplayName, $filename]), false);
494+
if ($note !== '') {
495+
$emailTemplate->addBodyListItem(
496+
htmlspecialchars($note),
497+
$this->l->t('Note:'),
498+
$this->getAbsoluteImagePath('caldav/description.png'),
499+
$note
500+
);
501+
}
502+
if ($expiration !== null) {
503+
$dateString = (string)$this->l->l('date', $expiration, ['width' => 'medium']);
504+
$emailTemplate->addBodyListItem(
505+
$this->l->t('This share is valid until %s at midnight', [$dateString]),
506+
$this->l->t('Expiration:'),
507+
$this->getAbsoluteImagePath('caldav/time.png'),
508+
);
509+
}
510+
$emailTemplate->addBodyButton(
511+
$this->l->t('Open shared item'),
512+
$link
513+
);
514+
}
515+
}
516+
}
517+
518+
// Fall back to the system mailer
519+
$message = $this->mailer->createMessage();
520+
388521
// If multiple recipients are given, we send the mail to all of them
389522
if (count($emails) > 1) {
390523
// We do not want to expose the email addresses of the other recipients
@@ -394,7 +527,6 @@ protected function sendEmail(IShare $share, array $emails): void {
394527
}
395528

396529
// The "From" contains the sharers name
397-
$instanceName = $this->defaults->getName();
398530
$senderName = $instanceName;
399531
if ($this->settingsManager->replyToInitiator()) {
400532
$senderName = $this->l->t(
@@ -457,8 +589,6 @@ protected function sendPassword(IShare $share, string $password, array $emails):
457589
$plainBodyPart = $this->l->t('%1$s shared %2$s with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]);
458590
$htmlBodyPart = $this->l->t('%1$s shared %2$s with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]);
459591

460-
$message = $this->mailer->createMessage();
461-
462592
$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [
463593
'filename' => $filename,
464594
'password' => $password,
@@ -481,6 +611,47 @@ protected function sendPassword(IShare $share, string $password, array $emails):
481611
$emailTemplate->addBodyText($this->l->t('This password will expire at %s', [$expirationTime->format('r')]));
482612
}
483613

614+
$instanceName = $this->defaults->getName();
615+
616+
// Try to send via the user's Mail Provider
617+
$mailService = $this->findMailService($initiator);
618+
if ($mailService !== null && $initiatorEmailAddress !== null) {
619+
$emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
620+
try {
621+
$this->sendViaMailProvider($mailService, $initiatorEmailAddress, $initiatorDisplayName, $emails, $emailTemplate);
622+
$this->createPasswordSendActivity($share, $shareWith, false);
623+
return true;
624+
} catch (\Exception $e) {
625+
$this->logger->warning('Failed to send share password email via Mail Provider, falling back to system mailer.', [
626+
'app' => 'sharebymail',
627+
'exception' => $e,
628+
]);
629+
// Re-create template for fallback
630+
$emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [
631+
'filename' => $filename,
632+
'password' => $password,
633+
'initiator' => $initiatorDisplayName,
634+
'initiatorEmail' => $initiatorEmailAddress,
635+
'shareWith' => $shareWith,
636+
]);
637+
$emailTemplate->setSubject($this->l->t('Password to access %1$s shared to you by %2$s', [$filename, $initiatorDisplayName]));
638+
$emailTemplate->addHeader();
639+
$emailTemplate->addHeading($this->l->t('Password to access %s', [$filename]), false);
640+
$emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart);
641+
$emailTemplate->addBodyText($this->l->t('It is protected with the following password:'));
642+
$emailTemplate->addBodyText($password);
643+
if ($this->config->getSystemValue('sharing.enable_mail_link_password_expiration', false) === true) {
644+
$expirationTime = new \DateTime();
645+
$expirationInterval = $this->config->getSystemValue('sharing.mail_link_password_expiration_interval', 3600);
646+
$expirationTime = $expirationTime->add(new \DateInterval('PT' . $expirationInterval . 'S'));
647+
$emailTemplate->addBodyText($this->l->t('This password will expire at %s', [$expirationTime->format('r')]));
648+
}
649+
}
650+
}
651+
652+
// Fall back to the system mailer
653+
$message = $this->mailer->createMessage();
654+
484655
// If multiple recipients are given, we send the mail to all of them
485656
if (count($emails) > 1) {
486657
// We do not want to expose the email addresses of the other recipients
@@ -490,7 +661,6 @@ protected function sendPassword(IShare $share, string $password, array $emails):
490661
}
491662

492663
// The "From" contains the sharers name
493-
$instanceName = $this->defaults->getName();
494664
$senderName = $instanceName;
495665
if ($this->settingsManager->replyToInitiator()) {
496666
$senderName = $this->l->t(
@@ -542,8 +712,6 @@ protected function sendNote(IShare $share): void {
542712
$plainHeading = $this->l->t('%1$s shared %2$s with you and wants to add:', [$initiatorDisplayName, $filename]);
543713
$htmlHeading = $this->l->t('%1$s shared %2$s with you and wants to add', [$initiatorDisplayName, $filename]);
544714

545-
$message = $this->mailer->createMessage();
546-
547715
$emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote');
548716

549717
$emailTemplate->setSubject($this->l->t('%s added a note to a file shared with you', [$initiatorDisplayName]));
@@ -558,8 +726,37 @@ protected function sendNote(IShare $share): void {
558726
$link
559727
);
560728

561-
// The "From" contains the sharers name
562729
$instanceName = $this->defaults->getName();
730+
731+
// Try to send via the user's Mail Provider
732+
$mailService = $this->findMailService($initiator);
733+
if ($mailService !== null && $initiatorEmailAddress !== null) {
734+
$emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : ''));
735+
try {
736+
$this->sendViaMailProvider($mailService, $initiatorEmailAddress, $initiatorDisplayName, [$recipient], $emailTemplate);
737+
return;
738+
} catch (\Exception $e) {
739+
$this->logger->warning('Failed to send share note email via Mail Provider, falling back to system mailer.', [
740+
'app' => 'sharebymail',
741+
'exception' => $e,
742+
]);
743+
// Re-create template for fallback
744+
$emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote');
745+
$emailTemplate->setSubject($this->l->t('%s added a note to a file shared with you', [$initiatorDisplayName]));
746+
$emailTemplate->addHeader();
747+
$emailTemplate->addHeading(htmlspecialchars($htmlHeading), $plainHeading);
748+
$emailTemplate->addBodyText(htmlspecialchars($note), $note);
749+
$emailTemplate->addBodyButton(
750+
$this->l->t('Open shared item'),
751+
$link
752+
);
753+
}
754+
}
755+
756+
// Fall back to the system mailer
757+
$message = $this->mailer->createMessage();
758+
759+
// The "From" contains the sharers name
563760
$senderName = $instanceName;
564761
if ($this->settingsManager->replyToInitiator()) {
565762
$senderName = $this->l->t(

‎apps/sharebymail/src/components/AdminSettings.vue‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
<NcCheckboxRadioSwitch v-model="replyToInitiator" type="switch">
1515
{{ t('sharebymail', 'Reply to initiator') }}
1616
</NcCheckboxRadioSwitch>
17+
18+
<NcCheckboxRadioSwitch v-model="useUserEmail" type="switch">
19+
{{ t('sharebymail', 'Send share emails from user\'s email address') }}
20+
</NcCheckboxRadioSwitch>
21+
<p v-if="useUserEmail" class="settings-hint">
22+
{{ t('sharebymail', 'When enabled, share notification emails will be sent from the user\'s personal email address via their Mail Provider (e.g. Nextcloud Mail), similar to calendar invitations. Falls back to the system email if no Mail Provider is available.') }}
23+
</p>
1724
</NcSettingsSection>
1825
</template>
1926

@@ -43,6 +50,7 @@ export default {
4350
return {
4451
sendPasswordMail: loadState('sharebymail', 'sendPasswordMail'),
4552
replyToInitiator: loadState('sharebymail', 'replyToInitiator'),
53+
useUserEmail: loadState('sharebymail', 'useUserEmail'),
4654
}
4755
},
4856
@@ -54,6 +62,10 @@ export default {
5462
replyToInitiator(newValue) {
5563
this.update('replyToInitiator', newValue)
5664
},
65+
66+
useUserEmail(newValue) {
67+
this.update('useUserEmail', newValue)
68+
},
5769
},
5870
5971
methods: {
@@ -88,3 +100,13 @@ export default {
88100
},
89101
}
90102
</script>
103+
104+
<style scoped>
105+
.settings-hint {
106+
color: var(--color-text-maxcontrast);
107+
margin-top: 4px;
108+
margin-left: 44px;
109+
font-size: 0.9em;
110+
}
111+
</style>
112+

0 commit comments

Comments
 (0)