Skip to content

Commit ede158c

Browse files
fix: improve invitation mail building
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
1 parent e4f8956 commit ede158c

6 files changed

Lines changed: 391 additions & 453 deletions

File tree

‎apps/dav/lib/CalDAV/Schedule/IMipPlugin.php‎

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,6 @@ public function schedule(Message $iTipMessage) {
215215
$vEvent = $primaryInstance['new'];
216216
/** @var VEvent|null $oldVevent */
217217
$oldVevent = $primaryInstance['old'];
218-
$isModified = $oldVevent !== null;
219218

220219
// we might not have an old event as this could be a new invitation,
221220
// or a new recurrence exception
@@ -248,68 +247,15 @@ public function schedule(Message $iTipMessage) {
248247
$senderName = $this->getSenderNameFor($sender);
249248
}
250249

251-
$replyingAttendee = null;
252-
switch ($method) {
253-
case self::METHOD_REPLY:
254-
$data = $this->imipService->buildReplyBodyData($vEvent);
255-
$replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage);
256-
break;
257-
case self::METHOD_CANCEL:
258-
$data = $this->imipService->buildCancelledBodyData($vEvent);
259-
break;
260-
default:
261-
$data = $this->imipService->buildBodyData($vEvent, $oldVevent);
262-
break;
263-
}
264-
265-
$data['attendee_name'] = ($recipientName ?: $recipient);
266-
$data['invitee_name'] = ($senderName ?: $sender);
250+
$template = match ($method) {
251+
self::METHOD_REPLY => $this->imipService->buildReplyEmail($iTipMessage, $vEvent, $recipient, $recipientName, $sender, $senderName),
252+
self::METHOD_CANCEL => $this->imipService->buildCancellationEmail($vEvent, $recipient, $recipientName, $sender, $senderName),
253+
default => $this->imipService->buildRequestEmail($iTipMessage, $vEvent, $oldVevent, $attendee, $recipient, $recipientName, $sender, $senderName, $lastOccurrence),
254+
};
267255

268256
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
269257
$fromName = $this->imipService->getFrom($senderName, $this->defaults->getName());
270258

271-
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
272-
$template->setLanguage($this->imipService->getLanguageCode());
273-
$template->addHeader();
274-
$template->addBodySender($data['invitee_name'], $senderName !== null ? $sender : '');
275-
276-
$this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified, $replyingAttendee);
277-
$this->imipService->addEventDetails($template, $vEvent, $data);
278-
279-
// Only add response buttons to invitation requests: Fix Issue #11230
280-
if (strcasecmp($method, self::METHOD_REQUEST) === 0 && $this->imipService->getAttendeeRsvpOrReqForParticipant($attendee)) {
281-
282-
/*
283-
** Only offer invitation accept/reject buttons, which link back to the
284-
** nextcloud server, to recipients who can access the nextcloud server via
285-
** their internet/intranet. Issue #12156
286-
**
287-
** The app setting is stored in the appconfig database table.
288-
**
289-
** For nextcloud servers accessible to the public internet, the default
290-
** "invitation_link_recipients" value "yes" (all recipients) is appropriate.
291-
**
292-
** When the nextcloud server is restricted behind a firewall, accessible
293-
** only via an internal network or via vpn, you can set "dav.invitation_link_recipients"
294-
** to the email address or email domain, or comma separated list of addresses or domains,
295-
** of recipients who can access the server.
296-
**
297-
** To always deliver URLs, set invitation_link_recipients to "yes".
298-
** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
299-
*/
300-
301-
$recipientDomain = substr(strrchr($recipient, '@'), 1);
302-
$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getValueString('dav', 'invitation_link_recipients', 'yes'))));
303-
304-
if (strcmp('yes', $invitationLinkRecipients[0]) === 0
305-
|| in_array(strtolower($recipient), $invitationLinkRecipients, true)
306-
|| in_array(strtolower($recipientDomain), $invitationLinkRecipients, true)) {
307-
$token = $this->imipService->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence);
308-
$this->imipService->addResponseButtons($template, $token);
309-
}
310-
}
311-
312-
$template->addFooter();
313259
// convert iTip Message to string
314260
$itip_msg = $iTipMessage->message->serialize();
315261

