Passed
Push — master ( 9c2d70...6ef7ba )
by Roeland
10:28
created

EmailProvider::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 5
dl 0
loc 7
rs 10
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2019, Thomas Citharel
5
 * @copyright Copyright (c) 2019, Georg Ehrke
6
 *
7
 * @author Thomas Citharel <[email protected]>
8
 * @author Georg Ehrke <[email protected]>
9
 *
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
27
28
use \DateTime;
29
use OCP\IConfig;
30
use OCP\IL10N;
31
use OCP\ILogger;
32
use OCP\IURLGenerator;
33
use OCP\L10N\IFactory as L10NFactory;
34
use OCP\Mail\IEMailTemplate;
35
use OCP\Mail\IMailer;
36
use OCP\IUser;
37
use Sabre\VObject\Component\VEvent;
38
use Sabre\VObject;
39
use Sabre\VObject\Parameter;
40
use Sabre\VObject\Property;
41
42
/**
43
 * Class EmailProvider
44
 *
45
 * @package OCA\DAV\CalDAV\Reminder\NotificationProvider
46
 */
47
class EmailProvider extends AbstractProvider {
48
49
	/** @var string */
50
	public const NOTIFICATION_TYPE = 'EMAIL';
51
52
	/** @var IMailer */
53
	private $mailer;
54
55
	/**
56
	 * @param IConfig $config
57
	 * @param IMailer $mailer
58
	 * @param ILogger $logger
59
	 * @param L10NFactory $l10nFactory
60
	 * @param IUrlGenerator $urlGenerator
61
	 */
62
	public function __construct(IConfig $config,
63
								IMailer $mailer,
64
								ILogger $logger,
65
								L10NFactory $l10nFactory,
66
								IURLGenerator $urlGenerator) {
67
		parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
68
		$this->mailer = $mailer;
69
	}
70
71
	/**
72
	 * Send out notification via email
73
	 *
74
	 * @param VEvent $vevent
75
	 * @param string $calendarDisplayName
76
	 * @param array $users
77
	 * @throws \Exception
78
	 */
79
	public function send(VEvent $vevent,
80
						 string $calendarDisplayName,
81
						 array $users=[]):void {
82
		$fallbackLanguage = $this->getFallbackLanguage();
83
84
		$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
85
		$emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
86
87
		// Quote from php.net:
88
		// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
89
		// => if there are duplicate email addresses, it will always take the system value
90
		$emailAddresses = array_merge(
91
			$emailAddressesOfAttendees,
92
			$emailAddressesOfSharees
93
		);
94
95
		$sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
96
		$organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
97
98
		foreach($sortedByLanguage as $lang => $emailAddresses) {
99
			if (!$this->hasL10NForLang($lang)) {
100
				$lang = $fallbackLanguage;
101
			}
102
			$l10n = $this->getL10NForLang($lang);
103
			$fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
104
105
			$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
106
			$template->addHeader();
107
			$this->addSubjectAndHeading($template, $l10n, $vevent);
108
			$this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
109
			$template->addFooter();
110
111
			foreach ($emailAddresses as $emailAddress) {
112
				$message = $this->mailer->createMessage();
113
				$message->setFrom([$fromEMail]);
114
				if ($organizer) {
115
					$message->setReplyTo($organizer);
116
				}
117
				$message->setTo([$emailAddress]);
118
				$message->useTemplate($template);
119
120
				try {
121
					$failed = $this->mailer->send($message);
122
					if ($failed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $failed of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
123
						$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
124
					}
125
				} catch (\Exception $ex) {
126
					$this->logger->logException($ex, ['app' => 'dav']);
127
				}
128
			}
129
		}
130
	}
131
132
	/**
133
	 * @param IEMailTemplate $template
134
	 * @param IL10N $l10n
135
	 * @param VEvent $vevent
136
	 */
137
	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
138
		$template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
139
		$template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
140
	}
