Passed
Push — master ( 65465b...5faa34 )
by Christoph
43:05 queued 27:14
created
apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php 1 patch
Indentation   +451 added lines, -451 removed lines patch added patch discarded remove patch
@@ -52,455 +52,455 @@
 block discarded – undo
52 52
  */
53 53
 class EmailProvider extends AbstractProvider {
54 54
 
55
-	/** @var string */
56
-	public const NOTIFICATION_TYPE = 'EMAIL';
57
-
58
-	/** @var IMailer */
59
-	private $mailer;
60
-
61
-	/**
62
-	 * @param IConfig $config
63
-	 * @param IMailer $mailer
64
-	 * @param ILogger $logger
65
-	 * @param L10NFactory $l10nFactory
66
-	 * @param IUrlGenerator $urlGenerator
67
-	 */
68
-	public function __construct(IConfig $config,
69
-								IMailer $mailer,
70
-								ILogger $logger,
71
-								L10NFactory $l10nFactory,
72
-								IURLGenerator $urlGenerator) {
73
-		parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
74
-		$this->mailer = $mailer;
75
-	}
76
-
77
-	/**
78
-	 * Send out notification via email
79
-	 *
80
-	 * @param VEvent $vevent
81
-	 * @param string $calendarDisplayName
82
-	 * @param array $users
83
-	 * @throws \Exception
84
-	 */
85
-	public function send(VEvent $vevent,
86
-						 string $calendarDisplayName,
87
-						 array $users = []):void {
88
-		$fallbackLanguage = $this->getFallbackLanguage();
89
-
90
-		$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
91
-		$emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
92
-
93
-		// Quote from php.net:
94
-		// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
95
-		// => if there are duplicate email addresses, it will always take the system value
96
-		$emailAddresses = array_merge(
97
-			$emailAddressesOfAttendees,
98
-			$emailAddressesOfSharees
99
-		);
100
-
101
-		$sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
102
-		$organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
103
-
104
-		foreach ($sortedByLanguage as $lang => $emailAddresses) {
105
-			if (!$this->hasL10NForLang($lang)) {
106
-				$lang = $fallbackLanguage;
107
-			}
108
-			$l10n = $this->getL10NForLang($lang);
109
-			$fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
110
-
111
-			$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
112
-			$template->addHeader();
113
-			$this->addSubjectAndHeading($template, $l10n, $vevent);
114
-			$this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
115
-			$template->addFooter();
116
-
117
-			foreach ($emailAddresses as $emailAddress) {
118
-				if (!$this->mailer->validateMailAddress($emailAddress)) {
119
-					$this->logger->error('Email address {address} for reminder notification is incorrect', ['app' => 'dav', 'address' => $emailAddress]);
120
-					continue;
121
-				}
122
-
123
-				$message = $this->mailer->createMessage();
124
-				$message->setFrom([$fromEMail]);
125
-				if ($organizer) {
126
-					$message->setReplyTo($organizer);
127
-				}
128
-				$message->setTo([$emailAddress]);
129
-				$message->useTemplate($template);
130
-
131
-				try {
132
-					$failed = $this->mailer->send($message);
133
-					if ($failed) {
134
-						$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
135
-					}
136
-				} catch (\Exception $ex) {
137
-					$this->logger->logException($ex, ['app' => 'dav']);
138
-				}
139
-			}
140
-		}
141
-	}
142
-
143
-	/**
144
-	 * @param IEMailTemplate $template
145
-	 * @param IL10N $l10n
146
-	 * @param VEvent $vevent
147
-	 */
148
-	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
149
-		$template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
150
-		$template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
151
-	}
152
-
153
-	/**
154
-	 * @param IEMailTemplate $template
155
-	 * @param IL10N $l10n
156
-	 * @param string $calendarDisplayName
157
-	 * @param array $eventData
158
-	 */
159
-	private function addBulletList(IEMailTemplate $template,
160
-								   IL10N $l10n,
161
-								   string $calendarDisplayName,
162
-								   VEvent $vevent):void {
163
-		$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
164
-			$this->getAbsoluteImagePath('actions/info.png'));
165
-
166
-		$template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
167
-			$this->getAbsoluteImagePath('places/calendar.png'));
168
-
169
-		if (isset($vevent->LOCATION)) {
170
-			$template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
171
-				$this->getAbsoluteImagePath('actions/address.png'));
172
-		}
173
-		if (isset($vevent->DESCRIPTION)) {
174
-			$template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
175
-				$this->getAbsoluteImagePath('actions/more.png'));
176
-		}
177
-	}
178
-
179
-	/**
180
-	 * @param string $path
181
-	 * @return string
182
-	 */
183
-	private function getAbsoluteImagePath(string $path):string {
184
-		return $this->urlGenerator->getAbsoluteURL(
185
-			$this->urlGenerator->imagePath('core', $path)
186
-		);
187
-	}
188
-
189
-	/**
190
-	 * @param VEvent $vevent
191
-	 * @return array|null
192
-	 */
193
-	private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
194
-		if (!$vevent->ORGANIZER) {
195
-			return null;
196
-		}
197
-
198
-		$organizer = $vevent->ORGANIZER;
199
-		if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
200
-			return null;
201
-		}
202
-
203
-		$organizerEMail = substr($organizer->getValue(), 7);
204
-
205
-		if ($organizerEMail === false || !$this->mailer->validateMailAddress($organizerEMail)) {
206
-			return null;
207
-		}
208
-
209
-		$name = $organizer->offsetGet('CN');
210
-		if ($name instanceof Parameter) {
211
-			return [$organizerEMail => $name];
212
-		}
213
-
214
-		return [$organizerEMail];
215
-	}
216
-
217
-	/**
218
-	 * @param array $emails
219
-	 * @param string $defaultLanguage
220
-	 * @return array
221
-	 */
222
-	private function sortEMailAddressesByLanguage(array $emails,
223
-												  string $defaultLanguage):array {
224
-		$sortedByLanguage = [];
225
-
226
-		foreach ($emails as $emailAddress => $parameters) {
227
-			if (isset($parameters['LANG'])) {
228
-				$lang = $parameters['LANG'];
229
-			} else {
230
-				$lang = $defaultLanguage;
231
-			}
232
-
233
-			if (!isset($sortedByLanguage[$lang])) {
234
-				$sortedByLanguage[$lang] = [];
235
-			}
236
-
237
-			$sortedByLanguage[$lang][] = $emailAddress;
238
-		}
239
-
240
-		return $sortedByLanguage;
241
-	}
242
-
243
-	/**
244
-	 * @param VEvent $vevent
245
-	 * @return array
246
-	 */
247
-	private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
248
-		$emailAddresses = [];
249
-
250
-		if (isset($vevent->ATTENDEE)) {
251
-			foreach ($vevent->ATTENDEE as $attendee) {
252
-				if (!($attendee instanceof VObject\Property)) {
253
-					continue;
254
-				}
255
-
256
-				$cuType = $this->getCUTypeOfAttendee($attendee);
257
-				if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
258
-					// Don't send emails to things
259
-					continue;
260
-				}
261
-
262
-				$partstat = $this->getPartstatOfAttendee($attendee);
263
-				if ($partstat === 'DECLINED') {
264
-					// Don't send out emails to people who declined
265
-					continue;
266
-				}
267
-				if ($partstat === 'DELEGATED') {
268
-					$delegates = $attendee->offsetGet('DELEGATED-TO');
269
-					if (!($delegates instanceof VObject\Parameter)) {
270
-						continue;
271
-					}
272
-
273
-					$emailAddressesOfDelegates = $delegates->getParts();
274
-					foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
275
-						if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
276
-							$emailAddresses[substr($addressesOfDelegate, 7)] = [];
277
-						}
278
-					}
279
-
280
-					continue;
281
-				}
282
-
283
-				$emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
284
-				if ($emailAddressOfAttendee !== null) {
285
-					$properties = [];
286
-
287
-					$langProp = $attendee->offsetGet('LANG');
288
-					if ($langProp instanceof VObject\Parameter) {
289
-						$properties['LANG'] = $langProp->getValue();
290
-					}
291
-
292
-					$emailAddresses[$emailAddressOfAttendee] = $properties;
293
-				}
294
-			}
295
-		}
296
-
297
-		if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
298
-			$emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
299
-		}
300
-
301
-		return $emailAddresses;
302
-	}
303
-
304
-
305
-
306
-	/**
307
-	 * @param VObject\Property $attendee
308
-	 * @return string
309
-	 */
310
-	private function getCUTypeOfAttendee(VObject\Property $attendee):string {
311
-		$cuType = $attendee->offsetGet('CUTYPE');
312
-		if ($cuType instanceof VObject\Parameter) {
313
-			return strtoupper($cuType->getValue());
314
-		}
315
-
316
-		return 'INDIVIDUAL';
317
-	}
318
-
319
-	/**
320
-	 * @param VObject\Property $attendee
321
-	 * @return string
322
-	 */
323
-	private function getPartstatOfAttendee(VObject\Property $attendee):string {
324
-		$partstat = $attendee->offsetGet('PARTSTAT');
325
-		if ($partstat instanceof VObject\Parameter) {
326
-			return strtoupper($partstat->getValue());
327
-		}
328
-
329
-		return 'NEEDS-ACTION';
330
-	}
331
-
332
-	/**
333
-	 * @param VObject\Property $attendee
334
-	 * @return bool
335
-	 */
336
-	private function hasAttendeeMailURI(VObject\Property $attendee):bool {
337
-		return stripos($attendee->getValue(), 'mailto:') === 0;
338
-	}
339
-
340
-	/**
341
-	 * @param VObject\Property $attendee
342
-	 * @return string|null
343
-	 */
344
-	private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
345
-		if (!$this->hasAttendeeMailURI($attendee)) {
346
-			return null;
347
-		}
348
-
349
-		return substr($attendee->getValue(), 7);
350
-	}
351
-
352
-	/**
353
-	 * @param array $users
354
-	 * @return array
355
-	 */
356
-	private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
357
-		$emailAddresses = [];
358
-
359
-		foreach ($users as $user) {
360
-			$emailAddress = $user->getEMailAddress();
361
-			if ($emailAddress) {
362
-				$lang = $this->l10nFactory->getUserLanguage($user);
363
-				if ($lang) {
364
-					$emailAddresses[$emailAddress] = [
365
-						'LANG' => $lang,
366
-					];
367
-				} else {
368
-					$emailAddresses[$emailAddress] = [];
369
-				}
370
-			}
371
-		}
372
-
373
-		return $emailAddresses;
374
-	}
375
-
376
-	/**
377
-	 * @param IL10N $l10n
378
-	 * @param VEvent $vevent
379
-	 * @return string
380
-	 * @throws \Exception
381
-	 */
382
-	private function generateDateString(IL10N $l10n, VEvent $vevent):string {
383
-		$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
384
-
385
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
386
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
387
-		/** @var \DateTimeImmutable $dtstartDt */
388
-		$dtstartDt = $vevent->DTSTART->getDateTime();
389
-		/** @var \DateTimeImmutable $dtendDt */
390
-		$dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
391
-
392
-		$diff = $dtstartDt->diff($dtendDt);
393
-
394
-		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
395
-		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
396
-
397
-		if ($isAllDay) {
398
-			// One day event
399
-			if ($diff->days === 1) {
400
-				return $this->getDateString($l10n, $dtstartDt);
401
-			}
402
-
403
-			return implode(' - ', [
404
-				$this->getDateString($l10n, $dtstartDt),
405
-				$this->getDateString($l10n, $dtendDt),
406
-			]);
407
-		}
408
-
409
-		$startTimezone = $endTimezone = null;
410
-		if (!$vevent->DTSTART->isFloating()) {
411
-			$startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
412
-			$endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
413
-		}
414
-
415
-		$localeStart = implode(', ', [
416
-			$this->getWeekDayName($l10n, $dtstartDt),
417
-			$this->getDateTimeString($l10n, $dtstartDt)
418
-		]);
419
-
420
-		// always show full date with timezone if timezones are different
421
-		if ($startTimezone !== $endTimezone) {
422
-			$localeEnd = implode(', ', [
423
-				$this->getWeekDayName($l10n, $dtendDt),
424
-				$this->getDateTimeString($l10n, $dtendDt)
425
-			]);
426
-
427
-			return $localeStart
428
-				. ' (' . $startTimezone . ') '
429
-				. ' - '
430
-				. $localeEnd
431
-				. ' (' . $endTimezone . ')';
432
-		}
433
-
434
-		// Show only the time if the day is the same
435
-		$localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
436
-			? $this->getTimeString($l10n, $dtendDt)
437
-			: implode(', ', [
438
-				$this->getWeekDayName($l10n, $dtendDt),
439
-				$this->getDateTimeString($l10n, $dtendDt)
440
-			]);
441
-
442
-		return $localeStart
443
-			. ' - '
444
-			. $localeEnd
445
-			. ' (' . $startTimezone . ')';
446
-	}
447
-
448
-	/**
449
-	 * @param DateTime $dtStart
450
-	 * @param DateTime $dtEnd
451
-	 * @return bool
452
-	 */
453
-	private function isDayEqual(DateTime $dtStart,
454
-								DateTime $dtEnd):bool {
455
-		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
456
-	}
457
-
458
-	/**
459
-	 * @param IL10N $l10n
460
-	 * @param DateTime $dt
461
-	 * @return string
462
-	 */
463
-	private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
464
-		return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
465
-	}
466
-
467
-	/**
468
-	 * @param IL10N $l10n
469
-	 * @param DateTime $dt
470
-	 * @return string
471
-	 */
472
-	private function getDateString(IL10N $l10n, DateTime $dt):string {
473
-		return $l10n->l('date', $dt, ['width' => 'medium']);
474
-	}
475
-
476
-	/**
477
-	 * @param IL10N $l10n
478
-	 * @param DateTime $dt
479
-	 * @return string
480
-	 */
481
-	private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
482
-		return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
483
-	}
484
-
485
-	/**
486
-	 * @param IL10N $l10n
487
-	 * @param DateTime $dt
488
-	 * @return string
489
-	 */
490
-	private function getTimeString(IL10N $l10n, DateTime $dt):string {
491
-		return $l10n->l('time', $dt, ['width' => 'short']);
492
-	}
493
-
494
-	/**
495
-	 * @param VEvent $vevent
496
-	 * @param IL10N $l10n
497
-	 * @return string
498
-	 */
499
-	private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
500
-		if (isset($vevent->SUMMARY)) {
501
-			return (string)$vevent->SUMMARY;
502
-		}
503
-
504
-		return $l10n->t('Untitled event');
505
-	}
55
+    /** @var string */
56
+    public const NOTIFICATION_TYPE = 'EMAIL';
57
+
58
+    /** @var IMailer */
59
+    private $mailer;
60
+
61
+    /**
62
+     * @param IConfig $config
63
+     * @param IMailer $mailer
64
+     * @param ILogger $logger
65
+     * @param L10NFactory $l10nFactory
66
+     * @param IUrlGenerator $urlGenerator
67
+     */
68
+    public function __construct(IConfig $config,
69
+                                IMailer $mailer,
70
+                                ILogger $logger,
71
+                                L10NFactory $l10nFactory,
72
+                                IURLGenerator $urlGenerator) {
73
+        parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
74
+        $this->mailer = $mailer;
75
+    }
76
+
77
+    /**
78
+     * Send out notification via email
79
+     *
80
+     * @param VEvent $vevent
81
+     * @param string $calendarDisplayName
82
+     * @param array $users
83
+     * @throws \Exception
84
+     */
85
+    public function send(VEvent $vevent,
86
+                            string $calendarDisplayName,
87
+                            array $users = []):void {
88
+        $fallbackLanguage = $this->getFallbackLanguage();
89
+
90
+        $emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
91
+        $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
92
+
93
+        // Quote from php.net:
94
+        // If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
95
+        // => if there are duplicate email addresses, it will always take the system value
96
+        $emailAddresses = array_merge(
97
+            $emailAddressesOfAttendees,
98
+            $emailAddressesOfSharees
99
+        );
100
+
101
+        $sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
102
+        $organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
103
+
104
+        foreach ($sortedByLanguage as $lang => $emailAddresses) {
105
+            if (!$this->hasL10NForLang($lang)) {
106
+                $lang = $fallbackLanguage;
107
+            }
108
+            $l10n = $this->getL10NForLang($lang);
109
+            $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
110
+
111
+            $template = $this->mailer->createEMailTemplate('dav.calendarReminder');
112
+            $template->addHeader();
113
+            $this->addSubjectAndHeading($template, $l10n, $vevent);
114
+            $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
115
+            $template->addFooter();
116
+
117
+            foreach ($emailAddresses as $emailAddress) {
118
+                if (!$this->mailer->validateMailAddress($emailAddress)) {
119
+                    $this->logger->error('Email address {address} for reminder notification is incorrect', ['app' => 'dav', 'address' => $emailAddress]);
120
+                    continue;
121
+                }
122
+
123
+                $message = $this->mailer->createMessage();
124
+                $message->setFrom([$fromEMail]);
125
+                if ($organizer) {
126
+                    $message->setReplyTo($organizer);
127
+                }
128
+                $message->setTo([$emailAddress]);
129
+                $message->useTemplate($template);
130
+
131
+                try {
132
+                    $failed = $this->mailer->send($message);
133
+                    if ($failed) {
134
+                        $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
135
+                    }
136
+                } catch (\Exception $ex) {
137
+                    $this->logger->logException($ex, ['app' => 'dav']);
138
+                }
139
+            }
140
+        }
141
+    }
142
+
143
+    /**
144
+     * @param IEMailTemplate $template
145
+     * @param IL10N $l10n
146
+     * @param VEvent $vevent
147
+     */
148
+    private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
149
+        $template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
150
+        $template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
151
+    }
152
+
153
+    /**
154
+     * @param IEMailTemplate $template
155
+     * @param IL10N $l10n
156
+     * @param string $calendarDisplayName
157
+     * @param array $eventData
158
+     */
159
+    private function addBulletList(IEMailTemplate $template,
160
+                                    IL10N $l10n,
161
+                                    string $calendarDisplayName,
162
+                                    VEvent $vevent):void {
163
+        $template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
164
+            $this->getAbsoluteImagePath('actions/info.png'));
165
+
166
+        $template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
167
+            $this->getAbsoluteImagePath('places/calendar.png'));
168
+
169
+        if (isset($vevent->LOCATION)) {
170
+            $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
171
+                $this->getAbsoluteImagePath('actions/address.png'));
172
+        }
173
+        if (isset($vevent->DESCRIPTION)) {
174
+            $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
175
+                $this->getAbsoluteImagePath('actions/more.png'));
176
+        }
177
+    }
178
+
179
+    /**
180
+     * @param string $path
181
+     * @return string
182
+     */
183
+    private function getAbsoluteImagePath(string $path):string {
184
+        return $this->urlGenerator->getAbsoluteURL(
185
+            $this->urlGenerator->imagePath('core', $path)
186
+        );
187
+    }
188
+
189
+    /**
190
+     * @param VEvent $vevent
191
+     * @return array|null
192
+     */
193
+    private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
194
+        if (!$vevent->ORGANIZER) {
195
+            return null;
196
+        }
197
+
198
+        $organizer = $vevent->ORGANIZER;
199
+        if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
200
+            return null;
201
+        }
202
+
203
+        $organizerEMail = substr($organizer->getValue(), 7);
204
+
205
+        if ($organizerEMail === false || !$this->mailer->validateMailAddress($organizerEMail)) {
206
+            return null;
207
+        }
208
+
209
+        $name = $organizer->offsetGet('CN');
210
+        if ($name instanceof Parameter) {
211
+            return [$organizerEMail => $name];
212
+        }
213
+
214
+        return [$organizerEMail];
215
+    }
216
+
217
+    /**
218
+     * @param array $emails
219
+     * @param string $defaultLanguage
220
+     * @return array
221
+     */
222
+    private function sortEMailAddressesByLanguage(array $emails,
223
+                                                    string $defaultLanguage):array {
224
+        $sortedByLanguage = [];
225
+
226
+        foreach ($emails as $emailAddress => $parameters) {
227
+            if (isset($parameters['LANG'])) {
228
+                $lang = $parameters['LANG'];
229
+            } else {
230
+                $lang = $defaultLanguage;
231
+            }
232
+
233
+            if (!isset($sortedByLanguage[$lang])) {
234
+                $sortedByLanguage[$lang] = [];
235
+            }
236
+
237
+            $sortedByLanguage[$lang][] = $emailAddress;
238
+        }
239
+
240
+        return $sortedByLanguage;
241
+    }
242
+
243
+    /**
244
+     * @param VEvent $vevent
245
+     * @return array
246
+     */
247
+    private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
248
+        $emailAddresses = [];
249
+
250
+        if (isset($vevent->ATTENDEE)) {
251
+            foreach ($vevent->ATTENDEE as $attendee) {
252
+                if (!($attendee instanceof VObject\Property)) {
253
+                    continue;
254
+                }
255
+
256
+                $cuType = $this->getCUTypeOfAttendee($attendee);
257
+                if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
258
+                    // Don't send emails to things
259
+                    continue;
260
+                }
261
+
262
+                $partstat = $this->getPartstatOfAttendee($attendee);
263
+                if ($partstat === 'DECLINED') {
264
+                    // Don't send out emails to people who declined
265
+                    continue;
266
+                }
267
+                if ($partstat === 'DELEGATED') {
268
+                    $delegates = $attendee->offsetGet('DELEGATED-TO');
269
+                    if (!($delegates instanceof VObject\Parameter)) {
270
+                        continue;
271
+                    }
272
+
273
+                    $emailAddressesOfDelegates = $delegates->getParts();
274
+                    foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
275
+                        if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
276
+                            $emailAddresses[substr($addressesOfDelegate, 7)] = [];
277
+                        }
278
+                    }
279
+
280
+                    continue;
281
+                }
282
+
283
+                $emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
284
+                if ($emailAddressOfAttendee !== null) {
285
+                    $properties = [];
286
+
287
+                    $langProp = $attendee->offsetGet('LANG');
288
+                    if ($langProp instanceof VObject\Parameter) {
289
+                        $properties['LANG'] = $langProp->getValue();
290
+                    }
291
+
292
+                    $emailAddresses[$emailAddressOfAttendee] = $properties;
293
+                }
294
+            }
295
+        }
296
+
297
+        if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
298
+            $emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
299
+        }
300
+
301
+        return $emailAddresses;
302
+    }
303
+
304
+
305
+
306
+    /**
307
+     * @param VObject\Property $attendee
308
+     * @return string
309
+     */
310
+    private function getCUTypeOfAttendee(VObject\Property $attendee):string {
311
+        $cuType = $attendee->offsetGet('CUTYPE');
312
+        if ($cuType instanceof VObject\Parameter) {
313
+            return strtoupper($cuType->getValue());
314
+        }
315
+
316
+        return 'INDIVIDUAL';
317
+    }
318
+
319
+    /**
320
+     * @param VObject\Property $attendee
321
+     * @return string
322
+     */
323
+    private function getPartstatOfAttendee(VObject\Property $attendee):string {
324
+        $partstat = $attendee->offsetGet('PARTSTAT');
325
+        if ($partstat instanceof VObject\Parameter) {
326
+            return strtoupper($partstat->getValue());
327
+        }
328
+
329
+        return 'NEEDS-ACTION';
330
+    }
331
+
332
+    /**
333
+     * @param VObject\Property $attendee
334
+     * @return bool
335
+     */
336
+    private function hasAttendeeMailURI(VObject\Property $attendee):bool {
337
+        return stripos($attendee->getValue(), 'mailto:') === 0;
338
+    }
339
+
340
+    /**
341
+     * @param VObject\Property $attendee
342
+     * @return string|null
343
+     */
344
+    private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
345
+        if (!$this->hasAttendeeMailURI($attendee)) {
346
+            return null;
347
+        }
348
+
349
+        return substr($attendee->getValue(), 7);
350
+    }
351
+
352
+    /**
353
+     * @param array $users
354
+     * @return array
355
+     */
356
+    private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
357
+        $emailAddresses = [];
358
+
359
+        foreach ($users as $user) {
360
+            $emailAddress = $user->getEMailAddress();
361
+            if ($emailAddress) {
362
+                $lang = $this->l10nFactory->getUserLanguage($user);
363
+                if ($lang) {
364
+                    $emailAddresses[$emailAddress] = [
365
+                        'LANG' => $lang,
366
+                    ];
367
+                } else {
368
+                    $emailAddresses[$emailAddress] = [];
369
+                }
370
+            }
371
+        }
372
+
373
+        return $emailAddresses;
374
+    }
375
+
376
+    /**
377
+     * @param IL10N $l10n
378
+     * @param VEvent $vevent
379
+     * @return string
380
+     * @throws \Exception
381
+     */
382
+    private function generateDateString(IL10N $l10n, VEvent $vevent):string {
383
+        $isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
384
+
385
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
386
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
387
+        /** @var \DateTimeImmutable $dtstartDt */
388
+        $dtstartDt = $vevent->DTSTART->getDateTime();
389
+        /** @var \DateTimeImmutable $dtendDt */
390
+        $dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
391
+
392
+        $diff = $dtstartDt->diff($dtendDt);
393
+
394
+        $dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
395
+        $dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
396
+
397
+        if ($isAllDay) {
398
+            // One day event
399
+            if ($diff->days === 1) {
400
+                return $this->getDateString($l10n, $dtstartDt);
401
+            }
402
+
403
+            return implode(' - ', [
404
+                $this->getDateString($l10n, $dtstartDt),
405
+                $this->getDateString($l10n, $dtendDt),
406
+            ]);
407
+        }
408
+
409
+        $startTimezone = $endTimezone = null;
410
+        if (!$vevent->DTSTART->isFloating()) {
411
+            $startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
412
+            $endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
413
+        }
414
+
415
+        $localeStart = implode(', ', [
416
+            $this->getWeekDayName($l10n, $dtstartDt),
417
+            $this->getDateTimeString($l10n, $dtstartDt)
418
+        ]);
419
+
420
+        // always show full date with timezone if timezones are different
421
+        if ($startTimezone !== $endTimezone) {
422
+            $localeEnd = implode(', ', [
423
+                $this->getWeekDayName($l10n, $dtendDt),
424
+                $this->getDateTimeString($l10n, $dtendDt)
425
+            ]);
426
+
427
+            return $localeStart
428
+                . ' (' . $startTimezone . ') '
429
+                . ' - '
430
+                . $localeEnd
431
+                . ' (' . $endTimezone . ')';
432
+        }
433
+
434
+        // Show only the time if the day is the same
435
+        $localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
436
+            ? $this->getTimeString($l10n, $dtendDt)
437
+            : implode(', ', [
438
+                $this->getWeekDayName($l10n, $dtendDt),
439
+                $this->getDateTimeString($l10n, $dtendDt)
440
+            ]);
441
+
442
+        return $localeStart
443
+            . ' - '
444
+            . $localeEnd
445
+            . ' (' . $startTimezone . ')';
446
+    }
447
+
448
+    /**
449
+     * @param DateTime $dtStart
450
+     * @param DateTime $dtEnd
451
+     * @return bool
452
+     */
453
+    private function isDayEqual(DateTime $dtStart,
454
+                                DateTime $dtEnd):bool {
455
+        return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
456
+    }
457
+
458
+    /**
459
+     * @param IL10N $l10n
460
+     * @param DateTime $dt
461
+     * @return string
462
+     */
463
+    private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
464
+        return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
465
+    }
466
+
467
+    /**
468
+     * @param IL10N $l10n
469
+     * @param DateTime $dt
470
+     * @return string
471
+     */
472
+    private function getDateString(IL10N $l10n, DateTime $dt):string {
473
+        return $l10n->l('date', $dt, ['width' => 'medium']);
474
+    }
475
+
476
+    /**
477
+     * @param IL10N $l10n
478
+     * @param DateTime $dt
479
+     * @return string
480
+     */
481
+    private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
482
+        return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
483
+    }
484
+
485
+    /**
486
+     * @param IL10N $l10n
487
+     * @param DateTime $dt
488
+     * @return string
489
+     */
490
+    private function getTimeString(IL10N $l10n, DateTime $dt):string {
491
+        return $l10n->l('time', $dt, ['width' => 'short']);
492
+    }
493
+
494
+    /**
495
+     * @param VEvent $vevent
496
+     * @param IL10N $l10n
497
+     * @return string
498
+     */
499
+    private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
500
+        if (isset($vevent->SUMMARY)) {
501
+            return (string)$vevent->SUMMARY;
502
+        }
503
+
504
+        return $l10n->t('Untitled event');
505
+    }
506 506
 }
