Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
55c1378
feat(mailer): send Symfony Mailer email through PostHog with posthog+…
Silthus Nov 5, 2026
abab0f9
fix(mailer): match the adapter contract on inline parts, sender names…
Silthus Nov 5, 2026
5bb178b
fix(mailer): send inline parts under the reference the HTML uses
Silthus Nov 5, 2026
d631f1c
fix(mailer): refuse a body with parts the send can't carry
Silthus Nov 5, 2026
b5974e0
fix(mailer): match whole cid references and read stream bodies once
Silthus Nov 5, 2026
072de81
fix(mailer): check only a custom body, and its text and HTML
Silthus Nov 5, 2026
8416825
fix(mailer): stop mapping X-Track onto capture_engagement
Silthus Nov 9, 2026
2ee36ae
fix(mailer): refuse opens=false in any X-Track header, not just the f…
Silthus Nov 9, 2026
617f4bd
fix(mailer): read X-Track raw and refuse any opens value that isn't on
Silthus Nov 9, 2026
319217b
style(mailer): wrap the skip message phpcs flags as too long
Silthus Nov 9, 2026
911a1fd
Merge branch 'feat/emails-integration-option' into feat/symfony-maile…
Silthus Nov 13, 2026
3c0e364
test(mailer): send through a fake PostHog that takes files by hash
Silthus Nov 13, 2026
e3a7e06
feat(mailer): map X-Track to tracking and pass sensitive through
Silthus Nov 13, 2026
c3dd2b1
fix(mailer): declare RemoteTemplateSupport where Composer's classmap …
Silthus Nov 13, 2026
879370f
test(mailer): a failed attachment upload says whether to retry
Silthus Nov 13, 2026
3ef0a99
fix(mailer): refuse a Sender, Return-Path or body part the send can't…
Silthus Nov 13, 2026
21a75c7
fix(mailer): refuse a repeated option, an attachment's own headers, a…
Silthus Nov 13, 2026
e473b90
fix(mailer): send through the app's http_client
Silthus Nov 13, 2026
3678781
fix(mailer): refuse a bad integration when the transport is built
Silthus Nov 13, 2026
097fbf8
refactor(mailer): name the mapper's steps instead of commenting them
Silthus Nov 13, 2026
9dec817
Merge branch 'feat/emails-integration-option' into feat/symfony-maile…
Silthus Nov 13, 2026
f903c76
fix(mailer): retry a busy PostHog through the app's http_client
Silthus Nov 13, 2026
d16e840
fix(mailer): match the bridge on body part headers and content IDs
Silthus Nov 13, 2026
fe3456c
fix(mailer): match the bridge on Email-built HTML, unnamed files and …
Silthus Nov 13, 2026
4081bc8
fix(mailer): keep the app client's retries and credentials off PostHo…
Silthus Nov 13, 2026
f1166c5
refactor(mailer): read X-Track with unencodedValue and put readOnce's…
Silthus Nov 13, 2026
9c601d0
fix(mailer): clear the app client's defaults through its decorators
Silthus Nov 13, 2026
ec1bca3
fix(mailer): keep a matching scope's credentials and retries off Post…
Silthus Nov 13, 2026
a0f38ce
fix(mailer): refuse a Message whose conversion to an Email would chan…
Silthus Nov 13, 2026
d8ed3a5
fix(mailer): check a converted Message without reading its body again
Silthus Nov 13, 2026
1fe2975
fix(mailer): refuse a Message attachment that conversion would turn i…
Silthus Nov 13, 2026
d11b9d0
fix(mailer): stop reading attachments that would make the email too l…
Silthus Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/symfony-mailer-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"posthog-php": minor
---

