blob: 0f022cc470c3b91ad230c06eb4cf78528bc32af6 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2
3declare(strict_types=1);
4
5namespace Ddeboer\Imap\Message;
6
7use Ddeboer\Imap\Exception\InvalidDateHeaderException;
8
9abstract class AbstractMessage extends AbstractPart
10{
11 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020012 * @var null|Attachment[]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010013 */
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020014 private ?array $attachments = null;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010015
16 /**
17 * Get message headers.
18 */
19 abstract public function getHeaders(): Headers;
20
21 /**
22 * Get message id.
23 *
24 * A unique message id in the form <...>
25 */
26 final public function getId(): ?string
27 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020028 $messageId = $this->getHeaders()->get('message_id');
29 \assert(null === $messageId || \is_string($messageId));
30
31 return $messageId;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010032 }
33
34 /**
35 * Get message sender (from headers).
36 */
37 final public function getFrom(): ?EmailAddress
38 {
39 $from = $this->getHeaders()->get('from');
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020040 \assert(null === $from || \is_array($from));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010041
42 return null !== $from ? $this->decodeEmailAddress($from[0]) : null;
43 }
44
45 /**
46 * Get To recipients.
47 *
48 * @return EmailAddress[] Empty array in case message has no To: recipients
49 */
50 final public function getTo(): array
51 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020052 $emails = $this->getHeaders()->get('to');
53 \assert(null === $emails || \is_array($emails));
54
55 return $this->decodeEmailAddresses($emails ?? []);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010056 }
57
58 /**
59 * Get Cc recipients.
60 *
61 * @return EmailAddress[] Empty array in case message has no CC: recipients
62 */
63 final public function getCc(): array
64 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020065 $emails = $this->getHeaders()->get('cc');
66 \assert(null === $emails || \is_array($emails));
67
68 return $this->decodeEmailAddresses($emails ?? []);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010069 }
70
71 /**
72 * Get Bcc recipients.
73 *
74 * @return EmailAddress[] Empty array in case message has no BCC: recipients
75 */
76 final public function getBcc(): array
77 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020078 $emails = $this->getHeaders()->get('bcc');
79 \assert(null === $emails || \is_array($emails));
80
81 return $this->decodeEmailAddresses($emails ?? []);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010082 }
83
84 /**
85 * Get Reply-To recipients.
86 *
87 * @return EmailAddress[] Empty array in case message has no Reply-To: recipients
88 */
89 final public function getReplyTo(): array
90 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020091 $emails = $this->getHeaders()->get('reply_to');
92 \assert(null === $emails || \is_array($emails));
93
94 return $this->decodeEmailAddresses($emails ?? []);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010095 }
96
97 /**
98 * Get Sender.
99 *
100 * @return EmailAddress[] Empty array in case message has no Sender: recipients
101 */
102 final public function getSender(): array
103 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200104 $emails = $this->getHeaders()->get('sender');
105 \assert(null === $emails || \is_array($emails));
106
107 return $this->decodeEmailAddresses($emails ?? []);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100108 }
109
110 /**
111 * Get Return-Path.
112 *
113 * @return EmailAddress[] Empty array in case message has no Return-Path: recipients
114 */
115 final public function getReturnPath(): array
116 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200117 $emails = $this->getHeaders()->get('return_path');
118 \assert(null === $emails || \is_array($emails));
119
120 return $this->decodeEmailAddresses($emails ?? []);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100121 }
122
123 /**
124 * Get date (from headers).
125 */
126 final public function getDate(): ?\DateTimeImmutable
127 {
128 /** @var null|string $dateHeader */
129 $dateHeader = $this->getHeaders()->get('date');
130 if (null === $dateHeader) {
131 return null;
132 }
133
134 $alteredValue = $dateHeader;
135 $alteredValue = \str_replace(',', '', $alteredValue);
136 $alteredValue = (string) \preg_replace('/^[a-zA-Z]+ ?/', '', $alteredValue);
137 $alteredValue = (string) \preg_replace('/\(.*\)/', '', $alteredValue);
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100138 $alteredValue = (string) \preg_replace('/\<.*\>/', '', $alteredValue);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100139 $alteredValue = (string) \preg_replace('/\bUT\b/', 'UTC', $alteredValue);
140 if (0 === \preg_match('/\d\d:\d\d:\d\d.* [\+\-]\d\d:?\d\d/', $alteredValue)) {
141 $alteredValue .= ' +0000';
142 }
143 // Handle numeric months
144 $alteredValue = (string) \preg_replace('/^(\d\d) (\d\d) (\d\d(?:\d\d)?) /', '$3-$2-$1 ', $alteredValue);
145
146 try {
147 $date = new \DateTimeImmutable($alteredValue);
148 } catch (\Throwable $ex) {
149 throw new InvalidDateHeaderException(\sprintf('Invalid Date header found: "%s"', $dateHeader), 0, $ex);
150 }
151
152 return $date;
153 }
154
155 /**
156 * Get message size (from headers).
157 *
158 * @return null|int|string
159 */
160 final public function getSize()
161 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200162 $size = $this->getHeaders()->get('size');
163 \assert(null === $size || \is_int($size) || \is_string($size));
164
165 return $size;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100166 }
167
168 /**
169 * Get message subject (from headers).
170 */
171 final public function getSubject(): ?string
172 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200173 $subject = $this->getHeaders()->get('subject');
174 \assert(null === $subject || \is_string($subject));
175
176 return $subject;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100177 }
178
179 /**
180 * Get message In-Reply-To (from headers).
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200181 *
182 * @return string[]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100183 */
184 final public function getInReplyTo(): array
185 {
186 $inReplyTo = $this->getHeaders()->get('in_reply_to');
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200187 \assert(null === $inReplyTo || \is_string($inReplyTo));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100188
189 return null !== $inReplyTo ? \explode(' ', $inReplyTo) : [];
190 }
191
192 /**
193 * Get message References (from headers).
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200194 *
195 * @return string[]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100196 */
197 final public function getReferences(): array
198 {
199 $references = $this->getHeaders()->get('references');
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200200 \assert(null === $references || \is_string($references));
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100201
202 return null !== $references ? \explode(' ', $references) : [];
203 }
204
205 /**
206 * Get body HTML.
207 */
208 final public function getBodyHtml(): ?string
209 {
210 $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
211 foreach ($iterator as $part) {
212 if (self::SUBTYPE_HTML === $part->getSubtype()) {
213 return $part->getDecodedContent();
214 }
215 }
216
217 // If message has no parts and is HTML, return content of message itself.
218 if (self::SUBTYPE_HTML === $this->getSubtype()) {
219 return $this->getDecodedContent();
220 }
221
222 return null;
223 }
224
225 /**
226 * Get body text.
227 */
228 final public function getBodyText(): ?string
229 {
230 $iterator = new \RecursiveIteratorIterator($this, \RecursiveIteratorIterator::SELF_FIRST);
231 foreach ($iterator as $part) {
232 if (self::SUBTYPE_PLAIN === $part->getSubtype()) {
233 return $part->getDecodedContent();
234 }
235 }
236
237 // If message has no parts, return content of message itself.
238 if (self::SUBTYPE_PLAIN === $this->getSubtype()) {
239 return $this->getDecodedContent();
240 }
241
242 return null;
243 }
244
245 /**
246 * Get attachments (if any) linked to this e-mail.
247 *
248 * @return AttachmentInterface[]
249 */
250 final public function getAttachments(): array
251 {
252 if (null === $this->attachments) {
253 $this->attachments = self::gatherAttachments($this);
254 }
255
256 return $this->attachments;
257 }
258
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200259 /**
260 * @param PartInterface<PartInterface> $part
261 *
262 * @return Attachment[]
263 */
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100264 private static function gatherAttachments(PartInterface $part): array
265 {
266 $attachments = [];
267 foreach ($part->getParts() as $childPart) {
268 if ($childPart instanceof Attachment) {
269 $attachments[] = $childPart;
270 }
271 if ($childPart->hasChildren()) {
272 $attachments = \array_merge($attachments, self::gatherAttachments($childPart));
273 }
274 }
275
276 return $attachments;
277 }
278
279 /**
280 * Does this message have attachments?
281 */
282 final public function hasAttachments(): bool
283 {
284 return \count($this->getAttachments()) > 0;
285 }
286
287 /**
288 * @param \stdClass[] $addresses
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200289 *
290 * @return EmailAddress[]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100291 */
292 private function decodeEmailAddresses(array $addresses): array
293 {
294 $return = [];
295 foreach ($addresses as $address) {
296 if (isset($address->mailbox)) {
297 $return[] = $this->decodeEmailAddress($address);
298 }
299 }
300
301 return $return;
302 }
303
304 private function decodeEmailAddress(\stdClass $value): EmailAddress
305 {
306 return new EmailAddress($value->mailbox, $value->host, $value->personal);
307 }
308}