glastree_on_gitea

This commit is contained in:
2026-05-26 08:14:29 +02:00
commit 0bed099d05
9556 changed files with 1186307 additions and 0 deletions
@@ -0,0 +1,35 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Commands;
use DirectoryTree\ImapEngine\MessageQueryInterface;
class ConfigureIdleQuery
{
/**
* Constructor.
*/
public function __construct(
protected array $with = []
) {}
/**
* Configure the query.
*/
public function __invoke(MessageQueryInterface $query): MessageQueryInterface
{
if (in_array('flags', $this->with)) {
$query->withFlags();
}
if (in_array('body', $this->with)) {
$query->withBody();
}
if (in_array('headers', $this->with)) {
$query->withHeaders();
}
return $query;
}
}
@@ -0,0 +1,37 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Commands;
use Carbon\CarbonInterface;
use DirectoryTree\ImapEngine\Laravel\Events\MessageReceived;
use DirectoryTree\ImapEngine\MessageInterface;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Event;
class HandleMessageReceived
{
/**
* Constructor.
*/
public function __construct(
protected WatchMailbox $command,
protected int &$attempts = 0,
protected ?CarbonInterface &$lastReceivedAt = null,
) {}
/**
* Handle the message received event.
*/
public function __invoke(MessageInterface $message): void
{
$this->command->info(
"Message received: [{$message->uid()}]"
);
$this->attempts = 0;
$this->lastReceivedAt = Date::now();
Event::dispatch(new MessageReceived($message));
}
}
@@ -0,0 +1,130 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Commands;
use DirectoryTree\ImapEngine\FolderInterface;
use DirectoryTree\ImapEngine\Laravel\Events\MailboxWatchAttemptsExceeded;
use DirectoryTree\ImapEngine\Laravel\Facades\Imap;
use DirectoryTree\ImapEngine\Laravel\Support\LoopInterface;
use DirectoryTree\ImapEngine\MailboxInterface;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Symfony\Component\Console\Exception\InvalidOptionException;
class WatchMailbox extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'imap:watch {mailbox} {folder?} {--method=idle} {--with=} {--timeout=30} {--attempts=5} {--debug=false}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Watch a mailbox for new messages.';
/**
* Execute the console command.
*/
public function handle(LoopInterface $loop): void
{
if (! in_array($method = $this->option('method'), ['idle', 'poll'])) {
throw new InvalidOptionException("Invalid method [{$method}]. Valid options are [idle, poll].");
}
$mailbox = Imap::mailbox($name = $this->argument('mailbox'));
$with = explode(',', $this->option('with'));
$this->info("Watching mailbox [$name]...");
$attempts = 0;
$lastReceivedAt = null;
$loop->run(function () use ($mailbox, $name, $with, &$attempts, &$lastReceivedAt) {
try {
$folder = $this->folder($mailbox);
match ($this->option('method')) {
'idle' => $folder->idle(
new HandleMessageReceived($this, $attempts, $lastReceivedAt),
new ConfigureIdleQuery($with),
$this->option('timeout'),
),
'poll' => $folder->poll(
new HandleMessageReceived($this, $attempts, $lastReceivedAt),
new ConfigureIdleQuery($with),
$this->option('timeout'),
),
};
} catch (Exception $e) {
if ($this->isMessageMissing($e)) {
return;
}
if ($this->isDisconnection($e)) {
sleep(2);
return;
}
if ($attempts >= $this->option('attempts')) {
$this->info("Exception: {$e->getMessage()}");
Event::dispatch(
new MailboxWatchAttemptsExceeded($name, $attempts, $e, $lastReceivedAt)
);
throw $e;
}
$attempts++;
}
});
}
/**
* Get the mailbox folder to idle.
*/
protected function folder(MailboxInterface $mailbox): FolderInterface
{
return ($folder = $this->argument('folder'))
? $mailbox->folders()->findOrFail($folder)
: $mailbox->inbox();
}
/**
* Determine if the exception is due to a message missing error.
*/
protected function isMessageMissing(Exception $e): bool
{
return Str::contains($e->getMessage(), [
'no longer exist',
], true);
}
/**
* Determine if the exception is caused by a disconnection.
*/
protected function isDisconnection(Exception $e): bool
{
return Str::contains($e->getMessage(), [
'connection reset by peer',
'temporary system problem',
'failed to fetch content',
'connection failed',
'empty response',
'not connected',
'no response',
'broken pipe',
'unavailable',
], true);
}
}
@@ -0,0 +1,19 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Events;
use Carbon\CarbonInterface;
use Exception;
class MailboxWatchAttemptsExceeded
{
/**
* Constructor.
*/
public function __construct(
public string $mailbox,
public int $attempts,
public Exception $exception,
public ?CarbonInterface $lastReceivedAt = null,
) {}
}
@@ -0,0 +1,15 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Events;
use DirectoryTree\ImapEngine\MessageInterface;
class MessageReceived
{
/**
* Create a new event instance.
*/
public function __construct(
public MessageInterface $message
) {}
}
@@ -0,0 +1,41 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Facades;
use DirectoryTree\ImapEngine\Laravel\ImapManager;
use DirectoryTree\ImapEngine\Testing\FakeMailbox;
use Illuminate\Support\Facades\Facade;
/**
* @method static \DirectoryTree\ImapEngine\MailboxInterface mailbox(string $name)
* @method static \DirectoryTree\ImapEngine\MailboxInterface swap(string $name, \DirectoryTree\ImapEngine\MailboxInterface $mailbox)
*/
class Imap extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return ImapManager::class;
}
/**
* Fake the given mailbox.
*/
public static function fake(
string $mailbox,
array $config = [],
array $folders = [],
array $capabilities = []
): FakeMailbox {
/** @var \DirectoryTree\ImapEngine\Laravel\ImapManager $manager */
$manager = static::getFacadeRoot();
$fake = new FakeMailbox($config, $folders, $capabilities);
$manager->swap($mailbox, $fake);
return $fake;
}
}
@@ -0,0 +1,82 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel;
use DirectoryTree\ImapEngine\Mailbox;
use DirectoryTree\ImapEngine\MailboxInterface;
use InvalidArgumentException;
class ImapManager
{
/**
* The IMAP configuration.
*/
protected array $config = [];
/**
* The mailbox instances.
*/
protected array $mailboxes = [];
/**
* Constructor.
*/
public function __construct(array $config)
{
$this->config = $config;
}
/**
* Get a mailbox instance.
*/
public function mailbox(string $name): MailboxInterface
{
if (isset($this->mailboxes[$name])) {
return $this->mailboxes[$name];
}
if (! array_key_exists($name, $this->config['mailboxes'] ?? [])) {
throw new InvalidArgumentException(
"Mailbox [{$name}] is not defined. Please check your IMAP configuration."
);
}
return $this->mailboxes[$name] = $this->build($this->config['mailboxes'][$name]);
}
/**
* Register a mailbox instance.
*/
public function register(string $name, array $config): static
{
$this->mailboxes[$name] = $this->build($config);
return $this;
}
/**
* Build an on-demand mailbox instance.
*/
public function build(array $config): MailboxInterface
{
return new Mailbox($config);
}
/**
* Remove a mailbox from the in-memory cache.
*/
public function forget(string $name): static
{
unset($this->mailboxes[$name]);
return $this;
}
/**
* Swap out a mailbox instance with a new one.
*/
public function swap(string $name, MailboxInterface $mailbox): void
{
$this->mailboxes[$name] = $mailbox;
}
}
@@ -0,0 +1,38 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel;
use DirectoryTree\ImapEngine\Laravel\Support\Loop;
use DirectoryTree\ImapEngine\Laravel\Support\LoopInterface;
use Illuminate\Support\ServiceProvider;
class ImapServiceProvider extends ServiceProvider
{
/**
* Register application services.
*/
public function register(): void
{
$this->app->singleton(ImapManager::class, function () {
return new ImapManager(config('imap', []));
});
$this->app->bind(LoopInterface::class, Loop::class);
}
/**
* Bootstrap application services.
*/
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
Commands\WatchMailbox::class,
]);
}
$this->publishes([
__DIR__.'/../config/imap.php' => config_path('imap.php'),
]);
}
}
@@ -0,0 +1,16 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Support;
class Loop implements LoopInterface
{
/**
* {@inheritDoc}
*/
public function run(callable $tick): void
{
while (true) {
$tick();
}
}
}
@@ -0,0 +1,14 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Support;
class LoopFake implements LoopInterface
{
/**
* {@inheritDoc}
*/
public function run(callable $tick): void
{
$tick();
}
}
@@ -0,0 +1,11 @@
<?php
namespace DirectoryTree\ImapEngine\Laravel\Support;
interface LoopInterface
{
/**
* Execute the loop.
*/
public function run(callable $tick): void;
}