‎apps/dav/lib/CalDAV/Schedule/IMipService.php‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use OCP\L10N\IFactory as L10NFactory;
2020
use OCP\Mail\EMailDetails;
2121
use OCP\Mail\IEMailTemplate;
22+
use OCP\Mail\IMailer;
2223
use OCP\Security\ISecureRandom;
2324
use Sabre\VObject\Component\VCalendar;
2425
use Sabre\VObject\Component\VEvent;
@@ -77,6 +78,7 @@ public function __construct(
7778
private readonly IUserManager $userManager,
7879
private readonly IUserConfig $userConfig,
7980
private readonly IAppConfig $appConfig,
81+
private readonly IMailer $mailer,
8082
) {
8183
$language = $this->l10nFactory->findGenericLanguage();
8284
$locale = $this->l10nFactory->findLocale($language);
@@ -150,6 +152,117 @@ public function diffInstance(VEvent $newEvent, VEvent $oldEvent): array {
150152
return $changed;
151153
}
152154

155+
/**
156+
* Builds the full REQUEST invitation/update email, including the
157+
* old-vs-new diff and, when the recipient is eligible, the Accept/Decline
158+
* response buttons.
159+
*/
160+
public function buildRequestEmail(
161+
Message $iTipMessage,
162+
VEvent $vEvent,
163+
?VEvent $oldVevent,
164+
Property $attendee,
165+
string $recipient,
166+
?string $recipientName,
167+
string $sender,
168+
?string $senderName,
169+
int $lastOccurrence,
170+
): IEMailTemplate {
171+
$data = $this->withRecipientNames($this->buildBodyData($vEvent, $oldVevent), $recipient, $recipientName, $sender, $senderName);
172+
173+
$template = $this->newTemplate(IMipPlugin::METHOD_REQUEST, $data, $senderName !== null ? $sender : '');
174+
$this->addSubjectAndHeading($template, IMipPlugin::METHOD_REQUEST, $data['invitee_name'], $data['meeting_title'], $oldVevent !== null);
175+
$this->addEventDetails($template, $vEvent, $data);
176+
177+
// Only add response buttons to invitation requests: Fix Issue #11230
178+
if ($this->getAttendeeRsvpOrReqForParticipant($attendee)) {
179+
/*
180+
** Only offer invitation accept/reject buttons, which link back to the
181+
** nextcloud server, to recipients who can access the nextcloud server via
182+
** their internet/intranet. Issue #12156
183+
**
184+
** The app setting is stored in the appconfig database table.
185+
**
186+
** For nextcloud servers accessible to the public internet, the default
187+
** "invitation_link_recipients" value "yes" (all recipients) is appropriate.
188+
**
189+
** When the nextcloud server is restricted behind a firewall, accessible
190+
** only via an internal network or via vpn, you can set "dav.invitation_link_recipients"
191+
** to the email address or email domain, or comma separated list of addresses or domains,
192+
** of recipients who can access the server.
193+
**
194+
** To always deliver URLs, set invitation_link_recipients to "yes".
195+
** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
196+
*/
197+
$recipientDomain = substr(strrchr($recipient, '@'), 1);
198+
$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->appConfig->getValueString('dav', 'invitation_link_recipients', 'yes'))));
199+
200+
if (strcmp('yes', $invitationLinkRecipients[0]) === 0
201+
|| in_array(strtolower($recipient), $invitationLinkRecipients, true)
202+
|| in_array(strtolower($recipientDomain), $invitationLinkRecipients, true)) {
203+
$token = $this->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence);
204+
$this->addResponseButtons($template, $token);
205+
}
206+
}
207+
208+
$template->addFooter();
209+
return $template;
210+
}
211+
212+
/**
213+
* Builds the REPLY email an organizer receives when an attendee responds.
214+
*/
215+
public function buildReplyEmail(
216+
Message $iTipMessage,
217+
VEvent $vEvent,
218+
string $recipient,
219+
?string $recipientName,
220+
string $sender,
221+
?string $senderName,
222+
): IEMailTemplate {
223+
$data = $this->withRecipientNames($this->buildReplyBodyData($vEvent), $recipient, $recipientName, $sender, $senderName);
224+
225+
$template = $this->newTemplate(IMipPlugin::METHOD_REPLY, $data, $senderName !== null ? $sender : '');
226+
$replyingAttendee = $this->getReplyingAttendee($iTipMessage);
227+
$this->addSubjectAndHeading($template, IMipPlugin::METHOD_REPLY, $data['invitee_name'], $data['meeting_title'], false, $replyingAttendee);
228+
$this->addEventDetails($template, $vEvent, $data);
229+
$template->addFooter();
230+
return $template;
231+
}
232+
233+
/**
234+
* Builds the CANCEL email an attendee receives when dropped from an event.
235+
*/
236+
public function buildCancellationEmail(
237+
VEvent $vEvent,
238+
string $recipient,
239+
?string $recipientName,
240+
string $sender,
241+
?string $senderName,
242+
): IEMailTemplate {
243+
$data = $this->withRecipientNames($this->buildCancelledBodyData($vEvent), $recipient, $recipientName, $sender, $senderName);
244+
245+
$template = $this->newTemplate(IMipPlugin::METHOD_CANCEL, $data, $senderName !== null ? $sender : '');
246+
$this->addSubjectAndHeading($template, IMipPlugin::METHOD_CANCEL, $data['invitee_name'], $data['meeting_title'], false);
247+
$this->addEventDetails($template, $vEvent, $data);
248+
$template->addFooter();
249+
return $template;
250+
}
251+
252+
private function withRecipientNames(array $data, string $recipient, ?string $recipientName, string $sender, ?string $senderName): array {
253+
$data['attendee_name'] = $recipientName ?: $recipient;
254+
$data['invitee_name'] = $senderName ?: $sender;
255+
return $data;
256+
}
257+
258+
private function newTemplate(string $method, array $data, string $senderEmail): IEMailTemplate {
259+
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
260+
$template->setLanguage($this->getLanguageCode());
261+
$template->addHeader();
262+
$template->addBodySender($data['invitee_name'], $senderEmail);
263+
return $template;
264+
}
265+
153266
/**
154267
* @param VEvent $vEvent
155268
* @param VEvent|null $oldVEvent

‎apps/dav/tests/unit/CalDAV/Schedule/IMipPluginCharsetTest.php‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ protected function setUp(): void {
9090
->willReturn($l10n);
9191
$this->userManager = $this->createMock(IUserManager::class);
9292
$this->userManager->method('getByEmail')->willReturn([]);
93+
$message = new \OC\Mail\Message(new Email(), false);
94+
$this->mailer = $this->createMock(IMailer::class);
95+
$this->mailer->method('createMessage')
96+
->willReturn($message);
9397
$this->imipService = new IMipService(
9498
$this->urlGenerator,
9599
$this->db,
@@ -99,13 +103,10 @@ protected function setUp(): void {
99103
$this->userManager,
100104
$this->userConfig,
101105
$this->appConfig,
106+
$this->mailer,
102107
);
103108

104109
// IMipPlugin
105-
$message = new \OC\Mail\Message(new Email(), false);
106-
$this->mailer = $this->createMock(IMailer::class);
107-
$this->mailer->method('createMessage')
108-
->willReturn($message);
109110
$this->logger = new NullLogger();
110111
$this->defaults = $this->createMock(Defaults::class);
111112
$this->defaults->method('getName')

0 commit comments

Comments
 (0)