blob: 7b50777f5afb337a0e5728f6a07688aa053c8d3e [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
112 $msgno = \imap_msgno($this->resource->getStream(), $messageNumber);
113 if (\is_numeric($msgno) && $msgno > 0) {
114 $this->imapMsgNo = $msgno;
115
116 return;
117 }
118
119 throw new MessageDoesNotExistException(\sprintf(
120 'Message "%s" does not exist',
121 $messageNumber
122 ));
123 }
124
125 private function getMsgNo(): int
126 {
127 // Triggers assertMessageExists()
128 $this->getNumber();
129
130 return $this->imapMsgNo;
131 }
132
133 /**
134 * Get raw message headers.
135 */
136 public function getRawHeaders(): string
137 {
138 if (null === $this->rawHeaders) {
139 $rawHeaders = \imap_fetchheader($this->resource->getStream(), $this->getNumber(), \FT_UID);
140
141 if (false === $rawHeaders) {
142 throw new ImapFetchheaderException('imap_fetchheader failed');
143 }
144
145 $this->rawHeaders = $rawHeaders;
146 }
147
148 return $this->rawHeaders;
149 }
150
151 /**
152 * Get the raw message, including all headers, parts, etc. unencoded and unparsed.
153 *
154 * @return string the raw message
155 */
156 public function getRawMessage(): string
157 {
158 if (null === $this->rawMessage) {
159 $this->rawMessage = $this->doGetContent('');
160 }
161
162 return $this->rawMessage;
163 }
164
165 /**
166 * Get message headers.
167 */
168 public function getHeaders(): Message\Headers
169 {
170 if (null === $this->headers) {
171 // imap_headerinfo is much faster than imap_fetchheader
172 // imap_headerinfo returns only a subset of all mail headers,
173 // but it does include the message flags.
174 $headers = \imap_headerinfo($this->resource->getStream(), $this->getMsgNo());
175 if (false === $headers) {
176 // @see https://github.com/ddeboer/imap/issues/358
177 throw new InvalidHeadersException(\sprintf('Message "%s" has invalid headers', $this->getNumber()));
178 }
179 $this->headers = new Message\Headers($headers);
180 }
181
182 return $this->headers;
183 }
184
185 /**
186 * Clearmessage headers.
187 */
188 private function clearHeaders(): void
189 {
190 $this->headers = null;
191 }
192
193 /**
194 * Get message recent flag value (from headers).
195 */
196 public function isRecent(): ?string
197 {
198 return $this->getHeaders()->get('recent');
199 }
200
201 /**
202 * Get message unseen flag value (from headers).
203 */
204 public function isUnseen(): bool
205 {
206 return 'U' === $this->getHeaders()->get('unseen');
207 }
208
209 /**
210 * Get message flagged flag value (from headers).
211 */
212 public function isFlagged(): bool
213 {
214 return 'F' === $this->getHeaders()->get('flagged');
215 }
216
217 /**
218 * Get message answered flag value (from headers).
219 */
220 public function isAnswered(): bool
221 {
222 return 'A' === $this->getHeaders()->get('answered');
223 }
224
225 /**
226 * Get message deleted flag value (from headers).
227 */
228 public function isDeleted(): bool
229 {
230 return 'D' === $this->getHeaders()->get('deleted');
231 }
232
233 /**
234 * Get message draft flag value (from headers).
235 */
236 public function isDraft(): bool
237 {
238 return 'X' === $this->getHeaders()->get('draft');
239 }
240
241 /**
242 * Has the message been marked as read?
243 */
244 public function isSeen(): bool
245 {
246 return 'N' !== $this->getHeaders()->get('recent') && 'U' !== $this->getHeaders()->get('unseen');
247 }
248
249 /**
250 * Mark message as seen.
251 *
252 * @deprecated since version 1.1, to be removed in 2.0
253 */
254 public function maskAsSeen(): bool
255 {
256 \trigger_error(\sprintf('%s is deprecated and will be removed in 2.0. Use %s::markAsSeen instead.', __METHOD__, __CLASS__), \E_USER_DEPRECATED);
257
258 return $this->markAsSeen();
259 }
260
261 /**
262 * Mark message as seen.
263 */
264 public function markAsSeen(): bool
265 {
266 return $this->setFlag('\\Seen');
267 }
268
269 /**
270 * Move message to another mailbox.
271 *
272 * @throws MessageCopyException
273 */
274 public function copy(MailboxInterface $mailbox): void
275 {
276 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
277 $this->clearHeaders();
278
279 if (!\imap_mail_copy($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
280 throw new MessageCopyException(\sprintf('Message "%s" cannot be copied to "%s"', $this->getNumber(), $mailbox->getName()));
281 }
282 }
283
284 /**
285 * Move message to another mailbox.
286 *
287 * @throws MessageMoveException
288 */
289 public function move(MailboxInterface $mailbox): void
290 {
291 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
292 $this->clearHeaders();
293
294 if (!\imap_mail_move($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
295 throw new MessageMoveException(\sprintf('Message "%s" cannot be moved to "%s"', $this->getNumber(), $mailbox->getName()));
296 }
297 }
298
299 /**
300 * Delete message.
301 *
302 * @throws MessageDeleteException
303 */
304 public function delete(): void
305 {
306 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
307 $this->clearHeaders();
308
309 if (!\imap_delete($this->resource->getStream(), $this->getNumber(), \FT_UID)) {
310 throw new MessageDeleteException(\sprintf('Message "%s" cannot be deleted', $this->getNumber()));
311 }
312 }
313
314 /**
315 * Undelete message.
316 *
317 * @throws MessageUndeleteException
318 */
319 public function undelete(): void
320 {
321 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to false on header
322 $this->clearHeaders();
323 if (!\imap_undelete($this->resource->getStream(), $this->getNumber(), \FT_UID)) {
324 throw new MessageUndeleteException(\sprintf('Message "%s" cannot be undeleted', $this->getNumber()));
325 }
326 }
327
328 /**
329 * Set Flag Message.
330 *
331 * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
332 */
333 public function setFlag(string $flag): bool
334 {
335 $result = \imap_setflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
336
337 $this->clearHeaders();
338
339 return $result;
340 }
341
342 /**
343 * Clear Flag Message.
344 *
345 * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
346 */
347 public function clearFlag(string $flag): bool
348 {
349 $result = \imap_clearflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
350
351 $this->clearHeaders();
352
353 return $result;
354 }
355}