• 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

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

14

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

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

31
        /** @internal */
32
        public const EOL = "\r\n";
33

34
        public const LineLength = 76;
35

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

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

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

48

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

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

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

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

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

90
                return $this;
1✔
91
        }
92

93

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

103

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

113

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

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

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

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

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

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

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

146

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

156

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

166

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

176

177
        /**
178
         * Returns Content-Transfer-Encoding header.
179
         */
180
        public function getEncoding(): string
181
        {
182
                return $this->getHeader('Content-Transfer-Encoding');
1✔
183
        }
184

185

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

194

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

204

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

213

214
        /********************* building ****************d*g**/
215

216

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

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

231
                        $output .= self::EOL;
1✔
232
                }
233

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

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

243
                                case self::EncodingBase64:
1✔
244
                                        $output .= rtrim(chunk_split(base64_encode($body), self::LineLength, self::EOL));
1✔
245
                                        break;
1✔
246

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

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

257
                                default:
UNCOV
258
                                        throw new Nette\InvalidStateException('Unknown encoding.');
×
259
                        }
260
                }
261

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

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

271
                        $output .= '--' . $boundary . '--';
1✔
272
                }
273

274
                return $output;
1✔
275
        }
276

277

278
        /********************* QuotedPrintable helpers ****************d*g**/
279

280

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

294
                        return self::append($s, $offset);
1✔
295
                }
296

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

303
                $s = iconv_mime_encode(str_repeat(' ', $old = $offset), $s, [
1✔
304
                        'scheme' => 'B', // Q is broken
1✔
305
                        'input-charset' => 'UTF-8',
306
                        'output-charset' => 'UTF-8',
307
                ]);
308

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

315
                $s = str_replace("\n ", "\n\t", $s);
1✔
316
                return $o . $s;
1✔
317
        }
318

319

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

327
                $offset += strlen($s);
1✔
328
                return $s;
1✔
329
        }
330
}
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