Please login to merge, or discard this patch.
apps/dav/lib/CalDAV/Schedule/IMipPlugin.php 2 patches
Indentation   +624 added lines, -624 removed lines patch added patch discarded remove patch
@@ -70,194 +70,194 @@  discard block
 block discarded – undo
70 70
  */
71 71
 class IMipPlugin extends SabreIMipPlugin {
72 72
 
73
-	/** @var string */
74
-	private $userId;
75
-
76
-	/** @var IConfig */
77
-	private $config;
78
-
79
-	/** @var IMailer */
80
-	private $mailer;
81
-
82
-	/** @var ILogger */
83
-	private $logger;
84
-
85
-	/** @var ITimeFactory */
86
-	private $timeFactory;
87
-
88
-	/** @var L10NFactory */
89
-	private $l10nFactory;
90
-
91
-	/** @var IURLGenerator */
92
-	private $urlGenerator;
93
-
94
-	/** @var ISecureRandom */
95
-	private $random;
96
-
97
-	/** @var IDBConnection */
98
-	private $db;
99
-
100
-	/** @var Defaults */
101
-	private $defaults;
102
-
103
-	/** @var IUserManager */
104
-	private $userManager;
105
-
106
-	public const MAX_DATE = '2038-01-01';
107
-
108
-	public const METHOD_REQUEST = 'request';
109
-	public const METHOD_REPLY = 'reply';
110
-	public const METHOD_CANCEL = 'cancel';
111
-	public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
112
-
113
-	/**
114
-	 * @param IConfig $config
115
-	 * @param IMailer $mailer
116
-	 * @param ILogger $logger
117
-	 * @param ITimeFactory $timeFactory
118
-	 * @param L10NFactory $l10nFactory
119
-	 * @param IUrlGenerator $urlGenerator
120
-	 * @param Defaults $defaults
121
-	 * @param ISecureRandom $random
122
-	 * @param IDBConnection $db
123
-	 * @param string $userId
124
-	 */
125
-	public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
126
-								ITimeFactory $timeFactory, L10NFactory $l10nFactory,
127
-								IURLGenerator $urlGenerator, Defaults $defaults,
128
-								ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
129
-								$userId) {
130
-		parent::__construct('');
131
-		$this->userId = $userId;
132
-		$this->config = $config;
133
-		$this->mailer = $mailer;
134
-		$this->logger = $logger;
135
-		$this->timeFactory = $timeFactory;
136
-		$this->l10nFactory = $l10nFactory;
137
-		$this->urlGenerator = $urlGenerator;
138
-		$this->random = $random;
139
-		$this->db = $db;
140
-		$this->defaults = $defaults;
141
-		$this->userManager = $userManager;
142
-	}
143
-
144
-	/**
145
-	 * Event handler for the 'schedule' event.
146
-	 *
147
-	 * @param Message $iTipMessage
148
-	 * @return void
149
-	 */
150
-	public function schedule(Message $iTipMessage) {
151
-
152
-		// Not sending any emails if the system considers the update
153
-		// insignificant.
154
-		if (!$iTipMessage->significantChange) {
155
-			if (!$iTipMessage->scheduleStatus) {
156
-				$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
157
-			}
158
-			return;
159
-		}
160
-
161
-		$summary = $iTipMessage->message->VEVENT->SUMMARY;
162
-
163
-		if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
164
-			return;
165
-		}
166
-
167
-		if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
168
-			return;
169
-		}
170
-
171
-		// don't send out mails for events that already took place
172
-		$lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
173
-		$currentTime = $this->timeFactory->getTime();
174
-		if ($lastOccurrence < $currentTime) {
175
-			return;
176
-		}
177
-
178
-		// Strip off mailto:
179
-		$sender = substr($iTipMessage->sender, 7);
180
-		$recipient = substr($iTipMessage->recipient, 7);
181
-		if ($recipient === false || !$this->mailer->validateMailAddress($recipient)) {
182
-			// Nothing to send if the recipient doesn't have a valid email address
183
-			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
184
-			return;
185
-		}
186
-
187
-		$senderName = $iTipMessage->senderName ?: null;
188
-		$recipientName = $iTipMessage->recipientName ?: null;
189
-
190
-		if ($senderName === null || empty(trim($senderName))) {
191
-			$user = $this->userManager->get($this->userId);
192
-			if ($user) {
193
-				// getDisplayName automatically uses the uid
194
-				// if no display-name is set
195
-				$senderName = $user->getDisplayName();
196
-			}
197
-		}
198
-
199
-		/** @var VEvent $vevent */
200
-		$vevent = $iTipMessage->message->VEVENT;
201
-
202
-		$attendee = $this->getCurrentAttendee($iTipMessage);
203
-		$defaultLang = $this->l10nFactory->findLanguage();
204
-		$lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
205
-		$l10n = $this->l10nFactory->get('dav', $lang);
206
-
207
-		$meetingAttendeeName = $recipientName ?: $recipient;
208
-		$meetingInviteeName = $senderName ?: $sender;
209
-
210
-		$meetingTitle = $vevent->SUMMARY;
211
-		$meetingDescription = $vevent->DESCRIPTION;
212
-
213
-
214
-		$meetingUrl = $vevent->URL;
215
-		$meetingLocation = $vevent->LOCATION;
216
-
217
-		$defaultVal = '--';
218
-
219
-		$method = self::METHOD_REQUEST;
220
-		switch (strtolower($iTipMessage->method)) {
221
-			case self::METHOD_REPLY:
222
-				$method = self::METHOD_REPLY;
223
-				break;
224
-			case self::METHOD_CANCEL:
225
-				$method = self::METHOD_CANCEL;
226
-				break;
227
-		}
228
-
229
-		$data = [
230
-			'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
231
-			'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
232
-			'meeting_title' => (string)$meetingTitle ?: $defaultVal,
233
-			'meeting_description' => (string)$meetingDescription ?: $defaultVal,
234
-			'meeting_url' => (string)$meetingUrl ?: $defaultVal,
235
-		];
236
-
237
-		$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
238
-		$fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
239
-
240
-		$message = $this->mailer->createMessage()
241
-			->setFrom([$fromEMail => $fromName])
242
-			->setTo([$recipient => $recipientName]);
243
-
244
-		if ($sender !== false) {
245
-			$message->setReplyTo([$sender => $senderName]);
246
-		}
247
-
248
-		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
249
-		$template->addHeader();
250
-
251
-		$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
252
-
253
-		$this->addSubjectAndHeading($template, $l10n, $method, $summary);
254
-		$this->addBulletList($template, $l10n, $vevent);
255
-
256
-
257
-		// Only add response buttons to invitation requests: Fix Issue #11230
258
-		if (($method == self::METHOD_REQUEST) && $this->getAttendeeRSVP($attendee)) {
73
+    /** @var string */
74
+    private $userId;
75
+
76
+    /** @var IConfig */
77
+    private $config;
78
+
79
+    /** @var IMailer */
80
+    private $mailer;
81
+
82
+    /** @var ILogger */
83
+    private $logger;
84
+
85
+    /** @var ITimeFactory */
86
+    private $timeFactory;
87
+
88
+    /** @var L10NFactory */
89
+    private $l10nFactory;
90
+
91
+    /** @var IURLGenerator */
92
+    private $urlGenerator;
93
+
94
+    /** @var ISecureRandom */
95
+    private $random;
96
+
97
+    /** @var IDBConnection */
98
+    private $db;
99
+
100
+    /** @var Defaults */
101
+    private $defaults;
102
+
103
+    /** @var IUserManager */
104
+    private $userManager;
105
+
106
+    public const MAX_DATE = '2038-01-01';
107
+
108
+    public const METHOD_REQUEST = 'request';
109
+    public const METHOD_REPLY = 'reply';
110
+    public const METHOD_CANCEL = 'cancel';
111
+    public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
112
+
113
+    /**
114
+     * @param IConfig $config
115
+     * @param IMailer $mailer
116
+     * @param ILogger $logger
117
+     * @param ITimeFactory $timeFactory
118
+     * @param L10NFactory $l10nFactory
119
+     * @param IUrlGenerator $urlGenerator
120
+     * @param Defaults $defaults
121
+     * @param ISecureRandom $random
122
+     * @param IDBConnection $db
123
+     * @param string $userId
124
+     */
125
+    public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
126
+                                ITimeFactory $timeFactory, L10NFactory $l10nFactory,
127
+                                IURLGenerator $urlGenerator, Defaults $defaults,
128
+                                ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
129
+                                $userId) {
130
+        parent::__construct('');
131
+        $this->userId = $userId;
132
+        $this->config = $config;
133
+        $this->mailer = $mailer;
134
+        $this->logger = $logger;
135
+        $this->timeFactory = $timeFactory;
136
+        $this->l10nFactory = $l10nFactory;
137
+        $this->urlGenerator = $urlGenerator;
138
+        $this->random = $random;
139
+        $this->db = $db;
140
+        $this->defaults = $defaults;
141
+        $this->userManager = $userManager;
142
+    }
143
+
144
+    /**
145
+     * Event handler for the 'schedule' event.
146
+     *
147
+     * @param Message $iTipMessage
148
+     * @return void
149
+     */
150
+    public function schedule(Message $iTipMessage) {
151
+
152
+        // Not sending any emails if the system considers the update
153
+        // insignificant.
154
+        if (!$iTipMessage->significantChange) {
155
+            if (!$iTipMessage->scheduleStatus) {
156
+                $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
157
+            }
158
+            return;
159
+        }
160
+
161
+        $summary = $iTipMessage->message->VEVENT->SUMMARY;
162
+
163
+        if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
164
+            return;
165
+        }
166
+
167
+        if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
168
+            return;
169
+        }
170
+
171
+        // don't send out mails for events that already took place
172
+        $lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
173
+        $currentTime = $this->timeFactory->getTime();
174
+        if ($lastOccurrence < $currentTime) {
175
+            return;
176
+        }
177
+
178
+        // Strip off mailto:
179
+        $sender = substr($iTipMessage->sender, 7);
180
+        $recipient = substr($iTipMessage->recipient, 7);
181
+        if ($recipient === false || !$this->mailer->validateMailAddress($recipient)) {
182
+            // Nothing to send if the recipient doesn't have a valid email address
183
+            $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
184
+            return;
185
+        }
186
+
187
+        $senderName = $iTipMessage->senderName ?: null;
188
+        $recipientName = $iTipMessage->recipientName ?: null;
189
+
190
+        if ($senderName === null || empty(trim($senderName))) {
191
+            $user = $this->userManager->get($this->userId);
192
+            if ($user) {
193
+                // getDisplayName automatically uses the uid
194
+                // if no display-name is set
195
+                $senderName = $user->getDisplayName();
196
+            }
197
+        }
198
+
199
+        /** @var VEvent $vevent */
200
+        $vevent = $iTipMessage->message->VEVENT;
201
+
202
+        $attendee = $this->getCurrentAttendee($iTipMessage);
203
+        $defaultLang = $this->l10nFactory->findLanguage();
204
+        $lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
205
+        $l10n = $this->l10nFactory->get('dav', $lang);
206
+
207
+        $meetingAttendeeName = $recipientName ?: $recipient;
208
+        $meetingInviteeName = $senderName ?: $sender;
209
+
210
+        $meetingTitle = $vevent->SUMMARY;
211
+        $meetingDescription = $vevent->DESCRIPTION;
212
+
213
+
214
+        $meetingUrl = $vevent->URL;
215
+        $meetingLocation = $vevent->LOCATION;
216
+
217
+        $defaultVal = '--';
218
+
219
+        $method = self::METHOD_REQUEST;
220
+        switch (strtolower($iTipMessage->method)) {
221
+            case self::METHOD_REPLY:
222
+                $method = self::METHOD_REPLY;
223
+                break;
224
+            case self::METHOD_CANCEL:
225
+                $method = self::METHOD_CANCEL;
226
+                break;
227
+        }
228
+
229
+        $data = [
230
+            'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
231
+            'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
232
+            'meeting_title' => (string)$meetingTitle ?: $defaultVal,
233
+            'meeting_description' => (string)$meetingDescription ?: $defaultVal,
234
+            'meeting_url' => (string)$meetingUrl ?: $defaultVal,
235
+        ];
236
+
237
+        $fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
238
+        $fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
239
+
240
+        $message = $this->mailer->createMessage()
241
+            ->setFrom([$fromEMail => $fromName])
242
+            ->setTo([$recipient => $recipientName]);
243
+
244
+        if ($sender !== false) {
245
+            $message->setReplyTo([$sender => $senderName]);
246
+        }
247
+
248
+        $template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
249
+        $template->addHeader();
250
+
251
+        $summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
252
+
253
+        $this->addSubjectAndHeading($template, $l10n, $method, $summary);
254
+        $this->addBulletList($template, $l10n, $vevent);
255
+
256
+
257
+        // Only add response buttons to invitation requests: Fix Issue #11230
258
+        if (($method == self::METHOD_REQUEST) && $this->getAttendeeRSVP($attendee)) {
259 259
 
260
-			/*
260
+            /*
261 261
 			** Only offer invitation accept/reject buttons, which link back to the
262 262
 			** nextcloud server, to recipients who can access the nextcloud server via
263 263
 			** their internet/intranet.  Issue #12156
@@ -276,441 +276,441 @@  discard block
 block discarded – undo
276 276
 			** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
277 277
 			*/
278 278
 
279
-			$recipientDomain = substr(strrchr($recipient, "@"), 1);
280
-			$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
281
-
282
-			if (strcmp('yes', $invitationLinkRecipients[0]) === 0
283
-				 || in_array(strtolower($recipient), $invitationLinkRecipients)
284
-				 || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
285
-				$this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
286
-			}
287
-		}
288
-
289
-		$template->addFooter();
290
-
291
-		$message->useTemplate($template);
292
-
293
-		$attachment = $this->mailer->createAttachment(
294
-			$iTipMessage->message->serialize(),
295
-			'event.ics',// TODO(leon): Make file name unique, e.g. add event id
296
-			'text/calendar; method=' . $iTipMessage->method
297
-		);
298
-		$message->attach($attachment);
299
-
300
-		try {
301
-			$failed = $this->mailer->send($message);
302
-			$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
303
-			if ($failed) {
304
-				$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
305
-				$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
306
-			}
307
-		} catch (\Exception $ex) {
308
-			$this->logger->logException($ex, ['app' => 'dav']);
309
-			$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
310
-		}
311
-	}
312
-
313
-	/**
314
-	 * check if event took place in the past already
315
-	 * @param VCalendar $vObject
316
-	 * @return int
317
-	 */
318
-	private function getLastOccurrence(VCalendar $vObject) {
319
-		/** @var VEvent $component */
320
-		$component = $vObject->VEVENT;
321
-
322
-		$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
323
-		// Finding the last occurrence is a bit harder
324
-		if (!isset($component->RRULE)) {
325
-			if (isset($component->DTEND)) {
326
-				$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
327
-			} elseif (isset($component->DURATION)) {
328
-				/** @var \DateTime $endDate */
329
-				$endDate = clone $component->DTSTART->getDateTime();
330
-				// $component->DTEND->getDateTime() returns DateTimeImmutable
331
-				$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
332
-				$lastOccurrence = $endDate->getTimestamp();
333
-			} elseif (!$component->DTSTART->hasTime()) {
334
-				/** @var \DateTime $endDate */
335
-				$endDate = clone $component->DTSTART->getDateTime();
336
-				// $component->DTSTART->getDateTime() returns DateTimeImmutable
337
-				$endDate = $endDate->modify('+1 day');
338
-				$lastOccurrence = $endDate->getTimestamp();
339
-			} else {
340
-				$lastOccurrence = $firstOccurrence;
341
-			}
342
-		} else {
343
-			$it = new EventIterator($vObject, (string)$component->UID);
344
-			$maxDate = new \DateTime(self::MAX_DATE);
345
-			if ($it->isInfinite()) {
346
-				$lastOccurrence = $maxDate->getTimestamp();
347
-			} else {
348
-				$end = $it->getDtEnd();
349
-				while ($it->valid() && $end < $maxDate) {
350
-					$end = $it->getDtEnd();
351
-					$it->next();
352
-				}
353
-				$lastOccurrence = $end->getTimestamp();
354
-			}
355
-		}
356
-
357
-		return $lastOccurrence;
358
-	}
359
-
360
-	/**
361
-	 * @param Message $iTipMessage
362
-	 * @return null|Property
363
-	 */
364
-	private function getCurrentAttendee(Message $iTipMessage) {
365
-		/** @var VEvent $vevent */
366
-		$vevent = $iTipMessage->message->VEVENT;
367
-		$attendees = $vevent->select('ATTENDEE');
368
-		foreach ($attendees as $attendee) {
369
-			/** @var Property $attendee */
370
-			if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
371
-				return $attendee;
372
-			}
373
-		}
374
-		return null;
375
-	}
376
-
377
-	/**
378
-	 * @param string $default
379
-	 * @param Property|null $attendee
380
-	 * @return string
381
-	 */
382
-	private function getAttendeeLangOrDefault($default, Property $attendee = null) {
383
-		if ($attendee !== null) {
384
-			$lang = $attendee->offsetGet('LANGUAGE');
385
-			if ($lang instanceof Parameter) {
386
-				return $lang->getValue();
387
-			}
388
-		}
389
-		return $default;
390
-	}
391
-
392
-	/**
393
-	 * @param Property|null $attendee
394
-	 * @return bool
395
-	 */
396
-	private function getAttendeeRSVP(Property $attendee = null) {
397
-		if ($attendee !== null) {
398
-			$rsvp = $attendee->offsetGet('RSVP');
399
-			if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
400
-				return true;
401
-			}
402
-		}
403
-		// RFC 5545 3.2.17: default RSVP is false
404
-		return false;
405
-	}
406
-
407
-	/**
408
-	 * @param IL10N $l10n
409
-	 * @param VEvent $vevent
410
-	 */
411
-	private function generateWhenString(IL10N $l10n, VEvent $vevent) {
412
-		$dtstart = $vevent->DTSTART;
413
-		if (isset($vevent->DTEND)) {
414
-			$dtend = $vevent->DTEND;
415
-		} elseif (isset($vevent->DURATION)) {
416
-			$isFloating = $vevent->DTSTART->isFloating();
417
-			$dtend = clone $vevent->DTSTART;
418
-			$endDateTime = $dtend->getDateTime();
419
-			$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
420
-			$dtend->setDateTime($endDateTime, $isFloating);
421
-		} elseif (!$vevent->DTSTART->hasTime()) {
422
-			$isFloating = $vevent->DTSTART->isFloating();
423
-			$dtend = clone $vevent->DTSTART;
424
-			$endDateTime = $dtend->getDateTime();
425
-			$endDateTime = $endDateTime->modify('+1 day');
426
-			$dtend->setDateTime($endDateTime, $isFloating);
427
-		} else {
428
-			$dtend = clone $vevent->DTSTART;
429
-		}
430
-
431
-		$isAllDay = $dtstart instanceof Property\ICalendar\Date;
432
-
433
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
434
-		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
435
-		/** @var \DateTimeImmutable $dtstartDt */
436
-		$dtstartDt = $dtstart->getDateTime();
437
-		/** @var \DateTimeImmutable $dtendDt */
438
-		$dtendDt = $dtend->getDateTime();
439
-
440
-		$diff = $dtstartDt->diff($dtendDt);
441
-
442
-		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
443
-		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
444
-
445
-		if ($isAllDay) {
446
-			// One day event
447
-			if ($diff->days === 1) {
448
-				return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
449
-			}
450
-
451
-			// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
452
-			// the email should show 2020-01-01 to 2020-01-04.
453
-			$dtendDt->modify('-1 day');
454
-
455
-			//event that spans over multiple days
456
-			$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
457
-			$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
458
-
459
-			return $localeStart . ' - ' . $localeEnd;
460
-		}
461
-
462
-		/** @var Property\ICalendar\DateTime $dtstart */
463
-		/** @var Property\ICalendar\DateTime $dtend */
464
-		$isFloating = $dtstart->isFloating();
465
-		$startTimezone = $endTimezone = null;
466
-		if (!$isFloating) {
467
-			$prop = $dtstart->offsetGet('TZID');
468
-			if ($prop instanceof Parameter) {
469
-				$startTimezone = $prop->getValue();
470
-			}
471
-
472
-			$prop = $dtend->offsetGet('TZID');
473
-			if ($prop instanceof Parameter) {
474
-				$endTimezone = $prop->getValue();
475
-			}
476
-		}
477
-
478
-		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
479
-			$l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
480
-
481
-		// always show full date with timezone if timezones are different
482
-		if ($startTimezone !== $endTimezone) {
483
-			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
484
-
485
-			return $localeStart . ' (' . $startTimezone . ') - ' .
486
-				$localeEnd . ' (' . $endTimezone . ')';
487
-		}
488
-
489
-		// show only end time if date is the same
490
-		if ($this->isDayEqual($dtstartDt, $dtendDt)) {
491
-			$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
492
-		} else {
493
-			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
494
-				$l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
495
-		}
496
-
497
-		return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
498
-	}
499
-
500
-	/**
501
-	 * @param \DateTime $dtStart
502
-	 * @param \DateTime $dtEnd
503
-	 * @return bool
504
-	 */
505
-	private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
506
-		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
507
-	}
508
-
509
-	/**
510
-	 * @param IEMailTemplate $template
511
-	 * @param IL10N $l10n
512
-	 * @param string $method
513
-	 * @param string $summary
514
-	 */
515
-	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
516
-										  $method, $summary) {
517
-		if ($method === self::METHOD_CANCEL) {
518
-			$template->setSubject('Canceled: ' . $summary);
519
-			$template->addHeading($l10n->t('Invitation canceled'));
520
-		} elseif ($method === self::METHOD_REPLY) {
521
-			$template->setSubject('Re: ' . $summary);
522
-			$template->addHeading($l10n->t('Invitation updated'));
523
-		} else {
524
-			$template->setSubject('Invitation: ' . $summary);
525
-			$template->addHeading($l10n->t('Invitation'));
526
-		}
527
-	}
528
-
529
-	/**
530
-	 * @param IEMailTemplate $template
531
-	 * @param IL10N $l10n
532
-	 * @param VEVENT $vevent
533
-	 */
534
-	private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
535
-		if ($vevent->SUMMARY) {
536
-			$template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
537
-				$this->getAbsoluteImagePath('caldav/title.svg'),'','',self::IMIP_INDENT);
538
-		}
539
-		$meetingWhen = $this->generateWhenString($l10n, $vevent);
540
-		if ($meetingWhen) {
541
-			$template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
542
-				$this->getAbsoluteImagePath('caldav/time.svg'),'','',self::IMIP_INDENT);
543
-		}
544
-		if ($vevent->LOCATION) {
545
-			$template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
546
-				$this->getAbsoluteImagePath('caldav/location.svg'),'','',self::IMIP_INDENT);
547
-		}
548
-		if ($vevent->URL) {
549
-			$url = $vevent->URL->getValue();
550
-			$template->addBodyListItem(sprintf('<a href="%s">%s</a>',
551
-					htmlspecialchars($url),
552
-					htmlspecialchars($url)),
553
-				$l10n->t('Link:'),
554
-				$this->getAbsoluteImagePath('caldav/link.svg'),
555
-				$url,'',self::IMIP_INDENT);
556
-		}
557
-
558
-		$this->addAttendees($template, $l10n, $vevent);
559
-
560
-		/* Put description last, like an email body, since it can be arbitrarily long */
561
-		if ($vevent->DESCRIPTION) {
562
-			$template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
563
-				$this->getAbsoluteImagePath('caldav/description.svg'),'','',self::IMIP_INDENT);
564
-		}
565
-	}
566
-
567
-	/**
568
-	 * addAttendees: add organizer and attendee names/emails to iMip mail.
569
-	 *
570
-	 * Enable with DAV setting: invitation_list_attendees (default: no)
571
-	 *
572
-	 * The default is 'no', which matches old behavior, and is privacy preserving.
573
-	 *
574
-	 * To enable including attendees in invitation emails:
575
-	 *   % php occ config:app:set dav invitation_list_attendees --value yes
576
-	 *
577
-	 * @param IEMailTemplate $template
578
-	 * @param IL10N $l10n
579
-	 * @param Message $iTipMessage
580
-	 * @param int $lastOccurrence
581
-	 * @author brad2014 on github.com
582
-	 */
583
-
584
-	private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
585
-		if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
586
-			return;
587
-		}
588
-
589
-		if (isset($vevent->ORGANIZER)) {
590
-			/** @var Property\ICalendar\CalAddress $organizer */
591
-			$organizer = $vevent->ORGANIZER;
592
-			$organizerURI = $organizer->getNormalizedValue();
593
-			list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto:
594
-			/** @var string|null $organizerName */
595
-			$organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
596
-			$organizerHTML = sprintf('<a href="%s">%s</a>',
597
-				htmlspecialchars($organizerURI),
598
-				htmlspecialchars($organizerName ?: $organizerEmail));
599
-			$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
600
-			if (isset($organizer['PARTSTAT'])) {
601
-				/** @var Parameter $partstat */
602
-				$partstat = $organizer['PARTSTAT'];
603
-				if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
604
-					$organizerHTML .= ' ✔︎';
605
-					$organizerText .= ' ✔︎';
606
-				}
607
-			}
608
-			$template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
609
-				$this->getAbsoluteImagePath('caldav/organizer.svg'),
610
-				$organizerText,'',self::IMIP_INDENT);
611
-		}
612
-
613
-		$attendees = $vevent->select('ATTENDEE');
614
-		if (count($attendees) === 0) {
615
-			return;
616
-		}
617
-
618
-		$attendeesHTML = [];
619
-		$attendeesText = [];
620
-		foreach ($attendees as $attendee) {
621
-			$attendeeURI = $attendee->getNormalizedValue();
622
-			list($scheme,$attendeeEmail) = explode(':',$attendeeURI,2); # strip off scheme mailto:
623
-			$attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
624
-			$attendeeHTML = sprintf('<a href="%s">%s</a>',
625
-				htmlspecialchars($attendeeURI),
626
-				htmlspecialchars($attendeeName ?: $attendeeEmail));
627
-			$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
628
-			if (isset($attendee['PARTSTAT'])
629
-				&& strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
630
-				$attendeeHTML .= ' ✔︎';
631
-				$attendeeText .= ' ✔︎';
632
-			}
633
-			array_push($attendeesHTML, $attendeeHTML);
634
-			array_push($attendeesText, $attendeeText);
635
-		}
636
-
637
-		$template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
638
-			$this->getAbsoluteImagePath('caldav/attendees.svg'),
639
-			implode("\n",$attendeesText),'',self::IMIP_INDENT);
640
-	}
641
-
642
-	/**
643
-	 * @param IEMailTemplate $template
644
-	 * @param IL10N $l10n
645
-	 * @param Message $iTipMessage
646
-	 * @param int $lastOccurrence
647
-	 */
648
-	private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
649
-										Message $iTipMessage, $lastOccurrence) {
650
-		$token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
651
-
652
-		$template->addBodyButtonGroup(
653
-			$l10n->t('Accept'),
654
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
655
-				'token' => $token,
656
-			]),
657
-			$l10n->t('Decline'),
658
-			$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
659
-				'token' => $token,
660
-			])
661
-		);
662
-
663
-		$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
664
-			'token' => $token,
665
-		]);
666
-		$html = vsprintf('<small><a href="%s">%s</a></small>', [
667
-			$moreOptionsURL, $l10n->t('More options …')
668
-		]);
669
-		$text = $l10n->t('More options at %s', [$moreOptionsURL]);
670
-
671
-		$template->addBodyText($html, $text);
672
-	}
673
-
674
-	/**
675
-	 * @param string $path
676
-	 * @return string
677
-	 */
678
-	private function getAbsoluteImagePath($path) {
679
-		return $this->urlGenerator->getAbsoluteURL(
680
-			$this->urlGenerator->imagePath('core', $path)
681
-		);
682
-	}
683
-
684
-	/**
685
-	 * @param Message $iTipMessage
686
-	 * @param int $lastOccurrence
687
-	 * @return string
688
-	 */
689
-	private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
690
-		$token = $this->random->generate(60, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
691
-
692
-		/** @var VEvent $vevent */
693
-		$vevent = $iTipMessage->message->VEVENT;
694
-		$attendee = $iTipMessage->recipient;
695
-		$organizer = $iTipMessage->sender;
696
-		$sequence = $iTipMessage->sequence;
697
-		$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
698
-			$vevent->{'RECURRENCE-ID'}->serialize() : null;
699
-		$uid = $vevent->{'UID'};
700
-
701
-		$query = $this->db->getQueryBuilder();
702
-		$query->insert('calendar_invitations')
703
-			->values([
704
-				'token' => $query->createNamedParameter($token),
705
-				'attendee' => $query->createNamedParameter($attendee),
706
-				'organizer' => $query->createNamedParameter($organizer),
707
-				'sequence' => $query->createNamedParameter($sequence),
708
-				'recurrenceid' => $query->createNamedParameter($recurrenceId),
709
-				'expiration' => $query->createNamedParameter($lastOccurrence),
710
-				'uid' => $query->createNamedParameter($uid)
711
-			])
712
-			->execute();
713
-
714
-		return $token;
715
-	}
279
+            $recipientDomain = substr(strrchr($recipient, "@"), 1);
280
+            $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
281
+
282
+            if (strcmp('yes', $invitationLinkRecipients[0]) === 0
283
+                 || in_array(strtolower($recipient), $invitationLinkRecipients)
284
+                 || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
285
+                $this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
286
+            }
287
+        }
288
+
289
+        $template->addFooter();
290
+
291
+        $message->useTemplate($template);
292
+
293
+        $attachment = $this->mailer->createAttachment(
294
+            $iTipMessage->message->serialize(),
295
+            'event.ics',// TODO(leon): Make file name unique, e.g. add event id
296
+            'text/calendar; method=' . $iTipMessage->method
297
+        );
298
+        $message->attach($attachment);
299
+
300
+        try {
301
+            $failed = $this->mailer->send($message);
302
+            $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
303
+            if ($failed) {
304
+                $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
305
+                $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
306
+            }
307
+        } catch (\Exception $ex) {
308
+            $this->logger->logException($ex, ['app' => 'dav']);
309
+            $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
310
+        }
311
+    }
312
+
313
+    /**
314
+     * check if event took place in the past already
315
+     * @param VCalendar $vObject
316
+     * @return int
317
+     */
318
+    private function getLastOccurrence(VCalendar $vObject) {
319
+        /** @var VEvent $component */
320
+        $component = $vObject->VEVENT;
321
+
322
+        $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
323
+        // Finding the last occurrence is a bit harder
324
+        if (!isset($component->RRULE)) {
325
+            if (isset($component->DTEND)) {
326
+                $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
327
+            } elseif (isset($component->DURATION)) {
328
+                /** @var \DateTime $endDate */
329
+                $endDate = clone $component->DTSTART->getDateTime();
330
+                // $component->DTEND->getDateTime() returns DateTimeImmutable
331
+                $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
332
+                $lastOccurrence = $endDate->getTimestamp();
333
+            } elseif (!$component->DTSTART->hasTime()) {
334
+                /** @var \DateTime $endDate */
335
+                $endDate = clone $component->DTSTART->getDateTime();
336
+                // $component->DTSTART->getDateTime() returns DateTimeImmutable
337
+                $endDate = $endDate->modify('+1 day');
338
+                $lastOccurrence = $endDate->getTimestamp();
339
+            } else {
340
+                $lastOccurrence = $firstOccurrence;
341
+            }
342
+        } else {
343
+            $it = new EventIterator($vObject, (string)$component->UID);
344
+            $maxDate = new \DateTime(self::MAX_DATE);
345
+            if ($it->isInfinite()) {
346
+                $lastOccurrence = $maxDate->getTimestamp();
347
+            } else {
348
+                $end = $it->getDtEnd();
349
+                while ($it->valid() && $end < $maxDate) {
350
+                    $end = $it->getDtEnd();
351
+                    $it->next();
352
+                }
353
+                $lastOccurrence = $end->getTimestamp();
354
+            }
355
+        }
356
+
357
+        return $lastOccurrence;
358
+    }
359
+
360
+    /**
361
+     * @param Message $iTipMessage
362
+     * @return null|Property
363
+     */
364
+    private function getCurrentAttendee(Message $iTipMessage) {
365
+        /** @var VEvent $vevent */
366
+        $vevent = $iTipMessage->message->VEVENT;
367
+        $attendees = $vevent->select('ATTENDEE');
368
+        foreach ($attendees as $attendee) {
369
+            /** @var Property $attendee */
370
+            if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
371
+                return $attendee;
372
+            }
373
+        }
374
+        return null;
375
+    }
376
+
377
+    /**
378
+     * @param string $default
379
+     * @param Property|null $attendee
380
+     * @return string
381
+     */
382
+    private function getAttendeeLangOrDefault($default, Property $attendee = null) {
383
+        if ($attendee !== null) {
384
+            $lang = $attendee->offsetGet('LANGUAGE');
385
+            if ($lang instanceof Parameter) {
386
+                return $lang->getValue();
387
+            }
388
+        }
389
+        return $default;
390
+    }
391
+
392
+    /**
393
+     * @param Property|null $attendee
394
+     * @return bool
395
+     */
396
+    private function getAttendeeRSVP(Property $attendee = null) {
397
+        if ($attendee !== null) {
398
+            $rsvp = $attendee->offsetGet('RSVP');
399
+            if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
400
+                return true;
401
+            }
402
+        }
403
+        // RFC 5545 3.2.17: default RSVP is false
404
+        return false;
405
+    }
406
+
407
+    /**
408
+     * @param IL10N $l10n
409
+     * @param VEvent $vevent
410
+     */
411
+    private function generateWhenString(IL10N $l10n, VEvent $vevent) {
412
+        $dtstart = $vevent->DTSTART;
413
+        if (isset($vevent->DTEND)) {
414
+            $dtend = $vevent->DTEND;
415
+        } elseif (isset($vevent->DURATION)) {
416
+            $isFloating = $vevent->DTSTART->isFloating();
417
+            $dtend = clone $vevent->DTSTART;
418
+            $endDateTime = $dtend->getDateTime();
419
+            $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
420
+            $dtend->setDateTime($endDateTime, $isFloating);
421
+        } elseif (!$vevent->DTSTART->hasTime()) {
422
+            $isFloating = $vevent->DTSTART->isFloating();
423
+            $dtend = clone $vevent->DTSTART;
424
+            $endDateTime = $dtend->getDateTime();
425
+            $endDateTime = $endDateTime->modify('+1 day');
426
+            $dtend->setDateTime($endDateTime, $isFloating);
427
+        } else {
428
+            $dtend = clone $vevent->DTSTART;
429
+        }
430
+
431
+        $isAllDay = $dtstart instanceof Property\ICalendar\Date;
432
+
433
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
434
+        /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
435
+        /** @var \DateTimeImmutable $dtstartDt */
436
+        $dtstartDt = $dtstart->getDateTime();
437
+        /** @var \DateTimeImmutable $dtendDt */
438
+        $dtendDt = $dtend->getDateTime();
439
+
440
+        $diff = $dtstartDt->diff($dtendDt);
441
+
442
+        $dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
443
+        $dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
444
+
445
+        if ($isAllDay) {
446
+            // One day event
447
+            if ($diff->days === 1) {
448
+                return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
449
+            }
450
+
451
+            // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
452
+            // the email should show 2020-01-01 to 2020-01-04.
453
+            $dtendDt->modify('-1 day');
454
+
455
+            //event that spans over multiple days
456
+            $localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
457
+            $localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
458
+
459
+            return $localeStart . ' - ' . $localeEnd;
460
+        }
461
+
462
+        /** @var Property\ICalendar\DateTime $dtstart */
463
+        /** @var Property\ICalendar\DateTime $dtend */
464
+        $isFloating = $dtstart->isFloating();
465
+        $startTimezone = $endTimezone = null;
466
+        if (!$isFloating) {
467
+            $prop = $dtstart->offsetGet('TZID');
468
+            if ($prop instanceof Parameter) {
469
+                $startTimezone = $prop->getValue();
470
+            }
471
+
472
+            $prop = $dtend->offsetGet('TZID');
473
+            if ($prop instanceof Parameter) {
474
+                $endTimezone = $prop->getValue();
475
+            }
476
+        }
477
+
478
+        $localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
479
+            $l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
480
+
481
+        // always show full date with timezone if timezones are different
482
+        if ($startTimezone !== $endTimezone) {
483
+            $localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
484
+
485
+            return $localeStart . ' (' . $startTimezone . ') - ' .
486
+                $localeEnd . ' (' . $endTimezone . ')';
487
+        }
488
+
489
+        // show only end time if date is the same
490
+        if ($this->isDayEqual($dtstartDt, $dtendDt)) {
491
+            $localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
492
+        } else {
493
+            $localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
494
+                $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
495
+        }
496
+
497
+        return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
498
+    }
499
+
500
+    /**
501
+     * @param \DateTime $dtStart
502
+     * @param \DateTime $dtEnd
503
+     * @return bool
504
+     */
505
+    private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
506
+        return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
507
+    }
508
+
509
+    /**
510
+     * @param IEMailTemplate $template
511
+     * @param IL10N $l10n
512
+     * @param string $method
513
+     * @param string $summary
514
+     */
515
+    private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
516
+                                            $method, $summary) {
517
+        if ($method === self::METHOD_CANCEL) {
518
+            $template->setSubject('Canceled: ' . $summary);
519
+            $template->addHeading($l10n->t('Invitation canceled'));
520
+        } elseif ($method === self::METHOD_REPLY) {
521
+            $template->setSubject('Re: ' . $summary);
522
+            $template->addHeading($l10n->t('Invitation updated'));
523
+        } else {
524
+            $template->setSubject('Invitation: ' . $summary);
525
+            $template->addHeading($l10n->t('Invitation'));
526
+        }
527
+    }
528
+
529
+    /**
530
+     * @param IEMailTemplate $template
531
+     * @param IL10N $l10n
532
+     * @param VEVENT $vevent
533
+     */
534
+    private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
535
+        if ($vevent->SUMMARY) {
536
+            $template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
537
+                $this->getAbsoluteImagePath('caldav/title.svg'),'','',self::IMIP_INDENT);
538
+        }
539
+        $meetingWhen = $this->generateWhenString($l10n, $vevent);
540
+        if ($meetingWhen) {
541
+            $template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
542
+                $this->getAbsoluteImagePath('caldav/time.svg'),'','',self::IMIP_INDENT);
543
+        }
544
+        if ($vevent->LOCATION) {
545
+            $template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
546
+                $this->getAbsoluteImagePath('caldav/location.svg'),'','',self::IMIP_INDENT);
547
+        }
548
+        if ($vevent->URL) {
549
+            $url = $vevent->URL->getValue();
550
+            $template->addBodyListItem(sprintf('<a href="%s">%s</a>',
551
+                    htmlspecialchars($url),
552
+                    htmlspecialchars($url)),
553
+                $l10n->t('Link:'),
554
+                $this->getAbsoluteImagePath('caldav/link.svg'),
555
+                $url,'',self::IMIP_INDENT);
556
+        }
557
+
558
+        $this->addAttendees($template, $l10n, $vevent);
559
+
560
+        /* Put description last, like an email body, since it can be arbitrarily long */
561
+        if ($vevent->DESCRIPTION) {
562
+            $template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
563
+                $this->getAbsoluteImagePath('caldav/description.svg'),'','',self::IMIP_INDENT);
564
+        }
565
+    }
566
+
567
+    /**
568
+     * addAttendees: add organizer and attendee names/emails to iMip mail.
569
+     *
570
+     * Enable with DAV setting: invitation_list_attendees (default: no)
571
+     *
572
+     * The default is 'no', which matches old behavior, and is privacy preserving.
573
+     *
574
+     * To enable including attendees in invitation emails:
575
+     *   % php occ config:app:set dav invitation_list_attendees --value yes
576
+     *
577
+     * @param IEMailTemplate $template
578
+     * @param IL10N $l10n
579
+     * @param Message $iTipMessage
580
+     * @param int $lastOccurrence
581
+     * @author brad2014 on github.com
582
+     */
583
+
584
+    private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
585
+        if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
586
+            return;
587
+        }
588
+
589
+        if (isset($vevent->ORGANIZER)) {
590
+            /** @var Property\ICalendar\CalAddress $organizer */
591
+            $organizer = $vevent->ORGANIZER;
592
+            $organizerURI = $organizer->getNormalizedValue();
593
+            list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto:
594
+            /** @var string|null $organizerName */
595
+            $organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
596
+            $organizerHTML = sprintf('<a href="%s">%s</a>',
597
+                htmlspecialchars($organizerURI),
598
+                htmlspecialchars($organizerName ?: $organizerEmail));
599
+            $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
600
+            if (isset($organizer['PARTSTAT'])) {
601
+                /** @var Parameter $partstat */
602
+                $partstat = $organizer['PARTSTAT'];
603
+                if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
604
+                    $organizerHTML .= ' ✔︎';
605
+                    $organizerText .= ' ✔︎';
606
+                }
607
+            }
608
+            $template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
609
+                $this->getAbsoluteImagePath('caldav/organizer.svg'),
610
+                $organizerText,'',self::IMIP_INDENT);
611
+        }
612
+
613
+        $attendees = $vevent->select('ATTENDEE');
614
+        if (count($attendees) === 0) {
615
+            return;
616
+        }
617
+
618
+        $attendeesHTML = [];
619
+        $attendeesText = [];
620
+        foreach ($attendees as $attendee) {
621
+            $attendeeURI = $attendee->getNormalizedValue();
622
+            list($scheme,$attendeeEmail) = explode(':',$attendeeURI,2); # strip off scheme mailto:
623
+            $attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
624
+            $attendeeHTML = sprintf('<a href="%s">%s</a>',
625
+                htmlspecialchars($attendeeURI),
626
+                htmlspecialchars($attendeeName ?: $attendeeEmail));
627
+            $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
628
+            if (isset($attendee['PARTSTAT'])
629
+                && strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
630
+                $attendeeHTML .= ' ✔︎';
631
+                $attendeeText .= ' ✔︎';
632
+            }
633
+            array_push($attendeesHTML, $attendeeHTML);
634
+            array_push($attendeesText, $attendeeText);
635
+        }
636
+
637
+        $template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
638
+            $this->getAbsoluteImagePath('caldav/attendees.svg'),
639
+            implode("\n",$attendeesText),'',self::IMIP_INDENT);
640
+    }
641
+
642
+    /**
643
+     * @param IEMailTemplate $template
644
+     * @param IL10N $l10n
645
+     * @param Message $iTipMessage
646
+     * @param int $lastOccurrence
647
+     */
648
+    private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
649
+                                        Message $iTipMessage, $lastOccurrence) {
650
+        $token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
651
+
652
+        $template->addBodyButtonGroup(
653
+            $l10n->t('Accept'),
654
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
655
+                'token' => $token,
656
+            ]),
657
+            $l10n->t('Decline'),
658
+            $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
659
+                'token' => $token,
660
+            ])
661
+        );
662
+
663
+        $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
664
+            'token' => $token,
665
+        ]);
666
+        $html = vsprintf('<small><a href="%s">%s</a></small>', [
667
+            $moreOptionsURL, $l10n->t('More options …')
668
+        ]);
669
+        $text = $l10n->t('More options at %s', [$moreOptionsURL]);
670
+
671
+        $template->addBodyText($html, $text);
672
+    }
673
+
674
+    /**
675
+     * @param string $path
676
+     * @return string
677
+     */
678
+    private function getAbsoluteImagePath($path) {
679
+        return $this->urlGenerator->getAbsoluteURL(
680
+            $this->urlGenerator->imagePath('core', $path)
681
+        );
682
+    }
683
+
684
+    /**
685
+     * @param Message $iTipMessage
686
+     * @param int $lastOccurrence
687
+     * @return string
688
+     */
689
+    private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
690
+        $token = $this->random->generate(60, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
691
+
692
+        /** @var VEvent $vevent */
693
+        $vevent = $iTipMessage->message->VEVENT;
694
+        $attendee = $iTipMessage->recipient;
695
+        $organizer = $iTipMessage->sender;
696
+        $sequence = $iTipMessage->sequence;
697
+        $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
698
+            $vevent->{'RECURRENCE-ID'}->serialize() : null;
699
+        $uid = $vevent->{'UID'};
700
+
701
+        $query = $this->db->getQueryBuilder();
702
+        $query->insert('calendar_invitations')
703
+            ->values([
704
+                'token' => $query->createNamedParameter($token),
705
+                'attendee' => $query->createNamedParameter($attendee),
706
+                'organizer' => $query->createNamedParameter($organizer),
707
+                'sequence' => $query->createNamedParameter($sequence),
708
+                'recurrenceid' => $query->createNamedParameter($recurrenceId),
709
+                'expiration' => $query->createNamedParameter($lastOccurrence),
710
+                'uid' => $query->createNamedParameter($uid)
711
+            ])
712
+            ->execute();
713
+
714
+        return $token;
715
+    }
716 716
 }
