├── LICENSE.md ├── README.md ├── composer.json ├── config └── postmark-webhooks.php ├── database └── migrations │ └── 2018_09_20_174247_create_postmark_webhook_logs_table.php └── src ├── Events └── PostmarkWebhookCalled.php ├── Http ├── Controllers │ └── StorePostmarkWebhookController.php └── Middleware │ └── PostmarkIpsWhitelist.php ├── PostmarkWebhook.php └── PostmarkWebhooksServiceProvider.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Mark van den Broek 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Postmark Inbound 2 | 3 | # Handle Postmark webhooks in a Laravel application 4 | 5 | [![Latest Version on Packagist][ico-version]][link-packagist] 6 | [![Software License][ico-license]](LICENSE.md) 7 | [![Tests][ico-tests]][link-tests] 8 | [![StyleCI][ico-style-ci]][link-style-ci] 9 | [![Total Downloads][ico-downloads]][link-downloads] 10 | 11 | Postmark can send out several webhooks to *your* application when an event occurs. 12 | This way Postmark is able to immediately notify you when something new occurs. 13 | 14 | This package can help you handle those webhooks. 15 | 16 | ## Installation 17 | 18 | You can install the package via composer: 19 | 20 | ``` bash 21 | composer require mvdnbrk/laravel-postmark-webhooks 22 | ``` 23 | 24 | This package will log all incoming webhooks to the database by default. 25 | Run the migrations to create a `postmark_webhook_logs` table in the database: 26 | 27 | ``` bash 28 | php artisan migrate 29 | ``` 30 | > If you want to disable database logging you can set `POSTMARK_WEBHOOKS_LOG_ENABLED=false` in your `.env` file. 31 | 32 | ## Setup webhooks with Postmark 33 | 34 | Visit the [servers](https://account.postmarkapp.com/servers) page on your [Postmark account](https://account.postmarkapp.com/). 35 | Choose the server you want to setup webhooks for. 36 | Go to `settings` > `webhooks` > `add webbook`. 37 | 38 | This package will register a route where Postmark can post their webhooks to: `/api/webhooks/postmark`. 39 | 40 | Fill in your webhook URL: `https:///api/webhooks/postmark` 41 | Pick the events Postmark should send to you and save the webhook. 42 | You are ready to receive webhook notifications from Postmark! 43 | 44 | > You may change the `/api/webhooks/postmark` endpoint to anything you like. 45 | > You can do this by changing the `path` key in the `config/postmark-webooks.php` file. 46 | 47 | ## Protection of your webhook 48 | 49 | This package protects your webhook automatically by only allowing requests from the [IP range](https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks) that Postmark uses. 50 | 51 | ## Usage 52 | 53 | Postmark can send out several event types by posting a webhook. 54 | You can find the [full list of webhooks](https://postmarkapp.com/developer/webhooks/webhooks-overview) in the Postmark documentation. 55 | 56 | All webhook requests will be logged in the `postmark_webhook_logs` table. 57 | The table has a `payload` column where the entire payload of the incoming webhook is saved. 58 | The ID Postmark assigned to the original message will be saved in the `message_id` column, 59 | the event type will be stored in the `record_type` column and the email address as well in the `email` column. 60 | > Note that event types will be converted to `snake_case`. 61 | For example `SpamComplaint` will be saved as `spam_complaint`. 62 | 63 | ### Events 64 | 65 | Whenever a webhook call comes in, this package will fire a `PostmarkWebhookCalled` event. 66 | You may register an event listener in the `EventServiceProvider`: 67 | 68 | ```php 69 | /** 70 | * The event listener mappings for the application. 71 | * 72 | * @var array 73 | */ 74 | protected $listen = [ 75 | PostmarkWebhookCalled::class => [ 76 | YourListener::class, 77 | ], 78 | ]; 79 | ``` 80 | 81 | Example of a listener: 82 | 83 | ```php 84 | payload. 103 | // The email address, message ID and record type are also available: 104 | // $event->email 105 | // $event->messageId 106 | // $event->recordType 107 | } 108 | } 109 | 110 | ``` 111 | 112 | You may also register an event listener for a specific type of event: 113 | 114 | ```php 115 | /** 116 | * The event listener mappings for the application. 117 | * 118 | * @var array 119 | */ 120 | protected $listen = [ 121 | 'webhook.postmark: spam_complaint' => [ 122 | YourSpamComplaintListener::class, 123 | ], 124 | ]; 125 | ``` 126 | 127 | Available events: `open`, `bounce`, `click`, `delivery`, `spam_complaint`. 128 | 129 | ### Advanced configuration 130 | 131 | You may optionally publish the config file with: 132 | 133 | ```bash 134 | php artisan vendor:publish --provider="Mvdnbrk\PostmarkWebhooks\PostmarkWebhooksServiceProvider" --tag="config" 135 | ``` 136 | 137 | Within the configuration file you may change the table name being used 138 | or the Eloquent model being used to save log records to the database. 139 | > If you want to use your own model to save the logs to the database you should extend 140 | the `Mvdnbrk\PostmarkWebhooks\PostmarkWebhook` class. 141 | 142 | You can also exclude one or more event types from being logged to the database. 143 | Place the events you want to exclude under the `except` key: 144 | 145 | ```php 146 | 'log' => [ 147 | ... 148 | 'except' => [ 149 | 'open', 150 | ... 151 | ], 152 | ], 153 | ``` 154 | 155 | You can map the events fired by this package to your own event classes: 156 | 157 | ```php 158 | 'events' => [ 159 | 'spam_complaint' => App\Events\SpamComplaint, 160 | ... 161 | ], 162 | ``` 163 | 164 | ## Change log 165 | 166 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 167 | 168 | ## Testing 169 | 170 | ``` bash 171 | $ composer test 172 | ``` 173 | 174 | ## Contributing 175 | 176 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 177 | 178 | ## Security Vulnerabilities 179 | 180 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 181 | 182 | ## Credits 183 | 184 | - [Mark van den Broek][link-author] 185 | - [All Contributors][link-contributors] 186 | 187 | Inspired by [Laravel Stripe Webooks](https://github.com/spatie/laravel-stripe-webhooks) from [Spatie](https://spatie.be/). 188 | 189 | ## License 190 | 191 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 192 | 193 | [ico-version]: https://img.shields.io/packagist/v/mvdnbrk/laravel-postmark-webhooks.svg?style=flat-square 194 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 195 | [ico-tests]: https://img.shields.io/github/workflow/status/mvdnbrk/laravel-postmark-webhooks/tests/main?label=tests&style=flat-square 196 | [ico-style-ci]: https://styleci.io/repos/149487979/shield?branch=main 197 | [ico-downloads]: https://img.shields.io/packagist/dt/mvdnbrk/laravel-postmark-webhooks.svg?style=flat-square 198 | 199 | [link-packagist]: https://packagist.org/packages/mvdnbrk/laravel-postmark-webhooks 200 | [link-tests]: https://github.com/mvdnbrk/laravel-postmark-webhooks/actions?query=workflow%3Atests 201 | [link-style-ci]: https://styleci.io/repos/149487979 202 | [link-downloads]: https://packagist.org/packages/mvdnbrk/laravel-postmark-webhooks 203 | [link-author]: https://github.com/mvdnbrk 204 | [link-contributors]: ../../contributors 205 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mvdnbrk/laravel-postmark-webhooks", 3 | "description": "Handle Postmark webhooks in a Laravel application.", 4 | "keywords": [ 5 | "Laravel", 6 | "Postmark", 7 | "webhook", 8 | "e-mail" 9 | ], 10 | "homepage": "https://github.com/mvdnbrk/laravel-postmark-webhooks", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Mark van den Broek", 15 | "email": "mvdnbrk@gmail.com", 16 | "homepage": "https://github.com/mvdnbrk", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.2 || ^8.0", 22 | "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0" 23 | }, 24 | "require-dev": { 25 | "orchestra/testbench": "^4.5 || ^5.0 || ^6.0 || ^7.0", 26 | "phpunit/phpunit": "^8.5 || ^9.3" 27 | }, 28 | "config": { 29 | "sort-packages": true 30 | }, 31 | "extra": { 32 | "branch-alias": { 33 | "dev-master": "2.0-dev" 34 | }, 35 | "laravel": { 36 | "providers": [ 37 | "Mvdnbrk\\PostmarkWebhooks\\PostmarkWebhooksServiceProvider" 38 | ] 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Mvdnbrk\\PostmarkWebhooks\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Mvdnbrk\\PostmarkWebhooks\\Tests\\": "tests/" 49 | } 50 | }, 51 | "minimum-stability": "dev", 52 | "prefer-stable": true, 53 | "scripts": { 54 | "test": "vendor/bin/phpunit" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /config/postmark-webhooks.php: -------------------------------------------------------------------------------- 1 | '/api/webhooks/postmark', 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Log Options 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Logging events to the database is enabled by default. You may set this 23 | | to false if you don't want to log the Postmark events to the database. 24 | | 25 | | You may specify one or more event types to be excluded from being 26 | | logged to the database. You can place them under the except key. 27 | | 28 | | Supported event types: "open", "bounce", "click", 29 | | "delivery", "spam_complaint" 30 | | 31 | */ 32 | 33 | 'log' => [ 34 | 'enabled' => env('POSTMARK_WEBHOOKS_LOG_ENABLED', true), 35 | 'model' => \Mvdnbrk\PostmarkWebhooks\PostmarkWebhook::class, 36 | 'table_name' => 'postmark_webhook_logs', 37 | 38 | 'except' => [ 39 | // 40 | ], 41 | ], 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Event Mapping 46 | |-------------------------------------------------------------------------- 47 | | 48 | | This option allows you to map Postmark webhook 49 | | events to your own object-based events. 50 | | 51 | */ 52 | 53 | 'events' => [ 54 | // 55 | ], 56 | 57 | ]; 58 | -------------------------------------------------------------------------------- /database/migrations/2018_09_20_174247_create_postmark_webhook_logs_table.php: -------------------------------------------------------------------------------- 1 | table_name = config('postmark-webhooks.log.table_name', config('postmark-webhooks.log.table')); 15 | } 16 | 17 | public function up(): void 18 | { 19 | Schema::create($this->table_name, function (Blueprint $table) { 20 | $table->bigIncrements('id'); 21 | $table->string('message_id', 100)->nullable(); 22 | $table->string('record_type', 32); 23 | $table->string('email')->index(); 24 | $table->json('payload'); 25 | $table->dateTime('created_at'); 26 | }); 27 | } 28 | 29 | public function down(): void 30 | { 31 | Schema::dropIfExists($this->table_name); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Events/PostmarkWebhookCalled.php: -------------------------------------------------------------------------------- 1 | email = $email; 22 | $this->recordType = $recordType; 23 | $this->messageId = $messageId; 24 | $this->payload = $payload; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Controllers/StorePostmarkWebhookController.php: -------------------------------------------------------------------------------- 1 | input()); 17 | 18 | tap(new PostmarkWebhookCalled( 19 | $postmarkWebhook->email, 20 | $postmarkWebhook->record_type, 21 | $postmarkWebhook->message_id, 22 | $postmarkWebhook->payload 23 | ), function ($event) use ($postmarkWebhook) { 24 | event($event); 25 | event("webhook.postmark: {$postmarkWebhook->record_type}", $event); 26 | 27 | if ($dispatchEvent = config("postmark-webhooks.events.{$postmarkWebhook->record_type}")) { 28 | event(new $dispatchEvent($event)); 29 | } 30 | }); 31 | 32 | return response()->json()->setStatusCode(202); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Middleware/PostmarkIpsWhitelist.php: -------------------------------------------------------------------------------- 1 | ips)->contains($request->getClientIp())) { 34 | return $next($request); 35 | } 36 | 37 | return response()->json(['error' => 'Unauthorized'], 401); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/PostmarkWebhook.php: -------------------------------------------------------------------------------- 1 | 'array', 16 | ]; 17 | 18 | public function __construct(array $attributes = []) 19 | { 20 | if (! isset($this->table)) { 21 | $this->setTable(config('postmark-webhooks.log.table_name')); 22 | } 23 | 24 | parent::__construct($attributes); 25 | } 26 | 27 | public static function createOrNewFromPayload(array $payload): self 28 | { 29 | $payload = collect($payload); 30 | 31 | $recordType = Str::snake($payload->get('RecordType')); 32 | 33 | $model = (new static)->forceFill([ 34 | 'email' => $payload->get('Recipient', $payload->get('Email')), 35 | 'message_id' => $payload->get('MessageID'), 36 | 'record_type' => $recordType, 37 | 'payload' => $payload->all(), 38 | ]); 39 | 40 | if (config('postmark-webhooks.log.enabled') && ! collect(config('postmark-webhooks.log.except'))->contains($recordType)) { 41 | $model->save(); 42 | } 43 | 44 | return $model; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/PostmarkWebhooksServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRoutes(); 15 | $this->registerMigrations(); 16 | $this->registerPublishing(); 17 | } 18 | 19 | public function register(): void 20 | { 21 | $this->mergeConfigFrom(__DIR__.'/../config/postmark-webhooks.php', 'postmark-webhooks'); 22 | } 23 | 24 | private function registerMigrations(): void 25 | { 26 | if ($this->app->runningInConsole() && $this->shouldMigrate()) { 27 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 28 | } 29 | } 30 | 31 | private function registerPublishing(): void 32 | { 33 | if ($this->app->runningInConsole()) { 34 | $this->publishes([ 35 | __DIR__.'/../config/postmark-webhooks.php' => config_path('postmark-webhooks.php'), 36 | ], 'config'); 37 | 38 | $this->publishes([ 39 | __DIR__.'/../database/migrations' => database_path('migrations'), 40 | ], 'migrations'); 41 | } 42 | } 43 | 44 | private function registerRoutes(): void 45 | { 46 | Route::group($this->routeConfiguration(), function () { 47 | Route::post(config('postmark-webhooks.path'), StorePostmarkWebhookController::class); 48 | }); 49 | } 50 | 51 | private function routeConfiguration(): array 52 | { 53 | return [ 54 | 'middleware' => [ 55 | 'api', 56 | PostmarkIpsWhitelist::class, 57 | ], 58 | ]; 59 | } 60 | 61 | protected function shouldMigrate(): bool 62 | { 63 | return config('postmark-webhooks.log.enabled'); 64 | } 65 | } 66 | --------------------------------------------------------------------------------