mirror of
https://github.com/itflow-org/itflow
synced 2026-06-13 13:21:05 +00:00
Rewrite email parser using ImapEngine, harden processing loop
Replace webklex/php-imap with directorytree/imapengine in the ticket email parser. ImapEngine is pure PHP over sockets. Parser improvements: - Wrap per-message processing in try/catch so one malformed email can't abort the run; failures are flagged and logged with UID - Query unseen + unflagged so previously-failed (flagged) messages are no longer re-processed on every cron run - Skip vacation/auto-responder emails (RFC 3834) to prevent mail loops with the ticket auto-reply - Cap messages per run (50) and attachment size (15MB); inline images over 2MB are stored as attachments instead of base64-embedded in ticket details - Atomic lock file creation - preg_quote() the ticket prefix in subject matching - Dedupe CC watchers and exclude the sender - Map webklex 'tls' encryption setting to STARTTLS for compatibility NDR/DSN parsing now walks MIME parts via the underlying zbateson parser instead of relying on attachment extraction.
This commit is contained in:
275
plugins/vendor/directorytree/imapengine/src/BodyStructureCollection.php
vendored
Normal file
275
plugins/vendor/directorytree/imapengine/src/BodyStructureCollection.php
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace DirectoryTree\ImapEngine;
|
||||
|
||||
use Countable;
|
||||
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
|
||||
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use IteratorAggregate;
|
||||
use JsonSerializable;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @implements IteratorAggregate<int, BodyStructurePart|BodyStructureCollection>
|
||||
*/
|
||||
class BodyStructureCollection implements Arrayable, Countable, IteratorAggregate, JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array<BodyStructurePart|BodyStructureCollection> $parts
|
||||
*/
|
||||
public function __construct(
|
||||
protected string $subtype = 'mixed',
|
||||
protected array $parameters = [],
|
||||
protected array $parts = [],
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parse a multipart BODYSTRUCTURE ListData into a BodyStructureCollection.
|
||||
*/
|
||||
public static function fromListData(ListData $data, ?string $partNumber = null): static
|
||||
{
|
||||
$tokens = $data->tokens();
|
||||
|
||||
$parts = [];
|
||||
$childIndex = 1;
|
||||
$subtypeIndex = null;
|
||||
|
||||
foreach ($tokens as $index => $token) {
|
||||
if ($token instanceof Token && ! $token instanceof Nil) {
|
||||
$subtypeIndex = $index;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (! $token instanceof ListData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$childPartNumber = $partNumber ? "{$partNumber}.{$childIndex}" : (string) $childIndex;
|
||||
|
||||
$parts[] = static::isMultipart($token)
|
||||
? static::fromListData($token, $childPartNumber)
|
||||
: BodyStructurePart::fromListData($token, $childPartNumber);
|
||||
|
||||
$childIndex++;
|
||||
}
|
||||
|
||||
$parameters = [];
|
||||
|
||||
if ($subtypeIndex) {
|
||||
foreach (array_slice($tokens, $subtypeIndex + 1) as $token) {
|
||||
if ($token instanceof ListData && ! static::isDispositionList($token)) {
|
||||
$parameters = $token->toKeyValuePairs();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new static(
|
||||
$subtypeIndex ? strtolower($tokens[$subtypeIndex]->value) : 'mixed',
|
||||
$parameters,
|
||||
$parts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a ListData represents a multipart structure.
|
||||
*/
|
||||
protected static function isMultipart(ListData $data): bool
|
||||
{
|
||||
return head($data->tokens()) instanceof ListData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a ListData represents a disposition (INLINE or ATTACHMENT).
|
||||
*/
|
||||
protected static function isDispositionList(ListData $data): bool
|
||||
{
|
||||
$tokens = $data->tokens();
|
||||
|
||||
if (count($tokens) < 2 || ! isset($tokens[0]) || ! $tokens[0] instanceof Token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(strtoupper($tokens[0]->value), ['INLINE', 'ATTACHMENT']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the multipart subtype (mixed, alternative, related, etc.).
|
||||
*/
|
||||
public function subtype(): string
|
||||
{
|
||||
return $this->subtype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content type.
|
||||
*/
|
||||
public function contentType(): string
|
||||
{
|
||||
return "multipart/{$this->subtype}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters (e.g., boundary).
|
||||
*/
|
||||
public function parameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the boundary parameter.
|
||||
*/
|
||||
public function boundary(): ?string
|
||||
{
|
||||
return $this->parameters['boundary'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the direct child parts.
|
||||
*
|
||||
* @return array<BodyStructurePart|BodyStructureCollection>
|
||||
*/
|
||||
public function parts(): array
|
||||
{
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all parts flattened (including nested parts).
|
||||
*
|
||||
* @return BodyStructurePart[]
|
||||
*/
|
||||
public function flatten(): array
|
||||
{
|
||||
$flattened = [];
|
||||
|
||||
foreach ($this->parts as $part) {
|
||||
if ($part instanceof self) {
|
||||
$flattened = array_merge($flattened, $part->flatten());
|
||||
} else {
|
||||
$flattened[] = $part;
|
||||
}
|
||||
}
|
||||
|
||||
return $flattened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a part by its part number.
|
||||
*/
|
||||
public function find(string $partNumber): BodyStructurePart|BodyStructureCollection|null
|
||||
{
|
||||
foreach ($this->parts as $part) {
|
||||
if ($part instanceof self) {
|
||||
if ($found = $part->find($partNumber)) {
|
||||
return $found;
|
||||
}
|
||||
} elseif ($part->partNumber() === $partNumber) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text/plain part if available.
|
||||
*/
|
||||
public function text(): ?BodyStructurePart
|
||||
{
|
||||
foreach ($this->flatten() as $part) {
|
||||
if ($part->isText()) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text/html part if available.
|
||||
*/
|
||||
public function html(): ?BodyStructurePart
|
||||
{
|
||||
foreach ($this->flatten() as $part) {
|
||||
if ($part->isHtml()) {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attachment parts.
|
||||
*
|
||||
* @return BodyStructurePart[]
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return array_values(array_filter(
|
||||
$this->flatten(),
|
||||
fn (BodyStructurePart $part) => $part->isAttachment()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the collection has attachments.
|
||||
*/
|
||||
public function hasAttachments(): bool
|
||||
{
|
||||
return count($this->attachments()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of attachments.
|
||||
*/
|
||||
public function attachmentCount(): int
|
||||
{
|
||||
return count($this->attachments());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of parts.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for the parts.
|
||||
*/
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
yield from $this->parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'subtype' => $this->subtype,
|
||||
'parameters' => $this->parameters,
|
||||
'content_type' => $this->contentType(),
|
||||
'parts' => array_map(fn (Arrayable $part) => $part->toArray(), $this->parts),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON representation.
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user