Send Symfony Mailer email through PostHog: set `MAILER_DSN=posthog+api://<project secret key>@default` and register `PostHog\Mailer\PostHogTransportFactory`. Rendered emails, attachments, inline images, and the PostHog options (distinct ID, template key, idempotency key, engagement capture, sensitive) map to a transactional send, and an email PostHog cannot carry is refused before any request. Sends go through the app's `http_client` service when it has one. Symfony's `X-Track` header sets which opens and clicks PostHog tracks. On Symfony Mailer 8.2, a `RemoteTemplateEmail` sends a PostHog template. `EmailError::isRetryable()` is now public.
52 changes: 52 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,58 @@ jobs:
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
run: python3 -m unittest discover -s sdk_compliance_adapter -v

symfony-mailer:
needs: detect-markdown-only
if: needs.detect-markdown-only.outputs.markdown_only != 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
# The lock tests Symfony Mailer 7.4; these cover the oldest and newest supported lines.
- php-version: "8.2"
symfony: "6.4.*"
stability: stable
# The first 6.4 release: before 6.4.37, Symfony renames inline parts when it builds the body.
# Composer blocks it for security advisories, which don't apply to a test run.
- php-version: "8.2"
symfony: "6.4.0"
stability: stable
allow-insecure: true
# Symfony Mailer 8.2 adds remote templates; move to stable once 8.2.0 ships.
- php-version: "8.4"
symfony: "8.2.*"
stability: beta
name: symfony-mailer (${{ matrix.symfony }})
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # v2.40.0
with:
php-version: ${{ matrix.php-version }}
extensions: zlib
tools: composer

- name: Install Symfony ${{ matrix.symfony }}
env:
SYMFONY: ${{ matrix.symfony }}
STABILITY: ${{ matrix.stability }}
ALLOW_INSECURE: ${{ matrix.allow-insecure || 'false' }}
run: |
composer config minimum-stability "$STABILITY"
if [ "$ALLOW_INSECURE" = "true" ]; then composer config policy.advisories.block false; fi
composer config prefer-stable true
composer require --dev --no-update "symfony/mailer:$SYMFONY" "symfony/mime:$SYMFONY" "symfony/framework-bundle:$SYMFONY" "symfony/http-client:$SYMFONY"
composer update --with-all-dependencies --prefer-dist --no-progress

# Apps that deploy with an authoritative classmap load only the classes Composer found.
- name: Use an authoritative classmap
run: composer dump-autoload --classmap-authoritative

- name: Run the Symfony Mailer tests
run: ./vendor/bin/phpunit --bootstrap vendor/autoload.php --configuration phpunit.xml --no-coverage test/Mailer

