Passed
Push — master ( 9cef2c...0459f5 )
by John
14:23 queued 13s
created

EmailProvider   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 409
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 186
c 0
b 0
f 0
dl 0
loc 409
rs 2.88
wmc 69

20 Methods

Rating   Name   Duplication   Size   Complexity  
A hasAttendeeMailURI() 0 2 1
A getEMailAddressesOfAllUsersWithWriteAccessToCalendar() 0 18 4
A getAbsoluteImagePath() 0 3 1
B generateDateString() 0 64 6
A getWeekDayName() 0 2 1
A sortEMailAddressesByLanguage() 0 19 4
A getTimeString() 0 2 1
A getTitleFromVEvent() 0 6 2
A addBulletList() 0 17 3
A addSubjectAndHeading() 0 3 1
A getDateTimeString() 0 2 1
A getEMailAddressOfAttendee() 0 10 3
A getCUTypeOfAttendee() 0 7 2
A getDateString() 0 2 1
C send() 0 64 12
A getPartstatOfAttendee() 0 7 2
C getAllEMailAddressesFromEvent() 0 61 17
A getOrganizerEMailAndNameFromEvent() 0 22 5
A __construct() 0 7 1
A isDayEqual() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like EmailProvider often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EmailProvider, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2019, Thomas Citharel
7
 * @copyright Copyright (c) 2019, Georg Ehrke
8
 *
9
 * @author Christoph Wurst <[email protected]>
10
 * @author Georg Ehrke <[email protected]>
11
 * @author Joas Schilling <[email protected]>
12
 * @author Richard Steinmetz <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 * @author Thomas Citharel <[email protected]>
15
 *
16
 * @license GNU AGPL version 3 or any later version
17
 *
18
 * This program is free software: you can redistribute it and/or modify
19
 * it under the terms of the GNU Affero General Public License as
20
 * published by the Free Software Foundation, either version 3 of the
21
 * License, or (at your option) any later version.
22
 *
23
 * This program is distributed in the hope that it will be useful,
24
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
25
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
 * GNU Affero General Public License for more details.
27
 *
28
 * You should have received a copy of the GNU Affero General Public License
29
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
30
 *
31
 */
32
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
33
34
use DateTime;
35
use OCP\IConfig;
36
use OCP\IL10N;
37
use OCP\IURLGenerator;
38
use OCP\IUser;
39
use OCP\L10N\IFactory as L10NFactory;
40
use OCP\Mail\IEMailTemplate;
41
use OCP\Mail\IMailer;
42
use Psr\Log\LoggerInterface;
43
use Sabre\VObject;
44
use Sabre\VObject\Component\VEvent;
45
use Sabre\VObject\Parameter;
46
use Sabre\VObject\Property;
47
48
/**
49
 * Class EmailProvider
50
 *
51
 * @package OCA\DAV\CalDAV\Reminder\NotificationProvider
52
 */
