diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index a641ddddb350b..0e61963e20e29 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -765,6 +765,8 @@ 'OCP\\Log\\ILogFactory' => $baseDir . '/lib/public/Log/ILogFactory.php', 'OCP\\Log\\IWriter' => $baseDir . '/lib/public/Log/IWriter.php', 'OCP\\Log\\RotationTrait' => $baseDir . '/lib/public/Log/RotationTrait.php', + 'OCP\\Mail\\EMailDetails' => $baseDir . '/lib/public/Mail/EMailDetails.php', + 'OCP\\Mail\\EMailDetailsRow' => $baseDir . '/lib/public/Mail/EMailDetailsRow.php', 'OCP\\Mail\\Events\\BeforeMessageSent' => $baseDir . '/lib/public/Mail/Events/BeforeMessageSent.php', 'OCP\\Mail\\Headers\\AutoSubmitted' => $baseDir . '/lib/public/Mail/Headers/AutoSubmitted.php', 'OCP\\Mail\\IAttachment' => $baseDir . '/lib/public/Mail/IAttachment.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 1931ad3bff8a3..6438d80271814 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -811,6 +811,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Log\\ILogFactory' => __DIR__ . '/../../..' . '/lib/public/Log/ILogFactory.php', 'OCP\\Log\\IWriter' => __DIR__ . '/../../..' . '/lib/public/Log/IWriter.php', 'OCP\\Log\\RotationTrait' => __DIR__ . '/../../..' . '/lib/public/Log/RotationTrait.php', + 'OCP\\Mail\\EMailDetails' => __DIR__ . '/../../..' . '/lib/public/Mail/EMailDetails.php', + 'OCP\\Mail\\EMailDetailsRow' => __DIR__ . '/../../..' . '/lib/public/Mail/EMailDetailsRow.php', 'OCP\\Mail\\Events\\BeforeMessageSent' => __DIR__ . '/../../..' . '/lib/public/Mail/Events/BeforeMessageSent.php', 'OCP\\Mail\\Headers\\AutoSubmitted' => __DIR__ . '/../../..' . '/lib/public/Mail/Headers/AutoSubmitted.php', 'OCP\\Mail\\IAttachment' => __DIR__ . '/../../..' . '/lib/public/Mail/IAttachment.php', diff --git a/lib/private/Mail/EMailTemplate.php b/lib/private/Mail/EMailTemplate.php index 46dbbab2c1490..b4da9f46f8128 100644 --- a/lib/private/Mail/EMailTemplate.php +++ b/lib/private/Mail/EMailTemplate.php @@ -12,6 +12,8 @@ use OCP\Defaults; use OCP\IURLGenerator; use OCP\L10N\IFactory; +use OCP\Mail\EMailDetails; +use OCP\Mail\EMailDetailsRow; use OCP\Mail\IEMailTemplate; /** @@ -319,6 +321,167 @@ class EMailTemplate implements IEMailTemplate { +EOF; + + protected string $sender = << + + + + + + + + +
%1\$s%2\$s%3\$s
+ + + + +EOF; + + protected string $senderSubline = <<%s +EOF; + + protected string $initials = <<%3\$s +EOF; + + protected string $note = << + + + + + + + +
%3\$s
+ + + + +EOF; + + protected string $noteLabel = <<%s
+EOF; + + /** @var array */ + protected array $noteColors = [ + IEMailTemplate::NOTE_NEUTRAL => ['background' => '#f5f5f5', 'border' => '#ccc'], + IEMailTemplate::NOTE_INFO => ['background' => '#e5f0f5', 'border' => '#0071ad'], + IEMailTemplate::NOTE_WARNING => ['background' => '#fdf6e3', 'border' => '#a37200'], + IEMailTemplate::NOTE_ERROR => ['background' => '#fbe5e5', 'border' => '#db0606'], + ]; + + protected string $detailsBegin = << + + + + + + + +EOF; + + protected string $detailsLeading = <<%s +EOF; + + protected string $detailsDateBadge = << + + +
+ + + %2\$s + +
%3\$s%4\$s
+
%3\$s
%4\$s
+EOF; + + protected string $detailsRow = << + %2\$s + %3\$s + +EOF; + + protected string $detailsEnd = << + + + + +EOF; + + protected string $buttonsBegin = << + + +   + + + + + + + + + +
+
+ %s + +EOF; + + protected string $buttonsLabel = <<%s