Please login to merge, or discard this patch.
Spacing   +29 added lines, -29 removed lines patch added patch discarded remove patch
@@ -227,11 +227,11 @@  discard block
 block discarded – undo
227 227
 		}
228 228
 
229 229
 		$data = [
230
-			'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
231
-			'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
232
-			'meeting_title' => (string)$meetingTitle ?: $defaultVal,
233
-			'meeting_description' => (string)$meetingDescription ?: $defaultVal,
234
-			'meeting_url' => (string)$meetingUrl ?: $defaultVal,
230
+			'attendee_name' => (string) $meetingAttendeeName ?: $defaultVal,
231
+			'invitee_name' => (string) $meetingInviteeName ?: $defaultVal,
232
+			'meeting_title' => (string) $meetingTitle ?: $defaultVal,
233
+			'meeting_description' => (string) $meetingDescription ?: $defaultVal,
234
+			'meeting_url' => (string) $meetingUrl ?: $defaultVal,
235 235
 		];
236 236
 
237 237
 		$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
@@ -245,7 +245,7 @@  discard block
 block discarded – undo
245 245
 			$message->setReplyTo([$sender => $senderName]);
246 246
 		}
247 247
 
248
-		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
248
+		$template = $this->mailer->createEMailTemplate('dav.calendarInvite.'.$method, $data);
249 249
 		$template->addHeader();
