blob: 843ce61059a5e9bd831dbf4315b5b5827032651d [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2
3declare(strict_types=1);
4
5namespace Ddeboer\Imap;
6
7use Ddeboer\Imap\Exception\ImapFetchheaderException;
8use Ddeboer\Imap\Exception\InvalidHeadersException;
9use Ddeboer\Imap\Exception\MessageCopyException;
10use Ddeboer\Imap\Exception\MessageDeleteException;
11use Ddeboer\Imap\Exception\MessageDoesNotExistException;
12use Ddeboer\Imap\Exception\MessageMoveException;
13use Ddeboer\Imap\Exception\MessageStructureException;
14use Ddeboer\Imap\Exception\MessageUndeleteException;
15
16/**
17 * An IMAP message (e-mail).
18 */
19final class Message extends Message\AbstractMessage implements MessageInterface
20{
21 /**
22 * @var bool
23 */
24 private $messageNumberVerified = false;
25
26 /**
27 * @var int
28 */
29 private $imapMsgNo = 0;
30
31 /**
32 * @var bool
33 */
34 private $structureLoaded = false;
35
36 /**
37 * @var null|Message\Headers
38 */
39 private $headers;
40
41 /**
42 * @var null|string
43 */
44 private $rawHeaders;
45
46 /**
47 * @var null|string
48 */
49 private $rawMessage;
50
51 /**
52 * Constructor.
53 *
54 * @param ImapResourceInterface $resource IMAP resource
55 * @param int $messageNumber Message number
56 */
57 public function __construct(ImapResourceInterface $resource, int $messageNumber)
58 {
59 parent::__construct($resource, $messageNumber, '1', new \stdClass());
60 }
61
62 /**
63 * Lazy load structure.
64 */
65 protected function lazyLoadStructure(): void
66 {
67 if (true === $this->structureLoaded) {
68 return;
69 }
70 $this->structureLoaded = true;
71
72 $messageNumber = $this->getNumber();
73
74 $errorMessage = null;
75 $errorNumber = 0;
76 \set_error_handler(static function ($nr, $message) use (&$errorMessage, &$errorNumber): bool {
77 $errorMessage = $message;
78 $errorNumber = $nr;
79
80 return true;
81 });
82
83 $structure = \imap_fetchstructure(
84 $this->resource->getStream(),
85 $messageNumber,
86 \FT_UID
87 );
88
89 \restore_error_handler();
90
91 if (!$structure instanceof \stdClass) {
92 throw new MessageStructureException(\sprintf(
93 'Message "%s" structure is empty: %s',
94 $messageNumber,
95 $errorMessage
96 ), $errorNumber);
97 }
98
99 $this->setStructure($structure);
100 }
101
102 /**
103 * Ensure message exists.
104 */
105 protected function assertMessageExists(int $messageNumber): void
106 {
107 if (true === $this->messageNumberVerified) {
108 return;
109 }
110 $this->messageNumberVerified = true;
111
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100112 $msgno = null;
113 \set_error_handler(static function (): bool {
114 return true;
115 });
116
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100117 $msgno = \imap_msgno($this->resource->getStream(), $messageNumber);
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100118
119 \restore_error_handler();
120
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100121 if (\is_numeric($msgno) && $msgno > 0) {
122 $this->imapMsgNo = $msgno;
123
124 return;
125 }
126
127 throw new MessageDoesNotExistException(\sprintf(
128 'Message "%s" does not exist',
129 $messageNumber
130 ));
131 }
132
133 private function getMsgNo(): int
134 {
135 // Triggers assertMessageExists()
136 $this->getNumber();
137
138 return $this->imapMsgNo;
139 }
140
141 /**
142 * Get raw message headers.
143 */
144 public function getRawHeaders(): string
145 {
146 if (null === $this->rawHeaders) {
147 $rawHeaders = \imap_fetchheader($this->resource->getStream(), $this->getNumber(), \FT_UID);
148
149 if (false === $rawHeaders) {
150 throw new ImapFetchheaderException('imap_fetchheader failed');
151 }
152
153 $this->rawHeaders = $rawHeaders;
154 }
155
156 return $this->rawHeaders;
157 }
158
159 /**
160 * Get the raw message, including all headers, parts, etc. unencoded and unparsed.
161 *
162 * @return string the raw message
163 */
164 public function getRawMessage(): string
165 {
166 if (null === $this->rawMessage) {
167 $this->rawMessage = $this->doGetContent('');
168 }
169
170 return $this->rawMessage;
171 }
172
173 /**
174 * Get message headers.
175 */
176 public function getHeaders(): Message\Headers
177 {
178 if (null === $this->headers) {
179 // imap_headerinfo is much faster than imap_fetchheader
180 // imap_headerinfo returns only a subset of all mail headers,
181 // but it does include the message flags.
182 $headers = \imap_headerinfo($this->resource->getStream(), $this->getMsgNo());
183 if (false === $headers) {
184 // @see https://github.com/ddeboer/imap/issues/358
185 throw new InvalidHeadersException(\sprintf('Message "%s" has invalid headers', $this->getNumber()));
186 }
187 $this->headers = new Message\Headers($headers);
188 }
189
190 return $this->headers;
191 }
192
193 /**
194 * Clearmessage headers.
195 */
196 private function clearHeaders(): void
197 {
198 $this->headers = null;
199 }
200
201 /**
202 * Get message recent flag value (from headers).
203 */
204 public function isRecent(): ?string
205 {
206 return $this->getHeaders()->get('recent');
207 }
208
209 /**
210 * Get message unseen flag value (from headers).
211 */
212 public function isUnseen(): bool
213 {
214 return 'U' === $this->getHeaders()->get('unseen');
215 }
216
217 /**
218 * Get message flagged flag value (from headers).
219 */
220 public function isFlagged(): bool
221 {
222 return 'F' === $this->getHeaders()->get('flagged');
223 }
224
225 /**
226 * Get message answered flag value (from headers).
227 */
228 public function isAnswered(): bool
229 {
230 return 'A' === $this->getHeaders()->get('answered');
231 }
232
233 /**
234 * Get message deleted flag value (from headers).
235 */
236 public function isDeleted(): bool
237 {
238 return 'D' === $this->getHeaders()->get('deleted');
239 }
240
241 /**
242 * Get message draft flag value (from headers).
243 */
244 public function isDraft(): bool
245 {
246 return 'X' === $this->getHeaders()->get('draft');
247 }
248
249 /**
250 * Has the message been marked as read?
251 */
252 public function isSeen(): bool
253 {
254 return 'N' !== $this->getHeaders()->get('recent') && 'U' !== $this->getHeaders()->get('unseen');
255 }
256
257 /**
258 * Mark message as seen.
259 *
260 * @deprecated since version 1.1, to be removed in 2.0
261 */
262 public function maskAsSeen(): bool
263 {
264 \trigger_error(\sprintf('%s is deprecated and will be removed in 2.0. Use %s::markAsSeen instead.', __METHOD__, __CLASS__), \E_USER_DEPRECATED);
265
266 return $this->markAsSeen();
267 }
268
269 /**
270 * Mark message as seen.
271 */
272 public function markAsSeen(): bool
273 {
274 return $this->setFlag('\\Seen');
275 }
276
277 /**
278 * Move message to another mailbox.
279 *
280 * @throws MessageCopyException
281 */
282 public function copy(MailboxInterface $mailbox): void
283 {
284 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
285 $this->clearHeaders();
286
287 if (!\imap_mail_copy($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
288 throw new MessageCopyException(\sprintf('Message "%s" cannot be copied to "%s"', $this->getNumber(), $mailbox->getName()));
289 }
290 }
291
292 /**
293 * Move message to another mailbox.
294 *
295 * @throws MessageMoveException
296 */
297 public function move(MailboxInterface $mailbox): void
298 {
299 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
300 $this->clearHeaders();
301
302 if (!\imap_mail_move($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
303 throw new MessageMoveException(\sprintf('Message "%s" cannot be moved to "%s"', $this->getNumber(), $mailbox->getName()));
304 }
305 }
306
307 /**
308 * Delete message.
309 *
310 * @throws MessageDeleteException
311 */
312 public function delete(): void
313 {
314 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
315 $this->clearHeaders();
316
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100317 if (!\imap_delete($this->resource->getStream(), (string) $this->getNumber(), \FT_UID)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100318 throw new MessageDeleteException(\sprintf('Message "%s" cannot be deleted', $this->getNumber()));
319 }
320 }
321
322 /**
323 * Undelete message.
324 *
325 * @throws MessageUndeleteException
326 */
327 public function undelete(): void
328 {
329 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to false on header
330 $this->clearHeaders();
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100331 if (!\imap_undelete($this->resource->getStream(), (string) $this->getNumber(), \FT_UID)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100332 throw new MessageUndeleteException(\sprintf('Message "%s" cannot be undeleted', $this->getNumber()));
333 }
334 }
335
336 /**
337 * Set Flag Message.
338 *
339 * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
340 */
341 public function setFlag(string $flag): bool
342 {
343 $result = \imap_setflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
344
345 $this->clearHeaders();
346
347 return $result;
348 }
349
350 /**
351 * Clear Flag Message.
352 *
353 * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
354 */
355 public function clearFlag(string $flag): bool
356 {
357 $result = \imap_clearflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
358
359 $this->clearHeaders();
360
361 return $result;
362 }
363}