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:
johnnyq
2026-06-12 16:56:39 -04:00
parent 300a1aff9f
commit 2204bd52f4
701 changed files with 111718 additions and 940 deletions

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class AddressLiteral extends Warning
{
public const CODE = 12;
public function __construct()
{
$this->message = 'Address literal in domain part';
$this->rfcNumber = 5321;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Egulias\EmailValidator\Warning;
class CFWSNearAt extends Warning
{
public const CODE = 49;
public function __construct()
{
$this->message = "Deprecated folding white space near @";
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Egulias\EmailValidator\Warning;
class CFWSWithFWS extends Warning
{
public const CODE = 18;
public function __construct()
{
$this->message = 'Folding whites space followed by folding white space';
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Egulias\EmailValidator\Warning;
class Comment extends Warning
{
public const CODE = 17;
public function __construct()
{
$this->message = "Comments found in this email";
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Egulias\EmailValidator\Warning;
class DeprecatedComment extends Warning
{
public const CODE = 37;
public function __construct()
{
$this->message = 'Deprecated comments';
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class DomainLiteral extends Warning
{
public const CODE = 70;
public function __construct()
{
$this->message = 'Domain Literal';
$this->rfcNumber = 5322;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Egulias\EmailValidator\Warning;
use Egulias\EmailValidator\EmailParser;
class EmailTooLong extends Warning
{
public const CODE = 66;
public function __construct()
{
$this->message = 'Email is too long, exceeds ' . EmailParser::EMAIL_MAX_LENGTH;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class IPV6BadChar extends Warning
{
public const CODE = 74;
public function __construct()
{
$this->message = 'Bad char in IPV6 domain literal';
$this->rfcNumber = 5322;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class IPV6ColonEnd extends Warning
{
public const CODE = 77;
public function __construct()
{
$this->message = ':: found at the end of the domain literal';
$this->rfcNumber = 5322;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class IPV6ColonStart extends Warning
{
public const CODE = 76;
public function __construct()
{
$this->message = ':: found at the start of the domain literal';
$this->rfcNumber = 5322;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class IPV6Deprecated extends Warning
{
public const CODE = 13;
public function __construct()
{
$this->message = 'Deprecated form of IPV6';
$this->rfcNumber = 5321;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class IPV6DoubleColon extends Warning
{
public const CODE = 73;
public function __construct()
{
$this->message = 'Double colon found after IPV6 tag';
$this->rfcNumber = 5322;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class IPV6GroupCount extends Warning
{
public const CODE = 72;
public function __construct()
{
$this->message = 'Group count is not IPV6 valid';
$this->rfcNumber = 5322;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class IPV6MaxGroups extends Warning
{
public const CODE = 75;
public function __construct()
{
$this->message = 'Reached the maximum number of IPV6 groups allowed';
$this->rfcNumber = 5321;
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Egulias\EmailValidator\Warning;
class LocalTooLong extends Warning
{
public const CODE = 64;
public const LOCAL_PART_LENGTH = 64;
public function __construct()
{
$this->message = 'Local part is too long, exceeds 64 chars (octets)';
$this->rfcNumber = 5322;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class NoDNSMXRecord extends Warning
{
public const CODE = 6;
public function __construct()
{
$this->message = 'No MX DSN record was found for this email';
$this->rfcNumber = 5321;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Egulias\EmailValidator\Warning;
class ObsoleteDTEXT extends Warning
{
public const CODE = 71;
public function __construct()
{
$this->rfcNumber = 5322;
$this->message = 'Obsolete DTEXT in domain literal';
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Egulias\EmailValidator\Warning;
use UnitEnum;
class QuotedPart extends Warning
{
public const CODE = 36;
/**
* @param UnitEnum|string|int|null $prevToken
* @param UnitEnum|string|int|null $postToken
*/
public function __construct($prevToken, $postToken)
{
if ($prevToken instanceof UnitEnum) {
$prevToken = $prevToken->name;
}
if ($postToken instanceof UnitEnum) {
$postToken = $postToken->name;
}
$this->message = "Deprecated Quoted String found between $prevToken and $postToken";
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Egulias\EmailValidator\Warning;
class QuotedString extends Warning
{
public const CODE = 11;
/**
* @param string|int $prevToken
* @param string|int $postToken
*/
public function __construct($prevToken, $postToken)
{
$this->message = "Quoted String found between $prevToken and $postToken";
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Egulias\EmailValidator\Warning;
class TLD extends Warning
{
public const CODE = 9;
public function __construct()
{
$this->message = "RFC5321, TLD";
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Egulias\EmailValidator\Warning;
abstract class Warning
{
/**
* @var int CODE
*/
public const CODE = 0;
/**
* @var string
*/
protected $message = '';
/**
* @var int
*/
protected $rfcNumber = 0;
/**
* @return string
*/
public function message()
{
return $this->message;
}
/**
* @return int
*/
public function code()
{
return self::CODE;
}
/**
* @return int
*/
public function RFCNumber()
{
return $this->rfcNumber;
}
/**
* @return string
*/
public function __toString(): string
{
return $this->message() . " rfc: " . $this->rfcNumber . "internal code: " . static::CODE;
}
}