250 250
 
251 251
 		$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
@@ -292,8 +292,8 @@  discard block
 block discarded – undo
292 292
 
293 293
 		$attachment = $this->mailer->createAttachment(
294 294
 			$iTipMessage->message->serialize(),
295
-			'event.ics',// TODO(leon): Make file name unique, e.g. add event id
296
-			'text/calendar; method=' . $iTipMessage->method
295
+			'event.ics', // TODO(leon): Make file name unique, e.g. add event id
296
+			'text/calendar; method='.$iTipMessage->method
297 297
 		);
298 298
 		$message->attach($attachment);
299 299
 
@@ -340,7 +340,7 @@  discard block
 block discarded – undo
340 340
 				$lastOccurrence = $firstOccurrence;
341 341
 			}
342 342
 		} else {
343
-			$it = new EventIterator($vObject, (string)$component->UID);
343
+			$it = new EventIterator($vObject, (string) $component->UID);
344 344
 			$maxDate = new \DateTime(self::MAX_DATE);
345 345
 			if ($it->isInfinite()) {
346 346
 				$lastOccurrence = $maxDate->getTimestamp();
@@ -456,7 +456,7 @@  discard block
 block discarded – undo
456 456
 			$localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
457 457
 			$localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
458 458
 
459
-			return $localeStart . ' - ' . $localeEnd;
459
+			return $localeStart.' - '.$localeEnd;
460 460
 		}
461 461
 
462 462
 		/** @var Property\ICalendar\DateTime $dtstart */
@@ -475,26 +475,26 @@  discard block
 block discarded – undo
475 475
 			}
