blob: 1544aff078cbf5ce5cabef1b0c6e04fcf8b2bcd7 [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2
3declare(strict_types=1);
4
5namespace Ddeboer\Imap;
6
7use DateTimeInterface;
8use Ddeboer\Imap\Exception\ImapNumMsgException;
9use Ddeboer\Imap\Exception\ImapStatusException;
10use Ddeboer\Imap\Exception\InvalidSearchCriteriaException;
11use Ddeboer\Imap\Exception\MessageCopyException;
12use Ddeboer\Imap\Exception\MessageMoveException;
13use Ddeboer\Imap\Search\ConditionInterface;
14use Ddeboer\Imap\Search\LogicalOperator\All;
15
16/**
17 * An IMAP mailbox (commonly referred to as a 'folder').
18 */
19final class Mailbox implements MailboxInterface
20{
21 /**
22 * @var ImapResourceInterface
23 */
24 private $resource;
25
26 /**
27 * @var string
28 */
29 private $name;
30
31 /**
32 * @var \stdClass
33 */
34 private $info;
35
36 /**
37 * Constructor.
38 *
39 * @param ImapResourceInterface $resource IMAP resource
40 * @param string $name Mailbox decoded name
41 * @param \stdClass $info Mailbox info
42 */
43 public function __construct(ImapResourceInterface $resource, string $name, \stdClass $info)
44 {
45 $this->resource = new ImapResource($resource->getStream(), $this);
46 $this->name = $name;
47 $this->info = $info;
48 }
49
50 /**
51 * Get mailbox decoded name.
52 */
53 public function getName(): string
54 {
55 return $this->name;
56 }
57
58 /**
59 * Get mailbox encoded path.
60 */
61 public function getEncodedName(): string
62 {
63 /** @var string $name */
64 $name = $this->info->name;
65
66 return (string) \preg_replace('/^{.+}/', '', $name);
67 }
68
69 /**
70 * Get mailbox encoded full name.
71 */
72 public function getFullEncodedName(): string
73 {
74 return $this->info->name;
75 }
76
77 /**
78 * Get mailbox attributes.
79 */
80 public function getAttributes(): int
81 {
82 return $this->info->attributes;
83 }
84
85 /**
86 * Get mailbox delimiter.
87 */
88 public function getDelimiter(): string
89 {
90 return $this->info->delimiter;
91 }
92
93 /**
94 * Get number of messages in this mailbox.
95 *
96 * @return int
97 */
98 public function count()
99 {
100 $return = \imap_num_msg($this->resource->getStream());
101
102 if (false === $return) {
103 throw new ImapNumMsgException('imap_num_msg failed');
104 }
105
106 return $return;
107 }
108
109 /**
110 * Get Mailbox status.
111 */
112 public function getStatus(int $flags = null): \stdClass
113 {
114 $return = \imap_status($this->resource->getStream(), $this->getFullEncodedName(), $flags ?? \SA_ALL);
115
116 if (false === $return) {
117 throw new ImapStatusException('imap_status failed');
118 }
119
120 return $return;
121 }
122
123 /**
124 * Bulk Set Flag for Messages.
125 *
126 * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
127 * @param array|MessageIterator|string $numbers Message numbers
128 */
129 public function setFlag(string $flag, $numbers): bool
130 {
131 return \imap_setflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
132 }
133
134 /**
135 * Bulk Clear Flag for Messages.
136 *
137 * @param string $flag \Seen, \Answered, \Flagged, \Deleted, and \Draft
138 * @param array|MessageIterator|string $numbers Message numbers
139 */
140 public function clearFlag(string $flag, $numbers): bool
141 {
142 return \imap_clearflag_full($this->resource->getStream(), $this->prepareMessageIds($numbers), $flag, \ST_UID);
143 }
144
145 /**
146 * Get message ids.
147 *
148 * @param ConditionInterface $search Search expression (optional)
149 */
150 public function getMessages(ConditionInterface $search = null, int $sortCriteria = null, bool $descending = false, string $charset = null): MessageIteratorInterface
151 {
152 if (null === $search) {
153 $search = new All();
154 }
155 $query = $search->toString();
156
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100157 if (\PHP_VERSION_ID < 80000) {
158 $descending = (int) $descending;
159 }
160
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100161 // We need to clear the stack to know whether imap_last_error()
162 // is related to this imap_search
163 \imap_errors();
164
165 if (null !== $sortCriteria) {
166 $params = [
167 $this->resource->getStream(),
168 $sortCriteria,
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100169 $descending,
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100170 \SE_UID,
171 $query,
172 ];
173 if (null !== $charset) {
174 $params[] = $charset;
175 }
176 $messageNumbers = \imap_sort(...$params);
177 } else {
178 $params = [
179 $this->resource->getStream(),
180 $query,
181 \SE_UID,
182 ];
183 if (null !== $charset) {
184 $params[] = $charset;
185 }
186 $messageNumbers = \imap_search(...$params);
187 }
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100188 if (false !== \imap_last_error()) {
189 // this way all errors occurred during search will be reported
190 throw new InvalidSearchCriteriaException(
191 \sprintf('Invalid search criteria [%s]', $query)
192 );
193 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100194 if (false === $messageNumbers) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100195 // imap_search can also return false
196 $messageNumbers = [];
197 }
198
199 return new MessageIterator($this->resource, $messageNumbers);
200 }
201
202 /**
203 * Get message iterator for a sequence.
204 *
205 * @param string $sequence Message numbers
206 */
207 public function getMessageSequence(string $sequence): MessageIteratorInterface
208 {
209 \imap_errors();
210
211 $overview = \imap_fetch_overview($this->resource->getStream(), $sequence, \FT_UID);
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100212 if (false !== \imap_last_error()) {
213 throw new InvalidSearchCriteriaException(
214 \sprintf('Invalid sequence [%s]', $sequence)
215 );
216 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100217 if (\is_array($overview) && [] !== $overview) {
218 $messageNumbers = \array_column($overview, 'uid');
219 } else {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100220 $messageNumbers = [];
221 }
222
223 return new MessageIterator($this->resource, $messageNumbers);
224 }
225
226 /**
227 * Get a message by message number.
228 *
229 * @param int $number Message number
230 */
231 public function getMessage(int $number): MessageInterface
232 {
233 return new Message($this->resource, $number);
234 }
235
236 /**
237 * Get messages in this mailbox.
238 */
239 public function getIterator(): MessageIteratorInterface
240 {
241 return $this->getMessages();
242 }
243
244 /**
245 * Add a message to the mailbox.
246 */
247 public function addMessage(string $message, string $options = null, DateTimeInterface $internalDate = null): bool
248 {
249 $arguments = [
250 $this->resource->getStream(),
251 $this->getFullEncodedName(),
252 $message,
253 ];
254 if (null !== $options) {
255 $arguments[] = $options;
256 if (null !== $internalDate) {
257 $arguments[] = $internalDate->format('d-M-Y H:i:s O');
258 }
259 }
260
261 return \imap_append(...$arguments);
262 }
263
264 /**
265 * Returns a tree of threaded message for the current Mailbox.
266 */
267 public function getThread(): array
268 {
269 \set_error_handler(static function (): bool {
270 return true;
271 });
272
273 /** @var array|false $tree */
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100274 $tree = \imap_thread($this->resource->getStream(), \SE_UID);
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100275
276 \restore_error_handler();
277
278 return false !== $tree ? $tree : [];
279 }
280
281 /**
282 * Bulk move messages.
283 *
284 * @param array|MessageIterator|string $numbers Message numbers
285 * @param MailboxInterface $mailbox Destination Mailbox to move the messages to
286 *
287 * @throws \Ddeboer\Imap\Exception\MessageMoveException
288 */
289 public function move($numbers, MailboxInterface $mailbox): void
290 {
291 if (!\imap_mail_move($this->resource->getStream(), $this->prepareMessageIds($numbers), $mailbox->getEncodedName(), \CP_UID)) {
292 throw new MessageMoveException(\sprintf('Messages cannot be moved to "%s"', $mailbox->getName()));
293 }
294 }
295
296 /**
297 * Bulk copy messages.
298 *
299 * @param array|MessageIterator|string $numbers Message numbers
300 * @param MailboxInterface $mailbox Destination Mailbox to copy the messages to
301 *
302 * @throws \Ddeboer\Imap\Exception\MessageCopyException
303 */
304 public function copy($numbers, MailboxInterface $mailbox): void
305 {
306 if (!\imap_mail_copy($this->resource->getStream(), $this->prepareMessageIds($numbers), $mailbox->getEncodedName(), \CP_UID)) {
307 throw new MessageCopyException(\sprintf('Messages cannot be copied to "%s"', $mailbox->getName()));
308 }
309 }
310
311 /**
312 * Prepare message ids for the use with bulk functions.
313 *
314 * @param array|MessageIterator|string $messageIds Message numbers
315 */
316 private function prepareMessageIds($messageIds): string
317 {
318 if ($messageIds instanceof MessageIterator) {
319 $messageIds = $messageIds->getArrayCopy();
320 }
321
322 if (\is_array($messageIds)) {
323 $messageIds = \implode(',', $messageIds);
324 }
325
326 return $messageIds;
327 }
328}