EmailInterface.php
PHP
Path: src/Mail/EmailInterface.php
<?php
namespace mini\Mail;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* Email Composition Interface
*
* Provides a declarative API for composing RFC 5322 email messages with MIME support.
* The implementation uses lazy compilation - you describe what you want (text, HTML,
* attachments, inline images) and the correct MIME structure is built automatically.
*
* ## Basic Usage
*
* ```php
* $email = (new Email())
* ->withFrom('sender@example.com')
* ->withTo('recipient@example.com')
* ->withSubject('Hello!')
* ->withTextBody('Plain text version')
* ->withHtmlBody('<h1>HTML version</h1>');
*
* // Cast to string for complete RFC 5322 message (headers + body)
* $raw = (string) $email;
*
* // Or stream it (recommended for large emails with attachments)
* while (!$email->eof()) {
* fwrite($pipe, $email->read(8192));
* }
*
* // Pipe to sendmail
* $pipe = popen('/usr/sbin/sendmail -t', 'w');
* fwrite($pipe, (string) $email);
* pclose($pipe);
* ```
*
* ## Mailbox Addresses
*
* Address methods accept both strings and MailboxInterface:
*
* ```php
* // Simple string
* $email->withFrom('sender@example.com');
*
* // String with display name
* $email->withFrom('Frode Børli <frode@ennerd.com>');
*
* // MailboxInterface
* $email->withFrom(new Mailbox('frode@ennerd.com', 'Frode Børli'));
*
* // Parsed from string
* $email->withFrom(Mailbox::fromString('Frode Børli <frode@ennerd.com>'));
*
* // Multiple recipients
* $email->withTo('alice@example.com', 'bob@example.com', $carolMailbox);
*
* // Add recipients incrementally
* $email->withTo('alice@example.com')
* ->withAddedTo('bob@example.com');
* ```
*
* ## HTML with Inline Images
*
* Inline images are referenced in HTML using the `cid:` URL scheme.
* The array keys become Content-IDs:
*
* ```php
* $email = (new Email())
* ->withFrom('newsletter@example.com')
* ->withTo('subscriber@example.com')
* ->withSubject('Weekly Update')
* ->withTextBody('View this email in a browser for images.')
* ->withHtmlBody(
* '<html>
* <body>
* <img src="cid:header" alt="Header">
* <p>Hello!</p>
* <img src="cid:logo" alt="Logo">
* </body>
* </html>',
* [
* 'header' => '/path/to/header.png', // String = file path
* 'logo' => Message::fromFile('logo.png'), // Or MessageInterface
* ]
* );
* ```
*
* ## Attachments
*
* Add file attachments with automatic MIME type detection:
*
* ```php
* $email = (new Email())
* ->withFrom('reports@example.com')
* ->withTo('manager@example.com')
* ->withSubject('Monthly Report')
* ->withTextBody('Please find the report attached.')
* ->withAttachments([
* '/path/to/report.pdf', // Filename from path
* '/path/to/data.csv',
* Message::fromFile('/tmp/generated.xlsx'), // Filename from Message
* ]);
* ```
*
* To override the filename shown to recipients, use string keys:
*
* ```php
* $email->withAttachments([
* 'Monthly Report.pdf' => '/tmp/report-2024-01.pdf',
* 'Raw Data.csv' => $csvMessageInterface,
* ]);
* ```
*
* ## Complete Example
*
* An email with text alternative, HTML with inline images, and attachments:
*
* ```php
* $email = (new Email())
* ->withFrom('Name <sender@example.com>')
* ->withTo('recipient@example.com')
* ->withCc('cc@example.com')
* ->withReplyTo('replies@example.com')
* ->withSubject('Project Update')
* ->withTextBody('Please view in HTML for the full experience.')
* ->withHtmlBody(
* file_get_contents('email-template.html'),
* [
* 'logo' => '/assets/logo.png',
* 'chart' => Message::fromFile('/tmp/chart.png'),
* ]
* )
* ->withAttachments([
* 'Project Plan.pdf' => '/documents/plan.pdf',
* 'meeting.ics' => $calendarInvite,
* ]);
* ```
*
* ## MIME Structure
*
* The implementation automatically builds the correct nested MIME structure:
*
* ```
* multipart/mixed
* ├── multipart/alternative
* │ ├── text/plain
* │ └── multipart/related
* │ ├── text/html
* │ ├── image/png (Content-ID: <logo>)
* │ └── image/png (Content-ID: <chart>)
* ├── application/pdf (attachment: Project Plan.pdf)
* └── text/calendar (attachment: meeting.ics)
* ```
*
* ## Templating Pattern
*
* Create reusable templates with immutable composition:
*
* ```php
* $template = (new Email())
* ->withFrom('noreply@example.com')
* ->withSubject('Welcome!')
* ->withHtmlBody('<h1>Welcome, {name}!</h1>', ['logo' => '/assets/logo.png']);
*
* foreach ($users as $user) {
* $email = $template
* ->withTo($user->email)
* ->withHtmlBody(
* str_replace('{name}', $user->name, $template->getHtmlBody()),
* $template->getInlines()
* );
*
* $transport->send($email);
* }
* ```
*
* @see https://datatracker.ietf.org/doc/html/rfc5322 Internet Message Format
* @see https://datatracker.ietf.org/doc/html/rfc2045 MIME Part One
* @see https://datatracker.ietf.org/doc/html/rfc2046 MIME Part Two: Media Types
*/
interface EmailInterface extends MessageInterface, StreamInterface
{
// =========================================================================
// Address Headers
// =========================================================================
/**
* Get the From addresses
*
* @return MailboxInterface[] Array of sender mailboxes
*/
public function getFrom(): array;
/**
* Return instance with the specified From address(es)
*
* Replaces any existing From addresses.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withFrom(MailboxInterface|string ...$mailboxes): static;
/**
* Get the To addresses
*
* @return MailboxInterface[] Array of recipient mailboxes
*/
public function getTo(): array;
/**
* Return instance with the specified To address(es)
*
* Replaces any existing To addresses.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withTo(MailboxInterface|string ...$mailboxes): static;
/**
* Return instance with additional To address(es)
*
* Adds to existing To addresses without replacing them.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes to add
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withAddedTo(MailboxInterface|string ...$mailboxes): static;
/**
* Get the Cc addresses
*
* @return MailboxInterface[] Array of CC mailboxes
*/
public function getCc(): array;
/**
* Return instance with the specified Cc address(es)
*
* Replaces any existing Cc addresses.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withCc(MailboxInterface|string ...$mailboxes): static;
/**
* Return instance with additional Cc address(es)
*
* Adds to existing Cc addresses without replacing them.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes to add
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withAddedCc(MailboxInterface|string ...$mailboxes): static;
/**
* Get the Bcc addresses
*
* @return MailboxInterface[] Array of BCC mailboxes
*/
public function getBcc(): array;
/**
* Return instance with the specified Bcc address(es)
*
* Replaces any existing Bcc addresses.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withBcc(MailboxInterface|string ...$mailboxes): static;
/**
* Return instance with additional Bcc address(es)
*
* Adds to existing Bcc addresses without replacing them.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes to add
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withAddedBcc(MailboxInterface|string ...$mailboxes): static;
/**
* Get the Reply-To addresses
*
* @return MailboxInterface[] Array of Reply-To mailboxes
*/
public function getReplyTo(): array;
/**
* Return instance with the specified Reply-To address(es)
*
* Replaces any existing Reply-To addresses.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withReplyTo(MailboxInterface|string ...$mailboxes): static;
/**
* Return instance with additional Reply-To address(es)
*
* Adds to existing Reply-To addresses without replacing them.
*
* @param MailboxInterface|string ...$mailboxes One or more mailboxes to add
* @return static
* @throws \InvalidArgumentException If a mailbox string is invalid
*/
public function withAddedReplyTo(MailboxInterface|string ...$mailboxes): static;
// =========================================================================
// Subject and Date
// =========================================================================
/**
* Get the subject line
*
* @return string|null The subject, or null if not set
*/
public function getSubject(): ?string;
/**
* Return instance with the specified subject
*
* @param string $subject The subject line
* @return static
*/
public function withSubject(string $subject): static;
/**
* Get the Date header
*
* @return string|null RFC 5322 formatted date, or null if not set
*/
public function getDate(): ?string;
/**
* Return instance with the specified Date
*
* If not set, the current date/time is used when the email is compiled.
*
* @param string|\DateTimeInterface $date RFC 5322 date string or DateTimeInterface
* @return static
*/
public function withDate(string|\DateTimeInterface $date): static;
// =========================================================================
// Body Content
// =========================================================================
/**
* Get the plain text body
*
* @return string|null The text body, or null if not set
*/
public function getTextBody(): ?string;
/**
* Return instance with the specified plain text body
*
* The text body serves as a fallback for email clients that don't support HTML.
*
* @param string $text Plain text content
* @return static
*/
public function withTextBody(string $text): static;
/**
* Get the HTML body
*
* @return string|null The HTML body, or null if not set
*/
public function getHtmlBody(): ?string;
/**
* Return instance with the specified HTML body and optional inline images
*
* Inline images are referenced in HTML using `cid:` URLs. The array keys
* become the Content-ID values:
*
* ```php
* ->withHtmlBody(
* '<img src="cid:logo">',
* ['logo' => '/path/to/logo.png']
* )
* ```
*
* @param string $html HTML content
* @param array<string, MessageInterface|string> $inlines Inline images keyed by Content-ID.
* Values can be file paths (string) or MessageInterface instances.
* @return static
* @throws \InvalidArgumentException If an inline value is not a valid file path or MessageInterface
*/
public function withHtmlBody(string $html, array $inlines = []): static;
/**
* Get the inline images
*
* @return array<string, MessageInterface> Inline images keyed by Content-ID
*/
public function getInlines(): array;
// =========================================================================
// Attachments
// =========================================================================
/**
* Get the attachments
*
* @return MessageInterface[] Array of attachment parts
*/
public function getAttachments(): array;
/**
* Return instance with the specified attachments
*
* Attachments can be specified as file paths or MessageInterface instances.
* Use string keys to override the displayed filename:
*
* ```php
* ->withAttachments([
* '/path/to/file.pdf', // Filename: file.pdf
* 'Report.pdf' => '/tmp/generated.pdf', // Filename: Report.pdf
* Message::fromFile('data.csv'), // Filename from Message
* ])
* ```
*
* This method replaces any existing attachments.
*
* @param array<int|string, MessageInterface|string> $attachments
* Numeric keys: filename derived from path or Message.
* String keys: override the displayed filename.
* @return static
* @throws \InvalidArgumentException If an attachment value is not a valid file path or MessageInterface
*/
public function withAttachments(array $attachments): static;
// =========================================================================
// Output
// =========================================================================
/**
* Get the compiled message body as a stream
*
* Returns the MIME body (without top-level headers) as a StreamInterface.
* The MIME structure is built automatically based on the content:
*
* - Text only: text/plain
* - HTML only: text/html (or multipart/related if inlines present)
* - Both: multipart/alternative
* - With attachments: wrapped in multipart/mixed
*
* @return StreamInterface
*/
public function getBody(): StreamInterface;
}