diff options
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/IMipPlugin.php | 178 | ||||
-rw-r--r-- | lib/private/Mail/EMailTemplate.php | 30 |
2 files changed, 151 insertions, 57 deletions
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 6358a3a0293..011314b41d6 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -108,6 +108,7 @@ class IMipPlugin extends SabreIMipPlugin { public const METHOD_REQUEST = 'request'; public const METHOD_REPLY = 'reply'; public const METHOD_CANCEL = 'cancel'; + public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages /** * @param IConfig $config @@ -204,26 +205,6 @@ class IMipPlugin extends SabreIMipPlugin { $meetingTitle = $vevent->SUMMARY; $meetingDescription = $vevent->DESCRIPTION; - $start = $vevent->DTSTART; - if (isset($vevent->DTEND)) { - $end = $vevent->DTEND; - } elseif (isset($vevent->DURATION)) { - $isFloating = $vevent->DTSTART->isFloating(); - $end = clone $vevent->DTSTART; - $endDateTime = $end->getDateTime(); - $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue())); - $end->setDateTime($endDateTime, $isFloating); - } elseif (!$vevent->DTSTART->hasTime()) { - $isFloating = $vevent->DTSTART->isFloating(); - $end = clone $vevent->DTSTART; - $endDateTime = $end->getDateTime(); - $endDateTime = $endDateTime->modify('+1 day'); - $end->setDateTime($endDateTime, $isFloating); - } else { - $end = clone $vevent->DTSTART; - } - - $meetingWhen = $this->generateWhenString($l10n, $start, $end); $meetingUrl = $vevent->URL; $meetingLocation = $vevent->LOCATION; @@ -263,8 +244,7 @@ class IMipPlugin extends SabreIMipPlugin { $this->addSubjectAndHeading($template, $l10n, $method, $summary, $meetingAttendeeName, $meetingInviteeName); - $this->addBulletList($template, $l10n, $meetingWhen, $meetingLocation, - $meetingDescription, $meetingUrl); + $this->addBulletList($template, $l10n, $vevent); // Only add response buttons to invitation requests: Fix Issue #11230 @@ -370,7 +350,6 @@ class IMipPlugin extends SabreIMipPlugin { return $lastOccurrence; } - /** * @param Message $iTipMessage * @return null|Property @@ -420,10 +399,29 @@ class IMipPlugin extends SabreIMipPlugin { /** * @param IL10N $l10n - * @param Property $dtstart - * @param Property $dtend + * @param VEvent $vevent */ - private function generateWhenString(IL10N $l10n, Property $dtstart, Property $dtend) { + private function generateWhenString(IL10N $l10n, VEvent $vevent) { + + $dtstart = $vevent->DTSTART; + if (isset($vevent->DTEND)) { + $dtend = $vevent->DTEND; + } elseif (isset($vevent->DURATION)) { + $isFloating = $vevent->DTSTART->isFloating(); + $dtend = clone $vevent->DTSTART; + $endDateTime = $end->getDateTime(); + $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue())); + $dtend->setDateTime($endDateTime, $isFloating); + } elseif (!$vevent->DTSTART->hasTime()) { + $isFloating = $vevent->DTSTART->isFloating(); + $dtend = clone $vevent->DTSTART; + $endDateTime = $end->getDateTime(); + $endDateTime = $endDateTime->modify('+1 day'); + $dtend->setDateTime($endDateTime, $isFloating); + } else { + $dtend = clone $vevent->DTSTART; + } + $isAllDay = $dtstart instanceof Property\ICalendar\Date; /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */ @@ -507,49 +505,127 @@ class IMipPlugin extends SabreIMipPlugin { * @param IL10N $l10n * @param string $method * @param string $summary - * @param string $attendeeName - * @param string $inviteeName */ private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, - $method, $summary, $attendeeName, $inviteeName) { + $method, $summary) { if ($method === self::METHOD_CANCEL) { - $template->setSubject('Cancelled: ' . $summary); - $template->addHeading($l10n->t('Invitation canceled'), $l10n->t('Hello %s,', [$attendeeName])); - $template->addBodyText($l10n->t('The meeting »%1$s« with %2$s was canceled.', [$summary, $inviteeName])); + $template->setSubject('Canceled: ' . $summary); + $template->addHeading($l10n->t('Invitation canceled')); } elseif ($method === self::METHOD_REPLY) { $template->setSubject('Re: ' . $summary); - $template->addHeading($l10n->t('Invitation updated'), $l10n->t('Hello %s,', [$attendeeName])); - $template->addBodyText($l10n->t('The meeting »%1$s« with %2$s was updated.', [$summary, $inviteeName])); + $template->addHeading($l10n->t('Invitation updated')); } else { $template->setSubject('Invitation: ' . $summary); - $template->addHeading($l10n->t('%1$s invited you to »%2$s«', [$inviteeName, $summary]), $l10n->t('Hello %s,', [$attendeeName])); + $template->addHeading($l10n->t('Invitation')); } } /** * @param IEMailTemplate $template * @param IL10N $l10n - * @param string $time - * @param string $location - * @param string $description - * @param string $url + * @param VEVENT $vevent */ - private function addBulletList(IEMailTemplate $template, IL10N $l10n, $time, $location, $description, $url) { - $template->addBodyListItem($time, $l10n->t('When:'), - $this->getAbsoluteImagePath('filetypes/text-calendar.svg')); + private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) { - if ($location) { - $template->addBodyListItem($location, $l10n->t('Where:'), - $this->getAbsoluteImagePath('filetypes/location.svg')); + if ($vevent->SUMMARY) { + $template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'), + $this->getAbsoluteImagePath('filetypes/text.svg'),'','',self::IMIP_INDENT); } - if ($description) { - $template->addBodyListItem((string)$description, $l10n->t('Description:'), - $this->getAbsoluteImagePath('filetypes/text.svg')); + $meetingWhen = $this->generateWhenString($l10n, $vevent); + if ($meetingWhen) { + $template->addBodyListItem($meetingWhen, $l10n->t('Time:'), + $this->getAbsoluteImagePath('filetypes/text-calendar.svg'),'','',self::IMIP_INDENT); } - if ($url) { - $template->addBodyListItem((string)$url, $l10n->t('Link:'), - $this->getAbsoluteImagePath('filetypes/link.svg')); + if ($vevent->LOCATION) { + $template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'), + $this->getAbsoluteImagePath('filetypes/location.svg'),'','',self::IMIP_INDENT); } + if ($vevent->URL) { + $template->addBodyListItem(sprintf('<a href="%s">%s</a>', + htmlspecialchars($vevent->URL), + htmlspecialchars($vevent->URL)), + $l10n->t('Link:'), + $this->getAbsoluteImagePath('filetypes/link.svg'), + $vevent->URL,'',self::IMIP_INDENT); + } + + $this->addAttendees($template, $l10n, $vevent); + + /* Put description last, like an email body, since it can be arbitrarily long */ + if ($vevent->DESCRIPTION) { + $template->addBodyListItem($vevent->DESCRIPTION, $l10n->t('Description:'), + $this->getAbsoluteImagePath('filetypes/text.svg'),'','',self::IMIP_INDENT); + } + } + + /** + * addAttendees: add organizer and attendee names/emails to iMip mail. + * + * Enable with DAV setting: invitation_list_attendees (default: no) + * + * The default is 'no', which matches old behavior, and is privacy preserving. + * + * To enable including attendees in invitation emails: + * % php occ config:app:set dav invitation_list_attendees --value yes + * + * @param IEMailTemplate $template + * @param IL10N $l10n + * @param Message $iTipMessage + * @param int $lastOccurrence + * @author brad2014 on github.com + */ + + private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) { + if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') { + return; + } + + if (isset($vevent->ORGANIZER)) { + $organizer = $vevent->ORGANIZER; + $organizerURI = $organizer->getNormalizedValue(); + list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto: + $organizerName = isset($organizer['CN']) ? $organizer['CN'] : null; + $organizerHTML = sprintf('<a href="%s">%s</a>', + htmlspecialchars($organizerURI), + htmlspecialchars($organizerName ?: $organizerEmail)); + $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail); + if (isset($organizer['PARTSTAT']) + && strcasecmp($organizer['PARTSTAT'], 'ACCEPTED') === 0) { + $organizerHTML .= ' ✔︎'; + $organizerText .= ' ✔︎'; + } + $template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'), + $this->getAbsoluteImagePath('filetypes/text-vcard.svg'), + $organizerText,'',self::IMIP_INDENT); + } + + $attendees = $vevent->select('ATTENDEE'); + if (count($attendees) === 0) { + return; + } + + $attendeesHTML = []; + $attendeesText = []; + foreach ($attendees as $attendee) { + $attendeeURI = $attendee->getNormalizedValue(); + list($scheme,$attendeeEmail) = explode(':',$attendeeURI,2); # strip off scheme mailto: + $attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null; + $attendeeHTML = sprintf('<a href="%s">%s</a>', + htmlspecialchars($attendeeURI), + htmlspecialchars($attendeeName ?: $attendeeEmail)); + $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail); + if (isset($attendee['PARTSTAT']) + && strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) { + $attendeeHTML .= ' ✔︎'; + $attendeeText .= ' ✔︎'; + } + array_push($attendeesHTML, $attendeeHTML); + array_push($attendeesText, $attendeeText); + } + + $template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'), + $this->getAbsoluteImagePath('filetypes/text-vcard.svg'), + implode("\n",$attendeesText),'',self::IMIP_INDENT); } /** diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php index 2c8efa7e010..054378c2afa 100644 --- a/lib/private/Mail/EMailTemplate.php +++ b/lib/private/Mail/EMailTemplate.php @@ -450,14 +450,16 @@ EOF; * if empty the $text is used, if false none will be used * @param string|bool $plainMetaInfo Meta info that is used in the plain text email * if empty the $metaInfo is used, if false none will be used + * @param integer plainIndent If > 0, Indent plainText by this amount. * @since 12.0.0 */ - public function addBodyListItem(string $text, string $metaInfo = '', string $icon = '', $plainText = '', $plainMetaInfo = '') { + public function addBodyListItem(string $text, string $metaInfo = '', string $icon = '', $plainText = '', $plainMetaInfo = '', $plainIndent = 0) { $this->ensureBodyListOpened(); if ($plainText === '') { $plainText = $text; $text = htmlspecialchars($text); + $text = str_replace("\n", "<br/>", $text); // convert newlines to HTML breaks } if ($plainMetaInfo === '') { $plainMetaInfo = $metaInfo; @@ -475,11 +477,27 @@ EOF; } $this->htmlBody .= vsprintf($this->listItem, [$icon, $htmlText]); if ($plainText !== false) { - $this->plainBody .= ' * ' . $plainText; - if ($plainMetaInfo !== false) { - $this->plainBody .= ' (' . $plainMetaInfo . ')'; + if ($plainIndent === 0) { + /* + * If plainIndent is not set by caller, this is the old NC17 layout code. + */ + $this->plainBody .= ' * ' . $plainText; + if ($plainMetaInfo !== false) { + $this->plainBody .= ' (' . $plainMetaInfo . ')'; + } + $this->plainBody .= PHP_EOL; + } else { + /* + * Caller can set plainIndent > 0 to format plainText in tabular fashion. + * with plainMetaInfo in column 1, and plainText in column 2. + * The plainMetaInfo label is right justified in a field of width + * "plainIndent". Multilines after the first are indented plainIndent+1 + * (to account for space after label). Fixes: #12391 + */ + $this->plainBody .= sprintf("%${plainIndent}s %s\n", + $plainMetaInfo, + str_replace("\n", "\n" . str_repeat(' ', $plainIndent+1), $plainText)); } - $this->plainBody .= PHP_EOL; } } @@ -538,7 +556,7 @@ EOF; $textColor = $this->themingDefaults->getTextColorPrimary(); $this->htmlBody .= vsprintf($this->buttonGroup, [$color, $color, $urlLeft, $color, $textColor, $textColor, $textLeft, $urlRight, $textRight]); - $this->plainBody .= $plainTextLeft . ': ' . $urlLeft . PHP_EOL; + $this->plainBody .= PHP_EOL . $plainTextLeft . ': ' . $urlLeft . PHP_EOL; $this->plainBody .= $plainTextRight . ': ' . $urlRight . PHP_EOL . PHP_EOL; } |