Issues (74)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/MessageBodyCollection.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
declare(strict_types=1);
3
4
namespace Genkgo\Mail;
5
6
use Genkgo\Mail\Header\Cc;
7
use Genkgo\Mail\Header\ContentType;
8
use Genkgo\Mail\Header\GenericHeader;
9
use Genkgo\Mail\Header\HeaderName;
10
use Genkgo\Mail\Header\ParsedHeader;
11
use Genkgo\Mail\Header\Subject;
12
use Genkgo\Mail\Header\To;
13
use Genkgo\Mail\Mime\Boundary;
14
use Genkgo\Mail\Mime\EmbeddedImage;
15
use Genkgo\Mail\Mime\HtmlPart;
16
use Genkgo\Mail\Mime\MultiPart;
17
use Genkgo\Mail\Mime\MultiPartInterface;
18
use Genkgo\Mail\Mime\PartInterface;
19
use Genkgo\Mail\Mime\PlainTextPart;
20
use Genkgo\Mail\Mime\ResourceAttachment;
21
use Genkgo\Mail\Stream\Base64DecodedStream;
22
use Genkgo\Mail\Stream\MimeBodyDecodedStream;
23
use Genkgo\Mail\Stream\QuotedPrintableDecodedStream;
24
25
final class MessageBodyCollection
26
{
27
    private const DEFAULT_CHARSET = '<meta http-equiv="Content-Type" content="text/html; charset=%s"/>';
28
29
    /**
30
     * @var string
31
     */
32
    private $html = '';
33
34
    /**
35
     * @var AlternativeText
36
     */
37
    private $text;
38
39
    /**
40
     * @var array|PartInterface[]
41
     */
42
    private $attachments = [];
43
44
    /**
45
     * @var array|PartInterface[]
46
     */
47
    private $embedImages = [];
48
49
    /**
50
     * @param string $html
51
     */
52 39
    public function __construct(string $html = '')
53
    {
54 39
        $this->html = self::ensureHtmlCharset($html);
55 39
        $this->text = AlternativeText::fromHtml($this->html);
56 39
    }
57
58
    /**
59
     * @param string $html
60
     * @return MessageBodyCollection
61
     */
62 6
    public function withHtml(string $html): self
63
    {
64 6
        $clone = clone $this;
65 6
        $clone->html = self::ensureHtmlCharset($html);
66 6
        $clone->text = AlternativeText::fromHtml($clone->html);
67 6
        return $clone;
68
    }
69
70
    /**
71
     * @param string $html
72
     * @return MessageBodyCollection
73
     */
74 18
    public function withHtmlAndNoGeneratedAlternativeText(string $html): self
75
    {
76 18
        $clone = clone $this;
77 18
        $clone->html = $html;
78 18
        return $clone;
79
    }
80
81
    /**
82
     * @param AlternativeText $text
83
     * @return MessageBodyCollection
84
     */
85 19
    public function withAlternativeText(AlternativeText $text): self
86
    {
87 19
        $clone = clone $this;
88 19
        $clone->text = $text;
89 19
        return $clone;
90
    }
91
92
    /**
93
     * @param PartInterface $part
94
     * @return MessageBodyCollection
95
     */
96 10
    public function withAttachment(PartInterface $part): self
97
    {
98
        try {
99 10
            $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
100 9
            if ($disposition !== 'attachment') {
101 1
                throw new \InvalidArgumentException(
102 9
                    'An attachment must have Content-Disposition header with value `attachment`'
103
                );
104
            }
105 2
        } catch (\UnexpectedValueException $e) {
106 1
            throw new \InvalidArgumentException(
107 1
                'An attachment must have an Content-Disposition header'
108
            );
109
        }
110
111 8
        $clone = clone $this;
112 8
        $clone->attachments[] = $part;
113 8
        return $clone;
114
    }
115
116
    /**
117
     * @param EmbeddedImage $embeddedImage
118
     * @return MessageBodyCollection
119
     */
120 5
    public function withEmbeddedImage(EmbeddedImage $embeddedImage): self
121
    {
122 5
        $clone = clone $this;
123 5
        $clone->embedImages[] = $embeddedImage;
124 5
        return $clone;
125
    }
126
127
    /**
128
     * @param MessageInterface $message
129
     * @return MessageBodyCollection
130
     */
131 1
    public function withAttachedMessage(MessageInterface $message): self
132
    {
133
        return $this
134 1
            ->withAttachment(
135 1
                ResourceAttachment::fromString(
136 1
                    (string)$message,
137 1
                    \transliterator_transliterate(
138 1
                        'Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;',
139 1
                        $this->extractSubject($message)
140 1
                    ) . '.eml',
141 1
                    new ContentType('message/rfc822')
142
                )
143
            );
144
    }
145
146
    /**
147
     * @param MessageInterface $message
148
     * @param QuotationInterface $quotation
149
     * @return MessageBodyCollection
150
     */
151 7
    public function withQuotedMessage(MessageInterface $message, QuotationInterface $quotation): self
152
    {
153 7
        return $quotation->quote($this, $message);
154
    }
155
156
    /**
157
     * @return string
158
     */
159 19
    public function getHtml(): string
160
    {
161 19
        return $this->html;
162
    }
163
164
    /**
165
     * @return AlternativeText
166
     */
167 19
    public function getText(): AlternativeText
168
    {
169 19
        return $this->text;
170
    }
171
172
    /**
173
     * @return array|PartInterface[]
174
     */
175 4
    public function getAttachments(): iterable
176
    {
177 4
        return $this->attachments;
178
    }
179
180
    /**
181
     * @return array|PartInterface[]
182
     */
183 4
    public function getEmbeddedImages(): iterable
184
    {
185 4
        return $this->embedImages;
186
    }
187
188
    /**
189
     * @return MessageInterface
190
     */
191 25
    public function createMessage(): MessageInterface
192
    {
193 25
        return (new MimeMessageFactory())->createMessage($this->createMessageRoot());
194
    }
195
196
    /**
197
     * @param MessageInterface $message
198
     * @return MessageInterface
199
     */
200 1
    public function attachToMessage(MessageInterface $message): MessageInterface
201
    {
202 1
        $newMessage = $this->createMessage();
203
204
        /** @var HeaderInterface[] $headers */
205 1
        foreach ($newMessage->getHeaders() as $headers) {
206 1
            foreach ($headers as $header) {
207 1
                $message = $message->withHeader($header);
208
            }
209
        }
210
211 1
        return $message->withBody($newMessage->getBody());
212
    }
213
214
    /**
215
     * @param MessageInterface $originalMessage
216
     * @return MessageInterface
217
     */
218 6
    public function inReplyTo(MessageInterface $originalMessage): MessageInterface
219
    {
220 6
        return $this->newReply(
221 6
            $originalMessage,
222 6
            $originalMessage->hasHeader('Reply-To') ? ['Reply-To'] : ['From']
223
        );
224
    }
225
226
    /**
227
     * @param MessageInterface $originalMessage
228
     * @return MessageInterface
229
     */
230 2
    public function inReplyToAll(MessageInterface $originalMessage): MessageInterface
231
    {
232 2
        return $this->newReply(
233 2
            $originalMessage,
234 2
            $originalMessage->hasHeader('Reply-To') ? ['Reply-To'] : ['From', 'Cc']
235
        );
236
    }
237
238
    /**
239
     * @param MessageInterface $originalMessage
240
     * @param array<int, string> $replyRecipientHeaderNames
241
     * @return MessageInterface
242
     */
243 8
    private function newReply(MessageInterface $originalMessage, array $replyRecipientHeaderNames): MessageInterface
244
    {
245 8
        $extractedSubject = $this->extractSubject($originalMessage);
246
247
        $reply = $this
248 8
            ->createReferencedMessage($originalMessage)
249 8
            ->withHeader(
250 8
                new Subject(
251 8
                    \substr($extractedSubject, 0, 3) === 'Re:' ? $extractedSubject : 'Re: ' . $extractedSubject
252
                )
253
            );
254
255 8
        foreach ($replyRecipientHeaderNames as $replyRecipientHeaderName) {
256 8
            foreach ($originalMessage->getHeader($replyRecipientHeaderName) as $recipientHeader) {
257 5
                $reply = $reply->withHeader($this->determineReplyHeader($recipientHeader));
258
            }
259
        }
260
261 8
        return $reply;
262
    }
263
264
    /**
265
     * @param MessageInterface $originalMessage
266
     * @return MessageInterface
267
     */
268 3
    public function asForwardTo(MessageInterface $originalMessage): MessageInterface
269
    {
270 3
        $extractedSubject = $this->extractSubject($originalMessage);
271
272
        return $this
273 3
            ->createReferencedMessage($originalMessage)
274 3
            ->withHeader(
275 3
                new Subject(
276 3
                    \substr($extractedSubject, 0, 4) === 'Fwd:' ? $extractedSubject : 'Fwd: ' . $extractedSubject
277
                )
278
            );
279
    }
280
281
    /**
282
     * @return PartInterface
283
     */
284 25
    private function createMessageRoot(): PartInterface
285
    {
286 25
        if (!empty($this->attachments)) {
287 6
            return (new MultiPart(
288 6
                Boundary::newRandom(),
289 6
                new ContentType('multipart/mixed')
290
            ))
291 6
                ->withPart($this->createMessageHumanReadable())
292 6
                ->withParts($this->attachments);
293
        }
294
295 19
        return $this->createMessageHumanReadable();
296
    }
297
298
    /**
299
     * @return PartInterface
300
     */
301 25
    private function createMessageHumanReadable(): PartInterface
302
    {
303 25
        if (!empty($this->embedImages)) {
304 3
            return (new MultiPart(
305 3
                Boundary::newRandom(),
306 3
                new ContentType('multipart/related')
307
            ))
308 3
                ->withPart($this->createMessageText())
309 3
                ->withParts($this->embedImages);
310
        }
311
312 22
        return $this->createMessageText();
313
    }
314
315
    /**
316
     * @return PartInterface
317
     */
318 25
    private function createMessageText(): PartInterface
319
    {
320 25
        if ($this->text->isEmpty() && $this->html === '') {
321 8
            return new PlainTextPart('', 'us-ascii');
322
        }
323
324 17
        if ($this->text->isEmpty()) {
325 2
            return new HtmlPart($this->html);
326
        }
327
328 15
        if ($this->html === '') {
329 1
            return new PlainTextPart((string)$this->text, 'UTF-8');
330
        }
331
332 14
        return (new MultiPart(
333 14
            Boundary::newRandom(),
334 14
            new ContentType('multipart/alternative')
335
        ))
336 14
            ->withPart(new PlainTextPart((string)$this->text, 'UTF-8'))
337 14
            ->withPart(new HtmlPart($this->html));
338
    }
339
340
    /**
341
     * @param MessageInterface $message
342
     * @return MessageBodyCollection
343
     */
344 20
    public static function extract(MessageInterface $message): MessageBodyCollection
345
    {
346 20
        $collection = new self();
347
348
        try {
349 20
            $collection->extractFromMimePart(MultiPart::fromMessage($message));
350 4
        } catch (\InvalidArgumentException $e) {
351 4
            foreach ($message->getHeader('Content-Type') as $header) {
352 4
                $contentType = $header->getValue()->getRaw();
353
                try {
354 4
                    $charset = $header->getValue()->getParameter('charset')->getValue();
355
                } catch (\UnexpectedValueException $e) {
356
                    $charset = '';
357
                }
358
359 4
                if ($contentType === 'text/html') {
360 1
                    $collection->html = self::ensureHtmlCharset(
361 1
                        \rtrim((string)self::decodeMessageBody($message)),
362
                        $charset
363
                    );
364
                }
365
366 4
                if ($contentType === 'text/plain') {
367 3
                    $collection->text = AlternativeText::fromEncodedText(
368 3
                        \rtrim((string)self::decodeMessageBody($message)),
369 3
                        $charset
370
                    );
371
                }
372
            }
373
        }
374
375 20
        return $collection;
376
    }
377
378
    /**
379
     * @param MultiPartInterface $parts
380
     */
381 16
    private function extractFromMimePart(MultiPartInterface $parts): void
382
    {
383 16
        foreach ($parts->getParts() as $part) {
384 16
            $header = $part->getHeader('Content-Type');
385 16
            $contentType = $header->getValue()->getRaw();
386
387
            try {
388 16
                $charset = $header->getValue()->getParameter('charset')->getValue();
389 2
            } catch (\UnexpectedValueException $e) {
390 2
                $charset = '';
391
            }
392
393 16
            try {
394
                $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
395 16
            } catch (\UnexpectedValueException $e) {
396 16
                $disposition = 'inline';
397 16
            }
398
399
            if ($contentType === 'text/html' && $disposition === 'inline') {
400 16
                $this->html = self::ensureHtmlCharset(
401
                    (string)new MimeBodyDecodedStream($part),
402
                    $charset
403 16
                );
404 16
                continue;
405 16
            }
406
407
            if ($contentType === 'text/plain' && $disposition === 'inline') {
408 2
                $this->text = AlternativeText::fromEncodedText((string)new MimeBodyDecodedStream($part), $charset);
409 2
                continue;
410
            }
411 2
412 2
            if ($disposition === 'attachment') {
413 2
                $this->attachments[] = $part;
414
                continue;
415
            }
416 2
417 2
            if ($disposition === 'inline' && \substr($contentType, 0, 6) === 'image/' && $part->hasHeader('Content-ID')) {
418 2
                $this->embedImages[] = $part;
419
                continue;
420
            }
421
422 2
            if ($part instanceof MultiPartInterface) {
423 2
                $this->extractFromMimePart($part);
424
            }
425
        }
426 16
    }
427
428
    /**
429
     * @param MessageInterface $message
430
     * @return string
431
     */
432 12
    private function extractSubject(MessageInterface $message): string
433
    {
434 12
        foreach ($message->getHeader('Subject') as $header) {
435 11
            return $header->getValue()->getRaw();
436
        }
437
438 1
        return '';
439
    }
440
441
    /**
442
     * @param HeaderInterface $header
443
     * @return HeaderInterface
444
     */
445 5
    private function determineReplyHeader(HeaderInterface $header): HeaderInterface
446
    {
447 5
        $headerName = $header->getName();
448 5
        if ($headerName->equals(new HeaderName('Reply-To')) || $headerName->equals(new HeaderName('From'))) {
0 ignored issues
show
new \Genkgo\Mail\Header\HeaderName('Reply-To') is of type object<Genkgo\Mail\Header\HeaderName>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
new \Genkgo\Mail\Header\HeaderName('From') is of type object<Genkgo\Mail\Header\HeaderName>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
449 5
            return new To(AddressList::fromString((string)$header->getValue()));
450
        }
451
452 1
        return new Cc(AddressList::fromString((string)$header->getValue()));
453
    }
454
455
    /**
456
     * @param MessageInterface $originalMessage
457
     * @return MessageInterface
458
     */
459 11
    private function createReferencedMessage(MessageInterface $originalMessage): MessageInterface
460
    {
461 11
        $reply = $this->createMessage();
462
463 11
        foreach ($originalMessage->getHeader('Message-ID') as $messageIdHeader) {
464 2
            $references = $messageIdHeader->getValue()->getRaw();
465 2
            foreach ($originalMessage->getHeader('References') as $referenceHeader) {
466 1
                $references = $referenceHeader->getValue()->getRaw() . ', ' . $references;
467
            }
468
469
            return $reply
470 2
                ->withHeader(
471 2
                    new ParsedHeader(
472 2
                        new HeaderName('In-Reply-To'),
473 2
                        $messageIdHeader->getValue()
474
                    )
475
                )
476 2
                ->withHeader(new GenericHeader('References', $references));
477
        }
478
479 9
        return $reply;
480
    }
481
482
    /**
483
     * @param MessageInterface $message
484
     * @return StreamInterface
485
     */
486 4
    private static function decodeMessageBody(MessageInterface $message): StreamInterface
487
    {
488 4
        foreach ($message->getHeader('Content-Transfer-Encoding') as $header) {
489 2
            $encoding = $header->getValue();
490 2
            switch ($encoding) {
491 2
                case 'quoted-printable':
492
                    return QuotedPrintableDecodedStream::fromString((string)$message->getBody());
493 2
                case 'base64':
494
                    return Base64DecodedStream::fromString((string)$message->getBody());
495 2
                case '7bit':
496
                case '8bit':
497 2
                    return $message->getBody();
498
                default:
499
                    throw new \UnexpectedValueException(
500
                        'Cannot decode message body, unknown transfer encoding ' . $encoding
501
                    );
502
            }
503
        }
504
505 2
        return $message->getBody();
506
    }
507
508
    /**
509
     * @param string $html
510
     * @param string $charset
511
     * @return string
512
     */
513 39
    private static function ensureHtmlCharset(string $html, string $charset = 'UTF-8'): string
514
    {
515 39
        if ($html === '') {
516 38
            return '';
517
        }
518
519 23
        if (\strpos($html, 'content="text/html') !== false || \strpos($html, 'charset="') !== false) {
520 12
            return $html;
521
        }
522
523 21
        $headCloseStart = \strpos($html, '</head>');
524 21
        if ($headCloseStart !== false) {
525 5
            return \substr_replace($html, \sprintf(self::DEFAULT_CHARSET, $charset), $headCloseStart, 0);
526
        }
527
528 19
        $bodyOpenStart = \strpos($html, '<body');
529 19
        if ($bodyOpenStart !== false) {
530 15
            return \substr_replace($html, '<head>' . \sprintf(self::DEFAULT_CHARSET, $charset) . '</head>', $bodyOpenStart, 0);
531
        }
532
533 4
        return '<html><head>' . \sprintf(self::DEFAULT_CHARSET, $charset) . '</head><body>' . $html . '</body></html>';
534
    }
535
}
536