141
142
	/**
143
	 * @param IEMailTemplate $template
144
	 * @param IL10N $l10n
145
	 * @param string $calendarDisplayName
146
	 * @param array $eventData
147
	 */
148
	private function addBulletList(IEMailTemplate $template,
149
								   IL10N $l10n,
150
								   string $calendarDisplayName,
151
								   VEvent $vevent):void {
152
		$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
153
			$this->getAbsoluteImagePath('actions/info.svg'));
154
155
		$template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
156
			$this->getAbsoluteImagePath('places/calendar.svg'));
157
158
		if (isset($vevent->LOCATION)) {
159
			$template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
160
				$this->getAbsoluteImagePath('actions/address.svg'));
161
		}
162
		if (isset($vevent->DESCRIPTION)) {
163
			$template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
164
				$this->getAbsoluteImagePath('actions/more.svg'));
165
		}
166
	}
167
168
	/**
169
	 * @param string $path
170
	 * @return string
171
	 */
172
	private function getAbsoluteImagePath(string $path):string {
173
		return $this->urlGenerator->getAbsoluteURL(
174
			$this->urlGenerator->imagePath('core', $path)
175
		);
176
	}
177
178
	/**
179
	 * @param VEvent $vevent
180
	 * @return array|null
181
	 */
182
	private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
183
		if (!$vevent->ORGANIZER) {
184
			return null;
185
		}
186
187
		$organizer = $vevent->ORGANIZER;
188
		if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
189
			return null;
190
		}
191
192
		$organizerEMail = substr($organizer->getValue(), 7);
193
194
		$name = $organizer->offsetGet('CN');
195
		if ($name instanceof Parameter) {
196
			return [$organizerEMail => $name];
197
		}
198
199
		return [$organizerEMail];
200
	}
201
202
	/**
203
	 * @param array $emails
204
	 * @param string $defaultLanguage
205
	 * @return array
206
	 */
207
	private function sortEMailAddressesByLanguage(array $emails,
208
												  string $defaultLanguage):array {
209
		$sortedByLanguage = [];
210
211
		foreach($emails as $emailAddress => $parameters) {
212
			if (isset($parameters['LANG'])) {
213
				$lang = $parameters['LANG'];
214
			} else {
215
				$lang = $defaultLanguage;
216
			}
217
218
			if (!isset($sortedByLanguage[$lang])) {
219
				$sortedByLanguage[$lang] = [];
220
			}
221
222
			$sortedByLanguage[$lang][] = $emailAddress;
223
		}
224
225
		return $sortedByLanguage;
226
	}
227
228
	/**
229
	 * @param VEvent $vevent
230
	 * @return array
231
	 */
232
	private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
233
		$emailAddresses = [];
234
235
		if (isset($vevent->ATTENDEE)) {
236
			foreach ($vevent->ATTENDEE as $attendee) {
237
				if (!($attendee instanceof VObject\Property)) {
238
					continue;
239
				}
240
241
				$cuType = $this->getCUTypeOfAttendee($attendee);
242
				if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
243
					// Don't send emails to things
244
					continue;
245
				}
246
247
				$partstat = $this->getPartstatOfAttendee($attendee);
248
				if ($partstat === 'DECLINED') {
249
					// Don't send out emails to people who declined
250
					continue;
251
				}
252
				if ($partstat === 'DELEGATED') {
253
					$delegates = $attendee->offsetGet('DELEGATED-TO');
254
					if (!($delegates instanceof VObject\Parameter)) {
255
						continue;
256
					}
257
258
					$emailAddressesOfDelegates = $delegates->getParts();
259
					foreach($emailAddressesOfDelegates as $addressesOfDelegate) {
260
						if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
261
							$emailAddresses[substr($addressesOfDelegate, 7)] = [];
262
						}
263
					}
264
265
					continue;
266
				}
267
268
				$emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
