-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMessage.php
More file actions
251 lines (201 loc) · 7.2 KB
/
Copy pathMessage.php
File metadata and controls
251 lines (201 loc) · 7.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
<?php
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/**
* Italix Mail - Message
*
* @package Italix\Mail
*/
declare(strict_types=1);
namespace Italix\Mail;
/**
* What to send, and to whom. An immutable descriptor — it does not know how
* mail leaves the building.
*
* $message = (new Message())
* ->to('a@b.it', 'Mario Rossi')
* ->subject($t->get('mail.invoice.subject'))
* ->view('mail/invoice', ['invoice' => $invoice])
* ->context('invoice ' . $invoice_id);
*
* `context()` is the field the log is worth having for. "Delivery failed" tells
* you nothing at 9am; "invoice 4471, party 147" tells you which customer to
* call.
*
* Every builder returns a new instance, so a base message can be prepared once
* and specialised per recipient without the first one changing underneath.
*/
final class Message
{
/** @var Address[] */
private array $to = [];
/** @var Address[] */
private array $cc = [];
/** @var Address[] */
private array $bcc = [];
private ?Address $from = null;
private ?Address $reply_to = null;
private string $subject = '';
private string $text = '';
private string $html = '';
private ?string $view_path = null;
/** @var array<string, mixed> */
private array $view_data = [];
private string $context_c = '';
/** @var array<string, string> */
private array $headers = [];
// -------------------------------------------------------------------------
// Recipients
// -------------------------------------------------------------------------
public function to(string $email, string $name = ''): self
{
$clone = clone $this;
$clone->to[] = new Address($email, $name);
return $clone;
}
public function cc(string $email, string $name = ''): self
{
$clone = clone $this;
$clone->cc[] = new Address($email, $name);
return $clone;
}
public function bcc(string $email, string $name = ''): self
{
$clone = clone $this;
$clone->bcc[] = new Address($email, $name);
return $clone;
}
public function from(string $email, string $name = ''): self
{
$clone = clone $this;
$clone->from = new Address($email, $name);
return $clone;
}
public function reply_to(string $email, string $name = ''): self
{
$clone = clone $this;
$clone->reply_to = new Address($email, $name);
return $clone;
}
// -------------------------------------------------------------------------
// Content
// -------------------------------------------------------------------------
public function subject(string $subject): self
{
if (preg_match('/[\r\n\x00]/', $subject) === 1) {
throw new MailException(
'A newline in a subject is header injection; refused rather than escaped.'
);
}
$clone = clone $this;
$clone->subject = trim($subject);
return $clone;
}
public function text(string $body): self
{
$clone = clone $this;
$clone->text = $body;
return $clone;
}
public function html(string $body): self
{
$clone = clone $this;
$clone->html = $body;
return $clone;
}
/**
* Render the body from a template.
*
* The template is resolved by whatever `BodyRenderer` the mailer was given
* — in an Italix application that is `ViewRenderer`, so mail templates get
* the same escaping guarantee, the same partials and the same theme
* fallback as every other page.
*
* @param array<string, mixed> $data
*/
public function view(string $path, array $data = []): self
{
$clone = clone $this;
$clone->view_path = $path;
$clone->view_data = $data;
return $clone;
}
/**
* Free-text note recorded with the log row: what this message was *about*.
*/
public function context(string $context_c): self
{
$clone = clone $this;
$clone->context_c = $context_c;
return $clone;
}
public function header(string $name, string $value): self
{
if (preg_match('/^[A-Za-z][A-Za-z0-9-]*$/', $name) !== 1) {
throw new MailException("Refusing \"{$name}\" as a header name.");
}
if (preg_match('/[\r\n\x00]/', $value) === 1) {
throw new MailException("A newline in the \"{$name}\" header is injection; refused.");
}
$clone = clone $this;
$clone->headers[$name] = $value;
return $clone;
}
// -------------------------------------------------------------------------
// Reading
// -------------------------------------------------------------------------
/** @return Address[] */
public function recipients_to(): array { return $this->to; }
/** @return Address[] */
public function recipients_cc(): array { return $this->cc; }
/** @return Address[] */
public function recipients_bcc(): array { return $this->bcc; }
/**
* Everyone the transport must deliver to — the envelope, which includes bcc
* even though no header mentions them.
*
* @return Address[]
*/
public function envelope_recipients(): array
{
return array_merge($this->to, $this->cc, $this->bcc);
}
public function get_from(): ?Address { return $this->from; }
public function get_reply_to(): ?Address { return $this->reply_to; }
public function get_subject(): string { return $this->subject; }
public function get_text(): string { return $this->text; }
public function get_html(): string { return $this->html; }
public function get_view(): ?string { return $this->view_path; }
public function get_context(): string { return $this->context_c; }
/** @return array<string, mixed> */
public function get_view_data(): array { return $this->view_data; }
/** @return array<string, string> */
public function get_headers(): array { return $this->headers; }
/**
* The first recipient, for the log's "who" column.
*/
public function primary_recipient(): string
{
return $this->to === [] ? '' : $this->to[0]->email();
}
/**
* Refuse a message that cannot be delivered, before anything is logged or
* connected. A missing recipient is a programming error, not a delivery
* failure, and treating it as the latter buries it in the mail log.
*/
public function assert_sendable(): void
{
if ($this->to === [] && $this->cc === [] && $this->bcc === []) {
throw new MailException('This message has no recipient.');
}
if ($this->subject === '') {
throw new MailException('This message has no subject.');
}
if ($this->text === '' && $this->html === '' && $this->view_path === null) {
throw new MailException('This message has no body: set text(), html() or view().');
}
}
}