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

nette / mail / 29282068903

13 Jul 2026 08:21PM UTC coverage: 79.487% (-0.7%) from 80.194%
29282068903

push

github

dg
added AGENTS.md & DOCS

496 of 624 relevant lines covered (79.49%)

0.79 hits per line

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

93.91
/src/Mail/Message.php
1
<?php declare(strict_types=1);
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, basename, date, finfo_buffer, finfo_open, is_array, is_numeric, is_string, ltrim, php_uname, preg_match, preg_replace, str_replace, strcasecmp, stripslashes, substr;
13

14

15
/**
16
 * Represents an email message with support for HTML body, attachments, and embedded files.
17
 *
18
 * @property-deprecated   string $subject
19
 * @property-deprecated   string $htmlBody
20
 */
21
class Message extends MimePart
22
{
23
        /** Priority */
24
        public const
25
                High = 1,
26
                Normal = 3,
27
                Low = 5;
28

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

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

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

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

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

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

51

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

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

61

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

71

72
        /**
73
         * Returns the sender of the message.
74
         * @return ?array<string, ?string>
75
         */
76
        public function getFrom(): ?array
77
        {
78
                $value = $this->getHeader('From');
1✔
79
                return is_array($value) ? $value : null;
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
        public function setSubject(string $subject): static
1✔
94
        {
95
                $this->setHeader('Subject', $subject);
1✔
96
                return $this;
1✔
97
        }
98

99

100
        public function getSubject(): ?string
101
        {
102
                $value = $this->getHeader('Subject');
1✔
103
                return is_string($value) ? $value : null;
1✔
104
        }
105

106

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

116

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

126

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

136

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

152
                return [$email => $name];
1✔
153
        }
154

155

156
        public function setReturnPath(string $email): static
1✔
157
        {
158
                $this->setHeader('Return-Path', $email);
1✔
159
                return $this;
1✔
160
        }
161

162

163
        public function getReturnPath(): ?string
164
        {
165
                $value = $this->getHeader('Return-Path');
×
166
                return is_string($value) ? $value : null;
×
167
        }
168

169

170
        public function setPriority(int $priority): static
1✔
171
        {
172
                $this->setHeader('X-Priority', (string) $priority);
1✔
173
                return $this;
1✔
174
        }
175

176

177
        public function getPriority(): ?int
178
        {
179
                $priority = $this->getHeader('X-Priority');
1✔
180
                return is_numeric($priority) ? (int) $priority : null;
1✔
181
        }
182

183

184
        /**
185
         * Sets HTML body. If $basePath is provided, local images referenced in the HTML
186
         * are automatically embedded as inline attachments with their src rewritten to cid: URIs.
187
         * Also sets the subject from the HTML <title> if not already set, and auto-generates a plain-text alternative.
188
         */
189
        public function setHtmlBody(string $html, ?string $basePath = null): static
1✔
190
        {
191
                $composer = new HtmlComposer($html);
1✔
192
                if ($basePath) {
1✔
193
                        $composer->embedImages($basePath);
1✔
194
                }
195
                $composer->applyTo($this);
1✔
196
                return $this;
1✔
197
        }
198

199

200
        public function getHtmlBody(): string
201
        {
202
                return $this->htmlBody;
1✔
203
        }
204

205

206
        /** @internal used by HtmlComposer */
207
        public function setRawHtmlBody(string $html): void
1✔
208
        {
209
                $this->htmlBody = ltrim(str_replace("\r", '', $html), "\n");
1✔
210
        }
1✔
211

212

213
        /**
214
         * Adds an embedded (inline) file. If $content is null, the file is read from disk.
215
         * In that case $file is the path; otherwise $file is used as the filename.
216
         */
217
        public function addEmbeddedFile(string $file, ?string $content = null, ?string $contentType = null): MimePart
1✔
218
        {
219
                return $this->inlines[$file] = $this->createAttachment($file, $content, $contentType, 'inline')
1✔
220
                        ->setHeader('Content-ID', $this->getRandomId());
1✔
221
        }
222

223

224
        /**
225
         * Adds a pre-built MIME part as an inline (embedded) attachment.
226
         */
227
        public function addInlinePart(MimePart $part): static
228
        {
229
                $this->inlines[] = $part;
×
230
                return $this;
×
231
        }
232

233

234
        /**
235
         * Adds an attachment. If $content is null, the file is read from disk.
236
         * In that case $file is the path; otherwise $file is used as the filename.
237
         */
238
        public function addAttachment(string $file, ?string $content = null, ?string $contentType = null): MimePart
1✔
239
        {
240
                return $this->attachments[] = $this->createAttachment($file, $content, $contentType, 'attachment');
1✔
241
        }
242

243

244
        /**
245
         * @return list<MimePart>
246
         */
247
        public function getAttachments(): array
248
        {
249
                return $this->attachments;
×
250
        }
251

252

253
        /**
254
         * Creates file MIME part.
255
         */
256
        private function createAttachment(
1✔
257
                string $file,
258
                ?string $content,
259
                ?string $contentType,
260
                string $disposition,
261
        ): MimePart
262
        {
263
                $part = new MimePart;
1✔
264
                if ($content === null) {
1✔
265
                        $content = Nette\Utils\FileSystem::read($file);
1✔
266
                        $file = Strings::fixEncoding(basename($file));
1✔
267
                }
268

269
                if (!$contentType) {
1✔
270
                        $finfo = finfo_open(FILEINFO_MIME_TYPE);
1✔
271
                        $contentType = $finfo ? finfo_buffer($finfo, $content) : false;
1✔
272
                        $contentType = $contentType ?: 'application/octet-stream';
1✔
273
                }
274

275
                if (!strcasecmp($contentType, 'message/rfc822')) { // not allowed for attached files
1✔
276
                        $contentType = 'application/octet-stream';
1✔
277
                } elseif (!strcasecmp($contentType, 'image/svg')) { // Troublesome for some mailers...
1✔
278
                        $contentType = 'image/svg+xml';
×
279
                }
280

281
                $part->setBody($content);
1✔
282
                $part->setContentType($contentType);
1✔
283
                $part->setEncoding(preg_match('#(multipart|message)/#A', $contentType) ? self::Encoding8Bit : self::EncodingBase64);
1✔
284
                $part->setHeader('Content-Disposition', $disposition . '; filename="' . addcslashes($file, '"\\') . '"');
1✔
285
                return $part;
1✔
286
        }
287

288

289
        /********************* building and sending ****************d*g**/
290

291

292
        /**
293
         * Returns encoded message.
294
         */
295
        public function generateMessage(): string
296
        {
297
                return $this->build()->getEncodedMessage();
1✔
298
        }
299

300

301
        /**
302
         * Builds email. Does not modify itself, but returns a new object.
303
         */
304
        public function build(): static
305
        {
306
                $mail = clone $this;
1✔
307
                $mail->setHeader('Message-ID', $mail->getHeader('Message-ID') ?? $this->getRandomId());
1✔
308

309
                $cursor = $mail;
1✔
310
                if ($mail->attachments) {
1✔
311
                        $tmp = $cursor->setContentType('multipart/mixed');
1✔
312
                        $cursor = $cursor->addPart();
1✔
313
                        foreach ($mail->attachments as $value) {
1✔
314
                                $tmp->addPart($value);
1✔
315
                        }
316
                }
317

318
                if ($mail->htmlBody !== '') {
1✔
319
                        $tmp = $cursor->setContentType('multipart/alternative');
1✔
320
                        $cursor = $cursor->addPart();
1✔
321
                        $alt = $tmp->addPart();
1✔
322
                        if ($mail->inlines) {
1✔
323
                                $tmp = $alt->setContentType('multipart/related');
1✔
324
                                $alt = $alt->addPart();
1✔
325
                                foreach ($mail->inlines as $value) {
1✔
326
                                        $tmp->addPart($value);
1✔
327
                                }
328
                        }
329

330
                        $alt->setContentType('text/html', 'UTF-8')
1✔
331
                                ->setEncoding(preg_match('#[^\n]{990}#', $mail->htmlBody)
1✔
332
                                        ? self::EncodingQuotedPrintable
×
333
                                        : (preg_match('#[\x80-\xFF]#', $mail->htmlBody) ? self::Encoding8Bit : self::Encoding7Bit))
1✔
334
                                ->setBody($mail->htmlBody);
1✔
335
                }
336

337
                $text = $mail->getBody();
1✔
338
                $mail->setBody('');
1✔
339
                $cursor->setContentType('text/plain', 'UTF-8')
1✔
340
                        ->setEncoding(preg_match('#[^\n]{990}#', $text)
1✔
341
                                ? self::EncodingQuotedPrintable
1✔
342
                                : (preg_match('#[\x80-\xFF]#', $text) ? self::Encoding8Bit : self::Encoding7Bit))
1✔
343
                        ->setBody($text);
1✔
344

345
                return $mail;
1✔
346
        }
347

348

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

© 2026 Coveralls, Inc