269
				if ($emailAddressOfAttendee !== null) {
270
					$properties = [];
271
272
					$langProp = $attendee->offsetGet('LANG');
273
					if ($langProp instanceof VObject\Parameter) {
274
						$properties['LANG'] = $langProp->getValue();
275
					}
276
277
					$emailAddresses[$emailAddressOfAttendee] = $properties;
278
				}
279
			}
280
		}
281
282
		if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
283
			$emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
284
		}
285
286
		return $emailAddresses;
287
	}
288
289
290
291
	/**
292
	 * @param VObject\Property $attendee
293
	 * @return string
294
	 */
295
	private function getCUTypeOfAttendee(VObject\Property $attendee):string {
296
		$cuType = $attendee->offsetGet('CUTYPE');
297
		if ($cuType instanceof VObject\Parameter) {
298
			return strtoupper($cuType->getValue());
299
		}
300
301
		return 'INDIVIDUAL';
302
	}
303
304
	/**
305
	 * @param VObject\Property $attendee
306
	 * @return string
307
	 */
308
	private function getPartstatOfAttendee(VObject\Property $attendee):string {
309
		$partstat = $attendee->offsetGet('PARTSTAT');
310
		if ($partstat instanceof VObject\Parameter) {
311
			return strtoupper($partstat->getValue());
312
		}
313
314
		return 'NEEDS-ACTION';
315
	}
316
317
	/**
318
	 * @param VObject\Property $attendee
319
	 * @return bool
320
	 */
321
	private function hasAttendeeMailURI(VObject\Property $attendee):bool {
322
		return stripos($attendee->getValue(), 'mailto:') === 0;
323
	}
324
325
	/**
326
	 * @param VObject\Property $attendee
327
	 * @return string|null
328
	 */
329
	private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
330
		if (!$this->hasAttendeeMailURI($attendee)) {
331
			return null;
332
		}
333
334
		return substr($attendee->getValue(), 7);
335
	}
336
337
	/**
338
	 * @param array $users
339
	 * @return array
340
	 */
341
	private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
342
		$emailAddresses = [];
343
344
		foreach($users as $user) {
345
			$emailAddress = $user->getEMailAddress();
346
			if ($emailAddress) {
347
				$lang = $this->getLangForUser($user);
348
				if ($lang) {
349
					$emailAddresses[$emailAddress] = [
350
						'LANG' => $lang,
351
					];
352
				} else {
353
					$emailAddresses[$emailAddress] = [];
354
				}
355
			}
356
		}
357
358
		return $emailAddresses;
359
	}
360
361
	/**
362
	 * @param IUser $user
363
	 * @return string
364
	 */
365
	private function getLangForUser(IUser $user): ?string {
366
		return $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
367
	}
368
369
	/**
370
	 * @param IL10N $l10n
371
	 * @param VEvent $vevent
372
	 * @return string
373
	 * @throws \Exception
374
	 */
375
	private function generateDateString(IL10N $l10n, VEvent $vevent):string {
376
		$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
377
378
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
379
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
380
		/** @var \DateTimeImmutable $dtstartDt */
381
		$dtstartDt = $vevent->DTSTART->getDateTime();
0 ignored issues
show
Bug introduced by
The method getDateTime() does not exist on Sabre\VObject\Property. It seems like you code against a sub-type of Sabre\VObject\Property such as Sabre\VObject\Property\ICalendar\DateTime or Sabre\VObject\Property\VCard\DateAndOrTime. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

381
		/** @scrutinizer ignore-call */ 
382
  $dtstartDt = $vevent->DTSTART->getDateTime();
Loading history...
382
		/** @var \DateTimeImmutable $dtendDt */
383
		$dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
384
385
		$diff = $dtstartDt->diff($dtendDt);
386
387
		/** @phan-suppress-next-line PhanUndeclaredClassMethod */
388
		$dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM));
389
		/** @phan-suppress-next-line PhanUndeclaredClassMethod */
390
		$dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM));
