22
33/**
44 * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
5+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
56 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
67 * SPDX-License-Identifier: AGPL-3.0-only
78 */
2526use OCP \IUserManager ;
2627use OCP \IUserSession ;
2728use OCP \L10N \IFactory ;
29+ use OCP \Mail \IMailer ;
2830use OCP \Share \IManager as IShareManager ;
2931use Psr \Container \ContainerExceptionInterface ;
32+ use Psr \Log \LoggerInterface ;
3033use Sabre \DAV \Exception ;
3134use Sabre \DAV \PropPatch ;
3235use Sabre \DAVACL \PrincipalBackend \BackendInterface ;
@@ -53,6 +56,8 @@ public function __construct(
5356 private KnownUserService $ knownUserService ,
5457 private IConfig $ config ,
5558 private IFactory $ languageFactory ,
59+ private IMailer $ mailer ,
60+ private LoggerInterface $ logger ,
5661 string $ principalPrefix = 'principals/users/ ' ,
5762 ) {
5863 $ this ->principalPrefix = trim ($ principalPrefix , '/ ' );
@@ -61,6 +66,7 @@ public function __construct(
6166
6267 use PrincipalProxyTrait {
6368 getGroupMembership as protected traitGetGroupMembership;
69+ setGroupMemberSet as protected traitSetGroupMemberSet;
6470 }
6571
6672 /**
@@ -124,6 +130,20 @@ public function getPrincipalPropertiesByPath($path, ?array $propertyFilter = nul
124130 if ($ user !== null ) {
125131 return [
126132 'uri ' => 'principals/users/ ' . $ user ->getUID () . '/ ' . $ name ,
133+ // Only the principal owner may modify their own proxy group.
134+ // Any authenticated user may read it (needed by Sabre internals).
135+ '{DAV:}acl ' => [
136+ [
137+ 'privilege ' => '{DAV:}read ' ,
138+ 'principal ' => '{DAV:}authenticated ' ,
139+ 'protected ' => true ,
140+ ],
141+ [
142+ 'privilege ' => '{DAV:}write ' ,
143+ 'principal ' => 'principals/users/ ' . $ user ->getUID (),
144+ 'protected ' => true ,
145+ ],
146+ ],
127147 ];
128148 }
129149 return null ;
@@ -222,6 +242,89 @@ public function updatePrincipal($path, PropPatch $propPatch) {
222242 return 0 ;
223243 }
224244
245+ /**
246+ * Updates the list of group members for a group principal.
247+ *
248+ * Overrides the trait implementation to send email notifications when new
249+ * write-proxy delegates are added.
250+ *
251+ * @param string $principal
252+ * @param string[] $members
253+ */
254+ public function setGroupMemberSet ($ principal , array $ members ): void {
255+ [$ principalUri , $ target ] = \Sabre \Uri \split ($ principal );
256+
257+ // Snapshot the current write-proxy members before applying changes so
258+ // we can diff and notify only the newly added ones.
259+ $ oldMemberUids = [];
260+ if ($ target === 'calendar-proxy-write ' ) {
261+ $ oldProxies = $ this ->proxyMapper ->getProxiesOf ($ principalUri );
262+ foreach ($ oldProxies as $ proxy ) {
263+ if ($ proxy ->getPermissions () === (ProxyMapper::PERMISSION_READ | ProxyMapper::PERMISSION_WRITE )) {
264+ $ oldMemberUids [] = $ proxy ->getProxyId ();
265+ }
266+ }
267+ }
268+
269+ // Apply the new member set via the trait implementation.
270+ $ this ->traitSetGroupMemberSet ($ principal , $ members );
271+
272+ // Notify newly added write-proxy delegates.
273+ if ($ target === 'calendar-proxy-write ' ) {
274+ [, $ ownerUid ] = \Sabre \Uri \split ($ principalUri );
275+ $ addedMembers = array_diff ($ members , $ oldMemberUids );
276+ foreach ($ addedMembers as $ memberUri ) {
277+ [, $ delegateUid ] = \Sabre \Uri \split ($ memberUri );
278+ $ this ->sendDelegationNotification ($ ownerUid , $ delegateUid );
279+ }
280+ }
281+ }
282+
283+ /**
284+ * Send an email to a newly added delegate informing them of the delegation.
285+ *
286+ * @param string $ownerUid User ID of the calendar owner who granted access
287+ * @param string $delegateUid User ID of the user who was just granted access
288+ */
289+ private function sendDelegationNotification (string $ ownerUid , string $ delegateUid ): void {
290+ $ delegateUser = $ this ->userManager ->get ($ delegateUid );
291+ $ ownerUser = $ this ->userManager ->get ($ ownerUid );
292+
293+ if ($ delegateUser === null || $ ownerUser === null ) {
294+ return ;
295+ }
296+
297+ $ delegateEmail = $ delegateUser ->getEMailAddress ();
298+ if ($ delegateEmail === null || $ delegateEmail === '' ) {
299+ return ; // No email address on file — skip silently.
300+ }
301+
302+ $ l = $ this ->languageFactory ->get ('dav ' );
303+
304+ $ ownerDisplayName = $ ownerUser ->getDisplayName () ?: $ ownerUid ;
305+ $ delegateDisplayName = $ delegateUser ->getDisplayName () ?: $ delegateUid ;
306+
307+ $ subject = $ l ->t ('%s has granted you access to their calendars ' , [$ ownerDisplayName ]);
308+ $ bodyText = $ l ->t (
309+ "Hello %1 \$s, \n\n%2 \$s has added you as a calendar delegate. You can now view and manage their \ncalendars in the Nextcloud Calendar app under the \"Delegated \" section. \n\nTo remove yourself as a delegate, ask %2 \$s to revoke your access in their \nCalendar settings. " ,
310+ [$ delegateDisplayName , $ ownerDisplayName ]
311+ );
312+
313+ try {
314+ $ message = $ this ->mailer ->createMessage ();
315+ $ message ->setTo ([$ delegateEmail => $ delegateDisplayName ]);
316+ $ message ->setSubject ($ subject );
317+ $ message ->setPlainBody ($ bodyText );
318+ $ this ->mailer ->send ($ message );
319+ } catch (\Exception $ e ) {
320+ // Notification failure must never block the PROPPATCH response.
321+ $ this ->logger ->warning (
322+ 'Could not send delegation notification email ' ,
323+ ['owner ' => $ ownerUid , 'delegate ' => $ delegateUid , 'error ' => $ e ->getMessage ()]
324+ );
325+ }
326+ }
327+
225328 /**
226329 * Search user principals
227330 *
0 commit comments