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

nette / mail / 20837253071

09 Jan 2026 12:49AM UTC coverage: 77.2%. Remained the same
20837253071

push

github

dg
added CLAUDE.md

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
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, 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 MimePart[] */
47
        private array $parts = [];
48
        private string $body = '';
49

50

51
        /**
52
         * Sets a header.
53
         * @param  string|array|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
         */
99
        public function getHeader(string $name): mixed
1✔
100
        {
101
                return $this->headers[$name] ?? null;
1✔
102
        }
103

104

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

114

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

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

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

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

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

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

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

147

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

157

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

167

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

177

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

186

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

195

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

205

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

214

215
        /********************* building ****************d*g**/
216

217

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

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

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

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

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

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

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

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

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

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

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

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

275
                return $output;
1✔
276
        }
277

278

279
        /********************* QuotedPrintable helpers ****************d*g**/
280

281

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

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

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

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

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

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

320

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

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