53
class EmailProvider extends AbstractProvider {
54
	/** @var string */
55
	public const NOTIFICATION_TYPE = 'EMAIL';
56
57
	private IMailer $mailer;
58
59
	public function __construct(IConfig $config,
60
								IMailer $mailer,
61
								LoggerInterface $logger,
62
								L10NFactory $l10nFactory,
63
								IURLGenerator $urlGenerator) {
64
		parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
65
		$this->mailer = $mailer;
66
	}
67
68
	/**
69
	 * Send out notification via email
70
	 *
71
	 * @param VEvent $vevent
72
	 * @param string $calendarDisplayName
73
	 * @param string[] $principalEmailAddresses
74
	 * @param array $users
75
	 * @throws \Exception
76
	 */
77
	public function send(VEvent $vevent,
78
						 string $calendarDisplayName,
79
						 array $principalEmailAddresses,
80
						 array $users = []):void {
81
		$fallbackLanguage = $this->getFallbackLanguage();
82
83
		$organizerEmailAddress = null;
84
		if (isset($vevent->ORGANIZER)) {
85
			$organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
86
		}
87
88
		$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
89
		$emailAddressesOfAttendees = [];
90
		if (count($principalEmailAddresses) === 0
91
			|| ($organizerEmailAddress && in_array($organizerEmailAddress, $principalEmailAddresses, true))
92
		) {
93
			$emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
94
		}
95
96
		// Quote from php.net:
97
		// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
98
		// => if there are duplicate email addresses, it will always take the system value
99
		$emailAddresses = array_merge(
100
			$emailAddressesOfAttendees,
101
			$emailAddressesOfSharees
102
		);
103
104
		$sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
105
		$organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
106
107
		foreach ($sortedByLanguage as $lang => $emailAddresses) {
108
			if (!$this->hasL10NForLang($lang)) {
109
				$lang = $fallbackLanguage;
110
			}
111
			$l10n = $this->getL10NForLang($lang);
112
			$fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
113
114
			$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
115
			$template->addHeader();
116
			$this->addSubjectAndHeading($template, $l10n, $vevent);
117
			$this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
118
			$template->addFooter();
119
120
			foreach ($emailAddresses as $emailAddress) {
121
				if (!$this->mailer->validateMailAddress($emailAddress)) {
122
					$this->logger->error('Email address {address} for reminder notification is incorrect', ['app' => 'dav', 'address' => $emailAddress]);
123
					continue;
124
				}
125
126
				$message = $this->mailer->createMessage();
127
				$message->setFrom([$fromEMail]);
128
				if ($organizer) {
129
					$message->setReplyTo($organizer);
130
				}
131
				$message->setTo([$emailAddress]);
132
				$message->useTemplate($template);
133
134
				try {
135
					$failed = $this->mailer->send($message);
136
					if ($failed) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $failed of type string[] 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...
137
						$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
138
					}
139
				} catch (\Exception $ex) {
140
					$this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
141
				}
142
			}
143
		}
144
	}
145
146
	/**
147
	 * @param IEMailTemplate $template
148
	 * @param IL10N $l10n
149
	 * @param VEvent $vevent
150
	 */
151
	private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
152
		$template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
153
		$template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
154
	}
155
156
	/**
157
	 * @param IEMailTemplate $template
158
	 * @param IL10N $l10n
159
	 * @param string $calendarDisplayName
160
	 * @param array $eventData
161
	 */
162
	private function addBulletList(IEMailTemplate $template,
163
								   IL10N $l10n,
164
								   string $calendarDisplayName,
165
								   VEvent $vevent):void {
166
		$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
167
			$this->getAbsoluteImagePath('actions/info.png'));
168
169
		$template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
170
			$this->getAbsoluteImagePath('places/calendar.png'));
171
172
		if (isset($vevent->LOCATION)) {
173
			$template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
174
				$this->getAbsoluteImagePath('actions/address.png'));
175
		}
176
		if (isset($vevent->DESCRIPTION)) {
177
			$template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
178
				$this->getAbsoluteImagePath('actions/more.png'));
179
		}
180
	}
181
182
	private function getAbsoluteImagePath(string $path):string {
183
		return $this->urlGenerator->getAbsoluteURL(
184
			$this->urlGenerator->imagePath('core', $path)
185
		);
186
	}
187
188
	/**
189
	 * @param VEvent $vevent
190
	 * @return array|null
191
	 */
192
	private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
193
		if (!$vevent->ORGANIZER) {
194
			return null;
195
		}
196
197
		$organizer = $vevent->ORGANIZER;
198
		if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
199
			return null;
200
		}
201
202
		$organizerEMail = substr($organizer->getValue(), 7);
203
204
		if (!$this->mailer->validateMailAddress($organizerEMail)) {
205
			return null;
206
		}
207
208
		$name = $organizer->offsetGet('CN');
209
		if ($name instanceof Parameter) {
210
			return [$organizerEMail => $name];
211
		}
212
213
		return [$organizerEMail];
214
	}
215
216
	/**
217
	 * @param array<string, array{LANG?: string}> $emails
218
	 * @return array<string, string[]>
219
	 */