476 476
 		}
477 477
 
478
-		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
478
+		$localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']).', '.
479 479
 			$l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
480 480
 
481 481
 		// always show full date with timezone if timezones are different
482 482
 		if ($startTimezone !== $endTimezone) {
483 483
 			$localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
484 484
 
485
-			return $localeStart . ' (' . $startTimezone . ') - ' .
486
-				$localeEnd . ' (' . $endTimezone . ')';
485
+			return $localeStart.' ('.$startTimezone.') - '.
486
+				$localeEnd.' ('.$endTimezone.')';
487 487
 		}
488 488
 
489 489
 		// show only end time if date is the same
490 490
 		if ($this->isDayEqual($dtstartDt, $dtendDt)) {
491 491
 			$localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
492 492
 		} else {
493
-			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
493
+			$localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']).', '.
494 494
 				$l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
495 495
 		}
496 496
 
497
-		return  $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
497
+		return  $localeStart.' - '.$localeEnd.' ('.$startTimezone.')';
498 498
 	}
499 499
 
500 500
 	/**
@@ -515,13 +515,13 @@  discard block
 block discarded – undo
515 515
 	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
516 516
 										  $method, $summary) {
517 517
 		if ($method === self::METHOD_CANCEL) {
518
-			$template->setSubject('Canceled: ' . $summary);
518
+			$template->setSubject('Canceled: '.$summary);
519 519
 			$template->addHeading($l10n->t('Invitation canceled'));
520 520
 		} elseif ($method === self::METHOD_REPLY) {
521
-			$template->setSubject('Re: ' . $summary);
521
+			$template->setSubject('Re: '.$summary);
522 522
 			$template->addHeading($l10n->t('Invitation updated'));
523 523
 		} else {
524
-			$template->setSubject('Invitation: ' . $summary);
524
+			$template->setSubject('Invitation: '.$summary);
525 525
 			$template->addHeading($l10n->t('Invitation'));
526 526
 		}
527 527
 	}
@@ -534,16 +534,16 @@  discard block
 block discarded – undo
534 534
 	private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
535 535
 		if ($vevent->SUMMARY) {
536 536
 			$template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
537
-				$this->getAbsoluteImagePath('caldav/title.svg'),'','',self::IMIP_INDENT);
537
+				$this->getAbsoluteImagePath('caldav/title.svg'), '', '', self::IMIP_INDENT);
538 538
 		}
539 539
 		$meetingWhen = $this->generateWhenString($l10n, $vevent);
540 540
 		if ($meetingWhen) {
541 541
 			$template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
542
-				$this->getAbsoluteImagePath('caldav/time.svg'),'','',self::IMIP_INDENT);
542
+				$this->getAbsoluteImagePath('caldav/time.svg'), '', '', self::IMIP_INDENT);
543 543
 		}
544 544
 		if ($vevent->LOCATION) {
545 545
 			$template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
546
-				$this->getAbsoluteImagePath('caldav/location.svg'),'','',self::IMIP_INDENT);
546
+				$this->getAbsoluteImagePath('caldav/location.svg'), '', '', self::IMIP_INDENT);
547 547
 		}
548 548
 		if ($vevent->URL) {
549 549
 			$url = $vevent->URL->getValue();
@@ -552,7 +552,7 @@  discard block
 block discarded – undo
552 552
 					htmlspecialchars($url)),
553 553
 				$l10n->t('Link:'),
554 554
 				$this->getAbsoluteImagePath('caldav/link.svg'),
555
-				$url,'',self::IMIP_INDENT);
555
+				$url, '', self::IMIP_INDENT);
556 556
 		}
557 557
 
558 558
 		$this->addAttendees($template, $l10n, $vevent);
@@ -560,7 +560,7 @@  discard block
 block discarded – undo
560 560
 		/* Put description last, like an email body, since it can be arbitrarily long */