+EOF; + + protected string $buttonsPrimary = << +
+ %4\$s +
+EOF; + + protected string $buttonsSecondary = << + + + %2\$s + + + +EOF; + + protected string $buttonsSeparator = << + + + +EOF; + + protected string $buttonsEnd = << + + + + + + + + + EOF; public function __construct( @@ -605,6 +768,182 @@ public function addBodyButton(string $text, string $url, $plainText = ''): void $this->plainBody .= $url . PHP_EOL; } + /** + * @param non-empty-list $buttons + */ + #[\Override] + public function addBodyButtons(array $buttons, string $label = ''): void { + if ($this->footerAdded) { + return; + } + + $this->ensureBodyIsOpened(); + $this->ensureBodyListClosed(); + + $color = $this->themingDefaults->getDefaultColorPrimary(); + $textColor = $this->themingDefaults->getDefaultTextColorPrimary(); + + $htmlButtons = []; + foreach ($buttons as $index => $button) { + $url = htmlspecialchars($button['url']); + $text = htmlspecialchars($button['text']); + $htmlButtons[] = $index === 0 + ? vsprintf($this->buttonsPrimary, [$color, $url, $textColor, $text]) + : vsprintf($this->buttonsSecondary, [$url, $text]); + } + + $htmlLabel = $label !== '' ? vsprintf($this->buttonsLabel, [htmlspecialchars($label)]) : ''; + $this->htmlBody .= vsprintf($this->buttonsBegin, [$htmlLabel]); + $this->htmlBody .= implode($this->buttonsSeparator, $htmlButtons); + $this->htmlBody .= $this->buttonsEnd; + + if ($label !== '') { + $this->plainBody .= $label . PHP_EOL; + } + foreach ($buttons as $button) { + $this->plainBody .= $button['text'] . ': ' . $button['url'] . PHP_EOL; + } + $this->plainBody .= PHP_EOL; + } + + #[\Override] + public function addBodySender(string $displayName, string $subline = ''): void { + if ($this->footerAdded) { + return; + } + + $this->ensureBodyListClosed(); + + $htmlSubline = $subline !== '' ? vsprintf($this->senderSubline, [htmlspecialchars($subline)]) : ''; + $this->htmlBody .= vsprintf($this->sender, [$this->renderInitials($displayName), htmlspecialchars($displayName), $htmlSubline]); + + $this->plainBody .= $displayName; + if ($subline !== '') { + $this->plainBody .= ' (' . $subline . ')'; + } + $this->plainBody .= PHP_EOL . PHP_EOL; + } + + #[\Override] + public function addBodyNote(string $text, string $label = '', string $type = IEMailTemplate::NOTE_NEUTRAL): void { + if ($this->footerAdded) { + return; + } + $colors = $this->noteColors[$type] ?? $this->noteColors[IEMailTemplate::NOTE_NEUTRAL]; + + $this->ensureBodyIsOpened(); + $this->ensureBodyListClosed(); + + $htmlText = str_replace("\n", '
', htmlspecialchars($text)); + if ($label !== '') { + $htmlLabel = htmlspecialchars($label); + $htmlText = $type === IEMailTemplate::NOTE_NEUTRAL + ? vsprintf($this->noteLabel, [$htmlLabel]) . $htmlText + : '' . $htmlLabel . ' ' . $htmlText; + } + $this->htmlBody .= vsprintf($this->note, [$colors['background'], $colors['border'], $htmlText]); + + if ($type === IEMailTemplate::NOTE_NEUTRAL) { + if ($label !== '') { + $this->plainBody .= $label . PHP_EOL; + } + $this->plainBody .= '> ' . str_replace("\n", PHP_EOL . '> ', $text); + } else { + $this->plainBody .= $label !== '' ? $label . ' ' . $text : $text; + } + $this->plainBody .= PHP_EOL . PHP_EOL; + } + + #[\Override] + public function addBodyDetails(EMailDetails $details): void { + if ($this->footerAdded) { + return; + } + + $this->ensureBodyIsOpened(); + $this->ensureBodyListClosed(); + + $leading = ''; + $initialsName = $details->getInitialsName(); + $dateBadge = $details->getDateBadge(); + if ($initialsName !== null) { + $leading = vsprintf($this->detailsLeading, [$this->renderInitials($initialsName)]); + } elseif ($dateBadge !== null) { + $leading = vsprintf($this->detailsLeading, [vsprintf($this->detailsDateBadge, [ + $this->themingDefaults->getDefaultColorPrimary(), + $this->themingDefaults->getDefaultTextColorPrimary(), + htmlspecialchars($dateBadge['month']), + htmlspecialchars($dateBadge['day']), + ])]); + } + + $subtitle = $details->getSubtitle(); + $htmlSubtitle = $subtitle !== '' ? vsprintf($this->senderSubline, [htmlspecialchars($subtitle)]) : ''; + $rows = $details->getRows(); + $separator = 'border-bottom:1px solid #ddd;'; + + $this->htmlBody .= vsprintf($this->detailsBegin, [$rows !== [] ? $separator : '', $leading, htmlspecialchars($details->getTitle()), $htmlSubtitle]); + $this->plainBody .= $details->getTitle() . PHP_EOL; + if ($subtitle !== '') { + $this->plainBody .= $subtitle . PHP_EOL; + } + + $linkColor = $this->themingDefaults->getDefaultColorPrimary(); + foreach ($rows as $index => $row) { + $htmlParts = []; + $plainParts = []; + foreach ($row->getParts() as $part) { + $text = htmlspecialchars($part['text']); + switch ($part['type']) { + case EMailDetailsRow::PART_LINK: + $htmlParts[] = '' . $text . ''; + $plainParts[] = $part['text'] === $part['url'] ? $part['url'] : $part['text'] . ' (' . $part['url'] . ')'; + break; + case EMailDetailsRow::PART_MUTED: + $htmlParts[] = '' . $text . ''; + $plainParts[] = $part['text']; + break; + default: + $htmlParts[] = $text; + $plainParts[] = $part['text']; + } + } + + $this->htmlBody .= vsprintf($this->detailsRow, [ + $index > 0 ? 'border-top:1px solid #eee;' : '', + htmlspecialchars($row->getLabel()), + implode('
', $htmlParts), + ]); + + $plainLabel = ' ' . $row->getLabel() . ': '; + $indent = PHP_EOL . str_repeat(' ', mb_strlen($plainLabel)); + $this->plainBody .= $plainLabel . implode($indent, $plainParts) . PHP_EOL; + } + + $this->htmlBody .= $this->detailsEnd; + $this->plainBody .= PHP_EOL; + } + + /** + * Initials circle in the theming colors, same letters as the generated avatars + */ + protected function renderInitials(string $name): string { + $name = trim($name); + $initials = '?'; + if ($name !== '') { + $initials = implode('', array_map( + static fn (string $namePart): string => mb_strtoupper(mb_substr(trim($namePart), 0, 1, 'UTF-8'), 'UTF-8'), + explode(' ', $name, 2), + )); + } + + return vsprintf($this->initials, [ + $this->themingDefaults->getDefaultColorPrimary(), + $this->themingDefaults->getDefaultTextColorPrimary(), + htmlspecialchars($initials), + ]); + } + /** * Close the HTML body when it is open */ diff --git a/lib/public/Mail/EMailDetails.php b/lib/public/Mail/EMailDetails.php new file mode 100644 index 0000000000000..d3390d346d787 --- /dev/null +++ b/lib/public/Mail/EMailDetails.php @@ -0,0 +1,129 @@ +setSubtitle('Thursday, October 15, 2026') + * ->setDateBadge('Oct', '15'); + * $details->addRow('Where') + * ->text('Meeting room 2') + * ->link('Join in Nextcloud Talk', 'https://cloud.example.com/call/abc'); + * $emailTemplate->addBodyDetails($details); + * + * @since 36.0.0 + */ +#[Consumable(since: '36.0.0')] +final class EMailDetails { + private string $subtitle = ''; + private ?string $initialsName = null; + /** @var array{month: string, day: string}|null */ + private ?array $dateBadge = null; + /** @var list */ + private array $rows = []; + + /** + * @since 36.0.0 + */ + public function __construct( + private string $title, + ) { + } + + /** + * @since 36.0.0 + */ + public function getTitle(): string { + return $this->title; + } + + /** + * @since 36.0.0 + */ + public function setSubtitle(string $subtitle): self { + $this->subtitle = $subtitle; + return $this; + } + + /** + * @since 36.0.0 + */ + public function getSubtitle(): string { + return $this->subtitle; + } + + /** + * Show an initials circle next to the title, replaces any date badge + * + * @param string $name Name the initials are computed from + * @since 36.0.0 + */ + public function setInitials(string $name): self { + $this->initialsName = $name; + $this->dateBadge = null; + return $this; + } + + /** + * @since 36.0.0 + */ + public function getInitialsName(): ?string { + return $this->initialsName; + } + + /** + * Show a calendar badge next to the title, replaces any initials circle + * + * @param string $month Localized short month name, e.g. "Oct" + * @param string $day Day of the month, e.g. "15" + * @since 36.0.0 + */ + public function setDateBadge(string $month, string $day): self { + $this->dateBadge = ['month' => $month, 'day' => $day]; + $this->initialsName = null; + return $this; + } + + /** + * @return array{month: string, day: string}|null + * @since 36.0.0 + */ + public function getDateBadge(): ?array { + return $this->dateBadge; + } + + /** + * Add a labelled row, fill it through the returned row + * + * @since 36.0.0 + */ + public function addRow(string $label): EMailDetailsRow { + $row = new EMailDetailsRow($label); + $this->rows[] = $row; + return $row; + } + + /** + * @return list + * @since 36.0.0 + */ + public function getRows(): array { + return $this->rows; + } +} diff --git a/lib/public/Mail/EMailDetailsRow.php b/lib/public/Mail/EMailDetailsRow.php new file mode 100644 index 0000000000000..2e7afeca1bb55 --- /dev/null +++ b/lib/public/Mail/EMailDetailsRow.php @@ -0,0 +1,85 @@ + */ + private array $parts = []; + + /** + * @since 36.0.0 + */ + public function __construct( + private string $label, + ) { + } + + /** + * @since 36.0.0 + */ + public function getLabel(): string { + return $this->label; + } + + /** + * @since 36.0.0 + */ + public function text(string $text): self { + $this->parts[] = ['type' => self::PART_TEXT, 'text' => $text, 'url' => '']; + return $this; + } + + /** + * @since 36.0.0 + */ + public function link(string $text, string $url): self { + $this->parts[] = ['type' => self::PART_LINK, 'text' => $text, 'url' => $url]; + return $this; + } + + /** + * Secondary information, rendered in a lighter color + * + * @since 36.0.0 + */ + public function muted(string $text): self { + $this->parts[] = ['type' => self::PART_MUTED, 'text' => $text, 'url' => '']; + return $this; + } + + /** + * @return list + * @since 36.0.0 + */ + public function getParts(): array { + return $this->parts; + } +} diff --git a/lib/public/Mail/IEMailTemplate.php b/lib/public/Mail/IEMailTemplate.php index de75c97730317..a36a3c8aa01c1 100644 --- a/lib/public/Mail/IEMailTemplate.php +++ b/lib/public/Mail/IEMailTemplate.php @@ -37,6 +37,25 @@ * @since 12.0.0 */ interface IEMailTemplate { + /** + * Note for content written by a user, e.g. a share note + * + * @since 36.0.0 + */ + public const NOTE_NEUTRAL = 'neutral'; + /** + * @since 36.0.0 + */ + public const NOTE_INFO = 'info'; + /** + * @since 36.0.0 + */ + public const NOTE_WARNING = 'warning'; + /** + * @since 36.0.0 + */ + public const NOTE_ERROR = 'error'; + /** * Sets the subject of the email * @@ -116,6 +135,46 @@ public function addBodyButtonGroup(string $textLeft, string $urlLeft, string $te */ public function addBodyButton(string $text, string $url, $plainText = ''); + /** + * Adds a row of buttons to the body of the email, the first one is the primary action + * + * @param non-empty-list $buttons + * @param string $label Optional text shown above the buttons, e.g. "Will you attend?" + * + * @since 36.0.0 + */ + public function addBodyButtons(array $buttons, string $label = ''): void; + + /** + * Adds the person an email is about (sharer, organizer...), shown with an initials circle + * + * @param string $subline Optional second line, e.g. the email address + * + * @since 36.0.0 + */ + public function addBodySender(string $displayName, string $subline = ''): void; + + /** + * Adds a highlighted box to the body of the email + * + * Use {@see self::NOTE_NEUTRAL} for content written by a user (share note, + * event description), the other types for messages from the server itself. + * Line breaks in $text are kept. + * + * @param string $label Label above the text for neutral notes, bold title before it for the other types + * @param self::NOTE_* $type + * + * @since 36.0.0 + */ + public function addBodyNote(string $text, string $label = '', string $type = self::NOTE_NEUTRAL): void; + + /** + * Adds a card with a title and labelled rows to the body of the email + * + * @since 36.0.0 + */ + public function addBodyDetails(EMailDetails $details): void; + /** * Adds a logo and a text to the footer.
in the text will be replaced by new lines in the plain text email * diff --git a/tests/data/REUSE.toml b/tests/data/REUSE.toml index 0b6124646cac6..ca8efc4e59779 100644 --- a/tests/data/REUSE.toml +++ b/tests/data/REUSE.toml @@ -87,6 +87,12 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2017 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" +[[annotations]] +path = ["emails/blocks-email.html", "emails/blocks-email.txt"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Nextcloud GmbH and Nextcloud contributors" +SPDX-License-Identifier = "AGPL-3.0-or-later" + [[annotations]] path = ["testimage.heic"] precedence = "aggregate" diff --git a/tests/data/emails/blocks-email.html b/tests/data/emails/blocks-email.html new file mode 100644 index 0000000000000..70c2178a1de10 --- /dev/null +++ b/tests/data/emails/blocks-email.html @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + +
+ + + +
+ +
+ + +
+
+
+ + + + + + +
 
