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

nette / mail / 24862033768

23 Apr 2026 10:27PM UTC coverage: 80.194% (+3.3%) from 76.863%
24862033768

push

github

dg
added CLAUDE.md

413 of 515 relevant lines covered (80.19%)

0.8 hits per line

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

91.45
/src/Mail/MimePart.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, 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;
13

14

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

24
        /** Content-Transfer-Encoding values */
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✔
73
                                        Nette\Utils\Validators::assert($recipient, 'unicode', "header '$name'");
×
74
                                } elseif (preg_match('#[\r\n]#', $recipient)) {
1✔
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✔
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 the header value, or null if not set.
96
         * @return string|array<string, ?string>|null
97
         */
98
        public function getHeader(string $name): string|array|null
1✔
99
        {
100
                return $this->headers[$name] ?? null;
1✔
101
        }
102

103

104
        public function clearHeader(string $name): static
105
        {
106
                unset($this->headers[$name]);
×
107
                return $this;
×
108
        }
109

110

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

118
                if (!isset($this->headers[$name])) {
1✔
119
                        return null;
×
120

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

129
                                $s .= self::append($email . ',', $offset);
1✔
130
                        }
131

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

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

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

143

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

153

154
        public function setContentType(string $contentType, ?string $charset = null): static
1✔
155
        {
156
                $this->setHeader('Content-Type', $contentType . ($charset ? "; charset=$charset" : ''));
1✔
157
                return $this;
1✔
158
        }
159

160

161
        public function setEncoding(string $encoding): static
1✔
162
        {
163
                $this->setHeader('Content-Transfer-Encoding', $encoding);
1✔
164
                return $this;
1✔
165
        }
166

167

168
        public function getEncoding(): string
169
        {
170
                $encoding = $this->getHeader('Content-Transfer-Encoding');
1✔
171
                return is_string($encoding) ? $encoding : '';
1✔
172
        }
173

174

175
        /**
176
         * Adds or creates new multipart.
177
         */
178
        public function addPart(?self $part = null): self
1✔
179
        {
180
                return $this->parts[] = $part ?? new self;
1✔
181
        }
182

183

184
        public function setBody(string $body): static
1✔
185
        {
186
                $this->body = $body;
1✔
187
                return $this;
1✔
188
        }
189

190

191
        public function getBody(): string
192
        {
193
                return $this->body;
1✔
194
        }
195

196

197
        /********************* building ****************d*g**/
198

199

200
        /**
201
         * Returns encoded message.
202
         */
203
        public function getEncodedMessage(): string
204
        {
205
                $output = '';
1✔
206
                $boundary = '--------' . Nette\Utils\Random::generate();
1✔
207

208
                foreach ($this->headers as $name => $value) {
1✔
209
                        $output .= $name . ': ' . $this->getEncodedHeader($name);
1✔
210
                        if ($this->parts && $name === 'Content-Type') {
1✔
211
                                $output .= ';' . self::EOL . "\tboundary=\"$boundary\"";
1✔
212
                        }
213

214
                        $output .= self::EOL;
1✔
215
                }
216

217
                $output .= self::EOL;
1✔
218

219
                $body = $this->body;
1✔
220
                if ($body !== '') {
1✔
221
                        switch ($this->getEncoding()) {
1✔
222
                                case self::EncodingQuotedPrintable:
223
                                        $output .= quoted_printable_encode($body);
1✔
224
                                        break;
1✔
225

226
                                case self::EncodingBase64:
227
                                        $output .= rtrim(chunk_split(base64_encode($body), self::LineLength, self::EOL));
1✔
228
                                        break;
1✔
229

230
                                case self::Encoding7Bit:
231
                                        $body = preg_replace('#[\x80-\xFF]+#', '', $body);
1✔
232
                                        // break omitted
233

234
                                case self::Encoding8Bit:
235
                                        $body = str_replace(["\x00", "\r"], '', $body);
1✔
236
                                        $body = str_replace("\n", self::EOL, $body);
1✔
237
                                        $output .= $body;
1✔
238
                                        break;
1✔
239

240
                                default:
241
                                        throw new Nette\InvalidStateException('Unknown encoding.');
×
242
                        }
243
                }
244

245
                if ($this->parts) {
1✔
246
                        if (!str_ends_with($output, self::EOL)) {
1✔
247
                                $output .= self::EOL;
×
248
                        }
249

250
                        foreach ($this->parts as $part) {
1✔
251
                                $output .= '--' . $boundary . self::EOL . $part->getEncodedMessage() . self::EOL;
1✔
252
                        }
253

254
                        $output .= '--' . $boundary . '--';
1✔
255
                }
256

257
                return $output;
1✔
258
        }
259

260

261
        /********************* QuotedPrintable helpers ****************d*g**/
262

263

264
        /**
265
         * MIME-encodes a string for use in a header, handling line length and folding.
266
         */
267
        private static function encodeSequence(string $s, int &$offset = 0, ?int $type = null): string
1✔
268
        {
269
                $escape = fn($s) => preg_match('#[^ a-zA-Z0-9!\#$%&\'*+/?^_`{|}~-]#', $s) === 1 // RFC 2822 atext except =
1✔
270
                        ? sprintf('"%s"', addcslashes($s, '"\\'))
1✔
271
                        : $s;
1✔
272

273
                if (
274
                        (strlen($s) < self::LineLength - 3) && // 3 is tab + quotes
1✔
275
                        strspn($s, "!\"#$%&\\'()*+,-./0123456789:;<>@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^`abcdefghijklmnopqrstuvwxyz{|}~=? _\r\n\t") === strlen($s)
1✔
276
                ) {
277
                        if ($type !== null) {
1✔
278
                                $s = $escape($s);
1✔
279
                        }
280

281
                        return self::append($s, $offset);
1✔
282
                }
283

284
                $o = '';
1✔
285
                if ($offset >= 55) { // maximum for iconv_mime_encode
1✔
286
                        $o = self::EOL . "\t";
1✔
287
                        $offset = 1;
1✔
288
                }
289

290
                if ($type === self::SequenceWord) {
1✔
291
                        $s = $escape($s);
1✔
292
                }
293

294
                $s = iconv_mime_encode(str_repeat(' ', $old = $offset), $s, [
1✔
295
                        'scheme' => 'B', // Q is broken
1✔
296
                        'input-charset' => 'UTF-8',
297
                        'output-charset' => 'UTF-8',
298
                ]) ?: '';
×
299

300
                $offset = strlen($s) - strrpos($s, "\n");
1✔
301
                $s = substr($s, $old + 2); // adds ': '
1✔
302
                if ($type === self::SequenceValue) {
1✔
303
                        $s = '"' . $s . '"';
1✔
304
                }
305

306
                $s = str_replace("\n ", "\n\t", $s);
1✔
307
                return $o . $s;
1✔
308
        }
309

310

311
        private static function append(string $s, int &$offset = 0): string
1✔
312
        {
313
                if ($offset + strlen($s) > self::LineLength) {
1✔
314
                        $offset = 1;
1✔
315
                        $s = self::EOL . "\t" . $s;
1✔
316
                }
317

318
                $offset += strlen($s);
1✔
319
                return $s;
1✔
320
        }
321
}
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