Marking mail
A mail carries its marking in its header section, where a signature can cover it.
Pushery\AiContentMarking\Mail\MailMarking writes the field, and MailMarkingReader reads it back
from the raw text of a mail, the form in which a mail is sent and stored.
The field
Digital-Source-Type: http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia;
generator="Amazon Bedrock"; generated-at="2026-10-15T07:30:00+00:00"
The value is the term's URI in the IPTC vocabulary, the same term the package writes into HTML and into images. The generator and the moment follow as parameters where the marking has them.
Every byte of the field is printable ASCII. A generator in other characters is written as an RFC 2231 parameter in UTF-8, and a long one in numbered sections, so a line break in a generator can never start a field of its own.
No standard names a header field for content made with generative AI, so this field is the package's own. Mail software that does not know it passes it on unread, as it does with every field it does not know.
Adding it to a mail
A mailable returns the field from headers():
use Illuminate\Mail\Mailables\Headers;
use Pushery\AiContentMarking\Mail\MailMarking;
public function headers(): Headers
{
return new Headers(text: MailMarking::headers($this->reply->marking));
}
A notification, or any mail built in a callback, adds it to the Symfony message:
use Illuminate\Notifications\Messages\MailMessage;
use Pushery\AiContentMarking\Mail\MailMarking;
use Symfony\Component\Mime\Email;
return (new MailMessage)
->subject('Your answer')
->line($this->reply->text)
->withSymfonyMessage(function (Email $message): void {
$message->getHeaders()->addTextHeader(MailMarking::HEADER, MailMarking::value($this->reply->marking));
});
MailMarking::value() refuses a generator that is not UTF-8 text with an
InvalidArgumentException, because a field written from it could not be read back.
Under a DKIM signature
A DKIM signature covers the fields its h= tag lists. A receiver whose verification passes knows
those fields arrived as they were signed, and a signature that carries a t= tag says when it was
made. Which fields a signature lists is the signer's choice:
- Symfony's
DkimSignersigns every field of the message exceptReturn-PathandX-Transport, and any you name in itsheaders_to_ignoreoption. A marking added before the message is signed is covered. - A provider that signs on its side chooses its own list. Read the
h=tag of a mail it delivered, or askMailMarkingReader, rather than assuming either way.
Amazon SES, for one, asks that a signature you add yourself does not cover Message-ID, Date,
Return-Path and Bounces-To, because it may change them on the way out. See
manual DKIM signing in Amazon SES.
Reading a mail
use Pushery\AiContentMarking\Mail\MailMarkingReader;
$reader = new MailMarkingReader;
$reader->read($rawMail); // every Marking, in the order of the fields
$reader->marksGenerativeAi($rawMail); // whether one says generative AI created or edited the content
$reader->signingDomains($rawMail); // ['example.com']: the DKIM signatures whose h= tag lists the field
The reader takes the mail as RFC 5322 text, header section and body, and reads the header section alone: a quoted mail or an attachment in the body is never taken for a marking of the mail itself. Folded fields are joined, the field name counts in any case, and the parameters are read in each form MIME allows. A value that is no term of the vocabulary is no marking. A moment that is no ISO 8601 date-time is left out, and the term and the generator still stand.
signingDomains() reports the signatures that claim to cover the field and verifies none of them.
Verification takes the signer's public key from DNS and the mail exactly as it was signed, so it
belongs to your mail infrastructure wherever the answer has to hold.
In your tests
Send through the array mailer and read the mail it holds:
use Illuminate\Support\Facades\Mail;
use Pushery\AiContentMarking\Mail\MailMarkingReader;
Mail::mailer('array')->to('reader@example.com')->send(new AnswerMail($reply));
$sent = Mail::mailer('array')->getSymfonyTransport()->messages()->first()->toString();
expect(app(MailMarkingReader::class)->marksGenerativeAi($sent))->toBeTrue();
Send a mail without generated content the same way and expect read() to return an empty list.
That control proves the first assertion could fail.