@@ -52,455 +52,455 @@ |
||
| 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 (!$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 (!$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 | } |