561 561
 		if ($vevent->DESCRIPTION) {
562 562
 			$template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
563
-				$this->getAbsoluteImagePath('caldav/description.svg'),'','',self::IMIP_INDENT);
563
+				$this->getAbsoluteImagePath('caldav/description.svg'), '', '', self::IMIP_INDENT);
564 564
 		}
565 565
 	}
566 566
 
@@ -590,7 +590,7 @@  discard block
 block discarded – undo
590 590
 			/** @var Property\ICalendar\CalAddress $organizer */
591 591
 			$organizer = $vevent->ORGANIZER;
592 592
 			$organizerURI = $organizer->getNormalizedValue();
593
-			list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto:
593
+			list($scheme, $organizerEmail) = explode(':', $organizerURI, 2); # strip off scheme mailto:
594 594
 			/** @var string|null $organizerName */
595 595
 			$organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
596 596
 			$organizerHTML = sprintf('<a href="%s">%s</a>',
@@ -607,7 +607,7 @@  discard block
 block discarded – undo
607 607
 			}
608 608
 			$template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
609 609
 				$this->getAbsoluteImagePath('caldav/organizer.svg'),
610
-				$organizerText,'',self::IMIP_INDENT);
610
+				$organizerText, '', self::IMIP_INDENT);
611 611
 		}