220
	private function sortEMailAddressesByLanguage(array $emails,
221
												  string $defaultLanguage):array {
222
		$sortedByLanguage = [];
223
224
		foreach ($emails as $emailAddress => $parameters) {
225
			if (isset($parameters['LANG'])) {
226
				$lang = $parameters['LANG'];
227
			} else {
228
				$lang = $defaultLanguage;
229
			}
230
231
			if (!isset($sortedByLanguage[$lang])) {
232
				$sortedByLanguage[$lang] = [];
233
			}
234
235
			$sortedByLanguage[$lang][] = $emailAddress;
236
		}
237
238
		return $sortedByLanguage;
239
	}
240
241
	/**
242
	 * @param VEvent $vevent
243
	 * @return array<string, array{LANG?: string}>
244
	 */
245
	private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
246
		$emailAddresses = [];
247
248
		if (isset($vevent->ATTENDEE)) {
249
			foreach ($vevent->ATTENDEE as $attendee) {
250
				if (!($attendee instanceof VObject\Property)) {
251
					continue;
252
				}
253
254
				$cuType = $this->getCUTypeOfAttendee($attendee);
255
				if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
256
					// Don't send emails to things
257
					continue;
258
				}
259
260
				$partstat = $this->getPartstatOfAttendee($attendee);
261
				if ($partstat === 'DECLINED') {
262
					// Don't send out emails to people who declined
263
					continue;
264
				}
265
				if ($partstat === 'DELEGATED') {
266
					$delegates = $attendee->offsetGet('DELEGATED-TO');
267
					if (!($delegates instanceof VObject\Parameter)) {
268
						continue;
269
					}
270
271
					$emailAddressesOfDelegates = $delegates->getParts();
272
					foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
273
						if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
274
							$delegateEmail = substr($addressesOfDelegate, 7);
275
							if ($this->mailer->validateMailAddress($delegateEmail)) {
276
								$emailAddresses[$delegateEmail] = [];
277
							}
278
						}
279
					}
280
281
					continue;
282
				}
283
284
				$emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
285
				if ($emailAddressOfAttendee !== null) {
286
					$properties = [];
287
288
					$langProp = $attendee->offsetGet('LANG');
289
					if ($langProp instanceof VObject\Parameter && $langProp->getValue() !== null) {
290
						$properties['LANG'] = $langProp->getValue();
291
					}
292
293
					$emailAddresses[$emailAddressOfAttendee] = $properties;
294
				}
295
			}
296
		}
297
298
		if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
299
			$organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
300
			if ($organizerEmailAddress !== null) {
301
				$emailAddresses[$organizerEmailAddress] = [];
302
			}
303
		}
304
305
		return $emailAddresses;
306
	}
307
308
	private function getCUTypeOfAttendee(VObject\Property $attendee):string {
309
		$cuType = $attendee->offsetGet('CUTYPE');
310
		if ($cuType instanceof VObject\Parameter) {
311
			return strtoupper($cuType->getValue());
312
		}
313
314
		return 'INDIVIDUAL';
315
	}
316
317
	private function getPartstatOfAttendee(VObject\Property $attendee):string {
318
		$partstat = $attendee->offsetGet('PARTSTAT');
319
		if ($partstat instanceof VObject\Parameter) {
320
			return strtoupper($partstat->getValue());
321
		}
322
323
		return 'NEEDS-ACTION';
324
	}
325
326
	private function hasAttendeeMailURI(VObject\Property $attendee): bool {
327
		return stripos($attendee->getValue(), 'mailto:') === 0;
328
	}
329
330
	private function getEMailAddressOfAttendee(VObject\Property $attendee): ?string {
331
		if (!$this->hasAttendeeMailURI($attendee)) {
332
			return null;
333
		}
334
		$attendeeEMail = substr($attendee->getValue(), 7);
335
		if (!$this->mailer->validateMailAddress($attendeeEMail)) {
336
			return null;
337
		}
338
339
		return $attendeeEMail;
340
	}
341
342
	/**
343
	 * @param IUser[] $users
344
	 * @return array<string, array{LANG?: string}>
345
	 */
