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