Skip to content

Commit fd8d257

Browse files
authored
Merge pull request #135 from DirectoryTree/folder-polling
Add polling support for new messages in folders
2 parents a8a6224 + 20ba181 commit fd8d257

File tree

4 files changed

+174
-1
lines changed

4 files changed

+174
-1
lines changed

src/Folder.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,29 @@ function (int $msgn) use ($callback, $fetch) {
137137
);
138138
}
139139

140+
/**
141+
* {@inheritDoc}
142+
*/
143+
public function poll(callable $callback, ?callable $query = null, callable|int $frequency = 60): void
144+
{
145+
(new Poll(clone $this->mailbox, $this->path, $frequency))->start(
146+
function (MessageInterface $message) use ($callback) {
147+
if (! $this->mailbox->connected()) {
148+
$this->mailbox->connect();
149+
}
150+
151+
try {
152+
$callback($message);
153+
} catch (Exception) {
154+
// Something unexpected happened. We will attempt
155+
// reconnecting and continue polling for messages.
156+
$this->mailbox->reconnect();
157+
}
158+
},
159+
$query ?? fn (MessageQuery $query) => $query
160+
);
161+
}
162+
140163
/**
141164
* {@inheritDoc}
142165
*/

src/FolderInterface.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ public function is(FolderInterface $folder): bool;
4242
public function messages(): MessageQueryInterface;
4343

4444
/**
45-
* Begin idling on the current folder.
45+
* Begin idling on the current folder for the given timeout in seconds.
4646
*/
4747
public function idle(callable $callback, ?callable $query = null, callable|int $timeout = 300): void;
4848

49+
/**
50+
* Begin polling for new messages at the given frequency in seconds.
51+
*/
52+
public function poll(callable $callback, ?callable $query = null, callable|int $frequency = 60): void;
53+
4954
/**
5055
* Move or rename the current folder.
5156
*/

src/Poll.php

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
namespace DirectoryTree\ImapEngine;
4+
5+
use Closure;
6+
use DirectoryTree\ImapEngine\Exceptions\Exception;
7+
use DirectoryTree\ImapEngine\Exceptions\ImapConnectionClosedException;
8+
9+
class Poll
10+
{
11+
/**
12+
* The last seen message UID.
13+
*/
14+
protected ?int $lastSeenUid = null;
15+
16+
/**
17+
* Constructor.
18+
*/
19+
public function __construct(
20+
protected Mailbox $mailbox,
21+
protected string $folder,
22+
protected Closure|int $frequency,
23+
) {}
24+
25+
/**
26+
* Destructor.
27+
*/
28+
public function __destruct()
29+
{
30+
$this->disconnect();
31+
}
32+
33+
/**
34+
* Poll for new messages at a given frequency.
35+
*/
36+
public function start(callable $callback, callable $query): void
37+
{
38+
$this->connect();
39+
40+
while ($frequency = $this->getNextFrequency()) {
41+
try {
42+
$this->check($callback, $query);
43+
} catch (ImapConnectionClosedException) {
44+
$this->reconnect();
45+
}
46+
47+
sleep($frequency);
48+
}
49+
}
50+
51+
/**
52+
* Check for new messages since the last seen UID.
53+
*/
54+
protected function check(callable $callback, callable $query): void
55+
{
56+
$folder = $this->folder();
57+
58+
// If we don't have a last seen UID, we will fetch
59+
// the last one in the folder as a starting point.
60+
if (! $this->lastSeenUid) {
61+
$this->lastSeenUid = $folder->messages()
62+
->first()
63+
?->uid() ?? 0;
64+
65+
return;
66+
}
67+
68+
$query($folder->messages())
69+
->uid($this->lastSeenUid + 1, INF)
70+
->each(function (MessageInterface $message) use ($callback) {
71+
// Avoid processing the same message twice on subsequent polls.
72+
// Some IMAP servers will always return the last seen UID in
73+
// the search results regardless of given UID search range.
74+
if ($this->lastSeenUid === $message->uid()) {
75+
return;
76+
}
77+
78+
$callback($message);
79+
80+
$this->lastSeenUid = $message->uid();
81+
});
82+
}
83+
84+
/**
85+
* Get the folder to poll.
86+
*/
87+
protected function folder(): FolderInterface
88+
{
89+
return $this->mailbox->folders()->findOrFail($this->folder);
90+
}
91+
92+
/**
93+
* Reconnect the client and restart the poll session.
94+
*/
95+
protected function reconnect(): void
96+
{
97+
$this->mailbox->disconnect();
98+
99+
$this->connect();
100+
}
101+
102+
/**
103+
* Connect the client and select the folder to poll.
104+
*/
105+
protected function connect(): void
106+
{
107+
$this->mailbox->connect();
108+
109+
$this->mailbox->select($this->folder(), true);
110+
}
111+
112+
/**
113+
* Disconnect the client.
114+
*/
115+
protected function disconnect(): void
116+
{
117+
try {
118+
$this->mailbox->disconnect();
119+
} catch (Exception) {
120+
// Do nothing.
121+
}
122+
}
123+
124+
/**
125+
* Get the next frequency in seconds.
126+
*/
127+
protected function getNextFrequency(): int|false
128+
{
129+
if (is_numeric($seconds = value($this->frequency))) {
130+
return abs((int) $seconds);
131+
}
132+
133+
return false;
134+
}
135+
}

src/Testing/FakeFolder.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ public function idle(callable $callback, ?callable $query = null, callable|int $
9696
}
9797
}
9898

99+
/**
100+
* {@inheritDoc}
101+
*/
102+
public function poll(callable $callback, ?callable $query = null, callable|int $frequency = 60): void
103+
{
104+
foreach ($this->messages as $message) {
105+
$callback($message);
106+
}
107+
}
108+
99109
/**
100110
* {@inheritDoc}
101111
*/

0 commit comments

Comments
 (0)