346
	private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
347
		$emailAddresses = [];
348
349
		foreach ($users as $user) {
350
			$emailAddress = $user->getEMailAddress();
351
			if ($emailAddress) {
352
				$lang = $this->l10nFactory->getUserLanguage($user);
353
				if ($lang) {
354
					$emailAddresses[$emailAddress] = [
355
						'LANG' => $lang,
356
					];
357
				} else {
358
					$emailAddresses[$emailAddress] = [];
359
				}
360
			}
361
		}
362
363
		return $emailAddresses;
364
	}
365
366
	/**
367
	 * @throws \Exception
368
	 */
369
	private function generateDateString(IL10N $l10n, VEvent $vevent): string {
370
		$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
371
372
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
373
		/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
374
		/** @var \DateTimeImmutable $dtstartDt */
375
		$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

375
		/** @scrutinizer ignore-call */ 
376
  $dtstartDt = $vevent->DTSTART->getDateTime();
Loading history...
Bug introduced by
The method getDateTime() does not exist on null. ( Ignorable by Annotation )

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

375
		/** @scrutinizer ignore-call */ 
376
  $dtstartDt = $vevent->DTSTART->getDateTime();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
376
		/** @var \DateTimeImmutable $dtendDt */
377
		$dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
378
379
		$diff = $dtstartDt->diff($dtendDt);
380
381
		$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
382
		$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
383
384
		if ($isAllDay) {
385
			// One day event
386
			if ($diff->days === 1) {
387
				return $this->getDateString($l10n, $dtstartDt);
388
			}
389
390
			return implode(' - ', [
391
				$this->getDateString($l10n, $dtstartDt),
392
				$this->getDateString($l10n, $dtendDt),
393
			]);
394
		}
395
396
		$startTimezone = $endTimezone = null;
397
		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

397
		if (!$vevent->DTSTART->/** @scrutinizer ignore-call */ isFloating()) {
Loading history...
398
			$startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
399
			$endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
400
		}
401
402
		$localeStart = implode(', ', [
403
			$this->getWeekDayName($l10n, $dtstartDt),
404
			$this->getDateTimeString($l10n, $dtstartDt)
405
		]);
406
407
		// always show full date with timezone if timezones are different
408
		if ($startTimezone !== $endTimezone) {
409
			$localeEnd = implode(', ', [
410
				$this->getWeekDayName($l10n, $dtendDt),
411
				$this->getDateTimeString($l10n, $dtendDt)
412
			]);
413
414
			return $localeStart
415
				. ' (' . $startTimezone . ') '
416
				. ' - '
417
				. $localeEnd
418
				. ' (' . $endTimezone . ')';
419
		}
420
421
		// Show only the time if the day is the same
422
		$localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
423
			? $this->getTimeString($l10n, $dtendDt)
424
			: implode(', ', [
425
				$this->getWeekDayName($l10n, $dtendDt),
426
				$this->getDateTimeString($l10n, $dtendDt)
427
			]);
428
429
		return $localeStart
430
			. ' - '
431
			. $localeEnd
432
			. ' (' . $startTimezone . ')';
433
	}
434
435
	private function isDayEqual(DateTime $dtStart,
436
								DateTime $dtEnd):bool {
437
		return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
438
	}
439
440
	private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
441
		return (string)$l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
442
	}
443
444
	private function getDateString(IL10N $l10n, DateTime $dt):string {
445
		return (string)$l10n->l('date', $dt, ['width' => 'medium']);
446
	}
447
448
	private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
449
		return (string)$l10n->l('datetime', $dt, ['width' => 'medium|short']);
450
	}
451
452
	private function getTimeString(IL10N $l10n, DateTime $dt):string {
453
		return (string)$l10n->l('time', $dt, ['width' => 'short']);
454
	}
455
456
	private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
457
		if (isset($vevent->SUMMARY)) {
458
			return (string)$vevent->SUMMARY;
459
		}
460
461
		return $l10n->t('Untitled event');
462
	}
463
}
464