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

nette / mail / 22836180643

09 Mar 2026 02:43AM UTC coverage: 77.2%. Remained the same
22836180643

push

github

dg
improved PHPDoc descriptions

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
        /** 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): mixed
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
                return $this->getHeader('Content-Transfer-Encoding');
1✔
171
        }
172

173

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

182

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

189

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

195

196
        /********************* building ****************d*g**/
197

198

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

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

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

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

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

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

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

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

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

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

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

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

256
                return $output;
1✔
257
        }
258

259

260
        /********************* QuotedPrintable helpers ****************d*g**/
261

262

263
        /**
264
         * MIME-encodes a string for use in a header, handling line length and folding.
265
         */
266
        private static function encodeSequence(string $s, int &$offset = 0, ?int $type = null): string
1✔
267
        {
268
                if (
269
                        (strlen($s) < self::LineLength - 3) && // 3 is tab + quotes
1✔
270
                        strspn($s, "!\"#$%&\\'()*+,-./0123456789:;<>@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^`abcdefghijklmnopqrstuvwxyz{|}~=? _\r\n\t") === strlen($s)
1✔
271
                ) {
272
                        if ($type && preg_match('#[^ a-zA-Z0-9!\#$%&\'*+/?^_`{|}~-]#', $s)) { // RFC 2822 atext except =
1✔
273
                                return self::append('"' . addcslashes($s, '"\\') . '"', $offset);
1✔
274
                        }
275

276
                        return self::append($s, $offset);
1✔
277
                }
278

279
                $o = '';
1✔
280
                if ($offset >= 55) { // maximum for iconv_mime_encode
1✔
281
                        $o = self::EOL . "\t";
1✔
282
                        $offset = 1;
1✔
283
                }
284

285
                $s = iconv_mime_encode(str_repeat(' ', $old = $offset), $s, [
1✔
286
                        'scheme' => 'B', // Q is broken
1✔
287
                        'input-charset' => 'UTF-8',
288
                        'output-charset' => 'UTF-8',
289
                ]);
290

291
                $offset = strlen($s) - strrpos($s, "\n");
1✔
292
                $s = substr($s, $old + 2); // adds ': '
1✔
293
                if ($type === self::SequenceValue) {
1✔
294
                        $s = '"' . $s . '"';
1✔
295
                }
296

297
                $s = str_replace("\n ", "\n\t", $s);
1✔
298
                return $o . $s;
1✔
299
        }
300

301

302
        private static function append(string $s, int &$offset = 0): string
1✔
303
        {
304
                if ($offset + strlen($s) > self::LineLength) {
1✔
305
                        $offset = 1;
1✔
306
                        $s = self::EOL . "\t" . $s;
1✔
307
                }
308

309
                $offset += strlen($s);
1✔
310
                return $s;
1✔
311
        }
312
}
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