+ + + + + +
+ + + + + +
AM
Alice Martin
alice.martin@example.org
+
+ + + + + +
+

Alice Martin invited you to an event

+
+ + + + + + +
 
+ + + +
+ + + + + + +
+ + + + + +
+ + + + + + + + + + + + +
+ + + + +
+ + +
Oct
15
Q3 board review
Thursday, October 15, 2026
+
When14:00 - 15:30 (Europe/Berlin)
WhereMeeting room 2
Join in Nextcloud Talk
AttendeesAlice Martin (organizer)
You
and 3 others
+
+ + + + + +
+ + + + +
Description
We'll go through the Q3 numbers.
Please review the report beforehand.
+
+ + + + + +
+ + + + +
This share is password protected. The password is sent in a separate email.
+
+ + + + + +
 
+ + + + + + +
+
+

Will you attend?

+ + + + +
+ Accept +
+ + + +
+ Maybe +
+ + + +
+ Decline +
+
+
+
+
+ + + + + +
 
+ + + + +
+
+ +
                                                           
+ + diff --git a/tests/data/emails/blocks-email.txt b/tests/data/emails/blocks-email.txt new file mode 100644 index 0000000000000..b00fc75ea0644 --- /dev/null +++ b/tests/data/emails/blocks-email.txt @@ -0,0 +1,27 @@ +Alice Martin (alice.martin@example.org) + +Alice Martin invited you to an event + +Q3 board review +Thursday, October 15, 2026 + When: 14:00 - 15:30 (Europe/Berlin) + Where: Meeting room 2 + Join in Nextcloud Talk (https://example.org/call/abc?x=1&y=2) + Attendees: Alice Martin (organizer) + You + and 3 others + +Description +> We'll go through the Q3 numbers. +> Please review the report beforehand. + +This share is password protected. The password is sent in a separate email. + +Will you attend? +Accept: https://example.org/invitation/accept +Maybe: https://example.org/invitation/tentative +Decline: https://example.org/invitation/decline + + +-- +TestCloud - A safe home for your data \ No newline at end of file diff --git a/tests/lib/Mail/EMailDetailsTest.php b/tests/lib/Mail/EMailDetailsTest.php new file mode 100644 index 0000000000000..091b7d7246e77 --- /dev/null +++ b/tests/lib/Mail/EMailDetailsTest.php @@ -0,0 +1,59 @@ +assertSame('Title', $details->getTitle()); + $this->assertSame('', $details->getSubtitle()); + $this->assertNull($details->getInitialsName()); + $this->assertNull($details->getDateBadge()); + $this->assertSame([], $details->getRows()); + } + + public function testLeadingVisualReplacesTheOther(): void { + $details = (new EMailDetails('Title')) + ->setInitials('Board prep') + ->setDateBadge('Oct', '15'); + + $this->assertNull($details->getInitialsName()); + $this->assertSame(['month' => 'Oct', 'day' => '15'], $details->getDateBadge()); + + $details->setInitials('Board prep'); + + $this->assertSame('Board prep', $details->getInitialsName()); + $this->assertNull($details->getDateBadge()); + } + + public function testRowsKeepOrder(): void { + $details = new EMailDetails('Title'); + $details->addRow('Where') + ->text('Room 2') + ->link('Join', 'https://example.org') + ->muted('Optional'); + $details->addRow('When')->text('Today'); + + $rows = $details->getRows(); + $this->assertCount(2, $rows); + $this->assertSame('Where', $rows[0]->getLabel()); + $this->assertSame([ + ['type' => EMailDetailsRow::PART_TEXT, 'text' => 'Room 2', 'url' => ''], + ['type' => EMailDetailsRow::PART_LINK, 'text' => 'Join', 'url' => 'https://example.org'], + ['type' => EMailDetailsRow::PART_MUTED, 'text' => 'Optional', 'url' => ''], + ], $rows[0]->getParts()); + $this->assertSame('When', $rows[1]->getLabel()); + } +} diff --git a/tests/lib/Mail/EMailTemplateTest.php b/tests/lib/Mail/EMailTemplateTest.php index 8bfa8670ea255..7fbc6d314ea59 100644 --- a/tests/lib/Mail/EMailTemplateTest.php +++ b/tests/lib/Mail/EMailTemplateTest.php @@ -12,6 +12,8 @@ use OCP\IL10N; use OCP\IURLGenerator; use OCP\L10N\IFactory; +use OCP\Mail\EMailDetails; +use OCP\Mail\IEMailTemplate; use Test\TestCase; class EMailTemplateTest extends TestCase { @@ -233,4 +235,117 @@ public function testEMailTemplateEmbedsLogo(): void { $this->emailTemplate->getInlineImages() ); } + + private function mockThemingColors(): void { + $this->defaults->method('getDefaultColorPrimary')->willReturn('#0082c9'); + $this->defaults->method('getDefaultTextColorPrimary')->willReturn('#ffffff'); + $this->defaults->method('getName')->willReturn('TestCloud'); + $this->defaults->method('getLogo')->willReturn('/img/logo-mail-header.png'); + $this->urlGenerator->method('getAbsoluteURL') + ->willReturn('https://example.org/img/logo-mail-header.png'); + } + + public function testEMailTemplateBlocks(): void { + $this->mockThemingColors(); + + $details = new EMailDetails('Q3 board review'); + $details->setSubtitle('Thursday, October 15, 2026') + ->setDateBadge('Oct', '15'); + $details->addRow('When') + ->text('14:00 - 15:30 (Europe/Berlin)'); + $details->addRow('Where') + ->text('Meeting room 2') + ->link('Join in Nextcloud Talk', 'https://example.org/call/abc?x=1&y=2'); + $details->addRow('Attendees') + ->text('Alice Martin (organizer)') + ->text('You') + ->muted('and 3 others'); + + $this->emailTemplate->addHeader(); + $this->emailTemplate->addBodySender('Alice Martin', 'alice.martin@example.org'); + $this->emailTemplate->addHeading('Alice Martin invited you to an event'); + $this->emailTemplate->addBodyDetails($details); + $this->emailTemplate->addBodyNote("We'll go through the Q3 numbers.\nPlease review the report beforehand.", 'Description'); + $this->emailTemplate->addBodyNote('The password is sent in a separate email.', 'This share is password protected.', IEMailTemplate::NOTE_WARNING); + $this->emailTemplate->addBodyButtons([ + ['text' => 'Accept', 'url' => 'https://example.org/invitation/accept'], + ['text' => 'Maybe', 'url' => 'https://example.org/invitation/tentative'], + ['text' => 'Decline', 'url' => 'https://example.org/invitation/decline'], + ], 'Will you attend?'); + $this->emailTemplate->addFooter('TestCloud - A safe home for your data'); + + $expectedHTML = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/blocks-email.html'); + $this->assertSame($expectedHTML, $this->emailTemplate->renderHtml()); + $expectedTXT = file_get_contents(\OC::$SERVERROOT . '/tests/data/emails/blocks-email.txt'); + $this->assertSame($expectedTXT, $this->emailTemplate->renderText()); + } + + public function testEMailTemplateBlocksEscapeHtml(): void { + $this->mockThemingColors(); + + $details = (new EMailDetails('title')) + ->setSubtitle('subtitle') + ->setInitials('