diff options
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/IMipPlugin.php | 186 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php | 12 | ||||
-rw-r--r-- | apps/settings/tests/Mailer/NewUserMailHelperTest.php | 2 | ||||
-rw-r--r-- | core/img/caldav/attendees.svg | 1 | ||||
-rw-r--r-- | core/img/caldav/description.svg | 1 | ||||
-rw-r--r-- | core/img/caldav/link.svg | 1 | ||||
-rw-r--r-- | core/img/caldav/location.svg | 1 | ||||
-rw-r--r-- | core/img/caldav/organizer.svg | 1 | ||||
-rw-r--r-- | core/img/caldav/time.svg | 1 | ||||
-rw-r--r-- | core/img/caldav/title.svg | 1 | ||||
-rw-r--r-- | lib/private/Mail/EMailTemplate.php | 40 | ||||
-rw-r--r-- | lib/public/Mail/IEMailTemplate.php | 3 | ||||
-rw-r--r-- | tests/data/emails/new-account-email-custom-text-alternative.txt | 1 | ||||
-rw-r--r-- | tests/data/emails/new-account-email-custom.txt | 1 | ||||
-rw-r--r-- | tests/data/emails/new-account-email.txt | 1 |
15 files changed, 183 insertions, 70 deletions
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 6358a3a0293..56517ab28c1 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -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 @@ 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; @@ -261,10 +241,8 @@ class IMipPlugin extends SabreIMipPlugin { $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 @@ class IMipPlugin extends SabreIMipPlugin { return $lastOccurrence; } - /** * @param Message $iTipMessage * @return null|Property @@ -420,10 +397,28 @@ 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 = $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 @@ 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 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') { + 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); } /** diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php index 8faa54f534a..a31fdfdc5f7 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php @@ -136,6 +136,7 @@ class IMipPluginTest extends TestCase { public function testDelivery() { $this->config + ->expects($this->at(1)) ->method('getAppValue') ->with('dav', 'invitation_link_recipients', 'yes') ->willReturn('yes'); @@ -148,6 +149,7 @@ class IMipPluginTest extends TestCase { public function testFailedDelivery() { $this->config + ->expects($this->at(1)) ->method('getAppValue') ->with('dav', 'invitation_link_recipients', 'yes') ->willReturn('yes'); @@ -163,6 +165,7 @@ class IMipPluginTest extends TestCase { public function testDeliveryWithNoCommonName() { $this->config + ->expects($this->at(1)) ->method('getAppValue') ->with('dav', 'invitation_link_recipients', 'yes') ->willReturn('yes'); @@ -188,9 +191,8 @@ class IMipPluginTest extends TestCase { */ public function testNoMessageSendForPastEvents(array $veventParams, bool $expectsMail) { $this->config - ->method('getAppValue') - ->with('dav', 'invitation_link_recipients', 'yes') - ->willReturn('yes'); + ->method('getAppValue') + ->willReturn('yes'); $message = $this->_testMessage($veventParams); @@ -228,6 +230,7 @@ class IMipPluginTest extends TestCase { $this->_expectSend($recipient, true, $has_buttons); $this->config + ->expects($this->at(1)) ->method('getAppValue') ->with('dav', 'invitation_link_recipients', 'yes') ->willReturn($config_setting); @@ -252,14 +255,13 @@ class IMipPluginTest extends TestCase { public function testMessageSendWhenEventWithoutName() { $this->config ->method('getAppValue') - ->with('dav', 'invitation_link_recipients', 'yes') ->willReturn('yes'); $message = $this->_testMessage(['SUMMARY' => '']); $this->_expectSend('frodo@hobb.it', true, true,'Invitation: Untitled event'); $this->emailTemplate->expects($this->once()) ->method('addHeading') - ->with('Mr. Wizard invited you to »Untitled event«'); + ->with('Invitation'); $this->plugin->schedule($message); $this->assertEquals('1.1', $message->getScheduleStatus()); } diff --git a/apps/settings/tests/Mailer/NewUserMailHelperTest.php b/apps/settings/tests/Mailer/NewUserMailHelperTest.php index 3d45b9e2ff8..8060b69da04 100644 --- a/apps/settings/tests/Mailer/NewUserMailHelperTest.php +++ b/apps/settings/tests/Mailer/NewUserMailHelperTest.php @@ -601,6 +601,7 @@ Welcome to your TestCloud account, you can add, protect, and share your data. Your username is: john + Go to TestCloud: https://example.com/ Install Client: https://nextcloud.com/install/#install-clients @@ -819,6 +820,7 @@ Welcome aboard John Doe Welcome to your TestCloud account, you can add, protect, and share your data. + Go to TestCloud: https://example.com/ Install Client: https://nextcloud.com/install/#install-clients diff --git a/core/img/caldav/attendees.svg b/core/img/caldav/attendees.svg new file mode 100644 index 00000000000..86c3d4a4132 --- /dev/null +++ b/core/img/caldav/attendees.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16" height="16" width="16" version="1.1"><path fill="#969696" d="m10 1c-1.75 0-3 1.43-3 2.8 0 1.4 0.1 2.4 0.8 3.5 0.2 0.29 0.5 0.35 0.7 0.6 0.135 0.5 0.24 1 0.1 1.5-0.28 0.1-0.525 0.22-0.8 0.33-0.085-0.15-0.23-0.2-0.47-0.4-0.73-0.44-1.56-0.75-2.33-1.04-0.1-0.37-0.1-0.65 0-1 0.156-0.166 0.37-0.27 0.5-0.43 0.46-0.6 0.5-1.654 0.5-2.37 0-1.06-0.954-1.9-2-1.9-1.17 0-2 1-2 1.9 0 0.93 0.034 1.64 0.5 2.37 0.13 0.2 0.367 0.26 0.5 0.43 0.1 0.33 0.1 0.654 0 1-0.85 0.3-1.6 0.64-2.34 1.04-0.57 0.4-0.52 0.205-0.66 1.53-0.11 1.06 2.335 1.13 4 1.13 0.06 0 0.11 0 0.17 0-0.054 0.274-0.1 0.63-0.17 1.3-0.16 1.59 3.5 1.7 6 1.7s6.16-0.1 6-1.7c-0.215-2-0.23-1.71-1-2.3-1.1-0.654-2.45-1.17-3.6-1.6-0.15-0.56-0.04-0.97 0.1-1.5 0.235-0.25 0.5-0.36 0.7-0.6 0.7-0.885 0.8-2.425 0.8-3.5 0-1.6-1.43-2.8-3-2.8z"/></svg> diff --git a/core/img/caldav/description.svg b/core/img/caldav/description.svg new file mode 100644 index 00000000000..57c2b1f5725 --- /dev/null +++ b/core/img/caldav/description.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" version="1.1" height="16"><path fill="#969696" d="m2.5 1c-0.28 0-0.5 0.22-0.5 0.5v13c0 0.28 0.22 0.5 0.5 0.5h11c0.28 0 0.5-0.22 0.5-0.5v-10.5l-3-3h-8.5zm1.5 2h6v1h-6v-1zm0 3h5v1h-5v-1zm0 3h8v1h-8v-1zm0 3h4v1h-4v-1z"/></svg> diff --git a/core/img/caldav/link.svg b/core/img/caldav/link.svg new file mode 100644 index 00000000000..7bfbe1eb2de --- /dev/null +++ b/core/img/caldav/link.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16" width="16" version="1.1" height="16"><path fill="#969696" d="m7.95 0.65c-4.1 0-7.4 3.3-7.4 7.4s3.3 7.4 7.4 7.4 7.4-3.3 7.4-7.4-3.3-7.4-7.4-7.4zm0.8 0.9c1.3 0 2.4 0.8 3.5 1.3l1.8 2.5-0.3 1.1 0.6 0.3v2.4c-0.2 0.7-0.6 1.3-0.9 2-0.2 0.1 0-0.8-0.1-1 0-0.6-0.5-0.6-0.9-0.2-0.4 0.3-1.4 0.3-1.5-0.4-0.3-0.8 0-1.7 0.3-2.5l-0.6-0.7 0.2-1.8-0.8-0.9 0.2-1-1-0.6c-0.2-0.2-0.6-0.2-0.7-0.4 0.1 0 0.2-0.1 0.2-0.1zm-2.6 0.1s0.1 0 0.1 0.1c0.4 0.2-0.1 0.4-0.2 0.6-0.5 0.3 0.3 0.7 0.5 1 0.4-0.1 0.8-0.7 1.4-0.5 0.7-0.2 0.6 0.6 1.1 1 0.1 0.2 0.9 0.8 0.4 0.6-0.5-0.4-1-0.4-1.3 0.1-0.8 0.5-0.3-0.9-0.7-1.2-0.6-0.7-0.4 0.5-0.4 0.9-0.4 0-1.1-0.3-1.5 0.2l0.4 0.6 0.5-0.7c0-0.3 0.1 0.2 0.3 0.3 0.1 0.2 0.8 0.7 0.3 0.9-0.8 0.4-1.4 1.1-2.1 1.7-0.2 0.5-0.7 0.4-1 0-0.7-0.4-0.7 0.7-0.6 1.1l0.6-0.4v1.1c-0.4 0.4-0.9-0.7-1.3-0.9v-1.6c0-0.4-0.1-0.9 0-1.3 0.8-0.9 1.7-1.9 2.2-3h0.8c0.6 0.2 0.3-0.7 0.5-0.6zm-1.2 8.2c0.1 0 0.2 0 0.3 0.1 0.8 0.1 1.4 0.7 2 1.1 0.5 0.5 1.6 0.3 1.7 1.2-0.2 0.9-1.1 1.4-1.8 1.7-0.2 0.1-0.4 0.2-0.6 0.2-0.7 0.2-1-0.6-1.2-1.1-0.3-0.7-1.1-1.2-1-2.1 0-0.4 0.2-1 0.6-1.1z"/></svg> diff --git a/core/img/caldav/location.svg b/core/img/caldav/location.svg new file mode 100644 index 00000000000..5e63f7563cd --- /dev/null +++ b/core/img/caldav/location.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" version="1.1" height="16"><circle stroke-width="2" stroke="#969696" cy="6" cx="8" r="4" fill="none"/><path fill="#969696" d="m4 9h8l-4 6z"/></svg> diff --git a/core/img/caldav/organizer.svg b/core/img/caldav/organizer.svg new file mode 100644 index 00000000000..7b75d9e29a6 --- /dev/null +++ b/core/img/caldav/organizer.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 16 16" height="16" width="16" version="1.1"><path fill="#969696" d="m5 3.8c0 1.4 0.1 2.4 0.8 3.5 0.2 0.286 0.5 0.35 0.7 0.6 0.135 0.5 0.24 0.98 0.1 1.5-1.275 0.45-2.49 1-3.6 1.6-0.85 0.6-0.785 0.31-1 2.3-0.16 1.59 3.5 1.7 6 1.7s6.163-0.1 6-1.7c-0.215-2-0.23-1.71-1-2.3-1.1-0.654-2.45-1.167-3.6-1.6-0.15-0.56-0.04-0.973 0.1-1.5 0.235-0.25 0.5-0.363 0.7-0.6 0.69-0.885 0.8-2.425 0.8-3.5 0-1.59-1.43-2.8-3-2.8-1.75 0-3 1.43-3 2.8z"/></svg> diff --git a/core/img/caldav/time.svg b/core/img/caldav/time.svg new file mode 100644 index 00000000000..2fdfde67960 --- /dev/null +++ b/core/img/caldav/time.svg @@ -0,0 +1 @@ +<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 16 16"><path fill="#969696" d="m4 1c-0.5 0-1 0.5-1 1v2c0 0.5 0.5 1 1 1s1-0.5 1-1v-2c0-0.5-0.5-1-1-1zm8 0c-0.5 0-1 0.5-1 1v2c0 0.5 0.5 1 1 1s1-0.5 1-1v-2c0-0.5-0.5-1-1-1zm-6.5 2v1c0 0.831-0.5 1.5-1.5 1.5s-1.5-0.5-1.5-1.5v-0.9375c-0.8841 0.227-1.5 1.0247-1.5 1.9375v8c0 1.108 0.892 2 2 2h10c1.108 0 2-0.892 2-2v-8c0-0.9128-0.61588-1.7105-1.5-1.9375v0.9375c0 0.831-0.5 1.5-1.5 1.5s-1.5-0.5-1.5-1.5v-1zm7.5 5v5h-10v-5z"/></svg> diff --git a/core/img/caldav/title.svg b/core/img/caldav/title.svg new file mode 100644 index 00000000000..57d674b9f2c --- /dev/null +++ b/core/img/caldav/title.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 16 16" width="16" height="16"><path fill="#969696" d="M2 2l12 6-12 6z"/></svg> diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php index 3cd4327c004..3edbea72aca 100644 --- a/lib/private/Mail/EMailTemplate.php +++ b/lib/private/Mail/EMailTemplate.php @@ -448,19 +448,21 @@ EOF; * @param string $metaInfo Note: When $plainMetaInfo falls back to this, HTML is automatically escaped in the HTML email * @param string $icon Absolute path, must be 16*16 pixels * @param string|bool $plainText Text that is used in the plain text email - * if empty the $text is used, if false none will be used + * if empty or true 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 + * if empty or true 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 === '') { + if ($plainText === '' || $plainText === true) { $plainText = $text; $text = htmlspecialchars($text); + $text = str_replace("\n", "<br/>", $text); // convert newlines to HTML breaks } - if ($plainMetaInfo === '') { + if ($plainMetaInfo === '' || $plainMetaInfo === true) { $plainMetaInfo = $metaInfo; $metaInfo = htmlspecialchars($metaInfo); } @@ -476,11 +478,29 @@ 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 + */ + /** @var string $label */ + $label = ($plainMetaInfo !== false)? $plainMetaInfo : ''; + $this->plainBody .= sprintf("%${plainIndent}s %s\n", + $label, + str_replace("\n", "\n" . str_repeat(' ', $plainIndent+1), $plainText)); } - $this->plainBody .= PHP_EOL; } } @@ -539,7 +559,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; } diff --git a/lib/public/Mail/IEMailTemplate.php b/lib/public/Mail/IEMailTemplate.php index 70046d5c508..5f4e235a7ee 100644 --- a/lib/public/Mail/IEMailTemplate.php +++ b/lib/public/Mail/IEMailTemplate.php @@ -106,9 +106,10 @@ interface IEMailTemplate { * 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); /** * Adds a button group of two buttons to the body of the email diff --git a/tests/data/emails/new-account-email-custom-text-alternative.txt b/tests/data/emails/new-account-email-custom-text-alternative.txt index f65744b20d9..03cb99c1d76 100644 --- a/tests/data/emails/new-account-email-custom-text-alternative.txt +++ b/tests/data/emails/new-account-email-custom-text-alternative.txt @@ -4,6 +4,7 @@ Welcome to your Nextcloud account, you can add, protect, and share your data. - Your username is: abc + Set your password - text: https://example.org/resetPassword/123 Install Client - text: https://nextcloud.com/install/#install-clients diff --git a/tests/data/emails/new-account-email-custom.txt b/tests/data/emails/new-account-email-custom.txt index 57c5202a744..c075c49d649 100644 --- a/tests/data/emails/new-account-email-custom.txt +++ b/tests/data/emails/new-account-email-custom.txt @@ -4,6 +4,7 @@ Welcome to your Nextcloud account, you can add, protect, and share your data. Your username is: abc + Set your password: https://example.org/resetPassword/123 Install Client: https://nextcloud.com/install/#install-clients diff --git a/tests/data/emails/new-account-email.txt b/tests/data/emails/new-account-email.txt index 89524134183..b246482af13 100644 --- a/tests/data/emails/new-account-email.txt +++ b/tests/data/emails/new-account-email.txt @@ -4,6 +4,7 @@ Welcome to your Nextcloud account, you can add, protect, and share your data. Your username is: abc + Set your password: https://example.org/resetPassword/123 Install Client: https://nextcloud.com/install/#install-clients |