├── .github └── workflows │ └── tests.yml ├── .gitignore ├── Dockerfile ├── README.md ├── composer.json ├── ecs.php ├── phpstan.neon ├── phpunit.xml ├── src ├── ApiException.php ├── Client.php ├── HttpClientFactory.php ├── Messages │ ├── Delivery.php │ └── Message.php ├── MessagesService.php ├── RetryMiddlewareFactory.php ├── Send │ ├── Message.php │ ├── RawMessage.php │ └── Result.php └── SendService.php └── tests ├── ClientTest.php ├── MessagesServiceTest.php ├── Send └── MessageTest.php └── SendServiceTest.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | php: [7.4, "8.0", 8.1, 8.2, 8.3] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: test on PHP ${{ matrix.php }} 17 | run: docker build . --build-arg PHP_VERSION=${{ matrix.php }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .DS_Store 3 | test.php 4 | composer.lock 5 | *.cache 6 | coverage 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION=8.3 2 | FROM php:$PHP_VERSION-cli-alpine 3 | 4 | RUN apk add git zip unzip autoconf make g++ icu-dev 5 | 6 | RUN docker-php-ext-configure intl \ 7 | && docker-php-ext-install -j $(nproc) intl 8 | 9 | RUN curl -sS https://getcomposer.org/installer | php \ 10 | && mv composer.phar /usr/local/bin/composer 11 | 12 | RUN adduser -S php 13 | 14 | WORKDIR /package 15 | 16 | RUN chown php /package 17 | 18 | USER php 19 | 20 | COPY composer.json ./ 21 | 22 | RUN composer install 23 | 24 | COPY src src 25 | COPY tests tests 26 | COPY ecs.php phpunit.xml phpstan.neon ./ 27 | 28 | RUN composer test 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Postal for PHP 2 | 3 | This library helps you send e-mails through [Postal](https://github.com/postalserver/postal) in PHP 7.4 and above. 4 | 5 | ## Installation 6 | 7 | Install the library using [Composer](https://getcomposer.org/): 8 | 9 | ``` 10 | $ composer require postal/postal 11 | ``` 12 | 13 | ## Usage 14 | 15 | Sending an email is very simple. Just follow the example below. Before you can begin, you'll 16 | need to login to our web interface and generate a new API credential. 17 | 18 | ```php 19 | // Create a new Postal client using the server key you generate in the web interface 20 | $client = new Postal\Client('https://postal.yourdomain.com', 'your-api-key'); 21 | 22 | // Create a new message 23 | $message = new Postal\Send\Message(); 24 | 25 | // Add some recipients 26 | $message->to('john@example.com'); 27 | $message->to('mary@example.com'); 28 | $message->cc('mike@example.com'); 29 | $message->bcc('secret@awesomeapp.com'); 30 | 31 | // Specify who the message should be from. This must be from a verified domain 32 | // on your mail server. 33 | $message->from('test@test.postal.io'); 34 | 35 | // Set the subject 36 | $message->subject('Hi there!'); 37 | 38 | // Set the content for the e-mail 39 | $message->plainBody('Hello world!'); 40 | $message->htmlBody('

Hello world!

'); 41 | 42 | // Add any custom headers 43 | $message->header('X-PHP-Test', 'value'); 44 | 45 | // Attach any files 46 | $message->attach('textmessage.txt', 'text/plain', 'Hello world!'); 47 | 48 | // Send the message and get the result 49 | $result = $client->send->message($message); 50 | 51 | // Loop through each of the recipients to get the message ID 52 | foreach ($result->recipients() as $email => $message) { 53 | $email; // The e-mail address of the recipient 54 | $message->id; // The message ID 55 | $message->token; // The message's token 56 | } 57 | ``` 58 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postal/postal", 3 | "description": "Postal for PHP library.", 4 | "keywords": ["postal", "mail"], 5 | "license": "MIT", 6 | "homepage":"https://github.com/atech/postal", 7 | "authors": [ 8 | { 9 | "name": "Adam Cooke", 10 | "email": "me@adamcooke.io" 11 | }, 12 | { 13 | "name": "Josh Grant", 14 | "email": "josh@grantj.io" 15 | }, 16 | { 17 | "name": "William Hall", 18 | "email": "william.hall@synergitech.co.uk" 19 | } 20 | ], 21 | "type": "library", 22 | "require": { 23 | "php": "^7.4 || ^8.0", 24 | "guzzlehttp/guzzle": "^6 || ^7" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Postal\\": "src/" 29 | } 30 | }, 31 | "require-dev": { 32 | "phpstan/phpstan": "^1.10", 33 | "phpunit/phpunit": "^9.6", 34 | "symplify/easy-coding-standard": "^11.3" 35 | }, 36 | "scripts": { 37 | "test": [ 38 | "ecs", 39 | "phpunit", 40 | "phpstan" 41 | ] 42 | }, 43 | "config": { 44 | "platform": { 45 | "php": "7.4" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([__DIR__ . '/src', __DIR__ . '/tests']); 8 | 9 | $ecsConfig->sets([ 10 | SetList::ARRAY, 11 | SetList::CLEAN_CODE, 12 | SetList::CONTROL_STRUCTURES, 13 | SetList::DOCBLOCK, 14 | SetList::NAMESPACES, 15 | SetList::PSR_12, 16 | SetList::SPACES, 17 | ]); 18 | }; 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 9 3 | paths: 4 | - src 5 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ApiException.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient ?: HttpClientFactory::create($host, $apiKey); 24 | $this->messages = new MessagesService($this); 25 | $this->send = new SendService($this); 26 | } 27 | 28 | public function getHttpClient(): HttpClient 29 | { 30 | return $this->httpClient; 31 | } 32 | 33 | /** 34 | * @template T 35 | * @param class-string $class 36 | * @return T 37 | */ 38 | public function prepareResponse(ResponseInterface $response, $class) 39 | { 40 | return new $class($this->validateResponse($response)); 41 | } 42 | 43 | /** 44 | * @template T 45 | * @param class-string $class 46 | * @return array 47 | */ 48 | public function prepareListResponse(ResponseInterface $response, $class) 49 | { 50 | $list = $this->validateResponse($response); 51 | 52 | // if (! array_is_list($list)) { 53 | if (! $this->arrayIsList($list)) { 54 | throw new ApiException('Unexpected response received, expected a list'); 55 | } 56 | 57 | return array_map(fn ($item) => new $class($item), $list); 58 | } 59 | 60 | /** 61 | * @return array 62 | */ 63 | protected function validateResponse(ResponseInterface $response): array 64 | { 65 | $json = json_decode((string) $response->getBody(), true); 66 | 67 | if (json_last_error() !== JSON_ERROR_NONE || ! is_array($json)) { 68 | throw new ApiException('Malformed response body received'); 69 | } 70 | 71 | if (! isset($json['status']) || $json['status'] !== 'success') { 72 | $message = $json['data']['message'] ?? 'An unexpected error was received'; 73 | $code = 0; 74 | if (isset($json['data']['code'])) { 75 | $message = $json['data']['code'] . ': ' . $message; 76 | } 77 | 78 | throw new ApiException($message); 79 | } 80 | 81 | if (! isset($json['data'])) { 82 | throw new ApiException('Unexpected response received'); 83 | } 84 | 85 | return $json['data']; 86 | } 87 | 88 | /** 89 | * @param array $array 90 | */ 91 | private function arrayIsList(array $array): bool 92 | { 93 | $i = 0; 94 | foreach ($array as $k => $v) { 95 | if ($k !== $i++) { 96 | return false; 97 | } 98 | } 99 | 100 | return true; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/HttpClientFactory.php: -------------------------------------------------------------------------------- 1 | push(RetryMiddlewareFactory::build()); 17 | } 18 | 19 | return new Client([ 20 | 'base_uri' => "{$host}/api/v1/", 21 | 'headers' => [ 22 | 'X-Server-API-Key' => $apiKey, 23 | ], 24 | 'handler' => $handler, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Messages/Delivery.php: -------------------------------------------------------------------------------- 1 | id = $attributes['id']; 33 | $this->status = $attributes['status']; 34 | $this->details = $attributes['details']; 35 | $this->output = $attributes['output']; 36 | $this->sent_with_ssl = $attributes['sent_with_ssl']; 37 | $this->log_id = $attributes['log_id']; 38 | $this->time = $attributes['time']; 39 | 40 | $this->timestamp = new DateTime('@' . $attributes['timestamp']); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Messages/Message.php: -------------------------------------------------------------------------------- 1 | $attributes 57 | */ 58 | public function __construct(array $attributes) 59 | { 60 | if (! is_int($attributes['id'])) { 61 | throw new ApiException('Unexpected API response, expected an integer ID'); 62 | } 63 | if (! is_string($attributes['token'])) { 64 | throw new ApiException('Unexpected API response, expected a string token'); 65 | } 66 | 67 | $this->id = $attributes['id']; 68 | $this->token = $attributes['token']; 69 | $this->status = $attributes['status'] ?? null; 70 | $this->details = $attributes['details'] ?? null; 71 | $this->inspection = $attributes['inspection'] ?? null; 72 | $this->plain_body = $attributes['plain_body'] ?? null; 73 | $this->html_body = $attributes['html_body'] ?? null; 74 | $this->attachments = $attributes['attachments'] ?? null; 75 | $this->headers = $attributes['headers'] ?? null; 76 | $this->raw_message = $attributes['raw_message'] ?? null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/MessagesService.php: -------------------------------------------------------------------------------- 1 | client = $client; 17 | } 18 | 19 | /** 20 | * @param array|true $expansions 21 | */ 22 | public function details(int $id, $expansions = []): Message 23 | { 24 | return $this->client->prepareResponse( 25 | $this->client->getHttpClient()->post('messages/message', [ 26 | 'json' => [ 27 | 'id' => $id, 28 | '_expansions' => $expansions, 29 | ], 30 | ]), 31 | Message::class, 32 | ); 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function deliveries(int $id): array 39 | { 40 | return $this->client->prepareListResponse( 41 | $this->client->getHttpClient()->post('messages/deliveries', [ 42 | 'json' => [ 43 | 'id' => $id, 44 | ], 45 | ]), 46 | Delivery::class, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/RetryMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | self::$maxRetries) { 32 | return false; 33 | } 34 | 35 | if (self::isConnectionError($exception)) { 36 | return true; 37 | } 38 | 39 | if (self::isInternalServerError($exception) && $request->getMethod() === 'GET') { 40 | return true; 41 | } 42 | 43 | return false; 44 | }; 45 | } 46 | 47 | public static function buildDelay(): callable 48 | { 49 | return function ( 50 | $retries, 51 | Response $response = null 52 | ) { 53 | return self::$defaultRetryDelay; 54 | }; 55 | } 56 | 57 | private static function isConnectionError(\Throwable $exception = null): bool 58 | { 59 | return $exception instanceof \GuzzleHttp\Exception\ConnectException; 60 | } 61 | 62 | private static function isInternalServerError(\Throwable $exception = null): bool 63 | { 64 | return $exception instanceof \GuzzleHttp\Exception\ServerException; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Send/Message.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public array $to = []; 13 | 14 | /** 15 | * @var array 16 | */ 17 | public array $cc = []; 18 | 19 | /** 20 | * @var array 21 | */ 22 | public array $bcc = []; 23 | 24 | public ?string $from = null; 25 | 26 | public ?string $sender = null; 27 | 28 | public ?string $subject = null; 29 | 30 | public ?string $tag = null; 31 | 32 | public ?string $reply_to = null; 33 | 34 | public ?string $plain_body = null; 35 | 36 | public ?string $html_body = null; 37 | 38 | /** 39 | * @var array 40 | */ 41 | public ?array $headers = null; 42 | 43 | /** 44 | * @var array> 45 | */ 46 | public array $attachments = []; 47 | 48 | public function to(string $address): self 49 | { 50 | $this->to[] = $address; 51 | 52 | return $this; 53 | } 54 | 55 | public function cc(string $address): self 56 | { 57 | $this->cc[] = $address; 58 | 59 | return $this; 60 | } 61 | 62 | public function bcc(string $address): self 63 | { 64 | $this->bcc[] = $address; 65 | 66 | return $this; 67 | } 68 | 69 | public function from(string $address): self 70 | { 71 | $this->from = $address; 72 | 73 | return $this; 74 | } 75 | 76 | public function sender(string $address): self 77 | { 78 | $this->sender = $address; 79 | 80 | return $this; 81 | } 82 | 83 | public function subject(string $subject): self 84 | { 85 | $this->subject = $subject; 86 | 87 | return $this; 88 | } 89 | 90 | public function tag(string $tag): self 91 | { 92 | $this->tag = $tag; 93 | 94 | return $this; 95 | } 96 | 97 | public function replyTo(string $replyTo): self 98 | { 99 | $this->reply_to = $replyTo; 100 | 101 | return $this; 102 | } 103 | 104 | public function plainBody(string $content): self 105 | { 106 | $this->plain_body = $content; 107 | 108 | return $this; 109 | } 110 | 111 | public function htmlBody(string $content): self 112 | { 113 | $this->html_body = $content; 114 | 115 | return $this; 116 | } 117 | 118 | public function header(string $key, string $value): self 119 | { 120 | if ($this->headers === null) { 121 | $this->headers = []; 122 | } 123 | 124 | $this->headers[$key] = $value; 125 | 126 | return $this; 127 | } 128 | 129 | public function attach(string $filename, string $content_type, string $data): self 130 | { 131 | $attachment = [ 132 | 'name' => $filename, 133 | 'content_type' => $content_type, 134 | 'data' => base64_encode($data), 135 | ]; 136 | 137 | $this->attachments[] = $attachment; 138 | 139 | return $this; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Send/RawMessage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public array $rcpt_to = []; 13 | 14 | public ?string $mail_from = null; 15 | 16 | public ?string $data = null; 17 | 18 | public function mailFrom(string $address): self 19 | { 20 | $this->mail_from = $address; 21 | 22 | return $this; 23 | } 24 | 25 | public function rcptTo(string $address): self 26 | { 27 | $this->rcpt_to[] = $address; 28 | 29 | return $this; 30 | } 31 | 32 | public function data(string $data): self 33 | { 34 | $this->data = base64_encode($data); 35 | 36 | return $this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Send/Result.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public array $messages; 17 | 18 | /** 19 | * @param array{message_id: string, messages: array>} $attributes 20 | */ 21 | public function __construct(array $attributes) 22 | { 23 | $this->message_id = $attributes['message_id']; 24 | $this->messages = array_change_key_case( 25 | array_map(fn ($message) => new Message($message), $attributes['messages']) 26 | ); 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function recipients(): array 33 | { 34 | return $this->messages; 35 | } 36 | 37 | public function size(): int 38 | { 39 | return count($this->messages); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/SendService.php: -------------------------------------------------------------------------------- 1 | client = $client; 18 | } 19 | 20 | public function message(Message $message): Result 21 | { 22 | return $this->client->prepareResponse( 23 | $this->client->getHttpClient()->post('send/message', [ 24 | 'json' => $message, 25 | ]), 26 | Result::class, 27 | ); 28 | } 29 | 30 | public function raw(RawMessage $message): Result 31 | { 32 | return $this->client->prepareResponse( 33 | $this->client->getHttpClient()->post('send/raw', [ 34 | 'json' => $message, 35 | ]), 36 | Result::class, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | expectException(ApiException::class); 20 | $this->expectExceptionMessage('Malformed response body received'); 21 | 22 | $client->prepareResponse($response, new class() { 23 | }); 24 | } 25 | 26 | public function testPrepareResponseThrowsJsonNotArray(): void 27 | { 28 | $client = new Client('', ''); 29 | $response = new Response(200, [], '"test"'); 30 | 31 | $this->expectException(ApiException::class); 32 | $this->expectExceptionMessage('Malformed response body received'); 33 | 34 | $client->prepareResponse($response, new class() { 35 | }); 36 | } 37 | 38 | public function testPrepareResponseThrowsApiErrors(): void 39 | { 40 | $client = new Client('', ''); 41 | $response = new Response(200, [], json_encode([ 42 | 'status' => 'error', 43 | 'data' => [ 44 | 'code' => 'TestExceptionCode', 45 | 'message' => 'my-test-error', 46 | ], 47 | ])); 48 | 49 | $this->expectException(ApiException::class); 50 | $this->expectExceptionMessage('TestExceptionCode: my-test-error'); 51 | 52 | $client->prepareResponse($response, new class() { 53 | }); 54 | } 55 | 56 | public function testPrepareResponseThrowsWithoutResponse(): void 57 | { 58 | $client = new Client('', ''); 59 | $response = new Response(200, [], json_encode([ 60 | 'status' => 'success', 61 | ])); 62 | 63 | $this->expectException(ApiException::class); 64 | $this->expectExceptionMessage('Unexpected response received'); 65 | 66 | $client->prepareResponse($response, new class() { 67 | }); 68 | } 69 | 70 | public function testPrepareResponseCoercesToClass(): void 71 | { 72 | $client = new Client('', ''); 73 | $response = new Response(200, [], json_encode([ 74 | 'status' => 'success', 75 | 'data' => [ 76 | 'id' => 123, 77 | 'string' => 'abc', 78 | ], 79 | ])); 80 | 81 | $result = $client->prepareResponse($response, new class([]) { 82 | public ?int $id; 83 | 84 | public ?string $string; 85 | 86 | public function __construct($attributes) 87 | { 88 | $this->id = $attributes['id'] ?? null; 89 | $this->string = $attributes['string'] ?? null; 90 | } 91 | }); 92 | 93 | $this->assertSame(123, $result->id); 94 | $this->assertSame('abc', $result->string); 95 | } 96 | 97 | public function testPrepareListResponseCoercesToClass(): void 98 | { 99 | $client = new Client('', ''); 100 | $response = new Response(200, [], json_encode([ 101 | 'status' => 'success', 102 | 'data' => [ 103 | [ 104 | 'id' => 123, 105 | 'string' => 'abc', 106 | ], 107 | [ 108 | 'id' => 456, 109 | 'string' => 'def', 110 | ], 111 | ], 112 | ])); 113 | 114 | $result = $client->prepareListResponse($response, new class([]) { 115 | public ?int $id; 116 | 117 | public ?string $string; 118 | 119 | public function __construct($attributes) 120 | { 121 | $this->id = $attributes['id'] ?? null; 122 | $this->string = $attributes['string'] ?? null; 123 | } 124 | }); 125 | 126 | $this->assertCount(2, $result); 127 | $this->assertSame(123, $result[0]->id); 128 | $this->assertSame('abc', $result[0]->string); 129 | $this->assertSame(456, $result[1]->id); 130 | $this->assertSame('def', $result[1]->string); 131 | } 132 | 133 | public function testPrepareListResponseThrowsWithoutList(): void 134 | { 135 | $client = new Client('', ''); 136 | $response = new Response(200, [], json_encode([ 137 | 'status' => 'success', 138 | 'data' => [ 139 | 'string' => 'this is not a list', 140 | ], 141 | ])); 142 | 143 | $this->expectException(ApiException::class); 144 | $this->expectExceptionMessage('Unexpected response received, expected a list'); 145 | 146 | $client->prepareListResponse($response, new class() { 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/MessagesServiceTest.php: -------------------------------------------------------------------------------- 1 | 'success', 22 | 'data' => [ 23 | 'id' => 123, 24 | 'token' => 'abc', 25 | 'status' => [ 26 | 'status' => 'Held', 27 | 'last_delivery_attempt' => 1666083425.913461, 28 | 'held' => true, 29 | 'hold_expiry' => 1666688225.924133, 30 | ], 31 | 'details' => [ 32 | 'rcpt_to' => 'rcpt_to@example.com', 33 | 'mail_from' => 'mail_from@example.com', 34 | 'subject' => 'my subject', 35 | 'message_id' => '969c4ad7-4cb1-464c-bdfd-14e9995342d3@example.com', 36 | ], 37 | 'inspection' => [ 38 | 'inspected' => true, 39 | 'spam' => false, 40 | 'spam_score' => 0, 41 | 'threat' => false, 42 | 'threat_details' => null, 43 | ], 44 | 'plain_body' => 'Plain Body', 45 | 'html_body' => '

HTML Body

', 46 | 'attachments' => [ 47 | [ 48 | 'filename' => 'file.txt', 49 | 'content_type' => 'text/plain', 50 | 'data' => 'dGHzdA==', 51 | 'size' => 4, 52 | 'hash' => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3', 53 | ], 54 | ], 55 | 'headers' => [ 56 | 'from' => [ 57 | 'from@example.com', 58 | ], 59 | ], 60 | 'raw_message' => 'raw message', 61 | ], 62 | ])), 63 | ]); 64 | $handlerStack = HandlerStack::create($mock); 65 | $guzzle = new GuzzleHttpClient([ 66 | 'handler' => $handlerStack, 67 | ]); 68 | 69 | $client = new Client('', '', $guzzle); 70 | 71 | $details = $client->messages->details(123, true); 72 | 73 | $this->assertSame(123, $details->id); 74 | $this->assertSame('abc', $details->token); 75 | $this->assertSame('Held', $details->status['status']); 76 | $this->assertSame('rcpt_to@example.com', $details->details['rcpt_to']); 77 | $this->assertTrue($details->inspection['inspected']); 78 | $this->assertSame('Plain Body', $details->plain_body); 79 | $this->assertSame('

HTML Body

', $details->html_body); 80 | $this->assertSame('file.txt', $details->attachments[0]['filename']); 81 | $this->assertSame('from@example.com', $details->headers['from'][0]); 82 | $this->assertSame('raw message', $details->raw_message); 83 | } 84 | 85 | public function testDeliveries(): void 86 | { 87 | $mock = new MockHandler([ 88 | new Response(200, [], json_encode([ 89 | 'status' => 'success', 90 | 'data' => [ 91 | [ 92 | 'id' => 1, 93 | 'status' => 'Held', 94 | 'details' => 'Credential is configured to hold all messages authenticated by it.', 95 | 'output' => null, 96 | 'sent_with_ssl' => false, 97 | 'log_id' => null, 98 | 'time' => null, 99 | 'timestamp' => 1666100297, 100 | ], 101 | [ 102 | 'id' => 2, 103 | 'status' => 'Sent', 104 | 'details' => 'Message for test@example.com accepted by mail.protection.outlook.com (0.0.0.0)', 105 | 'output' => '250', 106 | 'sent_with_ssl' => false, 107 | 'log_id' => 'ABCDEF', 108 | 'time' => 1.15, 109 | 'timestamp' => 1666100297, 110 | ], 111 | ], 112 | ])), 113 | ]); 114 | $handlerStack = HandlerStack::create($mock); 115 | $guzzle = new GuzzleHttpClient([ 116 | 'handler' => $handlerStack, 117 | ]); 118 | 119 | $client = new Client('', '', $guzzle); 120 | 121 | $deliveries = $client->messages->deliveries(123); 122 | 123 | $this->assertCount(2, $deliveries); 124 | 125 | $this->assertSame(1, $deliveries[0]->id); 126 | $this->assertSame('Held', $deliveries[0]->status); 127 | $this->assertSame('Credential is configured to hold all messages authenticated by it.', $deliveries[0]->details); 128 | $this->assertNull($deliveries[0]->output); 129 | $this->assertFalse($deliveries[0]->sent_with_ssl); 130 | $this->assertNull($deliveries[0]->log_id); 131 | $this->assertNull($deliveries[0]->time); 132 | $this->assertInstanceOf(DateTime::class, $deliveries[0]->timestamp); 133 | $this->assertSame('2022-10-18 13:38:17', $deliveries[0]->timestamp->format('Y-m-d H:i:s')); 134 | 135 | $this->assertSame(2, $deliveries[1]->id); 136 | $this->assertSame('Sent', $deliveries[1]->status); 137 | $this->assertSame('Message for test@example.com accepted by mail.protection.outlook.com (0.0.0.0)', $deliveries[1]->details); 138 | $this->assertSame('250', $deliveries[1]->output); 139 | $this->assertFalse($deliveries[1]->sent_with_ssl); 140 | $this->assertSame('ABCDEF', $deliveries[1]->log_id); 141 | $this->assertSame(1.15, $deliveries[1]->time); 142 | $this->assertInstanceOf(DateTime::class, $deliveries[1]->timestamp); 143 | $this->assertSame('2022-10-18 13:38:17', $deliveries[1]->timestamp->format('Y-m-d H:i:s')); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/Send/MessageTest.php: -------------------------------------------------------------------------------- 1 | assertNull($message->headers); 16 | 17 | $message->to('to@example.com'); 18 | $this->assertSame(['to@example.com'], $message->to); 19 | 20 | $message->cc('cc@example.com'); 21 | $this->assertSame(['cc@example.com'], $message->cc); 22 | 23 | $message->bcc('bcc@example.com'); 24 | $this->assertSame(['bcc@example.com'], $message->bcc); 25 | 26 | $message->from('from@example.com'); 27 | $this->assertSame('from@example.com', $message->from); 28 | 29 | $message->sender('sender@example.com'); 30 | $this->assertSame('sender@example.com', $message->sender); 31 | 32 | $message->subject('my-subject'); 33 | $this->assertSame('my-subject', $message->subject); 34 | 35 | $message->tag('my-tag'); 36 | $this->assertSame('my-tag', $message->tag); 37 | 38 | $message->replyTo('reply-to@example.com'); 39 | $this->assertSame('reply-to@example.com', $message->reply_to); 40 | 41 | $message->plainBody('my plain body'); 42 | $this->assertSame('my plain body', $message->plain_body); 43 | 44 | $message->htmlBody('my html body'); 45 | $this->assertSame('my html body', $message->html_body); 46 | 47 | $message->header('my-header', 'value'); 48 | $this->assertSame([ 49 | 'my-header' => 'value', 50 | ], $message->headers); 51 | 52 | $message->attach('test.txt', 'text/plain', 'test'); 53 | $this->assertSame([ 54 | [ 55 | 'name' => 'test.txt', 56 | 'content_type' => 'text/plain', 57 | 'data' => 'dGVzdA==', 58 | ], 59 | ], $message->attachments); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/SendServiceTest.php: -------------------------------------------------------------------------------- 1 | 'success', 24 | 'data' => [ 25 | 'message_id' => 'my-message-id', 26 | 'messages' => [ 27 | [ 28 | 'id' => 1, 29 | 'token' => 'A', 30 | ], 31 | [ 32 | 'id' => 2, 33 | 'token' => 'B', 34 | ], 35 | ], 36 | ], 37 | ])), 38 | ]); 39 | $handlerStack = HandlerStack::create($mock); 40 | 41 | $requests = []; 42 | $handlerStack->push(Middleware::history($requests)); 43 | 44 | $guzzle = new GuzzleHttpClient([ 45 | 'handler' => $handlerStack, 46 | ]); 47 | 48 | $client = new Client('', '', $guzzle); 49 | 50 | $message = new Message(); 51 | $result = $client->send->message($message); 52 | 53 | $this->assertSame('my-message-id', $result->message_id); 54 | $this->assertCount(2, $result->messages); 55 | $this->assertSame(1, $result->messages[0]->id); 56 | $this->assertSame('A', $result->messages[0]->token); 57 | $this->assertSame(2, $result->messages[1]->id); 58 | $this->assertSame('B', $result->messages[1]->token); 59 | 60 | $this->assertCount(1, $requests); 61 | $uri = (string) $requests[0]['request']->getUri(); 62 | 63 | $this->assertSame('send/message', $uri); 64 | } 65 | 66 | public function testRaw(): void 67 | { 68 | $mock = new MockHandler([ 69 | new Response(200, [], json_encode([ 70 | 'status' => 'success', 71 | 'data' => [ 72 | 'message_id' => 'my-message-id', 73 | 'messages' => [ 74 | [ 75 | 'id' => 1, 76 | 'token' => 'A', 77 | ], 78 | [ 79 | 'id' => 2, 80 | 'token' => 'B', 81 | ], 82 | ], 83 | ], 84 | ])), 85 | ]); 86 | $handlerStack = HandlerStack::create($mock); 87 | 88 | $requests = []; 89 | $handlerStack->push(Middleware::history($requests)); 90 | 91 | $guzzle = new GuzzleHttpClient([ 92 | 'handler' => $handlerStack, 93 | ]); 94 | 95 | $client = new Client('', '', $guzzle); 96 | 97 | $message = new RawMessage(); 98 | $result = $client->send->raw($message); 99 | 100 | $this->assertSame('my-message-id', $result->message_id); 101 | $this->assertCount(2, $result->messages); 102 | $this->assertSame(1, $result->messages[0]->id); 103 | $this->assertSame('A', $result->messages[0]->token); 104 | $this->assertSame(2, $result->messages[1]->id); 105 | $this->assertSame('B', $result->messages[1]->token); 106 | 107 | $this->assertCount(1, $requests); 108 | $uri = (string) $requests[0]['request']->getUri(); 109 | 110 | $this->assertSame('send/raw', $uri); 111 | } 112 | } 113 | --------------------------------------------------------------------------------