Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 22 additions & 13 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,44 @@ workflows:
# gives a fork PR any signal at all. Everything below is an integration suite
# against the live API and cannot start without the tokens above.
- static-analysis:
name: static
- unit-tests:
name: php81
version: "8.1"
name: static-guzzle8
guzzle: "^8.0.1"
# The constraint allows two Guzzle majors, so both have to be exercised or
# the compat claim is just an assertion. Guzzle 8 reclassified transport
# exceptions, which is the surface this SDK re-exports.
- static-analysis:
name: static-guzzle7
guzzle: "^7.15.2"
- unit-tests:
name: php82
version: "8.2"
requires:
- php81
- unit-tests:
name: php83
version: "8.3"
requires:
- php82
- unit-tests:
name: php84
version: "8.4"
requires:
- php83
- unit-tests:
name: php85
version: "8.5"

jobs:
static-analysis:
parameters:
guzzle:
description: "Guzzle constraint to resolve against"
type: string
default: "^8.0.1"
docker:
- image: cimg/php:8.1
- image: cimg/php:8.2
steps:
- checkout
- run:
name: Install dependencies
command: composer install --no-interaction
name: Install dependencies (Guzzle << parameters.guzzle >>)
command: |
composer require --no-update --no-interaction "guzzlehttp/guzzle:<< parameters.guzzle >>"
composer update --no-interaction --with-all-dependencies
composer show guzzlehttp/guzzle | grep '^versions'
- run:
name: PHPStan
command: vendor/bin/phpstan analyse --memory-limit=1G --no-progress
Expand Down
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,60 @@ you were catching the `TypeError` from any of the getters above as a workaround,
- CI gained a credential-free `static-analysis` job running PHPStan, which is the only check in
this repo a fork PR can currently exercise.

## [Unreleased] — v8.0.0 (breaking)

### Removed
- **Dropped support for PHP 8.1** (EOL 2025-12-31). `composer.json` now requires `^8.2`.
Projects on 8.1 stay on v7.x — Composer will not offer them this release.
Note the previous `~8.1 || ~8.2 || ~8.3 || ~8.4` already resolved to `>=8.1 <9.0`, so 8.5 was
always permitted; dropping 8.1 is the only real constraint change.

### Changed
- **BREAKING** — `PostmarkAttachment::fromRawData()`, `::fromBase64EncodedData()` and `::fromFile()`
now declare `string` for their first two parameters and a `PostmarkAttachment` return type.
Passing `null`, an array, or a non-Stringable object now raises a `TypeError`; previously it
silently produced an empty attachment. `int` and `Stringable` still coerce, except under
`declare(strict_types=1)`. **Subclasses overriding these factories must add the
`: PostmarkAttachment` return type or PHP will fatal at class-load.**
- **BREAKING** — `PostmarkAttachment::fromFile()` now throws `RuntimeException` when the file
cannot be read, instead of sending an attachment with empty content.

