Skip to content

Support RawMessage instead of Email for Mail/Mime assertions#232

Open
krrico wants to merge 2 commits into
Codeception:mainfrom
krrico:feature/support_encrypted_emails
Open

Support RawMessage instead of Email for Mail/Mime assertions#232
krrico wants to merge 2 commits into
Codeception:mainfrom
krrico:feature/support_encrypted_emails

Conversation

@krrico
Copy link
Copy Markdown

@krrico krrico commented May 22, 2026

Adjusted type hints of Mailer and Mime assertions traits to support RawMessage and Message instead of Email only in order to also support fetching sent Emails when they were encrypted or signed before which turns them into plain Message objects rather than Email objects (which is a child class of Message).

@krrico
Copy link
Copy Markdown
Author

krrico commented May 22, 2026

Just added the Test PR here:
Codeception/symfony-module-tests#40

* ```
*/
public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Email $email = null): void
public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Message $email = null): void
Copy link
Copy Markdown
Author

@krrico krrico May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether it's better to use RawMessage as type hint everywhere and let the Symfony Mime Constraints used here run into the RuntimeException when they don't support RawMessage or Message.

@TavoNiievez TavoNiievez force-pushed the feature/support_encrypted_emails branch from 746d68d to f89c7ce Compare June 5, 2026 17:06
@TavoNiievez TavoNiievez force-pushed the feature/support_encrypted_emails branch from f89c7ce to e8ab243 Compare June 5, 2026 17:13
@TavoNiievez
Copy link
Copy Markdown
Member

Hey @krrico , thanks for the thorough bug report and the PR — you identified the root cause precisely and your proposed fix was on the right track. 🙌

I went ahead and implemented a refined version of your approach in e8ab243. Here's a summary of the decisions made and why.

The core issue you correctly identified

When Symfony's SMimeEncrypter (or SMimeSigner) wraps an Email before sending, the result stored in the MessageLoggerListener is a plain Message — not an Email. The original grabLastSentEmail() returned ?Email, causing a type error, and all MimeAssertionsTrait methods expected ?Email, making them unusable in that scenario.

Why we didn't use RawMessage everywhere

Your PR took the straightforward approach of widening every type hint to RawMessage (mirroring what Symfony's FrameworkBundle MailerAssertionsTrait does). That works, but it pushes errors further down the stack: if a RawMessage (the bare base class) is passed to a Mime constraint that requires at least a Message, Symfony throws a LogicException at runtime — without a helpful message.

I chose to use the semantically correct minimum type for each assertion instead, which makes type errors explicit at the PHP level and produces better error messages:

Assertion Parameter type Why
assertEmailHasHeader ?Message EmailHasHeader constraint requires Message (rejects RawMessage)
assertEmailAddressContains ?Message EmailAddressContains constraint requires Message
assertEmailHeaderSame ?Message EmailHeaderSame constraint requires Message
assertEmailHeaderNotSame ?Message EmailHeaderSame constraint requires Message
assertEmailNotHasHeader ?Message EmailHasHeader constraint requires Message
assertEmailAttachmentCount ?Email EmailAttachmentCount constraint requires Email specifically
assertEmailHtmlBodyContains ?Email EmailHtmlBodyContains constraint requires Email specifically
assertEmailHtmlBodyNotContains ?Email EmailHtmlBodyContains constraint requires Email specifically
assertEmailTextBodyContains ?Email EmailTextBodyContains constraint requires Email specifically
assertEmailTextBodyNotContains ?Email EmailTextBodyContains constraint requires Email specifically

The distinction maps directly to what Symfony's own Mime constraints check at runtime: header-based assertions only need a Message (which carries headers), while body and attachment assertions need a full Email (which adds body and part accessors). Since encrypted/signed messages are still Message instances — they have headers but no plain-text body — the header-based assertions will work on them out of the box.

The internal method: verifyEmailObjectgetMessageOrFail

Your PR widened verifyEmailObject to (?RawMessage): RawMessage to match the broadest possible type. I renamed it to getMessageOrFail and gave it a tighter contract:

private function getMessageOrFail(?Message $message, string $function): Message
  • Accepts ?Message — so it is compatible with all public callers.
  • Returns Message — the minimum type all Mime constraints accept.
  • Fails explicitly on bare RawMessage — if neither a message is provided nor one was sent, or if what was sent is a plain RawMessage with no headers, it fails the test with a clear message rather than letting Symfony's constraint throw a LogicException deep inside the assertion.

What grabLastSentEmail() returns now

grabLastSentEmail() now returns ?RawMessage (via an internal grabLastSentRawMessage() helper), matching the raw output of MessageLoggerListener. This means it covers both Email and Message objects. If you need to type-narrow the result yourself, instanceof is the right tool:

$message = $I->grabLastSentEmail(); // ?RawMessage — works for Email and Message
if ($message instanceof Email) {
    // access Email-specific methods
}

Tests added

I also added tests that cover your exact use case — sending a plain Message through the mailer and asserting on its headers:

// In MimeAssertionsTest
public function testHeaderAssertionsWorkWithSentMessage(): void
{
    // /send-message dispatches a Message (not Email) through the mailer
    $this->client->request('GET', '/send-message');

    $this->assertEmailHasHeader('To');
    $this->assertEmailAddressContains('To', 'jane_doe@example.com');
    $this->assertEmailHeaderSame('Subject', 'Test message');
}

One thing I'd like to ask of you

Could you update your PR in the testing repository (Codeception/symfony-module-tests#40) to match the simplified approach I've taken here?

Your current commit (71d207e) uses actual S/MIME encryption — a Crypto helper that generates real OpenSSL certificates at runtime, an ext-openssl dev dependency, and a SendEncryptedEmailController that calls SMimeEncrypter::encrypt(). That's a faithful reproduction of the real-world scenario, but it introduces a non-trivial infrastructure burden on the test suite (an OpenSSL extension requirement, ephemeral certificate files, etc.).

The key insight is that for testing the module's assertion methods, we don't actually need to encrypt anything. What matters is that the mailer receives a Message object instead of an Email. The simplest way to achieve that is to just construct and send a Message directly — no encryption, no certificates, no OpenSSL.

The simplified version I've added here does exactly that:

// tests/_app/Mailer/MessageMailer.php
public function send(string $recipient): void
{
    $headers = new Headers();
    $headers->addMailboxListHeader('From', ['no-reply@example.com']);
    $headers->addMailboxListHeader('To', [$recipient]);
    $headers->addTextHeader('Subject', 'Test message');

    $this->mailer->send(new Message($headers, new TextPart('Message body content')));
}

So in the testing repo, the ideal update would be to:

  • Replace the SendEncryptedEmailController / Crypto / SMimeEncrypter setup with a simple controller that sends a plain Message
  • Remove the ext-openssl dev dependency from composer.json
  • Update IssuesCest to assert against this simpler /send-message endpoint

Thanks again for the detailed issue and the groundwork in the PR!

@TavoNiievez TavoNiievez linked an issue Jun 5, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mailer and Mime assertion trait cannot handle encrypted emails

2 participants