-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
iMIP email improvements (take 2) #17456
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,6 @@ | |
* @copyright Copyright (c) 2016, ownCloud, Inc. | ||
* @copyright Copyright (c) 2017, Georg Ehrke | ||
* | ||
* @author brad2014 <brad2014@users.noreply.github.com> | ||
* @author Brad Rubenstein <brad@wbr.tech> | ||
* @author Christoph Wurst <christoph@winzerhof-wurst.at> | ||
* @author Georg Ehrke <oc.list@georgehrke.com> | ||
|
@@ -108,6 +107,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 +204,6 @@ public function schedule(Message $iTipMessage) { | |
$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; | ||
|
@@ -261,10 +241,8 @@ public function schedule(Message $iTipMessage) { | |
|
||
$summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event'); | ||
|
||
$this->addSubjectAndHeading($template, $l10n, $method, $summary, | ||
$meetingAttendeeName, $meetingInviteeName); | ||
$this->addBulletList($template, $l10n, $meetingWhen, $meetingLocation, | ||
$meetingDescription, $meetingUrl); | ||
$this->addSubjectAndHeading($template, $l10n, $method, $summary); | ||
$this->addBulletList($template, $l10n, $vevent); | ||
|
||
|
||
// Only add response buttons to invitation requests: Fix Issue #11230 | ||
|
@@ -370,7 +348,6 @@ private function getLastOccurrence(VCalendar $vObject) { | |
return $lastOccurrence; | ||
} | ||
|
||
|
||
/** | ||
* @param Message $iTipMessage | ||
* @return null|Property | ||
|
@@ -420,10 +397,28 @@ private function getAttendeeRSVP(Property $attendee = null) { | |
|
||
/** | ||
* @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 = $dtend->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 = $dtend->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 +502,132 @@ private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) { | |
* @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')); | ||
Comment on lines
-516
to
+510
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made spellings consistent. "Canceled" is en_us. "Cancelled" is en_gb. Got rid of the non-en_us »« by moving the title to the front of the list (instead of a proceeding bodyText). Other notes: Also "Hello %s" is error-prone and disconcerting on an auto-generated email (the attendeeName is not necessarily appropriate for a salutation, and it may be missing altogether). I hope you agree the result is better without it. Also, in the cancelation case, it isn't right to say "The meeting ${subject} with ${inviteeName} was canceled," because:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
} 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 VEVENT $vevent | ||
*/ | ||
private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) { | ||
if ($vevent->SUMMARY) { | ||
$template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'), | ||
$this->getAbsoluteImagePath('caldav/title.svg'),'','',self::IMIP_INDENT); | ||
} | ||
$meetingWhen = $this->generateWhenString($l10n, $vevent); | ||
if ($meetingWhen) { | ||
$template->addBodyListItem($meetingWhen, $l10n->t('Time:'), | ||
$this->getAbsoluteImagePath('caldav/time.svg'),'','',self::IMIP_INDENT); | ||
} | ||
if ($vevent->LOCATION) { | ||
$template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'), | ||
$this->getAbsoluteImagePath('caldav/location.svg'),'','',self::IMIP_INDENT); | ||
} | ||
if ($vevent->URL) { | ||
$url = $vevent->URL->getValue(); | ||
$template->addBodyListItem(sprintf('<a href="%s">%s</a>', | ||
htmlspecialchars($url), | ||
htmlspecialchars($url)), | ||
$l10n->t('Link:'), | ||
$this->getAbsoluteImagePath('caldav/link.svg'), | ||
$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->getValue(), $l10n->t('Description:'), | ||
$this->getAbsoluteImagePath('caldav/description.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 string $time | ||
* @param string $location | ||
* @param string $description | ||
* @param string $url | ||
* @param Message $iTipMessage | ||
* @param int $lastOccurrence | ||
* @author brad2014 on github.com | ||
*/ | ||
private function addBulletList(IEMailTemplate $template, IL10N $l10n, $time, $location, $description, $url) { | ||
$template->addBodyListItem($time, $l10n->t('When:'), | ||
$this->getAbsoluteImagePath('filetypes/text-calendar.svg')); | ||
|
||
if ($location) { | ||
$template->addBodyListItem($location, $l10n->t('Where:'), | ||
$this->getAbsoluteImagePath('filetypes/location.svg')); | ||
private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) { | ||
if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This new function is a no-op until the admin enables it (it is disabled by default, to match current e-mails, and because there may be policy reasons why iMip mails should not disclose so prominently the names and email addresses of all invited attendees (though there is nothing in the text that is not also in the ics file). It adds to the list (before the description) the name/email/accept-status of the organizer and each attendee. TODO: Really, the checkmark for "accepted" is lovely, but too limited. There should be a mark for (a) not replied yet, (b) accepted, (c) declined. |
||
return; | ||
} | ||
if ($description) { | ||
$template->addBodyListItem((string)$description, $l10n->t('Description:'), | ||
$this->getAbsoluteImagePath('filetypes/text.svg')); | ||
|
||
if (isset($vevent->ORGANIZER)) { | ||
/** @var Property\ICalendar\CalAddress $organizer */ | ||
$organizer = $vevent->ORGANIZER; | ||
$organizerURI = $organizer->getNormalizedValue(); | ||
list($scheme,$organizerEmail) = explode(':',$organizerURI,2); # strip off scheme mailto: | ||
/** @var string|null $organizerName */ | ||
$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'])) { | ||
/** @var Parameter $partstat */ | ||
$partstat = $organizer['PARTSTAT']; | ||
if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) { | ||
$organizerHTML .= ' ✔︎'; | ||
$organizerText .= ' ✔︎'; | ||
} | ||
} | ||
$template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'), | ||
$this->getAbsoluteImagePath('caldav/organizer.svg'), | ||
$organizerText,'',self::IMIP_INDENT); | ||
} | ||
if ($url) { | ||
$template->addBodyListItem((string)$url, $l10n->t('Link:'), | ||
$this->getAbsoluteImagePath('filetypes/link.svg')); | ||
|
||
$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('caldav/attendees.svg'), | ||
implode("\n",$attendeesText),'',self::IMIP_INDENT); | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved into the generateWhenString function, where it belongs.