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

nette / mail / 22359390077

24 Feb 2026 04:11PM UTC coverage: 77.2%. Remained the same
22359390077

push

github

dg
improved phpDoc types

1 of 1 new or added line in 1 file covered. (100.0%)

70 existing lines in 6 files now uncovered.

386 of 500 relevant lines covered (77.2%)

0.77 hits per line

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

95.42
/src/Mail/Message.php
1
<?php declare(strict_types=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
namespace Nette\Mail;
9

10
use Nette;
11
use Nette\Utils\Strings;
12
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;
13
use const FILEINFO_MIME_TYPE;
14

15

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

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

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

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

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

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

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

52

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

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

62

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

72

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

82

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

92

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

102

103
        /**
104
         * Returns the subject of the message.
105
         */
106
        public function getSubject(): ?string
107
        {
108
                return $this->getHeader('Subject');
1✔
109
        }
110

111

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

121

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

131

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

141

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

157
                return [$email => $name];
1✔
158
        }
159

160

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

170

171
        /**
172
         * Returns the Return-Path header.
173
         */
174
        public function getReturnPath(): ?string
175
        {
UNCOV
176
                return $this->getHeader('Return-Path');
×
177
        }
178

179

180
        /**
181
         * Sets email priority.
182
         */
183
        public function setPriority(int $priority): static
1✔
184
        {
185
                $this->setHeader('X-Priority', (string) $priority);
1✔
186
                return $this;
1✔
187
        }
188

189

190
        /**
191
         * Returns email priority.
192
         */
193
        public function getPriority(): ?int
194
        {
195
                $priority = $this->getHeader('X-Priority');
1✔
196
                return is_numeric($priority) ? (int) $priority : null;
1✔
197
        }
198

199

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

225
                                $html = substr_replace(
1✔
226
                                        $html,
1✔
227
                                        "{$m[1][0]}{$m[2][0]}cid:{$cids[$file]}",
1✔
228
                                        $m[0][1],
1✔
229
                                        strlen($m[0][0]),
1✔
230
                                );
231
                        }
232
                }
233

234
                if ($this->getSubject() == null) { // intentionally ==
1✔
235
                        $html = Strings::replace($html, '#<title>(.+?)</title>#is', function (array $m): void {
1✔
236
                                $this->setSubject(Nette\Utils\Html::htmlToText($m[1]));
1✔
237
                        });
1✔
238
                }
239

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

242
                if ($this->getBody() === '' && $html !== '') {
1✔
243
                        $this->setBody($this->buildText($html));
1✔
244
                }
245

246
                return $this;
1✔
247
        }
248

249

250
        /**
251
         * Gets HTML body.
252
         */
253
        public function getHtmlBody(): string
254
        {
255
                return $this->htmlBody;
1✔
256
        }
257

258

259
        /**
260
         * Adds embedded file.
261
         */
262
        public function addEmbeddedFile(string $file, ?string $content = null, ?string $contentType = null): MimePart
1✔
263
        {
264
                return $this->inlines[$file] = $this->createAttachment($file, $content, $contentType, 'inline')
1✔
265
                        ->setHeader('Content-ID', $this->getRandomId());
1✔
266
        }
267

268

269
        /**
270
         * Adds inlined Mime Part.
271
         */
272
        public function addInlinePart(MimePart $part): static
273
        {
UNCOV
274
                $this->inlines[] = $part;
×
UNCOV
275
                return $this;
×
276
        }
277

278

279
        /**
280
         * Adds attachment.
281
         */
282
        public function addAttachment(string $file, ?string $content = null, ?string $contentType = null): MimePart
1✔
283
        {
284
                return $this->attachments[] = $this->createAttachment($file, $content, $contentType, 'attachment');
1✔
285
        }
286

287

288
        /**
289
         * Gets all email attachments.
290
         * @return list<MimePart>
291
         */
292
        public function getAttachments(): array
293
        {
UNCOV
294
                return $this->attachments;
×
295
        }
296

297

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

314
                if (!$contentType) {
1✔
315
                        $contentType = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $content);
1✔
316
                }
317

318
                if (!strcasecmp($contentType, 'message/rfc822')) { // not allowed for attached files
1✔
319
                        $contentType = 'application/octet-stream';
1✔
320
                } elseif (!strcasecmp($contentType, 'image/svg')) { // Troublesome for some mailers...
1✔
UNCOV
321
                        $contentType = 'image/svg+xml';
×
322
                }
323

324
                $part->setBody($content);
1✔
325
                $part->setContentType($contentType);
1✔
326
                $part->setEncoding(preg_match('#(multipart|message)/#A', $contentType) ? self::Encoding8Bit : self::EncodingBase64);
1✔
327
                $part->setHeader('Content-Disposition', $disposition . '; filename="' . addcslashes($file, '"\\') . '"');
1✔
328
                return $part;
1✔
329
        }
330

331

332
        /********************* building and sending ****************d*g**/
333

334

335
        /**
336
         * Returns encoded message.
337
         */
338
        public function generateMessage(): string
339
        {
340
                return $this->build()->getEncodedMessage();
1✔
341
        }
342

343

344
        /**
345
         * Builds email. Does not modify itself, but returns a new object.
346
         */
347
        public function build(): static
348
        {
349
                $mail = clone $this;
1✔
350
                $mail->setHeader('Message-ID', $mail->getHeader('Message-ID') ?? $this->getRandomId());
1✔
351

352
                $cursor = $mail;
1✔
353
                if ($mail->attachments) {
1✔
354
                        $tmp = $cursor->setContentType('multipart/mixed');
1✔
355
                        $cursor = $cursor->addPart();
1✔
356
                        foreach ($mail->attachments as $value) {
1✔
357
                                $tmp->addPart($value);
1✔
358
                        }
359
                }
360

361
                if ($mail->htmlBody !== '') {
1✔
362
                        $tmp = $cursor->setContentType('multipart/alternative');
1✔
363
                        $cursor = $cursor->addPart();
1✔
364
                        $alt = $tmp->addPart();
1✔
365
                        if ($mail->inlines) {
1✔
366
                                $tmp = $alt->setContentType('multipart/related');
1✔
367
                                $alt = $alt->addPart();
1✔
368
                                foreach ($mail->inlines as $value) {
1✔
369
                                        $tmp->addPart($value);
1✔
370
                                }
371
                        }
372

373
                        $alt->setContentType('text/html', 'UTF-8')
1✔
374
                                ->setEncoding(preg_match('#[^\n]{990}#', $mail->htmlBody)
1✔
UNCOV
375
                                        ? self::EncodingQuotedPrintable
×
376
                                        : (preg_match('#[\x80-\xFF]#', $mail->htmlBody) ? self::Encoding8Bit : self::Encoding7Bit))
1✔
377
                                ->setBody($mail->htmlBody);
1✔
378
                }
379

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

388
                return $mail;
1✔
389
        }
390

391

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

410

411
        private function getRandomId(): string
412
        {
413
                return '<' . Nette\Utils\Random::generate() . '@'
1✔
414
                        . preg_replace('#[^\w.-]+#', '', $_SERVER['HTTP_HOST'] ?? php_uname('n'))
1✔
415
                        . '>';
1✔
416
        }
417
}
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