Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
15 / 15
CRAP
100.00% covered (success)
100.00%
1 / 1
MessageBuilder
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
15 / 15
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 from
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 to
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 cc
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 bcc
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 replyTo
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 subject
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 textBody
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 htmlBody
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 attach
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 attachContent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 header
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 build
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 resolveEmailAddress
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10namespace Phexium\Plugin\Mailer;
11
12use Phexium\Plugin\Mailer\Port\Exception\InvalidMessageException;
13
14final class MessageBuilder
15{
16    private ?EmailAddress $from = null;
17
18    /** @var EmailAddress[] */
19    private array $to = [];
20
21    /** @var EmailAddress[] */
22    private array $cc = [];
23
24    /** @var EmailAddress[] */
25    private array $bcc = [];
26
27    private ?EmailAddress $replyTo = null;
28
29    private ?string $subject = null;
30
31    private ?string $textBody = null;
32
33    private ?string $htmlBody = null;
34
35    /** @var Attachment[] */
36    private array $attachments = [];
37
38    /** @var Header[] */
39    private array $headers = [];
40
41    private function __construct()
42    {
43        // Private constructor: use static create() factory method
44    }
45
46    public static function create(): self
47    {
48        return new self();
49    }
50
51    public function from(EmailAddress|string $email, ?string $name = null): self
52    {
53        $this->from = $this->resolveEmailAddress($email, $name);
54
55        return $this;
56    }
57
58    public function to(EmailAddress|string $email, ?string $name = null): self
59    {
60        $this->to[] = $this->resolveEmailAddress($email, $name);
61
62        return $this;
63    }
64
65    public function cc(EmailAddress|string $email, ?string $name = null): self
66    {
67        $this->cc[] = $this->resolveEmailAddress($email, $name);
68
69        return $this;
70    }
71
72    public function bcc(EmailAddress|string $email): self
73    {
74        $this->bcc[] = $this->resolveEmailAddress($email, null);
75
76        return $this;
77    }
78
79    public function replyTo(EmailAddress|string $email, ?string $name = null): self
80    {
81        $this->replyTo = $this->resolveEmailAddress($email, $name);
82
83        return $this;
84    }
85
86    public function subject(string $subject): self
87    {
88        $this->subject = $subject;
89
90        return $this;
91    }
92
93    public function textBody(string $body): self
94    {
95        $this->textBody = $body;
96
97        return $this;
98    }
99
100    public function htmlBody(string $body): self
101    {
102        $this->htmlBody = $body;
103
104        return $this;
105    }
106
107    public function attach(string $path, ?string $filename = null, ?string $mimeType = null): self
108    {
109        $this->attachments[] = Attachment::fromPath($path, $filename, $mimeType);
110
111        return $this;
112    }
113
114    public function attachContent(string $content, string $filename, ?string $mimeType = null): self
115    {
116        $this->attachments[] = Attachment::fromContent($content, $filename, $mimeType);
117
118        return $this;
119    }
120
121    public function header(string $name, string $value): self
122    {
123        $this->headers[] = new Header($name, $value);
124
125        return $this;
126    }
127
128    public function build(): Message
129    {
130        if (!$this->from instanceof EmailAddress) {
131            throw new InvalidMessageException('Sender (from) is required');
132        }
133
134        if ($this->to === []) {
135            throw InvalidMessageException::missingRecipient();
136        }
137
138        if ($this->subject === null) {
139            throw new InvalidMessageException('Subject is required');
140        }
141
142        if ($this->textBody === null && $this->htmlBody === null) {
143            throw InvalidMessageException::missingBody();
144        }
145
146        return new Message(
147            from: $this->from,
148            to: $this->to,
149            subject: $this->subject,
150            textBody: $this->textBody,
151            htmlBody: $this->htmlBody,
152            cc: $this->cc,
153            bcc: $this->bcc,
154            replyTo: $this->replyTo,
155            attachments: $this->attachments,
156            headers: $this->headers,
157        );
158    }
159
160    private function resolveEmailAddress(EmailAddress|string $email, ?string $name): EmailAddress
161    {
162        if (is_string($email)) {
163            $email = EmailAddress::withEmail($email);
164        }
165
166        if ($name !== null) {
167            return $email->withName($name);
168        }
169
170        return $email;
171    }
172}