612 612
 
613 613
 		$attendees = $vevent->select('ATTENDEE');
@@ -619,7 +619,7 @@  discard block
 block discarded – undo
619 619
 		$attendeesText = [];
620 620
 		foreach ($attendees as $attendee) {
621 621
 			$attendeeURI = $attendee->getNormalizedValue();
622
-			list($scheme,$attendeeEmail) = explode(':',$attendeeURI,2); # strip off scheme mailto:
622
+			list($scheme, $attendeeEmail) = explode(':', $attendeeURI, 2); # strip off scheme mailto:
623 623
 			$attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
624 624
 			$attendeeHTML = sprintf('<a href="%s">%s</a>',
625 625
 				htmlspecialchars($attendeeURI),
@@ -634,9 +634,9 @@  discard block
 block discarded – undo
634 634
 			array_push($attendeesText, $attendeeText);
635 635
 		}
636 636
 
637
-		$template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
637
+		$template->addBodyListItem(implode('<br/>', $attendeesHTML), $l10n->t('Attendees:'),
638 638
 			$this->getAbsoluteImagePath('caldav/attendees.svg'),
639
-			implode("\n",$attendeesText),'',self::IMIP_INDENT);
639
+			implode("\n", $attendeesText), '', self::IMIP_INDENT);
640 640
 	}
641 641
 
642 642
 	/**
@@ -687,7 +687,7 @@  discard block
 block discarded – undo
687 687
 	 * @return string
688 688
 	 */
689 689
 	private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
690
-		$token = $this->random->generate(60, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
690
+		$token = $this->random->generate(60, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
691 691
 
692 692
 		/** @var VEvent $vevent */
693 693
 		$vevent = $iTipMessage->message->VEVENT;
Please login to merge, or discard this patch.