• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / mail / 22293273211

23 Feb 2026 04:46AM UTC coverage: 76.863% (-0.4%) from 77.246%
22293273211

push

github

dg
fixed PHPStan errors WIP

31 of 45 new or added lines in 5 files covered. (68.89%)

5 existing lines in 2 files now uncovered.

392 of 510 relevant lines covered (76.86%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.93
/src/Mail/Message.php
1
<?php
1✔
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Mail;
11

12
use Nette;
13
use Nette\Utils\Strings;
14
use function addcslashes, array_map, array_reverse, basename, date, explode, finfo_buffer, finfo_open, implode, is_numeric, ltrim, php_uname, preg_match, preg_replace, rtrim, str_replace, strcasecmp, stripslashes, strlen, substr, substr_replace, trim, urldecode;
15
use const FILEINFO_MIME_TYPE;
16

17

18
/**
19
 * Mail provides functionality to compose and send both text and MIME-compliant multipart email messages.
20
 *
21
 * @property-deprecated   string $subject
22
 * @property-deprecated   string $htmlBody
23
 */
24
class Message extends MimePart
25
{
26
        /** Priority */
27
        public const
28
                High = 1,
29
                Normal = 3,
30
                Low = 5;
31

32
        #[\Deprecated('use Message::High')]
33
        public const HIGH = self::High;
34

35
        #[\Deprecated('use Message::Normal')]
36
        public const NORMAL = self::Normal;
37

38
        #[\Deprecated('use Message::Low')]
39
        public const LOW = self::Low;
40

41
        /** @var array<string, string> */
42
        public static array $defaultHeaders = [
43
                'MIME-Version' => '1.0',
44
                'X-Mailer' => 'Nette Framework',
45
        ];
46

47
        /** @var list<MimePart> */
48
        private array $attachments = [];
49

50
        /** @var array<MimePart> */
51
        private array $inlines = [];
52
        private string $htmlBody = '';
53

54

55
        public function __construct()
56
        {
57
                foreach (static::$defaultHeaders as $name => $value) {
1✔
58
                        $this->setHeader($name, $value);
1✔
59
                }
60

61
                $this->setHeader('Date', date('r'));
1✔
62
        }
1✔
63

64

65
        /**
66
         * Sets the sender of the message. Email or format "John Doe" <doe@example.com>
67
         */
68
        public function setFrom(string $email, ?string $name = null): static
1✔
69
        {
70
                $this->setHeader('From', $this->formatEmail($email, $name));
1✔
71
                return $this;
1✔
72
        }
73

74

75
        /**
76
         * Returns the sender of the message.
77
         * @return ?array<string, ?string>
78
         */
79
        public function getFrom(): ?array
80
        {
81
                $value = $this->getHeader('From');
1✔
82
                return is_array($value) ? $value : null;
1✔
83
        }
84

85

86
        /**
87
         * Adds the reply-to address. Email or format "John Doe" <doe@example.com>
88
         */
89
        public function addReplyTo(string $email, ?string $name = null): static
1✔
90
        {
91
                $this->setHeader('Reply-To', $this->formatEmail($email, $name), append: true);
1✔
92
                return $this;
1✔
93
        }
94

95

96
        /**
97
         * Sets the subject of the message.
98
         */
99
        public function setSubject(string $subject): static
1✔
100
        {
101
                $this->setHeader('Subject', $subject);
1✔
102
                return $this;
1✔
103
        }
104

105

106
        /**
107
         * Returns the subject of the message.
108
         */
109
        public function getSubject(): ?string
110
        {
111
                $value = $this->getHeader('Subject');
1✔
112
                return is_string($value) ? $value : null;
1✔
113
        }
114

115

116
        /**
117
         * Adds email recipient. Email or format "John Doe" <doe@example.com>
118
         */
119
        public function addTo(string $email, ?string $name = null): static // addRecipient()
1✔
120
        {
121
                $this->setHeader('To', $this->formatEmail($email, $name), append: true);
1✔
122
                return $this;
1✔
123
        }
124

125

126
        /**
127
         * Adds carbon copy email recipient. Email or format "John Doe" <doe@example.com>
128
         */
129
        public function addCc(string $email, ?string $name = null): static
1✔
130
        {
131
                $this->setHeader('Cc', $this->formatEmail($email, $name), append: true);
1✔
132
                return $this;
1✔
133
        }
134

135

136
        /**
137
         * Adds blind carbon copy email recipient. Email or format "John Doe" <doe@example.com>
138
         */
139
        public function addBcc(string $email, ?string $name = null): static
1✔
140
        {
141
                $this->setHeader('Bcc', $this->formatEmail($email, $name), append: true);
1✔
142
                return $this;
1✔
143
        }
144

145

146
        /**
147
         * Formats recipient email.
148
         * @return array<string, ?string>
149
         */
150
        private function formatEmail(string $email, ?string $name = null): array
1✔
151
        {
152
                if (!$name && preg_match('#^(.+) +<(.*)>$#D', $email, $matches)) {
1✔
153
                        [, $name, $email] = $matches;
1✔
154
                        $name = stripslashes($name);
1✔
155
                        $tmp = substr($name, 1, -1);
1✔
156
                        if ($name === '"' . $tmp . '"') {
1✔
157
                                $name = $tmp;
1✔
158
                        }
159
                }
160

161
                return [$email => $name];
1✔
162
        }
163

164

165
        /**
166
         * Sets the Return-Path header of the message.
167
         */
168
        public function setReturnPath(string $email): static
1✔
169
        {
170
                $this->setHeader('Return-Path', $email);
1✔
171
                return $this;
1✔
172
        }
173

174

175
        /**
176
         * Returns the Return-Path header.
177
         */
178
        public function getReturnPath(): ?string
179
        {
NEW
180
                $value = $this->getHeader('Return-Path');
×
NEW
181
                return is_string($value) ? $value : null;
×
182
        }
183

184

185
        /**
186
         * Sets email priority.
187
         */
188
        public function setPriority(int $priority): static
1✔
189
        {
190
                $this->setHeader('X-Priority', (string) $priority);
1✔
191
                return $this;
1✔
192
        }
193

194

195
        /**
196
         * Returns email priority.
197
         */
198
        public function getPriority(): ?int
199
        {
200
                $priority = $this->getHeader('X-Priority');
1✔
201
                return is_numeric($priority) ? (int) $priority : null;
1✔
202
        }
203

204

205
        /**
206
         * Sets HTML body.
207
         */
208
        public function setHtmlBody(string $html, ?string $basePath = null): static
1✔
209
        {
210
                if ($basePath) {
1✔
211
                        $cids = [];
1✔
212
                        $matches = Strings::matchAll(
1✔
213
                                $html,
1✔
214
                                '#
1✔
215
                                        (<img[^<>]*\s src\s*=\s*
216
                                        |<body[^<>]*\s background\s*=\s*
217
                                        |<[^<>]+\s style\s*=\s* ["\'][^"\'>]+[:\s] url\(
218
                                        |<style[^>]*>[^<]+ [:\s] url\()
219
                                        (["\']?)(?![a-z]+:|[/\#])([^"\'>)\s]+)
220
                                        |\[\[ ([\w()+./@~-]+) \]\]
221
                                #ix',
222
                                captureOffset: true,
1✔
223
                        );
224
                        foreach (array_reverse($matches) as $m) {
1✔
225
                                $file = rtrim($basePath, '/\\') . '/' . (isset($m[4]) ? $m[4][0] : urldecode($m[3][0]));
1✔
226
                                if (!isset($cids[$file])) {
1✔
227
                                        $contentId = $this->addEmbeddedFile($file)->getHeader('Content-ID');
1✔
228
                                        $cids[$file] = is_string($contentId) ? substr($contentId, 1, -1) : '';
1✔
229
                                }
230

231
                                $html = substr_replace(
1✔
232
                                        $html,
1✔
233
                                        "{$m[1][0]}{$m[2][0]}cid:{$cids[$file]}",
1✔
234
                                        $m[0][1],
1✔
235
                                        strlen($m[0][0]),
1✔
236
                                );
237
                        }
238
                }
239

240
                if ($this->getSubject() == null) { // intentionally ==
1✔
241
                        $html = Strings::replace($html, '#<title>(.+?)</title>#is', function (array $m): void {
1✔
242
                                $this->setSubject(Nette\Utils\Html::htmlToText($m[1]));
1✔
243
                        });
1✔
244
                }
245

246
                $this->htmlBody = ltrim(str_replace("\r", '', $html), "\n");
1✔
247

248
                if ($this->getBody() === '' && $html !== '') {
1✔
249
                        $this->setBody($this->buildText($html));
1✔
250
                }
251

252
                return $this;
1✔
253
        }
254

255

256
        /**
257
         * Gets HTML body.
258
         */
259
        public function getHtmlBody(): string
260
        {
261
                return $this->htmlBody;
1✔
262
        }
263

264

265
        /**
266
         * Adds embedded file.
267
         */
268
        public function addEmbeddedFile(string $file, ?string $content = null, ?string $contentType = null): MimePart
1✔
269
        {
270
                return $this->inlines[$file] = $this->createAttachment($file, $content, $contentType, 'inline')
1✔
271
                        ->setHeader('Content-ID', $this->getRandomId());
1✔
272
        }
273

274

275
        /**
276
         * Adds inlined Mime Part.
277
         */
278
        public function addInlinePart(MimePart $part): static
279
        {
UNCOV
280
                $this->inlines[] = $part;
×
281
                return $this;
×
282
        }
283

284

285
        /**
286
         * Adds attachment.
287
         */
288
        public function addAttachment(string $file, ?string $content = null, ?string $contentType = null): MimePart
1✔
289
        {
290
                return $this->attachments[] = $this->createAttachment($file, $content, $contentType, 'attachment');
1✔
291
        }
292

293

294
        /**
295
         * Gets all email attachments.
296
         * @return list<MimePart>
297
         */
298
        public function getAttachments(): array
299
        {
UNCOV
300
                return $this->attachments;
×
301
        }
302

303

304
        /**
305
         * Creates file MIME part.
306
         */
307
        private function createAttachment(
1✔
308
                string $file,
309
                ?string $content,
310
                ?string $contentType,
311
                string $disposition,
312
        ): MimePart
313
        {
314
                $part = new MimePart;
1✔
315
                if ($content === null) {
1✔
316
                        $content = Nette\Utils\FileSystem::read($file);
1✔
317
                        $file = Strings::fixEncoding(basename($file));
1✔
318
                }
319

320
                if (!$contentType) {
1✔
321
                        $finfo = finfo_open(FILEINFO_MIME_TYPE);
1✔
322
                        $contentType = $finfo ? finfo_buffer($finfo, $content) : false;
1✔
323
                        $contentType = $contentType ?: 'application/octet-stream';
1✔
324
                }
325

326
                if (!strcasecmp($contentType, 'message/rfc822')) { // not allowed for attached files
1✔
327
                        $contentType = 'application/octet-stream';
1✔
328
                } elseif (!strcasecmp($contentType, 'image/svg')) { // Troublesome for some mailers...
1✔
UNCOV
329
                        $contentType = 'image/svg+xml';
×
330
                }
331

332
                $part->setBody($content);
1✔
333
                $part->setContentType($contentType);
1✔
334
                $part->setEncoding(preg_match('#(multipart|message)/#A', $contentType) ? self::Encoding8Bit : self::EncodingBase64);
1✔
335
                $part->setHeader('Content-Disposition', $disposition . '; filename="' . addcslashes($file, '"\\') . '"');
1✔
336
                return $part;
1✔
337
        }
338

339

340
        /********************* building and sending ****************d*g**/
341

342

343
        /**
344
         * Returns encoded message.
345
         */
346
        public function generateMessage(): string
347
        {
348
                return $this->build()->getEncodedMessage();
1✔
349
        }
350

351

352
        /**
353
         * Builds email. Does not modify itself, but returns a new object.
354
         */
355
        public function build(): static
356
        {
357
                $mail = clone $this;
1✔
358
                $mail->setHeader('Message-ID', $mail->getHeader('Message-ID') ?? $this->getRandomId());
1✔
359

360
                $cursor = $mail;
1✔
361
                if ($mail->attachments) {
1✔
362
                        $tmp = $cursor->setContentType('multipart/mixed');
1✔
363
                        $cursor = $cursor->addPart();
1✔
364
                        foreach ($mail->attachments as $value) {
1✔
365
                                $tmp->addPart($value);
1✔
366
                        }
367
                }
368

369
                if ($mail->htmlBody !== '') {
1✔
370
                        $tmp = $cursor->setContentType('multipart/alternative');
1✔
371
                        $cursor = $cursor->addPart();
1✔
372
                        $alt = $tmp->addPart();
1✔
373
                        if ($mail->inlines) {
1✔
374
                                $tmp = $alt->setContentType('multipart/related');
1✔
375
                                $alt = $alt->addPart();
1✔
376
                                foreach ($mail->inlines as $value) {
1✔
377
                                        $tmp->addPart($value);
1✔
378
                                }
379
                        }
380

381
                        $alt->setContentType('text/html', 'UTF-8')
1✔
382
                                ->setEncoding(preg_match('#[^\n]{990}#', $mail->htmlBody)
1✔
UNCOV
383
                                        ? self::EncodingQuotedPrintable
×
384
                                        : (preg_match('#[\x80-\xFF]#', $mail->htmlBody) ? self::Encoding8Bit : self::Encoding7Bit))
1✔
385
                                ->setBody($mail->htmlBody);
1✔
386
                }
387

388
                $text = $mail->getBody();
1✔
389
                $mail->setBody('');
1✔
390
                $cursor->setContentType('text/plain', 'UTF-8')
1✔
391
                        ->setEncoding(preg_match('#[^\n]{990}#', $text)
1✔
392
                                ? self::EncodingQuotedPrintable
1✔
393
                                : (preg_match('#[\x80-\xFF]#', $text) ? self::Encoding8Bit : self::Encoding7Bit))
1✔
394
                        ->setBody($text);
1✔
395

396
                return $mail;
1✔
397
        }
398

399

400
        /**
401
         * Builds text content.
402
         */
403
        protected function buildText(string $html): string
1✔
404
        {
405
                $html = Strings::replace($html, [
1✔
406
                        '#<(style|script|head).*</\1>#Uis' => '',
1✔
407
                        '#<t[dh][ >]#i' => ' $0',
408
                        '#<a\s[^>]*href=(?|"([^"]+)"|\'([^\']+)\')[^>]*>(.*?)</a>#is' => '$2 &lt;$1&gt;',
409
                        '#[\r\n]+#' => ' ',
410
                        '#<(/?p|/?h\d|li|br|/tr)[ >/]#i' => "\n$0",
411
                ]);
412
                $text = Nette\Utils\Html::htmlToText($html);
1✔
413
                $text = Strings::replace($text, '#[ \t]+#', ' ');
1✔
414
                $text = implode("\n", array_map('trim', explode("\n", $text)));
1✔
415
                return trim($text);
1✔
416
        }
417

418

419
        private function getRandomId(): string
420
        {
421
                return '<' . Nette\Utils\Random::generate() . '@'
1✔
422
                        . preg_replace('#[^\w.-]+#', '', $_SERVER['HTTP_HOST'] ?? php_uname('n'))
1✔
423
                        . '>';
1✔
424
        }
425
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc