1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Kodus\Mail; |
4
|
|
|
|
5
|
|
|
class MIMEWriter extends Writer |
6
|
|
|
{ |
7
|
15 |
|
public function writeMessage(Message $message) |
8
|
|
|
{ |
9
|
15 |
|
$this->writeMessageHeaders($message); |
10
|
|
|
|
11
|
15 |
|
$inline_attachments = $message->getInlineAttachments(); |
12
|
|
|
|
13
|
15 |
|
if (count($inline_attachments)) { |
14
|
5 |
|
$boundary = $this->createMultipartBoundaryName("related"); |
15
|
|
|
|
16
|
5 |
|
$this->writeRelatedContentTypeHeader($boundary); |
17
|
5 |
|
$this->writeLine(); |
18
|
|
|
|
19
|
5 |
|
$this->writeMultipartBoundary($boundary); |
20
|
5 |
|
$this->writeMessageWithAttachments($message); |
21
|
|
|
|
22
|
5 |
|
foreach ($inline_attachments as $inline) { |
23
|
5 |
|
$this->writeMultipartBoundary($boundary); |
24
|
5 |
|
$this->writeAttachmentPart($inline->getAttachment(), $inline->getContentID()); |
25
|
5 |
|
} |
26
|
|
|
|
27
|
5 |
|
$this->writeMultipartBoundaryEnd($boundary); |
28
|
5 |
|
} else { |
29
|
11 |
|
$this->writeMessageWithAttachments($message); |
30
|
|
|
} |
31
|
15 |
|
} |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @param Message $message |
35
|
|
|
*/ |
36
|
15 |
|
public function writeMessageHeaders(Message $message) |
37
|
|
|
{ |
38
|
15 |
|
$this->writeHeader("Date", date("r", $message->getDate())); |
39
|
|
|
|
40
|
15 |
|
$this->writeAddressHeader("To", $message->getTo()); |
|
|
|
|
41
|
15 |
|
$this->writeAddressHeader("From", $message->getFrom()); |
|
|
|
|
42
|
15 |
|
$this->writeAddressHeader("Cc", $message->getCC()); |
|
|
|
|
43
|
15 |
|
$this->writeAddressHeader("Bcc", $message->getBCC()); |
|
|
|
|
44
|
15 |
|
$this->writeAddressHeader("Reply-To", $message->getReplyTo()); |
|
|
|
|
45
|
|
|
|
46
|
15 |
|
$sender = $message->getSender(); |
47
|
|
|
|
48
|
15 |
|
if ($sender) { |
49
|
3 |
|
$this->writeAddressHeader("Sender", [$sender]); |
50
|
3 |
|
} else { |
51
|
13 |
|
$from = $message->getFrom(); |
52
|
|
|
|
53
|
13 |
|
if (count($from) > 1) { |
54
|
|
|
$this->writeAddressHeader("Sender", [$from[0]]); |
55
|
|
|
} else { |
|
|
|
|
56
|
|
|
// The contents of this field would be completely redundant with the "From" field. |
57
|
|
|
// The "Sender" field need not be present, and its use is discouraged - it's therefore left out. |
58
|
|
|
} |
59
|
|
|
} |
60
|
|
|
|
61
|
15 |
|
$this->writeHeader("Subject", $message->getSubject()); |
62
|
|
|
|
63
|
15 |
|
$this->writeHeader("MIME-Version", "1.0"); |
64
|
|
|
|
65
|
15 |
|
foreach ($message->getHeaders() as $header) { |
66
|
2 |
|
$this->writeHeader($header->getName(), $header->getValue()); |
67
|
15 |
|
} |
68
|
15 |
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Write a multipart Message with Attachments |
72
|
|
|
* |
73
|
|
|
* @param Message $message |
74
|
|
|
*/ |
75
|
15 |
|
public function writeMessageWithAttachments(Message $message) |
76
|
|
|
{ |
77
|
15 |
|
if (empty($message->getAttachments())) { |
78
|
9 |
|
$this->writeMessageBody($message); |
79
|
|
|
|
80
|
9 |
|
return; |
81
|
|
|
} |
82
|
|
|
|
83
|
7 |
|
$boundary = $this->createMultipartBoundaryName("mixed"); |
84
|
|
|
|
85
|
7 |
|
$this->writeMixedContentTypeHeader($boundary); |
86
|
|
|
|
87
|
7 |
|
$this->writeLine(); |
88
|
7 |
|
$this->writeLine("This is a multipart message in MIME format."); |
89
|
7 |
|
$this->writeLine(); |
90
|
|
|
|
91
|
7 |
|
$this->writeMultipartBoundary($boundary); |
92
|
|
|
|
93
|
7 |
|
$this->writeMessageBody($message); |
94
|
|
|
|
95
|
7 |
|
foreach ($message->getAttachments() as $attachment) { |
96
|
7 |
|
$this->writeMultipartBoundary($boundary); |
97
|
7 |
|
$this->writeAttachmentPart($attachment); |
98
|
7 |
|
} |
99
|
|
|
|
100
|
7 |
|
$this->writeMultipartBoundaryEnd($boundary); |
101
|
7 |
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Write the text and/or HTML message body parts |
105
|
|
|
* |
106
|
|
|
* @param Message $message |
107
|
|
|
*/ |
108
|
15 |
|
public function writeMessageBody(Message $message) |
109
|
|
|
{ |
110
|
15 |
|
$text = $message->getText(); |
111
|
15 |
|
$html = $message->getHTML(); |
112
|
|
|
|
113
|
15 |
|
if (! empty($text)) { |
114
|
11 |
|
if (! empty($html)) { |
115
|
5 |
|
$boundary = $this->createMultipartBoundaryName("alternative"); |
116
|
|
|
|
117
|
5 |
|
$this->writeAlternativeContentTypeHeader($boundary); |
118
|
5 |
|
$this->writeLine(); |
119
|
|
|
|
120
|
5 |
|
$this->writeMultipartBoundary($boundary); |
121
|
5 |
|
$this->writeTextPart($text); |
122
|
|
|
|
123
|
5 |
|
$this->writeMultipartBoundary($boundary); |
124
|
5 |
|
$this->writeHTMLPart($html); |
125
|
|
|
|
126
|
5 |
|
$this->writeMultipartBoundaryEnd($boundary); |
127
|
5 |
|
} else { |
128
|
7 |
|
$this->writeTextPart($text); |
129
|
|
|
} |
130
|
15 |
|
} elseif (! empty($html)) { |
131
|
5 |
|
$this->writeHTMLPart($html); |
132
|
5 |
|
} |
133
|
15 |
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Write the "Content-Type" header and the plain-text body in quoted-printable format |
137
|
|
|
* |
138
|
|
|
* @param string $content |
139
|
|
|
*/ |
140
|
11 |
|
public function writeTextPart($content) |
141
|
|
|
{ |
142
|
11 |
|
$this->writeContentTypeHeader("text/plain; charset=UTF-8"); |
143
|
11 |
|
$this->writeQuotedPrintableEncodingHeader(); |
144
|
11 |
|
$this->writeLine(); |
145
|
11 |
|
$this->writeQuotedPrintable($this->adjustLineBreaks($content)); |
146
|
11 |
|
$this->writeLine(); |
147
|
11 |
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Write the "Content-Type" header and the plain-text body in quoted-printable format |
151
|
|
|
* |
152
|
|
|
* @param string $content |
153
|
|
|
*/ |
154
|
9 |
|
public function writeHTMLPart($content) |
155
|
|
|
{ |
156
|
9 |
|
$this->writeContentTypeHeader("text/html; charset=UTF-8"); |
157
|
9 |
|
$this->writeQuotedPrintableEncodingHeader(); |
158
|
9 |
|
$this->writeLine(); |
159
|
9 |
|
$this->writeQuotedPrintable($this->adjustLineBreaks($content)); |
160
|
9 |
|
$this->writeLine(); |
161
|
9 |
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* Write the "Content-Type" header and the Attachment Content in base-64 encoded format |
165
|
|
|
* |
166
|
|
|
* @param Attachment $attachment |
167
|
|
|
* @param string|null Content ID (for inline Attachments) |
168
|
|
|
*/ |
169
|
9 |
|
public function writeAttachmentPart(Attachment $attachment, $content_id = null) |
170
|
|
|
{ |
171
|
9 |
|
$filename = $attachment->getFilename(); |
172
|
|
|
|
173
|
9 |
|
$this->writeContentTypeHeader($attachment->getMIMEType()); |
174
|
9 |
|
$this->writeBase64EncodingHeader(); |
175
|
|
|
|
176
|
9 |
|
if ($content_id === null) { |
177
|
7 |
|
$this->writeHeader("Content-Disposition", "attachment; filename=\"{$filename}\""); |
178
|
7 |
|
} else { |
179
|
|
|
// inline Attachment with Content ID: |
180
|
5 |
|
$this->writeHeader("Content-Disposition", "inline; filename=\"{$filename}\""); |
181
|
5 |
|
$this->writeHeader("Content-ID", "<{$content_id}>"); |
182
|
|
|
} |
183
|
|
|
|
184
|
9 |
|
$this->writeLine(); |
185
|
9 |
|
$this->writeBase64($attachment->getContent()); |
186
|
9 |
|
$this->writeLine(); |
187
|
9 |
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* @param string $boundary |
191
|
|
|
*/ |
192
|
10 |
|
public function writeMultipartBoundary($boundary) |
193
|
|
|
{ |
194
|
10 |
|
$this->writeLine("--{$boundary}"); |
195
|
10 |
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* @param string $boundary |
199
|
|
|
*/ |
200
|
10 |
|
public function writeMultipartBoundaryEnd($boundary) |
201
|
|
|
{ |
202
|
10 |
|
$this->writeLine("--{$boundary}--"); |
203
|
10 |
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* @param string $name |
207
|
|
|
* @param string $value |
208
|
|
|
*/ |
209
|
15 |
|
public function writeHeader($name, $value) |
210
|
|
|
{ |
211
|
15 |
|
$value = $this->escapeHeaderValue($value); |
212
|
|
|
|
213
|
15 |
|
$this->writeLine("{$name}: {$value}"); |
214
|
15 |
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* @param string $name header name |
218
|
|
|
* @param Address[] $addresses list of Address objects |
219
|
|
|
*/ |
220
|
15 |
|
public function writeAddressHeader($name, $addresses) |
221
|
|
|
{ |
222
|
15 |
|
if (count($addresses)) { |
223
|
15 |
|
$this->writeHeader( |
224
|
15 |
|
$name, |
225
|
15 |
|
implode( |
226
|
15 |
|
", ", |
227
|
15 |
|
array_map( |
228
|
15 |
View Code Duplication |
function (Address $address) { |
|
|
|
|
229
|
15 |
|
$email = $address->getEmail(); |
230
|
15 |
|
$name = $address->getName(); |
231
|
|
|
|
232
|
15 |
|
return empty($name) |
233
|
15 |
|
? $email |
234
|
15 |
|
: $this->escapeHeaderValue($name) . " <{$email}>"; |
235
|
15 |
|
}, |
236
|
|
|
$addresses |
237
|
15 |
|
) |
238
|
15 |
|
) |
239
|
15 |
|
); |
240
|
15 |
|
} |
241
|
15 |
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* @param string $type |
245
|
|
|
*/ |
246
|
15 |
|
public function writeContentTypeHeader($type) |
247
|
|
|
{ |
248
|
15 |
|
$this->writeHeader("Content-Type", $type); |
249
|
15 |
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* @param string $boundary |
253
|
|
|
*/ |
254
|
7 |
|
public function writeMixedContentTypeHeader($boundary) |
255
|
|
|
{ |
256
|
7 |
|
$this->writeContentTypeHeader("multipart/mixed; boundary=\"{$boundary}\""); |
257
|
7 |
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* @param string $boundary |
261
|
|
|
*/ |
262
|
5 |
|
public function writeAlternativeContentTypeHeader($boundary) |
263
|
|
|
{ |
264
|
5 |
|
$this->writeContentTypeHeader("multipart/alternative; boundary=\"{$boundary}\""); |
265
|
5 |
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* @param string $boundary |
269
|
|
|
*/ |
270
|
5 |
|
public function writeRelatedContentTypeHeader($boundary) |
271
|
|
|
{ |
272
|
5 |
|
$this->writeContentTypeHeader("multipart/related; boundary=\"{$boundary}\""); |
273
|
5 |
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Writes the "Content-Transfer-Encoding" header with value "quoted-printable" |
277
|
|
|
*/ |
278
|
15 |
|
public function writeQuotedPrintableEncodingHeader() |
279
|
|
|
{ |
280
|
15 |
|
$this->writeContentEncodingHeader("quoted-printable"); |
281
|
15 |
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Writes the "Content-Transfer-Encoding" header with value "base64" |
285
|
|
|
*/ |
286
|
9 |
|
public function writeBase64EncodingHeader() |
287
|
|
|
{ |
288
|
9 |
|
$this->writeContentEncodingHeader("base64"); |
289
|
9 |
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* @param string $encoding encoding (e.g. "quoted-printable", "base64" or "8bit") |
293
|
|
|
*/ |
294
|
15 |
|
protected function writeContentEncodingHeader($encoding) |
295
|
|
|
{ |
296
|
15 |
|
$this->writeHeader("Content-Transfer-Encoding", $encoding); |
297
|
15 |
|
} |
298
|
|
|
|
299
|
|
|
/** |
300
|
|
|
* Generates a unique MIME boundary name |
301
|
|
|
* |
302
|
|
|
* @param string $prefix static prefix (helps developers diagnose the output) |
303
|
|
|
* |
304
|
|
|
* @return string |
305
|
|
|
*/ |
306
|
1 |
|
protected function createMultipartBoundaryName($prefix) |
307
|
|
|
{ |
308
|
1 |
|
static $boundary_index = 1; |
309
|
|
|
|
310
|
1 |
|
return "++++{$prefix}-" . sha1(microtime(true) . $boundary_index++) . "++++"; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Escape UTF-8 string (if necessary) for use in a header-value |
315
|
|
|
* |
316
|
|
|
* @param string $value |
317
|
|
|
* |
318
|
|
|
* @return string |
319
|
|
|
*/ |
320
|
15 |
|
protected function escapeHeaderValue($value) |
321
|
|
|
{ |
322
|
15 |
|
return preg_match('/[\x80-\xFF]/', $value) === 1 |
323
|
15 |
|
? "=?UTF-8?Q?" . quoted_printable_encode($value) . "?=" |
324
|
15 |
|
: $value; // as-is |
325
|
|
|
} |
326
|
|
|
|
327
|
|
|
/** |
328
|
|
|
* Adjusts line-breaks, correcting CR or LF as CRLF, to improve quoted-printable encoding. |
329
|
|
|
* |
330
|
|
|
* @param string $value |
331
|
|
|
* |
332
|
|
|
* @return string |
333
|
|
|
*/ |
334
|
15 |
|
protected function adjustLineBreaks($value) |
335
|
|
|
{ |
336
|
15 |
|
return preg_replace('/(?>\r\n|\n|\r)/u', "\r\n", $value); |
337
|
|
|
} |
338
|
|
|
} |
339
|
|
|
|
This check looks at variables that are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.