blob: 1a9d16e732611c24f35a5af0385d038a4368dbce [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{
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020021 private bool $messageNumberVerified = false;
22 private int $imapMsgNo = 0;
23 private bool $structureLoaded = false;
24 private ?Message\Headers $headers = null;
25 private ?string $rawHeaders = null;
26 private ?string $rawMessage = null;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010027
28 /**
29 * Constructor.
30 *
31 * @param ImapResourceInterface $resource IMAP resource
32 * @param int $messageNumber Message number
33 */
34 public function __construct(ImapResourceInterface $resource, int $messageNumber)
35 {
36 parent::__construct($resource, $messageNumber, '1', new \stdClass());
37 }
38
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010039 protected function lazyLoadStructure(): void
40 {
41 if (true === $this->structureLoaded) {
42 return;
43 }
44 $this->structureLoaded = true;
45
46 $messageNumber = $this->getNumber();
47
48 $errorMessage = null;
49 $errorNumber = 0;
50 \set_error_handler(static function ($nr, $message) use (&$errorMessage, &$errorNumber): bool {
51 $errorMessage = $message;
52 $errorNumber = $nr;
53
54 return true;
55 });
56
57 $structure = \imap_fetchstructure(
58 $this->resource->getStream(),
59 $messageNumber,
60 \FT_UID
61 );
62
63 \restore_error_handler();
64
65 if (!$structure instanceof \stdClass) {
66 throw new MessageStructureException(\sprintf(
67 'Message "%s" structure is empty: %s',
68 $messageNumber,
69 $errorMessage
70 ), $errorNumber);
71 }
72
73 $this->setStructure($structure);
74 }
75
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010076 protected function assertMessageExists(int $messageNumber): void
77 {
78 if (true === $this->messageNumberVerified) {
79 return;
80 }
81 $this->messageNumberVerified = true;
82
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010083 $msgno = null;
84 \set_error_handler(static function (): bool {
85 return true;
86 });
87
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010088 $msgno = \imap_msgno($this->resource->getStream(), $messageNumber);
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +010089
90 \restore_error_handler();
91
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010092 if (\is_numeric($msgno) && $msgno > 0) {
93 $this->imapMsgNo = $msgno;
94
95 return;
96 }
97
98 throw new MessageDoesNotExistException(\sprintf(
99 'Message "%s" does not exist',
100 $messageNumber
101 ));
102 }
103
104 private function getMsgNo(): int
105 {
106 // Triggers assertMessageExists()
107 $this->getNumber();
108
109 return $this->imapMsgNo;
110 }
111
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100112 public function getRawHeaders(): string
113 {
114 if (null === $this->rawHeaders) {
115 $rawHeaders = \imap_fetchheader($this->resource->getStream(), $this->getNumber(), \FT_UID);
116
117 if (false === $rawHeaders) {
118 throw new ImapFetchheaderException('imap_fetchheader failed');
119 }
120
121 $this->rawHeaders = $rawHeaders;
122 }
123
124 return $this->rawHeaders;
125 }
126
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100127 public function getRawMessage(): string
128 {
129 if (null === $this->rawMessage) {
130 $this->rawMessage = $this->doGetContent('');
131 }
132
133 return $this->rawMessage;
134 }
135
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100136 public function getHeaders(): Message\Headers
137 {
138 if (null === $this->headers) {
139 // imap_headerinfo is much faster than imap_fetchheader
140 // imap_headerinfo returns only a subset of all mail headers,
141 // but it does include the message flags.
142 $headers = \imap_headerinfo($this->resource->getStream(), $this->getMsgNo());
143 if (false === $headers) {
144 // @see https://github.com/ddeboer/imap/issues/358
145 throw new InvalidHeadersException(\sprintf('Message "%s" has invalid headers', $this->getNumber()));
146 }
147 $this->headers = new Message\Headers($headers);
148 }
149
150 return $this->headers;
151 }
152
153 /**
154 * Clearmessage headers.
155 */
156 private function clearHeaders(): void
157 {
158 $this->headers = null;
159 }
160
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100161 public function isRecent(): ?string
162 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200163 $recent = $this->getHeaders()->get('recent');
164 \assert(null === $recent || \is_string($recent));
165
166 return $recent;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100167 }
168
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100169 public function isUnseen(): bool
170 {
171 return 'U' === $this->getHeaders()->get('unseen');
172 }
173
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100174 public function isFlagged(): bool
175 {
176 return 'F' === $this->getHeaders()->get('flagged');
177 }
178
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100179 public function isAnswered(): bool
180 {
181 return 'A' === $this->getHeaders()->get('answered');
182 }
183
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100184 public function isDeleted(): bool
185 {
186 return 'D' === $this->getHeaders()->get('deleted');
187 }
188
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100189 public function isDraft(): bool
190 {
191 return 'X' === $this->getHeaders()->get('draft');
192 }
193
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100194 public function isSeen(): bool
195 {
196 return 'N' !== $this->getHeaders()->get('recent') && 'U' !== $this->getHeaders()->get('unseen');
197 }
198
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100199 public function maskAsSeen(): bool
200 {
201 \trigger_error(\sprintf('%s is deprecated and will be removed in 2.0. Use %s::markAsSeen instead.', __METHOD__, __CLASS__), \E_USER_DEPRECATED);
202
203 return $this->markAsSeen();
204 }
205
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100206 public function markAsSeen(): bool
207 {
208 return $this->setFlag('\\Seen');
209 }
210
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100211 public function copy(MailboxInterface $mailbox): void
212 {
213 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
214 $this->clearHeaders();
215
216 if (!\imap_mail_copy($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
217 throw new MessageCopyException(\sprintf('Message "%s" cannot be copied to "%s"', $this->getNumber(), $mailbox->getName()));
218 }
219 }
220
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100221 public function move(MailboxInterface $mailbox): void
222 {
223 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
224 $this->clearHeaders();
225
226 if (!\imap_mail_move($this->resource->getStream(), (string) $this->getNumber(), $mailbox->getEncodedName(), \CP_UID)) {
227 throw new MessageMoveException(\sprintf('Message "%s" cannot be moved to "%s"', $this->getNumber(), $mailbox->getName()));
228 }
229 }
230
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100231 public function delete(): void
232 {
233 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to true on header
234 $this->clearHeaders();
235
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100236 if (!\imap_delete($this->resource->getStream(), (string) $this->getNumber(), \FT_UID)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100237 throw new MessageDeleteException(\sprintf('Message "%s" cannot be deleted', $this->getNumber()));
238 }
239 }
240
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100241 public function undelete(): void
242 {
243 // 'deleted' header changed, force to reload headers, would be better to set deleted flag to false on header
244 $this->clearHeaders();
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100245 if (!\imap_undelete($this->resource->getStream(), (string) $this->getNumber(), \FT_UID)) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100246 throw new MessageUndeleteException(\sprintf('Message "%s" cannot be undeleted', $this->getNumber()));
247 }
248 }
249
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100250 public function setFlag(string $flag): bool
251 {
252 $result = \imap_setflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
253
254 $this->clearHeaders();
255
256 return $result;
257 }
258
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100259 public function clearFlag(string $flag): bool
260 {
261 $result = \imap_clearflag_full($this->resource->getStream(), (string) $this->getNumber(), $flag, \ST_UID);
262
263 $this->clearHeaders();
264
265 return $result;
266 }
267}