391
392
		if ($isAllDay) {
393
			// One day event
394
			if ($diff->days === 1) {
395
				return $this->getDateString($l10n, $dtstartDt);
396
			}
397
398
			return implode(' - ', [
399
				$this->getDateString($l10n, $dtstartDt),
400
				$this->getDateString($l10n, $dtendDt),
401
			]);
402
		}
403
404
		$startTimezone = $endTimezone = null;
405
		if (!$vevent->DTSTART->isFloating()) {
0 ignored issues
show
Bug introduced by
The method isFloating() does not exist on Sabre\VObject\Property. It seems like you code against a sub-type of Sabre\VObject\Property such as Sabre\VObject\Property\ICalendar\DateTime. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

405
		if (!$vevent->DTSTART->/** @scrutinizer ignore-call */ isFloating()) {
Loading history...
406
			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
407
			$startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
408
			/** @phan-suppress-next-line PhanUndeclaredClassMethod */
409
			$endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
410
		}
411
412
		$localeStart = implode(', ', [
413
			$this->getWeekDayName($l10n, $dtstartDt),
414
			$this->getDateTimeString($l10n, $dtstartDt)
415
		]);
416
417
		// always show full date with timezone if timezones are different
418
		if ($startTimezone !== $endTimezone) {
419
			$localeEnd = implode(', ', [
420
				$this->getWeekDayName($l10n, $dtendDt),
421
				$this->getDateTimeString($l10n, $dtendDt)
422
			]);
423
424
			return $localeStart
425
				. ' (' . $startTimezone . ') '
426
				. ' - '
427
				. $localeEnd
428
				. ' (' . $endTimezone . ')';
429
		}
430
431
		// Show only the time if the day is the same
432
		$localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
433
			? $this->getTimeString($l10n, $dtendDt)
434
			: implode(', ', [
435
				$this->getWeekDayName($l10n, $dtendDt),
436
				$this->getDateTimeString($l10n, $dtendDt)
437
			]);
438
439
		return $localeStart
440
			. ' - '
441
			. $localeEnd
442
			. ' (' . $startTimezone . ')';
443
	}
444
445
	/**
446
	 * @param DateTime $dtStart
447
	 * @param DateTime $dtEnd
448
	 * @return bool
449
	 */
450
	private function isDayEqual(DateTime $dtStart,
451
								DateTime $dtEnd):bool {
452
		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
453
	}
454
455
	/**
456
	 * @param IL10N $l10n
457
	 * @param DateTime $dt
458
	 * @return string
459
	 */
460
	private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
461
		return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $l10n->l('weekday...dth' => 'abbreviated')) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
462
	}
463
464
	/**
465
	 * @param IL10N $l10n
466
	 * @param DateTime $dt
467
	 * @return string
468
	 */
469
	private function getDateString(IL10N $l10n, DateTime $dt):string {
470
		return $l10n->l('date', $dt, ['width' => 'medium']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $l10n->l('date', ...y('width' => 'medium')) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
471
	}
472
473
	/**
474
	 * @param IL10N $l10n
475
	 * @param DateTime $dt
476
	 * @return string
477
	 */
478
	private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
479
		return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $l10n->l('datetim...th' => 'medium|short')) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
480
	}
481
482
	/**
483
	 * @param IL10N $l10n
484
	 * @param DateTime $dt
485
	 * @return string
486
	 */
487
	private function getTimeString(IL10N $l10n, DateTime $dt):string {
488
		return $l10n->l('time', $dt, ['width' => 'short']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $l10n->l('time', ...ay('width' => 'short')) could return the type false which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
489
	}
490
491
	/**
492
	 * @param VEvent $vevent
493
	 * @param IL10N $l10n
494
	 * @return string
495
	 */
496
	private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
497
		if (isset($vevent->SUMMARY)) {
498
			return (string)$vevent->SUMMARY;
499
		}
500
501
		return $l10n->t('Untitled event');
502
	}
503
}
504