1<?php
2
3/*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <fabien@symfony.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Symfony\Component\Mime;
13
14use Symfony\Component\Mime\Exception\LogicException;
15use Symfony\Component\Mime\Header\Headers;
16use Symfony\Component\Mime\Part\AbstractPart;
17use Symfony\Component\Mime\Part\TextPart;
18
19/**
20 * @author Fabien Potencier <fabien@symfony.com>
21 */
22class Message extends RawMessage
23{
24    private $headers;
25    private $body;
26
27    public function __construct(Headers $headers = null, AbstractPart $body = null)
28    {
29        $this->headers = $headers ? clone $headers : new Headers();
30        $this->body = $body;
31    }
32
33    public function __clone()
34    {
35        $this->headers = clone $this->headers;
36
37        if (null !== $this->body) {
38            $this->body = clone $this->body;
39        }
40    }
41
42    /**
43     * @return $this
44     */
45    public function setBody(AbstractPart $body = null)
46    {
47        $this->body = $body;
48
49        return $this;
50    }
51
52    public function getBody(): ?AbstractPart
53    {
54        return $this->body;
55    }
56
57    /**
58     * @return $this
59     */
60    public function setHeaders(Headers $headers)
61    {
62        $this->headers = $headers;
63
64        return $this;
65    }
66
67    public function getHeaders(): Headers
68    {
69        return $this->headers;
70    }
71
72    public function getPreparedHeaders(): Headers
73    {
74        $headers = clone $this->headers;
75
76        if (!$headers->has('From')) {
77            if (!$headers->has('Sender')) {
78                throw new LogicException('An email must have a "From" or a "Sender" header.');
79            }
80            $headers->addMailboxListHeader('From', [$headers->get('Sender')->getAddress()]);
81        }
82
83        if (!$headers->has('MIME-Version')) {
84            $headers->addTextHeader('MIME-Version', '1.0');
85        }
86
87        if (!$headers->has('Date')) {
88            $headers->addDateHeader('Date', new \DateTimeImmutable());
89        }
90
91        // determine the "real" sender
92        if (!$headers->has('Sender') && \count($froms = $headers->get('From')->getAddresses()) > 1) {
93            $headers->addMailboxHeader('Sender', $froms[0]);
94        }
95
96        if (!$headers->has('Message-ID')) {
97            $headers->addIdHeader('Message-ID', $this->generateMessageId());
98        }
99
100        // remove the Bcc field which should NOT be part of the sent message
101        $headers->remove('Bcc');
102
103        return $headers;
104    }
105
106    public function toString(): string
107    {
108        if (null === $body = $this->getBody()) {
109            $body = new TextPart('');
110        }
111
112        return $this->getPreparedHeaders()->toString().$body->toString();
113    }
114
115    public function toIterable(): iterable
116    {
117        if (null === $body = $this->getBody()) {
118            $body = new TextPart('');
119        }
120
121        yield $this->getPreparedHeaders()->toString();
122        yield from $body->toIterable();
123    }
124
125    public function ensureValidity()
126    {
127        if (!$this->headers->has('To') && !$this->headers->has('Cc') && !$this->headers->has('Bcc')) {
128            throw new LogicException('An email must have a "To", "Cc", or "Bcc" header.');
129        }
130
131        if (!$this->headers->has('From') && !$this->headers->has('Sender')) {
132            throw new LogicException('An email must have a "From" or a "Sender" header.');
133        }
134
135        parent::ensureValidity();
136    }
137
138    public function generateMessageId(): string
139    {
140        if ($this->headers->has('Sender')) {
141            $sender = $this->headers->get('Sender')->getAddress();
142        } elseif ($this->headers->has('From')) {
143            $sender = $this->headers->get('From')->getAddresses()[0];
144        } else {
145            throw new LogicException('An email must have a "From" or a "Sender" header.');
146        }
147
148        return bin2hex(random_bytes(16)).strstr($sender->getAddress(), '@');
149    }
150
151    public function __serialize(): array
152    {
153        return [$this->headers, $this->body];
154    }
155
156    public function __unserialize(array $data): void
157    {
158        [$this->headers, $this->body] = $data;
159    }
160}
161