### Added
- PHP 8.5 to the CI matrix.
- **Guzzle 8 is now supported** alongside Guzzle 7 (`^7.15.2 || ^8.0.1`), thanks to
[@simPod](https://github.com/simPod) (#165). Both majors are exercised in CI rather than
assumed compatible. The floors are deliberate: Guzzle 8.0.0 and 7.x below 7.15.2 carry
[GHSA-v5mv-p594-2x33](https://github.com/advisories/GHSA-v5mv-p594-2x33) (high, host-check
bypass) and [GHSA-f7vp-7xgx-4w4r](https://github.com/advisories/GHSA-f7vp-7xgx-4w4r).

**Read this if you catch Guzzle exceptions.** Composer resolves the highest satisfying
version, so upgrading puts you on Guzzle 8 unless you pin otherwise — this is not opt-in.
Guzzle 8 reclassified transport exceptions, and because this SDK sets `http_errors => false`
and maps responses to `PostmarkException` itself, the transport family is the *only* Guzzle
family that reaches your code. Most notably a plain timeout is no longer a `ConnectException`:

| cURL condition | Guzzle 7 | Guzzle 8 |
| --- | --- | --- |
| timeout, connect phase | `ConnectException` | `ConnectTimeoutException` (extends `ConnectException`) |
| timeout, no response | `ConnectException` | **`NetworkTimeoutException`** |
| timeout, body stalled | `ConnectException` | **`ResponseTimeoutException`** |
| send/recv error | `RequestException` | **`NetworkException`** |

Everything still implements `GuzzleException`, so the SDK's documented contract is unchanged —
but `catch (ConnectException $e)` around a send will silently stop matching a timeout.

### Fixed
- **`getDeliveryStatistics()` reported `Count = 0` for every bounce category, in every released
version.** `PostmarkBounceSummary` read the `FirstOpen` key instead of `Count` — a copy-paste
from `PostmarkOpen`. Any dashboard calibrated against the broken zero will start seeing real
numbers.
- `PostmarkBounce` assigned its constructor fallbacks to the wrong properties (`Type` got `0`,
`TypeCode` got `''`), throwing `TypeError` on a response missing either field.
- List models no longer emit `Undefined array key` / `foreach() argument must be of type
array|object` warnings when the API response omits the collection key. These were fatal under
application error handlers that promote warnings to exceptions (Laravel, Symfony).


## [v7.0.0](https://github.com/ActiveCampaign/postmark-php/tree/v7.0.0)

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ With Postmark, you can send and _receive_ emails effortlessly.

## Requirements

- PHP 8.1, 8.2, 8.3, or 8.4
- PHP 8.2, 8.3, 8.4, or 8.5
- Guzzle HTTP client

## Getting Started
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"license": "MIT",
"description": "The officially supported client for Postmark (https://postmarkapp.com)",
"require": {
"php": "~8.1 || ~8.2|| ~8.3 || ~8.4",
"guzzlehttp/guzzle": "^7.8"
"php": "^8.2",
"guzzlehttp/guzzle": "^7.15.2 || ^8.0.1"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempMessageStreams = [];
foreach ($values['MessageStreams'] as $open) {
foreach ($values['MessageStreams'] ?? [] as $open) {
$obj = json_decode(json_encode($open));
$postmarkMessageStreams = new PostmarkMessageStream((array) $obj);

Expand Down
20 changes: 16 additions & 4 deletions src/Postmark/Models/PostmarkAttachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,31 @@ private function __construct($base64EncodedData, $attachmentName, $mimeType = 'a
$this->contentId = $contentId;
}

public static function fromRawData($data, $attachmentName, ?string $mimeType = null, ?string $contentId = null)
public static function fromRawData(string $data, string $attachmentName, ?string $mimeType = null, ?string $contentId = null): PostmarkAttachment
{
return new PostmarkAttachment(base64_encode($data), $attachmentName, $mimeType, $contentId);
}

public static function fromBase64EncodedData($base64EncodedData, $attachmentName, ?string $mimeType = null, ?string $contentId = null)
public static function fromBase64EncodedData(string $base64EncodedData, string $attachmentName, ?string $mimeType = null, ?string $contentId = null): PostmarkAttachment
{
return new PostmarkAttachment($base64EncodedData, $attachmentName, $mimeType, $contentId);
}

public static function fromFile($filePath, $attachmentName, ?string $mimeType = null, ?string $contentId = null)
/**
* @throws \RuntimeException if the file cannot be read
*/
public static function fromFile(string $filePath, string $attachmentName, ?string $mimeType = null, ?string $contentId = null): PostmarkAttachment
{
return new PostmarkAttachment(base64_encode(file_get_contents($filePath)), $attachmentName, $mimeType, $contentId);
// file_get_contents() returns false on failure and base64_encode(false) is "",
// so an unreadable path previously produced a silently empty attachment that
// still went out with the message.
$contents = @file_get_contents($filePath);

if (false === $contents) {
throw new \RuntimeException(sprintf('Unable to read attachment file "%s".', $filePath));
}

return new PostmarkAttachment(base64_encode($contents), $attachmentName, $mimeType, $contentId);
}

#[ReturnTypeWillChange]
Expand Down
4 changes: 2 additions & 2 deletions src/Postmark/Models/PostmarkBounce.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ public function __construct(array $values)
{
$this->RecordType = !empty($values['RecordType']) ? $values['RecordType'] : '';
$this->ID = !empty($values['ID']) ? $values['ID'] : 0;
$this->Type = !empty($values['Type']) ? $values['Type'] : 0;
$this->TypeCode = !empty($values['TypeCode']) ? $values['TypeCode'] : '';
$this->Type = !empty($values['Type']) ? $values['Type'] : '';
$this->TypeCode = !empty($values['TypeCode']) ? $values['TypeCode'] : 0;
$this->Name = !empty($values['Name']) ? $values['Name'] : '';
$this->Tag = !empty($values['Tag']) ? $values['Tag'] : '';
$this->MessageID = !empty($values['MessageID']) ? $values['MessageID'] : '';
Expand Down
3 changes: 2 additions & 1 deletion src/Postmark/Models/PostmarkBounceList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempBounce = [];
foreach ($values['Bounces'] as $bounce) {
$bounces = $values['Bounces'] ?? [];
foreach ($bounces as $bounce) {
$obj = json_decode(json_encode($bounce));
$postmarkBounce = new PostmarkBounce((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkBounceSummary.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public function __construct(array $values)
{
$this->Type = !empty($values['Type']) ? $values['Type'] : '';
$this->Name = !empty($values['Name']) ? $values['Name'] : '';
$this->Count = !empty($values['FirstOpen']) ? $values['FirstOpen'] : 0;
$this->Count = !empty($values['Count']) ? $values['Count'] : 0;
}

public function getType(): string
Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkClickList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempClicks = [];
foreach ($values['Clicks'] as $click) {
foreach ($values['Clicks'] ?? [] as $click) {
$obj = json_decode(json_encode($click));
$postmarkClick = new PostmarkClick((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkDeliveryStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->InactiveMails = !empty($values['InactiveMails']) ? $values['InactiveMails'] : 0;
$tempBounces = [];
foreach ($values['Bounces'] as $bounce) {
foreach ($values['Bounces'] ?? [] as $bounce) {
$obj = json_decode(json_encode($bounce));
$postmarkBounce = new PostmarkBounceSummary((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkDomainList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempDomains = [];
foreach ($values['Domains'] as $domain) {
foreach ($values['Domains'] ?? [] as $domain) {
$obj = json_decode(json_encode($domain));
$postmarkDomain = new PostmarkDomain((array) $obj);

Expand Down
3 changes: 2 additions & 1 deletion src/Postmark/Models/PostmarkInboundMessageList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempInboundMessages = [];
foreach ($values['InboundMessages'] as $message) {
$inboundMessages = $values['InboundMessages'] ?? [];
foreach ($inboundMessages as $message) {
$obj = json_decode(json_encode($message));
$postmarkMessage = new PostmarkInboundMessage((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkInboundRuleTriggerList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempRules = [];
foreach ($values['InboundRules'] as $rule) {
foreach ($values['InboundRules'] ?? [] as $rule) {
$obj = json_decode(json_encode($rule));
$postmarkServer = new PostmarkInboundRuleTrigger((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkOpenList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempOpens = [];
foreach ($values['Opens'] as $open) {
foreach ($values['Opens'] ?? [] as $open) {
$obj = json_decode(json_encode($open));
$postmarkOpen = new PostmarkOpen((array) $obj);

Expand Down
3 changes: 2 additions & 1 deletion src/Postmark/Models/PostmarkOutboundMessageList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempMessages = [];
foreach ($values['Messages'] as $message) {
$messages = $values['Messages'] ?? [];
foreach ($messages as $message) {
$obj = json_decode(json_encode($message));
$postmarkMessage = new PostmarkOutboundMessage((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkSenderSignatureList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempSigs = [];
foreach ($values['SenderSignatures'] as $open) {
foreach ($values['SenderSignatures'] ?? [] as $open) {
$obj = json_decode(json_encode($open));
$postmarkSenderSig = new PostmarkSenderSignature((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkServerList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempServers = [];
foreach ($values['Servers'] as $server) {
foreach ($values['Servers'] ?? [] as $server) {
$obj = json_decode(json_encode($server));
$postmarkServer = new PostmarkServer((array) $obj);

Expand Down
2 changes: 1 addition & 1 deletion src/Postmark/Models/PostmarkTemplateList.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function __construct(array $values)
{
$this->TotalCount = !empty($values['TotalCount']) ? $values['TotalCount'] : 0;
$tempTemplates = [];
foreach ($values['Templates'] as $template) {
foreach ($values['Templates'] ?? [] as $template) {
$obj = json_decode(json_encode($template));
$postmarkTemplate = new PostmarkTemplate((array) $obj);

Expand Down
3 changes: 2 additions & 1 deletion src/Postmark/Models/Suppressions/PostmarkSuppressionList.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class PostmarkSuppressionList
public function __construct(array $values)
{
$tempSuppressions = [];
foreach ($values['Suppressions'] as $sups) {
$suppressions = $values['Suppressions'] ?? [];
foreach ($suppressions as $sups) {
$obj = json_decode(json_encode($sups));
$postmarkSup = new PostmarkSuppression((array) $obj);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class PostmarkSuppressionResultList
public function __construct(array $values)
{
$tempSuppressions = [];
foreach ($values['Suppressions'] as $sups) {
$suppressions = $values['Suppressions'] ?? [];
foreach ($suppressions as $sups) {
$obj = json_decode(json_encode($sups));
$postmarkSup = new PostmarkSuppressionRequestResult((array) $obj);

Expand Down
12 changes: 4 additions & 8 deletions tests/PostmarkAdminClientSenderSignatureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ public function testClientCanCreateSignature()
$tk = parent::$testKeys;
$client = new PostmarkAdminClient($tk->WRITE_ACCOUNT_TOKEN, $tk->TEST_TIMEOUT);

$i = $tk->WRITE_TEST_SENDER_SIGNATURE_PROTOTYPE;
$sender = str_replace('[TOKEN]', 'test-php-create' . date('U'), $i);
$sender = $this->uniqueSenderAddress('test-php-create');
$name = 'test-php-create-' . date('U');
$note = 'This is a test note';

Expand All @@ -74,8 +73,7 @@ public function testClientCanEditSignature()

$name = 'test-php-edit-' . date('U');

$i = $tk->WRITE_TEST_SENDER_SIGNATURE_PROTOTYPE;
$sender = str_replace('[TOKEN]', 'test-php-edit' . date('U'), $i);
$sender = $this->uniqueSenderAddress('test-php-edit');

$exploded = explode('@', $tk->WRITE_TEST_SENDER_SIGNATURE_PROTOTYPE);
$returnPath = 'test.' . $exploded[1];
Expand All @@ -98,8 +96,7 @@ public function testClientCanDeleteSignature()
$tk = parent::$testKeys;
$client = new PostmarkAdminClient($tk->WRITE_ACCOUNT_TOKEN, $tk->TEST_TIMEOUT);

$i = $tk->WRITE_TEST_SENDER_SIGNATURE_PROTOTYPE;
$sender = str_replace('[TOKEN]', 'test-php-delete' . date('U'), $i);
$sender = $this->uniqueSenderAddress('test-php-delete');

$name = 'test-php-delete-' . date('U');
$sig = $client->createSenderSignature($sender, $name);
Expand All @@ -118,8 +115,7 @@ public function testClientCanRequestNewVerificationForSignature()
$tk = parent::$testKeys;
$client = new PostmarkAdminClient($tk->WRITE_ACCOUNT_TOKEN, $tk->TEST_TIMEOUT);

$i = $tk->WRITE_TEST_SENDER_SIGNATURE_PROTOTYPE;
$sender = str_replace('[TOKEN]', 'test-php-reverify' . date('U'), $i);
$sender = $this->uniqueSenderAddress('test-php-reverify');

$name = 'test-php-reverify-' . date('U');
$sig = $client->createSenderSignature($sender, $name);
Expand Down
Loading