coverage:
runs-on: ubuntu-latest
needs:
Expand Down
205 changes: 205 additions & 0 deletions api/public-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -1843,6 +1843,13 @@
"final": false,
"returnType": "?string",
"parameters": []
},
"isRetryable": {
"static": false,
"abstract": false,
"final": false,
"returnType": "bool",
"parameters": []
}
}
},
Expand Down Expand Up @@ -4155,6 +4162,204 @@
}
}
},
"PostHog\\Mailer\\PostHogTransport": {
"type": "class",
"abstract": false,
"final": true,
"readonly": false,
"extends": "Symfony\\Component\\Mailer\\Transport\\AbstractTransport",
"implements": [
"PostHog\\Internal\\Mailer\\RemoteTemplateSupport",
"Stringable",
"Symfony\\Component\\Mailer\\Transport\\TransportInterface"
],
"constants": {
"INTEGRATION": {
"type": "string",
"value": "posthog-php-mailer"
}
},
"properties": [],
"methods": {
"__construct": {
"static": false,
"abstract": false,
"final": false,
"returnType": null,
"parameters": [
{
"name": "emails",
"type": "PostHog\\Emails",
"byReference": false,
"variadic": false,
"optional": false,
"default": null,
"defaultConstant": null,
"hasDefault": false
},
{
"name": "integration",
"type": "?string",
"byReference": false,
"variadic": false,
"optional": true,
"default": null,
"defaultConstant": null,
"hasDefault": true
},
{
"name": "defaultTemplateKey",
"type": "?string",
"byReference": false,
"variadic": false,
"optional": true,
"default": null,
"defaultConstant": null,
"hasDefault": true
},
{
"name": "templates",
"type": "bool",
"byReference": false,
"variadic": false,
"optional": true,
"default": true,
"defaultConstant": null,
"hasDefault": true
},
{
"name": "dispatcher",
"type": "?Psr\\EventDispatcher\\EventDispatcherInterface",
"byReference": false,
"variadic": false,
"optional": true,
"default": null,
"defaultConstant": null,
"hasDefault": true
},
{
"name": "logger",
"type": "?Psr\\Log\\LoggerInterface",
"byReference": false,
"variadic": false,
"optional": true,
"default": null,
"defaultConstant": null,
"hasDefault": true
}
]
},
"__toString": {
"static": false,
"abstract": false,
"final": false,
"returnType": "string",
"parameters": []
}
}
},
"PostHog\\Mailer\\PostHogTransportException": {
"type": "class",
"abstract": false,
"final": true,
"readonly": false,
"extends": "Symfony\\Component\\Mailer\\Exception\\TransportException",
"implements": [
"Stringable",
"Symfony\\Component\\Mailer\\Exception\\ExceptionInterface",
"Symfony\\Component\\Mailer\\Exception\\TransportExceptionInterface",
"Throwable"
],
"constants": [],
"properties": [],
"methods": {
"__construct": {
"static": false,
"abstract": false,
"final": false,
"returnType": null,
"parameters": [
{
"name": "error",
"type": "PostHog\\EmailError",
"byReference": false,
"variadic": false,
"optional": false,
"default": null,
"defaultConstant": null,
"hasDefault": false
}
]
},
"getErrorCode": {
"static": false,
"abstract": false,
"final": false,
"returnType": "string",
"parameters": []
},
"getField": {
"static": false,
"abstract": false,
"final": false,
"returnType": "?string",
"parameters": []
},
"getRetryAfter": {
"static": false,
"abstract": false,
"final": false,
"returnType": "?int",
"parameters": []
},
"getStatusCode": {
"static": false,
"abstract": false,
"final": false,
"returnType": "?int",
"parameters": []
},
"isRetryable": {
"static": false,
"abstract": false,
"final": false,
"returnType": "bool",
"parameters": []
}
}
},
"PostHog\\Mailer\\PostHogTransportFactory": {
"type": "class",
"abstract": false,
"final": true,
"readonly": false,
"extends": "Symfony\\Component\\Mailer\\Transport\\AbstractTransportFactory",
"implements": [
"Symfony\\Component\\Mailer\\Transport\\TransportFactoryInterface"
],
"constants": [],
"properties": [],
"methods": {
"create": {
"static": false,
"abstract": false,
"final": false,
"returnType": "Symfony\\Component\\Mailer\\Transport\\TransportInterface",
"parameters": [
{
"name": "dsn",
"type": "Symfony\\Component\\Mailer\\Transport\\Dsn",
"byReference": false,
"variadic": false,
"optional": false,
"default": null,
"defaultConstant": null,
"hasDefault": false
}
]
}
}
},
"PostHog\\OpenTelemetry\\ContextSpanProcessor": {
"type": "class",
"abstract": false,
Expand Down
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@
"squizlabs/php_codesniffer": "^4.0",
"ext-zlib": "*",
"open-telemetry/api": "^1.0",
"open-telemetry/sdk": "^1.0"
"open-telemetry/sdk": "^1.0",
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
"symfony/http-client": "^6.4|^7.0|^8.0",
"symfony/mailer": "^6.4|^7.0|^8.0"
},
"suggest": {
"symfony/mailer": "Send Symfony Mailer email through PostHog with the posthog+api:// transport."
},
"autoload": {
"psr-4": {
Expand Down
Loading
Loading