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

nette / mail / 22293330675

23 Feb 2026 04:49AM UTC coverage: 76.863%. Remained the same
22293330675

push

github

dg
fixed PHPStan errors WIP

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

36 existing lines in 3 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

91.07
/src/Mail/MimePart.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, base64_encode, chunk_split, iconv_mime_encode, is_array, is_string, ltrim, preg_match, preg_replace, quoted_printable_encode, rtrim, str_ends_with, str_repeat, str_replace, stripslashes, strlen, strrpos, strspn, substr;
15

16

17
/**
18
 * MIME message part.
19
 *
20
 * @property-deprecated   string $body
21
 */
22
class MimePart
23
{
24
        use Nette\SmartObject;
25

26
        /** encoding */
27
        public const
28
                EncodingBase64 = 'base64',
29
                Encoding7Bit = '7bit',
30
                Encoding8Bit = '8bit',
31
                EncodingQuotedPrintable = 'quoted-printable';
32

33
        /** @internal */
34
        public const EOL = "\r\n";
35

36
        public const LineLength = 76;
37

38
        /** value (RFC 2231), encoded-word (RFC 2047) */
39
        private const
40
                SequenceValue = 1,
41
                SequenceWord = 2;
42

43
        /** @var array<string, string|array<string, ?string>> */
44
        private array $headers = [];
45

46
        /** @var list<MimePart> */
47
        private array $parts = [];
48
        private string $body = '';
49

50

51
        /**
52
         * Sets a header.
53
         * @param  string|array<string, ?string>|null  $value  value or pair email => name
54
         */
55
        public function setHeader(string $name, string|array|null $value, bool $append = false): static
1✔
56
        {
57
                if (!$name || preg_match('#[^a-z0-9-]#i', $name)) {
1✔
58
                        throw new Nette\InvalidArgumentException("Header name must be non-empty alphanumeric string, '$name' given.");
1✔
59
                }
60

61
                if ($value == null) { // intentionally ==
1✔
62
                        if (!$append) {
1✔
63
                                unset($this->headers[$name]);
1✔
64
                        }
65
                } elseif (is_array($value)) { // email
1✔
66
                        $tmp = &$this->headers[$name];
1✔
67
                        if (!$append || !is_array($tmp)) {
1✔
68
                                $tmp = [];
1✔
69
                        }
70

71
                        foreach ($value as $email => $recipient) {
1✔
72
                                if ($recipient === null) {
1✔
73
                                        // continue
74
                                } elseif (!Strings::checkEncoding($recipient)) {
1✔
75
                                        Nette\Utils\Validators::assert($recipient, 'unicode', "header '$name'");
×
76
                                } elseif (preg_match('#[\r\n]#', $recipient)) {
1✔
77
                                        throw new Nette\InvalidArgumentException('Name must not contain line separator.');
×
78
                                }
79

80
                                Nette\Utils\Validators::assert($email, 'email', "header '$name'");
1✔
81
                                $tmp[$email] = $recipient;
1✔
82
                        }
83
                } else {
84
                        $value = (string) $value;
1✔
85
                        if (!Strings::checkEncoding($value)) {
1✔
86
                                throw new Nette\InvalidArgumentException('Header is not valid UTF-8 string.');
×
87
                        }
88

89
                        $this->headers[$name] = preg_replace('#[\r\n]+#', ' ', $value);
1✔
90
                }
91

92
                return $this;
1✔
93
        }
94

95

96
        /**
97
         * Returns a header.
98
         * @return string|array<string, ?string>|null
99
         */
100
        public function getHeader(string $name): string|array|null
1✔
101
        {
102
                return $this->headers[$name] ?? null;
1✔
103
        }
104

105

106
        /**
107
         * Removes a header.
108
         */
109
        public function clearHeader(string $name): static
110
        {
111
                unset($this->headers[$name]);
×
112
                return $this;
×
113
        }
114

115

116
        /**
117
         * Returns an encoded header.
118
         */
119
        public function getEncodedHeader(string $name): ?string
1✔
120
        {
121
                $offset = strlen($name) + 2; // colon + space
1✔
122

123
                if (!isset($this->headers[$name])) {
1✔
124
                        return null;
×
125

126
                } elseif (is_array($this->headers[$name])) {
1✔
127
                        $s = '';
1✔
128
                        foreach ($this->headers[$name] as $email => $name) {
1✔
129
                                if ($name != null) { // intentionally ==
1✔
130
                                        $s .= self::encodeSequence($name, $offset, self::SequenceWord);
1✔
131
                                        $email = " <$email>";
1✔
132
                                }
133

134
                                $s .= self::append($email . ',', $offset);
1✔
135
                        }
136

137
                        return ltrim(substr($s, 0, -1)); // last comma
1✔
138

139
                } elseif (preg_match('#^(\S+; (?:file)?name=)"(.*)"$#D', $this->headers[$name], $m)) { // Content-Disposition
1✔
140
                        $offset += strlen($m[1]);
1✔
141
                        return $m[1] . self::encodeSequence(stripslashes($m[2]), $offset, self::SequenceValue);
1✔
142

143
                } else {
144
                        return ltrim(self::encodeSequence($this->headers[$name], $offset));
1✔
145
                }
146
        }
147

148

149
        /**
150
         * Returns all headers.
151
         * @return array<string, string|array<string, ?string>>
152
         */
153
        public function getHeaders(): array
154
        {
155
                return $this->headers;
×
156
        }
157

158

159
        /**
160
         * Sets Content-Type header.
161
         */
162
        public function setContentType(string $contentType, ?string $charset = null): static
1✔
163
        {
164
                $this->setHeader('Content-Type', $contentType . ($charset ? "; charset=$charset" : ''));
1✔
165
                return $this;
1✔
166
        }
167

168

169
        /**
170
         * Sets Content-Transfer-Encoding header.
171
         */
172
        public function setEncoding(string $encoding): static
1✔
173
        {
174
                $this->setHeader('Content-Transfer-Encoding', $encoding);
1✔
175
                return $this;
1✔
176
        }
177

178

179
        /**
180
         * Returns Content-Transfer-Encoding header.
181
         */
182
        public function getEncoding(): string
183
        {
184
                $encoding = $this->getHeader('Content-Transfer-Encoding');
1✔
185
                return is_string($encoding) ? $encoding : '';
1✔
186
        }
187

188

189
        /**
190
         * Adds or creates new multipart.
191
         */
192
        public function addPart(?self $part = null): self
1✔
193
        {
194
                return $this->parts[] = $part ?? new self;
1✔
195
        }
196

197

198
        /**
199
         * Sets textual body.
200
         */
201
        public function setBody(string $body): static
1✔
202
        {
203
                $this->body = $body;
1✔
204
                return $this;
1✔
205
        }
206

207

208
        /**
209
         * Gets textual body.
210
         */
211
        public function getBody(): string
212
        {
213
                return $this->body;
1✔
214
        }
215

216

217
        /********************* building ****************d*g**/
218

219

220
        /**
221
         * Returns encoded message.
222
         */
223
        public function getEncodedMessage(): string
224
        {
225
                $output = '';
1✔
226
                $boundary = '--------' . Nette\Utils\Random::generate();
1✔
227

228
                foreach ($this->headers as $name => $value) {
1✔
229
                        $output .= $name . ': ' . $this->getEncodedHeader($name);
1✔
230
                        if ($this->parts && $name === 'Content-Type') {
1✔
231
                                $output .= ';' . self::EOL . "\tboundary=\"$boundary\"";
1✔
232
                        }
233

234
                        $output .= self::EOL;
1✔
235
                }
236

237
                $output .= self::EOL;
1✔
238

239
                $body = $this->body;
1✔
240
                if ($body !== '') {
1✔
241
                        switch ($this->getEncoding()) {
1✔
242
                                case self::EncodingQuotedPrintable:
243
                                        $output .= quoted_printable_encode($body);
1✔
244
                                        break;
1✔
245

246
                                case self::EncodingBase64:
247
                                        $output .= rtrim(chunk_split(base64_encode($body), self::LineLength, self::EOL));
1✔
248
                                        break;
1✔
249

250
                                case self::Encoding7Bit:
251
                                        $body = preg_replace('#[\x80-\xFF]+#', '', $body);
1✔
252
                                        // break omitted
253

254
                                case self::Encoding8Bit:
255
                                        $body = str_replace(["\x00", "\r"], '', $body);
1✔
256
                                        $body = str_replace("\n", self::EOL, $body);
1✔
257
                                        $output .= $body;
1✔
258
                                        break;
1✔
259

260
                                default:
UNCOV
261
                                        throw new Nette\InvalidStateException('Unknown encoding.');
×
262
                        }
263
                }
264

265
                if ($this->parts) {
1✔
266
                        if (!str_ends_with($output, self::EOL)) {
1✔
UNCOV
267
                                $output .= self::EOL;
×
268
                        }
269

270
                        foreach ($this->parts as $part) {
1✔
271
                                $output .= '--' . $boundary . self::EOL . $part->getEncodedMessage() . self::EOL;
1✔
272
                        }
273

274
                        $output .= '--' . $boundary . '--';
1✔
275
                }
276

277
                return $output;
1✔
278
        }
279

280

281
        /********************* QuotedPrintable helpers ****************d*g**/
282

283

284
        /**
285
         * Converts a 8 bit header to a string.
286
         */
287
        private static function encodeSequence(string $s, int &$offset = 0, ?int $type = null): string
1✔
288
        {
289
                if (
290
                        (strlen($s) < self::LineLength - 3) && // 3 is tab + quotes
1✔
291
                        strspn($s, "!\"#$%&\\'()*+,-./0123456789:;<>@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^`abcdefghijklmnopqrstuvwxyz{|}~=? _\r\n\t") === strlen($s)
1✔
292
                ) {
293
                        if ($type && preg_match('#[^ a-zA-Z0-9!\#$%&\'*+/?^_`{|}~-]#', $s)) { // RFC 2822 atext except =
1✔
294
                                return self::append('"' . addcslashes($s, '"\\') . '"', $offset);
1✔
295
                        }
296

297
                        return self::append($s, $offset);
1✔
298
                }
299

300
                $o = '';
1✔
301
                if ($offset >= 55) { // maximum for iconv_mime_encode
1✔
302
                        $o = self::EOL . "\t";
1✔
303
                        $offset = 1;
1✔
304
                }
305

306
                $s = iconv_mime_encode(str_repeat(' ', $old = $offset), $s, [
1✔
307
                        'scheme' => 'B', // Q is broken
1✔
308
                        'input-charset' => 'UTF-8',
309
                        'output-charset' => 'UTF-8',
NEW
310
                ]) ?: '';
×
311

312
                $offset = strlen($s) - strrpos($s, "\n");
1✔
313
                $s = substr($s, $old + 2); // adds ': '
1✔
314
                if ($type === self::SequenceValue) {
1✔
315
                        $s = '"' . $s . '"';
1✔
316
                }
317

318
                $s = str_replace("\n ", "\n\t", $s);
1✔
319
                return $o . $s;
1✔
320
        }
321

322

323
        private static function append(string $s, int &$offset = 0): string
1✔
324
        {
325
                if ($offset + strlen($s) > self::LineLength) {
1✔
326
                        $offset = 1;
1✔
327
                        $s = self::EOL . "\t" . $s;
1✔
328
                }
329

330
                $offset += strlen($s);
1✔
331
                return $s;
1✔
332
        }
333
}
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