Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
339 changes: 339 additions & 0 deletions lib/private/Mail/EMailTemplate.php

Large diffs are not rendered by default.

129 changes: 129 additions & 0 deletions lib/public/Mail/EMailDetails.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\Mail;

use OCP\AppFramework\Attribute\Consumable;

/**
* Card describing an item (share, event, conversation...) in an email,
* rendered by {@see IEMailTemplate::addBodyDetails()}.
*
* All values are plain text and escaped by the template.
*
* Example:
*
* $details = new EMailDetails('Q3 board review');
* $details->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<EMailDetailsRow> */
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<EMailDetailsRow>
* @since 36.0.0
*/
public function getRows(): array {
return $this->rows;
}
}
85 changes: 85 additions & 0 deletions lib/public/Mail/EMailDetailsRow.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\Mail;

use OCP\AppFramework\Attribute\Consumable;

/**
* Labelled row of an {@see EMailDetails} card. Each part is rendered on its own line.
*
* @since 36.0.0
*/
#[Consumable(since: '36.0.0')]
final class EMailDetailsRow {
/**
* @since 36.0.0
*/
public const PART_TEXT = 'text';
/**
* @since 36.0.0
*/
public const PART_LINK = 'link';
/**
* @since 36.0.0
*/
public const PART_MUTED = 'muted';

/** @var list<array{type: self::PART_*, text: string, url: string}> */
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<array{type: self::PART_*, text: string, url: string}>
* @since 36.0.0
*/
public function getParts(): array {
return $this->parts;
}
}
59 changes: 59 additions & 0 deletions lib/public/Mail/IEMailTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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<array{text: string, url: string}> $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. <br> in the text will be replaced by new lines in the plain text email
*
Expand Down
6 changes: 6 additions & 0 deletions tests/data/REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading