1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* File: Folder.php |
4
|
|
|
* Category: - |
5
|
|
|
* Author: M. Goldenbaum |
6
|
|
|
* Created: 19.01.17 22:21 |
7
|
|
|
* Updated: - |
8
|
|
|
* |
9
|
|
|
* Description: |
10
|
|
|
* - |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace Webklex\PHPIMAP; |
14
|
|
|
|
15
|
|
|
use Carbon\Carbon; |
16
|
|
|
use Webklex\PHPIMAP\Exceptions\ConnectionFailedException; |
17
|
|
|
use Webklex\PHPIMAP\Exceptions\NotSupportedCapabilityException; |
18
|
|
|
use Webklex\PHPIMAP\Exceptions\RuntimeException; |
19
|
|
|
use Webklex\PHPIMAP\Query\WhereQuery; |
20
|
|
|
use Webklex\PHPIMAP\Support\FolderCollection; |
21
|
|
|
use Webklex\PHPIMAP\Traits\HasEvents; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Class Folder |
25
|
|
|
* |
26
|
|
|
* @package Webklex\PHPIMAP |
27
|
|
|
*/ |
28
|
|
|
class Folder { |
29
|
|
|
use HasEvents; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Client instance |
33
|
|
|
* |
34
|
|
|
* @var Client |
35
|
|
|
*/ |
36
|
|
|
protected $client; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Folder full path |
40
|
|
|
* |
41
|
|
|
* @var string |
42
|
|
|
*/ |
43
|
|
|
public $path; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Folder name |
47
|
|
|
* |
48
|
|
|
* @var string |
49
|
|
|
*/ |
50
|
|
|
public $name; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Folder fullname |
54
|
|
|
* |
55
|
|
|
* @var string |
56
|
|
|
*/ |
57
|
|
|
public $full_name; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Children folders |
61
|
|
|
* |
62
|
|
|
* @var FolderCollection|array |
63
|
|
|
*/ |
64
|
|
|
public $children = []; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Delimiter for folder |
68
|
|
|
* |
69
|
|
|
* @var string |
70
|
|
|
*/ |
71
|
|
|
public $delimiter; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Indicates if folder can't containg any "children". |
75
|
|
|
* CreateFolder won't work on this folder. |
76
|
|
|
* |
77
|
|
|
* @var boolean |
78
|
|
|
*/ |
79
|
|
|
public $no_inferiors; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Indicates if folder is only container, not a mailbox - you can't open it. |
83
|
|
|
* |
84
|
|
|
* @var boolean |
85
|
|
|
*/ |
86
|
|
|
public $no_select; |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* Indicates if folder is marked. This means that it may contain new messages since the last time it was checked. |
90
|
|
|
* Not provided by all IMAP servers. |
91
|
|
|
* |
92
|
|
|
* @var boolean |
93
|
|
|
*/ |
94
|
|
|
public $marked; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Indicates if folder containg any "children". |
98
|
|
|
* Not provided by all IMAP servers. |
99
|
|
|
* |
100
|
|
|
* @var boolean |
101
|
|
|
*/ |
102
|
|
|
public $has_children; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Indicates if folder refers to other. |
106
|
|
|
* Not provided by all IMAP servers. |
107
|
|
|
* |
108
|
|
|
* @var boolean |
109
|
|
|
*/ |
110
|
|
|
public $referral; |
111
|
|
|
|
112
|
|
|
/** @var array */ |
113
|
|
|
public $status; |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Folder constructor. |
117
|
|
|
* @param Client $client |
118
|
|
|
* @param string $folder_name |
119
|
|
|
* @param string $delimiter |
120
|
|
|
* @param string[] $attributes |
121
|
|
|
*/ |
122
|
|
|
public function __construct(Client $client, string $folder_name, string $delimiter, array $attributes) { |
123
|
|
|
$this->client = $client; |
124
|
|
|
|
125
|
|
|
$this->events["message"] = $client->getDefaultEvents("message"); |
126
|
|
|
$this->events["folder"] = $client->getDefaultEvents("folder"); |
127
|
|
|
|
128
|
|
|
$this->setDelimiter($delimiter); |
129
|
|
|
$this->path = $folder_name; |
130
|
|
|
$this->full_name = $this->decodeName($folder_name); |
|
|
|
|
131
|
|
|
$this->name = $this->getSimpleName($this->delimiter, $this->full_name); |
132
|
|
|
|
133
|
|
|
$this->parseAttributes($attributes); |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Get a new search query instance |
138
|
|
|
* @param string[] $extensions |
139
|
|
|
* |
140
|
|
|
* @return WhereQuery |
141
|
|
|
* @throws Exceptions\ConnectionFailedException |
142
|
|
|
* @throws Exceptions\RuntimeException |
143
|
|
|
*/ |
144
|
|
|
public function query(array $extensions = []): WhereQuery { |
145
|
|
|
$this->getClient()->checkConnection(); |
146
|
|
|
$this->getClient()->openFolder($this->path); |
147
|
|
|
$extensions = count($extensions) > 0 ? $extensions : $this->getClient()->extensions; |
148
|
|
|
|
149
|
|
|
return new WhereQuery($this->getClient(), $extensions); |
|
|
|
|
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Get a new search query instance |
154
|
|
|
* @param string[] $extensions |
155
|
|
|
* |
156
|
|
|
* @return WhereQuery |
157
|
|
|
* @throws Exceptions\ConnectionFailedException |
158
|
|
|
* @throws Exceptions\RuntimeException |
159
|
|
|
*/ |
160
|
|
|
public function search(array $extensions = []): WhereQuery { |
161
|
|
|
return $this->query($extensions); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Get a new search query instance |
166
|
|
|
* @param string[] $extensions |
167
|
|
|
* |
168
|
|
|
* @return WhereQuery |
169
|
|
|
* @throws Exceptions\ConnectionFailedException |
170
|
|
|
* @throws Exceptions\RuntimeException |
171
|
|
|
*/ |
172
|
|
|
public function messages(array $extensions = []): WhereQuery { |
173
|
|
|
return $this->query($extensions); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Determine if folder has children. |
178
|
|
|
* |
179
|
|
|
* @return bool |
180
|
|
|
*/ |
181
|
|
|
public function hasChildren(): bool { |
182
|
|
|
return $this->has_children; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Set children. |
187
|
|
|
* @param FolderCollection|array $children |
188
|
|
|
* |
189
|
|
|
* @return self |
190
|
|
|
*/ |
191
|
|
|
public function setChildren($children = []): Folder { |
192
|
|
|
$this->children = $children; |
193
|
|
|
|
194
|
|
|
return $this; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Decode name. |
199
|
|
|
* It converts UTF7-IMAP encoding to UTF-8. |
200
|
|
|
* @param $name |
201
|
|
|
* |
202
|
|
|
* @return array|false|string|string[]|null |
203
|
|
|
*/ |
204
|
|
|
protected function decodeName($name) { |
205
|
|
|
return mb_convert_encoding($name, "UTF-8", "UTF7-IMAP"); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Get simple name (without parent folders). |
210
|
|
|
* @param $delimiter |
211
|
|
|
* @param $full_name |
212
|
|
|
* |
213
|
|
|
* @return mixed |
214
|
|
|
*/ |
215
|
|
|
protected function getSimpleName($delimiter, $full_name) { |
216
|
|
|
$arr = explode($delimiter, $full_name); |
217
|
|
|
|
218
|
|
|
return end($arr); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Parse attributes and set it to object properties. |
223
|
|
|
* @param $attributes |
224
|
|
|
*/ |
225
|
|
|
protected function parseAttributes($attributes) { |
226
|
|
|
$this->no_inferiors = in_array('\NoInferiors', $attributes); |
227
|
|
|
$this->no_select = in_array('\NoSelect', $attributes); |
228
|
|
|
$this->marked = in_array('\Marked', $attributes); |
229
|
|
|
$this->referral = in_array('\Referral', $attributes); |
230
|
|
|
$this->has_children = in_array('\HasChildren', $attributes); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Move or rename the current folder |
235
|
|
|
* @param string $new_name |
236
|
|
|
* @param boolean $expunge |
237
|
|
|
* |
238
|
|
|
* @return bool |
239
|
|
|
* @throws ConnectionFailedException |
240
|
|
|
* @throws Exceptions\EventNotFoundException |
241
|
|
|
* @throws Exceptions\FolderFetchingException |
242
|
|
|
* @throws Exceptions\RuntimeException |
243
|
|
|
*/ |
244
|
|
|
public function move(string $new_name, bool $expunge = true): bool { |
245
|
|
|
$this->client->checkConnection(); |
246
|
|
|
$status = $this->client->getConnection()->renameFolder($this->full_name, $new_name); |
247
|
|
|
if($expunge) $this->client->expunge(); |
248
|
|
|
|
249
|
|
|
$folder = $this->client->getFolder($new_name); |
250
|
|
|
$event = $this->getEvent("folder", "moved"); |
251
|
|
|
$event::dispatch($this, $folder); |
252
|
|
|
|
253
|
|
|
return $status; |
|
|
|
|
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* Get a message overview |
258
|
|
|
* @param string|null $sequence uid sequence |
259
|
|
|
* |
260
|
|
|
* @return array |
261
|
|
|
* @throws ConnectionFailedException |
262
|
|
|
* @throws Exceptions\InvalidMessageDateException |
263
|
|
|
* @throws Exceptions\MessageNotFoundException |
264
|
|
|
* @throws Exceptions\RuntimeException |
265
|
|
|
*/ |
266
|
|
|
public function overview(string $sequence = null): array { |
267
|
|
|
$this->client->openFolder($this->path); |
268
|
|
|
$sequence = $sequence === null ? "1:*" : $sequence; |
269
|
|
|
$uid = ClientManager::get('options.sequence', IMAP::ST_MSGN) == IMAP::ST_UID; |
270
|
|
|
return $this->client->getConnection()->overview($sequence, $uid); |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Append a string message to the current mailbox |
275
|
|
|
* @param string $message |
276
|
|
|
* @param array|null $options |
277
|
|
|
* @param string|null|Carbon $internal_date |
278
|
|
|
* |
279
|
|
|
* @return bool |
280
|
|
|
* @throws Exceptions\ConnectionFailedException |
281
|
|
|
* @throws Exceptions\RuntimeException |
282
|
|
|
*/ |
283
|
|
|
public function appendMessage(string $message, array $options = null, $internal_date = null): bool { |
284
|
|
|
/** |
285
|
|
|
* Check if $internal_date is parsed. If it is null it should not be set. Otherwise, the message can't be stored. |
286
|
|
|
* If this parameter is set, it will set the INTERNALDATE on the appended message. The parameter should be a |
287
|
|
|
* date string that conforms to the rfc2060 specifications for a date_time value or be a Carbon object. |
288
|
|
|
*/ |
289
|
|
|
|
290
|
|
|
if ($internal_date instanceof Carbon){ |
291
|
|
|
$internal_date = $internal_date->format('d-M-Y H:i:s O'); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
return $this->client->getConnection()->appendMessage($this->path, $message, $options, $internal_date); |
|
|
|
|
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Rename the current folder |
299
|
|
|
* @param string $new_name |
300
|
|
|
* @param boolean $expunge |
301
|
|
|
* |
302
|
|
|
* @return bool |
303
|
|
|
* @throws ConnectionFailedException |
304
|
|
|
* @throws Exceptions\EventNotFoundException |
305
|
|
|
* @throws Exceptions\FolderFetchingException |
306
|
|
|
* @throws Exceptions\RuntimeException |
307
|
|
|
*/ |
308
|
|
|
public function rename(string $new_name, bool $expunge = true): bool { |
309
|
|
|
return $this->move($new_name, $expunge); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Delete the current folder |
314
|
|
|
* @param boolean $expunge |
315
|
|
|
* |
316
|
|
|
* @return bool |
317
|
|
|
* @throws Exceptions\ConnectionFailedException |
318
|
|
|
* @throws Exceptions\RuntimeException |
319
|
|
|
* @throws Exceptions\EventNotFoundException |
320
|
|
|
*/ |
321
|
|
|
public function delete(bool $expunge = true): bool { |
322
|
|
|
$status = $this->client->getConnection()->deleteFolder($this->path); |
323
|
|
|
if($expunge) $this->client->expunge(); |
324
|
|
|
|
325
|
|
|
$event = $this->getEvent("folder", "deleted"); |
326
|
|
|
$event::dispatch($this); |
327
|
|
|
|
328
|
|
|
return $status; |
|
|
|
|
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Subscribe the current folder |
333
|
|
|
* |
334
|
|
|
* @return bool |
335
|
|
|
* @throws Exceptions\ConnectionFailedException |
336
|
|
|
* @throws Exceptions\RuntimeException |
337
|
|
|
*/ |
338
|
|
|
public function subscribe(): bool { |
339
|
|
|
$this->client->openFolder($this->path); |
340
|
|
|
return $this->client->getConnection()->subscribeFolder($this->path); |
|
|
|
|
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Unsubscribe the current folder |
345
|
|
|
* |
346
|
|
|
* @return bool |
347
|
|
|
* @throws Exceptions\ConnectionFailedException |
348
|
|
|
* @throws Exceptions\RuntimeException |
349
|
|
|
*/ |
350
|
|
|
public function unsubscribe(): bool { |
351
|
|
|
$this->client->openFolder($this->path); |
352
|
|
|
return $this->client->getConnection()->unsubscribeFolder($this->path); |
|
|
|
|
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Idle the current connection |
357
|
|
|
* @param callable $callback |
358
|
|
|
* @param integer $timeout max 1740 seconds - recommended by rfc2177 §3. Should not be lower than the servers "* OK Still here" message interval |
359
|
|
|
* @param boolean $auto_reconnect try to reconnect on connection close (@deprecated is no longer required) |
360
|
|
|
* |
361
|
|
|
* @throws ConnectionFailedException |
362
|
|
|
* @throws Exceptions\InvalidMessageDateException |
363
|
|
|
* @throws Exceptions\MessageContentFetchingException |
364
|
|
|
* @throws Exceptions\MessageHeaderFetchingException |
365
|
|
|
* @throws Exceptions\RuntimeException |
366
|
|
|
* @throws Exceptions\EventNotFoundException |
367
|
|
|
* @throws Exceptions\MessageFlagException |
368
|
|
|
* @throws Exceptions\MessageNotFoundException |
369
|
|
|
* @throws Exceptions\NotSupportedCapabilityException |
370
|
|
|
*/ |
371
|
|
|
public function idle(callable $callback, int $timeout = 300, bool $auto_reconnect = false) { |
|
|
|
|
372
|
|
|
$this->client->setTimeout($timeout); |
373
|
|
|
if (!in_array("IDLE", $this->client->getConnection()->getCapabilities())) { |
374
|
|
|
throw new NotSupportedCapabilityException("IMAP server does not support IDLE"); |
375
|
|
|
} |
376
|
|
|
$this->client->openFolder($this->path, true); |
377
|
|
|
$connection = $this->client->getConnection(); |
378
|
|
|
$connection->idle(); |
379
|
|
|
|
380
|
|
|
$sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN); |
381
|
|
|
|
382
|
|
|
while (true) { |
383
|
|
|
try { |
384
|
|
|
// This polymorphic call is fine - Protocol::idle() will throw an exception beforehand |
385
|
|
|
$line = $connection->nextLine(); |
|
|
|
|
386
|
|
|
|
387
|
|
|
if (($pos = strpos($line, "EXISTS")) !== false) { |
388
|
|
|
$connection->done(); |
389
|
|
|
$msgn = (int) substr($line, 2, $pos -2); |
390
|
|
|
|
391
|
|
|
$this->client->openFolder($this->path, true); |
392
|
|
|
$message = $this->query()->getMessageByMsgn($msgn); |
393
|
|
|
$message->setSequence($sequence); |
394
|
|
|
$callback($message); |
395
|
|
|
|
396
|
|
|
$event = $this->getEvent("message", "new"); |
397
|
|
|
$event::dispatch($message); |
398
|
|
|
$connection->idle(); |
399
|
|
|
} elseif (strpos($line, "OK") === false) { |
400
|
|
|
$connection->done(); |
401
|
|
|
$connection->idle(); |
402
|
|
|
} |
403
|
|
|
}catch (Exceptions\RuntimeException $e) { |
404
|
|
|
if(strpos($e->getMessage(), "empty response") >= 0 && $connection->connected()) { |
405
|
|
|
$connection->done(); |
406
|
|
|
$connection->idle(); |
407
|
|
|
continue; |
408
|
|
|
} |
409
|
|
|
if(strpos($e->getMessage(), "connection closed") === false) { |
410
|
|
|
throw $e; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
$this->client->reconnect(); |
414
|
|
|
$this->client->openFolder($this->path, true); |
415
|
|
|
|
416
|
|
|
$connection = $this->client->getConnection(); |
417
|
|
|
$connection->idle(); |
418
|
|
|
} |
419
|
|
|
} |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Get folder status information |
424
|
|
|
* |
425
|
|
|
* @return array |
426
|
|
|
* @throws Exceptions\ConnectionFailedException |
427
|
|
|
* @throws Exceptions\RuntimeException |
428
|
|
|
*/ |
429
|
|
|
public function getStatus(): array { |
430
|
|
|
return $this->examine(); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
/** |
434
|
|
|
* @throws RuntimeException |
435
|
|
|
* @throws ConnectionFailedException |
436
|
|
|
*/ |
437
|
|
|
public function loadStatus(): Folder |
438
|
|
|
{ |
439
|
|
|
$this->status = $this->getStatus(); |
440
|
|
|
return $this; |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* Examine the current folder |
445
|
|
|
* |
446
|
|
|
* @return array |
447
|
|
|
* @throws Exceptions\ConnectionFailedException |
448
|
|
|
* @throws Exceptions\RuntimeException |
449
|
|
|
*/ |
450
|
|
|
public function examine(): array { |
451
|
|
|
$result = $this->client->getConnection()->examineFolder($this->path); |
452
|
|
|
return is_array($result) ? $result : []; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* Get the current Client instance |
457
|
|
|
* |
458
|
|
|
* @return Client |
459
|
|
|
*/ |
460
|
|
|
public function getClient(): Client { |
461
|
|
|
return $this->client; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* Set the delimiter |
466
|
|
|
* @param $delimiter |
467
|
|
|
*/ |
468
|
|
|
public function setDelimiter($delimiter){ |
469
|
|
|
if(in_array($delimiter, [null, '', ' ', false]) === true) { |
470
|
|
|
$delimiter = ClientManager::get('options.delimiter', '/'); |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
$this->delimiter = $delimiter; |
474
|
|
|
} |
475
|
|
|
} |
476
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.