New
- Inbound mail. A plugin that wants to read mail sent to a site — a helpdesk turning replies into ticket updates, a forum taking posts by email — now has one place to ask.
Providers\Inbound\InboundGateway::receive()takes a receiver key, the request and that receiver's config, and runs the same four steps for every provider in the same order: find the receiver (404), check the size against the receiver's limit and the caller's (413, before any verification work), verify (401), and parse, which never throws. What comes back is anInboundResultholding the verdict, the HTTP status to answer with, and the messages. Each message is anInboundMessagewith every field a consumer needs already normalised: message ids without brackets and lower-cased, the envelope recipient kept apart from the visibleToso asupport+token@address survives, the receiving server's SPF, DKIM and DMARC verdicts read fromAuthentication-Results, and attachments with their inlinecid:ids. AskEmail::supportsFeature('inbound')first; it is true on PHP 8.1 and later, like the rest of the provider contract - A provider plugin receives mail by implementing
InboundCapableon the provider class it already registers. It is a separate interface rather than a new method onProvider, because every transport plugin implementsProvidertoday and a new method there would be a fatal error in each one not updated in the same release. The rules are the delivery-report rules — authenticate before acting, over the raw bytes wherever the scheme signs raw bytes;parse()never throws and does no network I/O — plusfetch()for providers that send only metadata, which a consumer runs later from its own worker. Written up indocs/providers.md, which also refines the delivery-report rule to the same wording, since SNS and Mailgun sign inside the payload rather than over it - Two receivers that need no provider account at all.
cloudflaretakes each message from a free Cloudflare Email Routing Worker, byte for byte, signed end to end with an HMAC over the raw body, a timestamp and a 300-second window.genericis the same scheme for a Postfix pipe, a forwarder or a cron job.docs/inbound-cloudflare.mdhas the Worker source, the dashboard steps (including the Subaddressing setting that plus addresses need), Cloudflare's size and CPU limits, and a shell and a PHP sender, all checked against the real verifier - An IMAP client that does not need ext/imap, which left PHP core in 8.4 and was never on most hosts anyway.
Inbound\Imap\ImapMailboxconnects over implicit TLS or STARTTLS (never falling back to plain text), logs in, finds new mail by UID withUIDVALIDITYtracking, downloads it without marking it read, and marks it\Seenand moves it once the caller has stored it — withUID MOVEwhere the server has it and copy, delete and expunge where it does not. Messages over a size limit are reported without being downloaded, and every failure says whether it was the credentials, the network or the server. For Gmail with an app password and most hosting mailboxes;AUTHENTICATE XOAUTH2has its place reserved for later - A MIME parser with no dependencies. Every maintained MIME library for PHP brings packages Grav core also ships at its own version (
guzzlehttp/psr7,pimple/pimple,psr/container) or a dependency-injection container, and Grav loads every plugin's autoloader into one process.Providers\Inbound\MimeParserreads what received mail needs — encoded-word headers, nested multiparts, quoted-printable and base64, charsets (ISO-8859-1 read as Windows-1252, the way mail clients do), RFC 2231 filenames, inline images, forwarded messages kept whole as attachments, delivery reports — and never throws. Tested against Gmail, Outlook (with awinmail.dat) and Apple Mail replies, Latin-1 and Windows-1252 mail, a bounce, an out-of-office and a forward