├── .github └── workflows │ └── laravel.yml ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── UPGRADING.md ├── bitbucket-pipelines.yml ├── composer.json ├── config └── mailgun-webhooks.php ├── phpunit.xml └── src ├── Contracts └── WebhookEvent.php ├── Event.php ├── Exceptions ├── UnexpectedValueException.php └── WebhookFailed.php ├── Jobs └── HandleDelivered.php ├── MailgunSignatureValidator.php ├── MailgunWebhooksController.php ├── MailgunWebhooksServiceProvider.php ├── ProcessMailgunWebhookJob.php ├── Webhook.php └── WebhookSignature.php /.github/workflows/laravel.yml: -------------------------------------------------------------------------------- 1 | name: Laravel 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | laravel-tests: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Copy .env 17 | run: php -r "file_exists('.env') || copy('.env.example', '.env');" 18 | - name: Install Dependencies 19 | run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist 20 | - name: Execute tests (Unit and Feature tests) via PHPUnit 21 | run: vendor/bin/phpunit 22 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-mailgun-webhooks` will be documented in this file 4 | 5 | ## 9.2.0 - 2023-04-08 6 | 7 | - Add Laravel 10 integration 8 | 9 | ## 9.1.0 - 2022-01-26 10 | 11 | - Drop support for PHP 7 12 | - Upgrade spatie/laravel-webhook-client to version 3.0 13 | - Test Laravel 9 integration 14 | 15 | ## 9.0.0 - 2022-01-20 16 | 17 | - Add support for Laravel 9 18 | 19 | ## 1.1.0 - 2020-03-04 20 | 21 | - add support for Laravel 7 22 | 23 | ## 1.0.0 - 2019-12-10 24 | 25 | - initial release 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Binary Cats 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 | [](https://supportukrainenow.org) 2 | 3 | # Handle Mailgun Webhooks in a Laravel application 4 | 5 | ![https://github.com/binary-cats/laravel-mailgun-webhooks/actions](https://github.com/binary-cats/laravel-mailgun-webhooks/workflows/Laravel/badge.svg) 6 | ![https://github.styleci.io/repos/230519748](https://github.styleci.io/repos/230519748/shield) 7 | ![https://scrutinizer-ci.com/g/binary-cats/laravel-mailgun-webhooks/](https://scrutinizer-ci.com/g/binary-cats/laravel-mailgun-webhooks/badges/quality-score.png?b=master) 8 | 9 | [Mailgun](https://mailgun.com) can notify your application of mail events using webhooks. This package can help you handle those webhooks. Out of the box it will verify the Mailgun signature of all incoming requests. All valid calls will be logged to the database. You can easily define jobs or events that should be dispatched when specific events hit your app. 10 | 11 | This package will not handle what should be done after the webhook request has been validated and the right job or event is called. You should still code up any work (eg. what should happen) yourself. 12 | 13 |

14 | 15 | 16 | Before using this package we highly recommend reading [the entire documentation on webhooks over at Mailgun](https://documentation.mailgun.com/en/latest/api-webhooks.html). 17 | 18 | This package is an adapted copy of absolutely amazing [spatie/laravel-stripe-webhooks](https://github.com/spatie/laravel-stripe-webhooks) 19 | 20 | ## Upgrade 21 | 22 | If you are upgrading from previous version, please note that spatie/laravel-webhook-client has been upgraded to ^3.0 - which adds an extra field into the webhooks table. Read [upgrading instructions](https://github.com/spatie/laravel-webhook-client/blob/main/UPGRADING.md) for more details. 23 | 24 | ## Installation 25 | 26 | You can install the package via composer: 27 | 28 | ```bash 29 | composer require binary-cats/laravel-mailgun-webhooks 30 | ``` 31 | 32 | The service provider will automatically register itself. 33 | 34 | You must publish the config file with: 35 | ```bash 36 | php artisan vendor:publish --provider="BinaryCats\MailgunWebhooks\MailgunWebhooksServiceProvider" --tag="config" 37 | ``` 38 | 39 | This is the contents of the config file that will be published at `config/mailgun-webhooks.php`: 40 | 41 | ```php 42 | return [ 43 | 44 | /* 45 | * Mailgun will sign each webhook using a secret. You can find the used secret at the 46 | * webhook configuration settings: https://app.mailgun.com/app/account/security/api_keys. 47 | */ 48 | 'signing_secret' => env('MAILGUN_WEBHOOK_SECRET'), 49 | 50 | /* 51 | * You can define the job that should be run when a certain webhook hits your application 52 | * here. The key is the name of the Mailgun event type with the `.` replaced by a `_`. 53 | * 54 | * You can find a list of Mailgun webhook types here: 55 | * https://documentation.mailgun.com/en/latest/api-webhooks.html#webhooks. 56 | * 57 | * The package will automatically convert the keys to lowercase, but you should 58 | * be congnisant of the fact that array keys are case sensitive 59 | */ 60 | 'jobs' => [ 61 | // 'delivered' => \BinaryCats\MailgunWebhooks\Jobs\HandleDelivered::class, 62 | ], 63 | 64 | /* 65 | * The classname of the model to be used. The class should equal or extend 66 | * Spatie\WebhookClient\Models\WebhookCall 67 | */ 68 | 'model' => \Spatie\WebhookClient\Models\WebhookCall::class, 69 | 70 | /* 71 | * The classname of the model to be used. The class should equal or extend 72 | * BinaryCats\MailgunWebhooks\ProcessMailgunWebhookJob 73 | */ 74 | 'process_webhook_job' => \BinaryCats\MailgunWebhooks\ProcessMailgunWebhookJob::class, 75 | ]; 76 | ``` 77 | 78 | In the `signing_secret` key of the config file you should add a valid webhook secret. You can find the secret used at [HTTP webhook signing key](https://app.mailgun.com/app/account/security/api_keys). 79 | 80 | **You can skip migrating is you have already installed `Spatie\WebhookClient`** 81 | 82 | Next, you must publish the migration with: 83 | ```bash 84 | php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations" 85 | ``` 86 | 87 | After migration has been published you can create the `webhook_calls` table by running the migrations: 88 | 89 | ```bash 90 | php artisan migrate 91 | ``` 92 | 93 | ### Routing 94 | Finally, take care of the routing: At [the Mailgun dashboard](https://app.mailgun.com/app/sending/domains) you must configure at what url Mailgun webhooks should hit your app. In the routes file of your app you must pass that route to `Route::mailgunWebhooks()`: 95 | 96 | I like to group functionality by domain, so I would suggest `webhooks/mailgun` (especially if you plan to have more webhooks), but it is up to you. 97 | 98 | ```php 99 | # routes\web.php 100 | Route::mailgunWebhooks('webhooks/mailgun'); 101 | ``` 102 | 103 | Behind the scenes this will register a `POST` route to a controller provided by this package. Because Mailgun has no way of getting a csrf-token, you must add that route to the `except` array of the `VerifyCsrfToken` middleware: 104 | 105 | ```php 106 | protected $except = [ 107 | 'webhooks/mailgun', 108 | ]; 109 | ``` 110 | 111 | ## Usage 112 | 113 | Mailgun will send out webhooks for several event types. You can find the [full list of events types](https://documentation.mailgun.com/en/latest/user_manual.html#events) in Mailgun documentation. 114 | 115 | Mailgun will sign all requests hitting the webhook url of your app. This package will automatically verify if the signature is valid. If it is not, the request was probably not sent by Mailgun. 116 | 117 | Unless something goes terribly wrong, this package will always respond with a `200` to webhook requests. Sending a `200` will prevent Mailgun from resending the same event over and over again. All webhook requests with a valid signature will be logged in the `webhook_calls` table. The table has a `payload` column where the entire payload of the incoming webhook is saved. 118 | 119 | If the signature is not valid, the request will not be logged in the `webhook_calls` table but a `BinaryCats\MailgunWebhooks\Exceptions\WebhookFailed` exception will be thrown. 120 | If something goes wrong during the webhook request the thrown exception will be saved in the `exception` column. In that case the controller will send a `500` instead of `200`. 121 | 122 | There are two ways this package enables you to handle webhook requests: you can opt to queue a job or listen to the events the package will fire. 123 | 124 | **Due to the apparent differences between MailGun sandbox and production environment event casing, the package will ALWAYS cast mailgun events to lowercase - so your configured keys must be lowercase, too** 125 | 126 | **The package does not handle legacy webhooks, as they have a different schema.** Let me know if this is something that is needed. 127 | 128 | ### Handling webhook requests using jobs 129 | If you want to do something when a specific event type comes in you can define a job that does the work. Here's an example of such a job: 130 | 131 | ```php 132 | webhookCall = $webhookCall; 152 | } 153 | 154 | public function handle() 155 | { 156 | // do your work here 157 | 158 | // you can access the payload of the webhook call with `$this->webhookCall->payload` 159 | } 160 | } 161 | ``` 162 | 163 | Spatie highly recommends that you make this job queueable, because this will minimize the response time of the webhook requests. This allows you to handle more Mailgun webhook requests and avoid timeouts. 164 | 165 | Just keep in mind that mailgun places both `signature` and `event-data` into response body. 166 | 167 | After having created your job you must register it at the `jobs` array in the `mailgun-webhooks.php` config file. The key should be the name of [mailgun event type](https://documentation.mailgun.com/en/latest/user_manual.html#events). The value should be the fully qualified classname. 168 | 169 | ```php 170 | // config/mailgun-webhooks.php 171 | 172 | 'jobs' => [ 173 | 'delivered' => \App\Jobs\MailgunWebhooks\HandleDelievered::class, 174 | ], 175 | ``` 176 | 177 | ### Handling webhook requests using events 178 | 179 | Instead of queueing jobs to perform some work when a webhook request comes in, you can opt to listen to the events this package will fire. Whenever a valid request hits your app, the package will fire a `mailgun-webhooks::` event. 180 | 181 | The payload of the events will be the instance of `WebhookCall` that was created for the incoming request. 182 | 183 | Let's take a look at how you can listen for such an event. In the `EventServiceProvider` you can register listeners. 184 | 185 | ```php 186 | /** 187 | * The event listener mappings for the application. 188 | * 189 | * @var array 190 | */ 191 | protected $listen = [ 192 | 'mailgun-webhooks::delivered' => [ 193 | App\Listeners\DeliveredSource::class, 194 | ], 195 | ]; 196 | ``` 197 | 198 | Here's an example of such a listener: 199 | 200 | ```php 201 | payload` 215 | } 216 | } 217 | ``` 218 | 219 | Spatie highly recommends that you make the event listener queueable, as this will minimize the response time of the webhook requests. This allows you to handle more Mailgun webhook requests and avoid timeouts. 220 | 221 | The above example is only one way to handle events in Laravel. To learn the other options, read [the Laravel documentation on handling events](https://laravel.com/docs/6.x/events). 222 | 223 | ## Advanced usage 224 | 225 | ### Retry handling a webhook 226 | 227 | All incoming webhook requests are written to the database. This is incredibly valuable when something goes wrong while handling a webhook call. You can easily retry processing the webhook call, after you've investigated and fixed the cause of failure, like this: 228 | 229 | ```php 230 | use Spatie\WebhookClient\Models\WebhookCall; 231 | use BinaryCats\MailgunWebhooks\ProcessMailgunWebhookJob; 232 | 233 | dispatch(new ProcessMailgunWebhookJob(WebhookCall::find($id))); 234 | ``` 235 | 236 | ### Performing custom logic 237 | 238 | You can add some custom logic that should be executed before and/or after the scheduling of the queued job by using your own job class. You can do this by specifying your own job class in the `process_webhook_job` key of the `mailgun-webhooks` config file. The class should extend `BinaryCats\MailgunWebhooks\ProcessMailgunWebhookJob`. 239 | 240 | Here's an example: 241 | 242 | ```php 243 | use BinaryCats\MailgunWebhooks\ProcessMailgunWebhookJob; 244 | 245 | class MyCustomMailgunWebhookJob extends ProcessMailgunWebhookJob 246 | { 247 | public function handle() 248 | { 249 | // do some custom stuff beforehand 250 | 251 | parent::handle(); 252 | 253 | // do some custom stuff afterwards 254 | } 255 | } 256 | ``` 257 | ### Handling multiple signing secrets 258 | 259 | When needed might want to the package to handle multiple endpoints and secrets. Here's how to configure that behaviour. 260 | 261 | If you are using the `Route::mailgunWebhooks` macro, you can append the `configKey` as follows: 262 | 263 | ```php 264 | Route::mailgunWebhooks('webhooks/mailgun/{configKey}'); 265 | ``` 266 | 267 | Alternatively, if you are manually defining the route, you can add `configKey` like so: 268 | 269 | ```php 270 | Route::post('webhooks/mailgun/{configKey}', 'BinaryCats\MailgunWebhooks\MailgunWebhooksController'); 271 | ``` 272 | 273 | If this route parameter is present verify middleware will look for the secret using a different config key, by appending the given the parameter value to the default config key. E.g. If Mailgun posts to `webhooks/mailgun/my-named-secret` you'd add a new config named `signing_secret_my-named-secret`. 274 | 275 | Example config might look like: 276 | 277 | ```php 278 | // secret for when Mailgun posts to webhooks/mailgun/account 279 | 'signing_secret_account' => 'whsec_abc', 280 | // secret for when Mailgun posts to webhooks/mailgun/my-named-secret 281 | 'signing_secret_my-named-secret' => 'whsec_123', 282 | ``` 283 | 284 | ### About Mailgun 285 | 286 | [Mailgun](https://www.mailgun.com/) allows you to send transactional or bulk email effortlessly with our SMTP relay and flexible HTTP API. 287 | 288 | ## Changelog 289 | 290 | Please see [CHANGELOG](CHANGELOG.md) for more information about what has changed recently. 291 | 292 | ## Testing 293 | 294 | ```bash 295 | composer test 296 | ``` 297 | 298 | ## Contributing 299 | 300 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 301 | 302 | ## Security 303 | 304 | If you discover any security related issues, please email cyrill.kalita@gmail.com instead of using issue tracker. 305 | 306 | ## Postcardware 307 | 308 | You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. 309 | 310 | ## Credits 311 | 312 | - [Cyrill Kalita](https://github.com/binary-cats) 313 | - [All Contributors](../../contributors) 314 | 315 | Big shout-out to [Spatie](https://spatie.be/) for their work, which is a huge inspiration. 316 | 317 | ## Support us 318 | 319 | Binary Cats is a webdesign agency based in Illinois, US. 320 | 321 | ## License 322 | 323 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 324 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | # This is a sample build configuration for PHP. 2 | # Check our guides at https://confluence.atlassian.com/x/e8YWN for more examples. 3 | # Only use spaces to indent your .yml configuration. 4 | # ----- 5 | # You can specify a custom docker image from Docker Hub as your build environment. 6 | image: php:7.2.25 7 | 8 | pipelines: 9 | default: 10 | - step: 11 | caches: 12 | - composer 13 | script: 14 | - apt-get update && apt-get install -y unzip 15 | - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 16 | - composer install 17 | - vendor/bin/phpunit 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binary-cats/laravel-mailgun-webhooks", 3 | "description": "Handle Mailgun webhooks in a Laravel application", 4 | "keywords": [ 5 | "binary-cats", 6 | "laravel", 7 | "mailgun", 8 | "webhooks" 9 | ], 10 | "homepage": "https://github.com/binary-cats/laravel-mailgun-webhooks", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Cyrill Kalita", 15 | "email": "cyrill.kalita@gmail.com", 16 | "homepage": "https://github.com/binary-cats", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.0", 22 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0", 23 | "spatie/laravel-webhook-client": "^3.0" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0", 27 | "phpunit/phpunit": "^9.4|^10.0|^11.5.3" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "BinaryCats\\MailgunWebhooks\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Tests\\": "tests/" 37 | } 38 | }, 39 | "suggest": { 40 | "binary-cats/laravel-lob-webhooks": "^9.0" 41 | }, 42 | "scripts": { 43 | "coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-html coverage -d pcov.enabled", 44 | "test": "./vendor/bin/phpunit --color=always -vvv" 45 | }, 46 | "config": { 47 | "optimize-autoloader": true, 48 | "sort-packages": true 49 | }, 50 | "extra": { 51 | "branch-alias": { 52 | "dev-master": "9.x-dev" 53 | }, 54 | "laravel": { 55 | "providers": [ 56 | "BinaryCats\\MailgunWebhooks\\MailgunWebhooksServiceProvider" 57 | ] 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config/mailgun-webhooks.php: -------------------------------------------------------------------------------- 1 | env('MAILGUN_WEBHOOK_SECRET'), 10 | 11 | /* 12 | * You can define the job that should be run when a certain webhook hits your application 13 | * here. The key is the name of the Mailgun event type with the `.` replaced by a `_`. 14 | * 15 | * You can find a list of Mailgun webhook types here: 16 | * https://documentation.mailgun.com/en/latest/user_manual.html#events. 17 | * 18 | * The package will automatically convert the keys to lowercase, but you should 19 | * be congnisant of the fact that array keys are case sensitive 20 | */ 21 | 'jobs' => [ 22 | // 'delivered' => \BinaryCats\MailgunWebhooks\Jobs\HandleDelivered::class, 23 | ], 24 | 25 | /* 26 | * The classname of the model to be used. The class should equal or extend 27 | * Spatie\WebhookClient\Models\WebhookCall 28 | */ 29 | 'model' => \Spatie\WebhookClient\Models\WebhookCall::class, 30 | 31 | /* 32 | * The classname of the model to be used. The class should equal or extend 33 | * BinaryCats\MailgunWebhooks\ProcessMailgunWebhookJob 34 | */ 35 | 'process_webhook_job' => \BinaryCats\MailgunWebhooks\ProcessMailgunWebhookJob::class, 36 | ]; 37 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./tests 9 | 10 | 11 | ./tests 12 | 13 | 14 | 15 | 16 | ./src 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Contracts/WebhookEvent.php: -------------------------------------------------------------------------------- 1 | attributes = $attributes; 24 | } 25 | 26 | /** 27 | * Static event constructor. 28 | * 29 | * @param mixed[] $data 30 | * @return static 31 | */ 32 | public static function constructFrom(array $data): self 33 | { 34 | return new static($data); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Exceptions/UnexpectedValueException.php: -------------------------------------------------------------------------------- 1 | $this->getMessage()], 400); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Exceptions/WebhookFailed.php: -------------------------------------------------------------------------------- 1 | getKey()}` of type `{$webhookCall->getAttribute('type')} because the configured jobclass `$jobClass` does not exist."); 34 | } 35 | 36 | /** 37 | * @param \Spatie\WebhookClient\Models\WebhookCall $webhookCall 38 | * @return static 39 | */ 40 | public static function missingType(WebhookCall $webhookCall): self 41 | { 42 | return new static("Webhook call id `{$webhookCall->getKey()}` did not contain a type. Valid Mailgun webhook calls should always contain a type."); 43 | } 44 | 45 | /** 46 | * @param \Illuminate\Http\Request $request 47 | * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response 48 | */ 49 | public function render($request) 50 | { 51 | return response(['error' => $this->getMessage()], 400); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Jobs/HandleDelivered.php: -------------------------------------------------------------------------------- 1 | webhookCall = $webhookCall; 28 | } 29 | 30 | /** 31 | * Execute the job. 32 | * 33 | * @return void 34 | */ 35 | public function handle() 36 | { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/MailgunSignatureValidator.php: -------------------------------------------------------------------------------- 1 | signature($request); 22 | 23 | $secret = $config->signingSecret; 24 | 25 | try { 26 | Webhook::constructEvent($request->all(), $signature, $secret); 27 | } catch (Exception $exception) { 28 | // make the app aware 29 | report($exception); 30 | 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | /** 38 | * Validate the incoming signature' schema. 39 | * 40 | * @param \Illuminate\Http\Request $request 41 | * @return string[] 42 | */ 43 | protected function signature(Request $request): array 44 | { 45 | $validated = $request->validate([ 46 | 'signature.signature' => 'bail|required', 47 | 'signature.timestamp' => 'required', 48 | 'signature.token' => 'required', 49 | ]); 50 | 51 | return $validated['signature']; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MailgunWebhooksController.php: -------------------------------------------------------------------------------- 1 | 'mailgun', 23 | 'signing_secret' => ($configKey) ? 24 | config('mailgun-webhooks.signing_secret_'.$configKey) : 25 | config('mailgun-webhooks.signing_secret'), 26 | 'signature_header_name' => null, 27 | 'signature_validator' => MailgunSignatureValidator::class, 28 | 'webhook_profile' => ProcessEverythingWebhookProfile::class, 29 | 'webhook_model' => config('mailgun-webhooks.model'), 30 | 'process_webhook_job' => config('mailgun-webhooks.process_webhook_job'), 31 | ]); 32 | 33 | return (new WebhookProcessor($request, $webhookConfig))->process(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MailgunWebhooksServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 18 | $this->publishes([ 19 | __DIR__.'/../config/mailgun-webhooks.php' => config_path('mailgun-webhooks.php'), 20 | ], 'config'); 21 | } 22 | 23 | Route::macro('mailgunWebhooks', function ($url) { 24 | return Route::post($url, '\BinaryCats\MailgunWebhooks\MailgunWebhooksController'); 25 | }); 26 | } 27 | 28 | /** 29 | * Register application services. 30 | * 31 | * @return void 32 | */ 33 | public function register() 34 | { 35 | $this->mergeConfigFrom(__DIR__.'/../config/mailgun-webhooks.php', 'mailgun-webhooks'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ProcessMailgunWebhookJob.php: -------------------------------------------------------------------------------- 1 | webhookCall, "payload.{$this->key}"); 27 | 28 | if (! $type) { 29 | throw WebhookFailed::missingType($this->webhookCall); 30 | } 31 | 32 | event($this->determineEventKey($type), $this->webhookCall); 33 | 34 | $jobClass = $this->determineJobClass($type); 35 | 36 | if ('' === $jobClass) { 37 | return; 38 | } 39 | 40 | if (! class_exists($jobClass)) { 41 | throw WebhookFailed::jobClassDoesNotExist($jobClass, $this->webhookCall); 42 | } 43 | 44 | dispatch(new $jobClass($this->webhookCall)); 45 | } 46 | 47 | /** 48 | * @param string $eventType 49 | * @return string 50 | */ 51 | protected function determineJobClass(string $eventType): string 52 | { 53 | return config($this->determineJobConfigKey($eventType), ''); 54 | } 55 | 56 | /** 57 | * @param string $eventType 58 | * @return string 59 | */ 60 | protected function determineJobConfigKey(string $eventType): string 61 | { 62 | return Str::of($eventType) 63 | ->replace('.', '_') 64 | ->prepend('mailgun-webhooks.jobs.') 65 | ->lower(); 66 | } 67 | 68 | /** 69 | * @param string $eventType 70 | * @return string 71 | */ 72 | protected function determineEventKey(string $eventType): string 73 | { 74 | return Str::of($eventType) 75 | ->prepend('mailgun-webhooks::') 76 | ->lower(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Webhook.php: -------------------------------------------------------------------------------- 1 | verify()) { 23 | throw WebhookFailed::invalidSignature(); 24 | } 25 | 26 | // Make an event 27 | return Event::constructFrom($payload); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/WebhookSignature.php: -------------------------------------------------------------------------------- 1 | signatureArray = $signatureArray; 33 | $this->secret = $secret; 34 | } 35 | 36 | /** 37 | * Static accessor into the class constructor. 38 | * 39 | * @param string[] $signatureArray 40 | * @param string $secret 41 | * @return WebhookSignature static 42 | */ 43 | public static function make($signatureArray, string $secret) 44 | { 45 | return new static(Arr::wrap($signatureArray), $secret); 46 | } 47 | 48 | /** 49 | * True if the signature is valid. 50 | * 51 | * @return bool 52 | */ 53 | public function verify(): bool 54 | { 55 | return hash_equals($this->signature, $this->computeSignature()); 56 | } 57 | 58 | /** 59 | * Compute expected signature. 60 | * 61 | * @return string 62 | */ 63 | protected function computeSignature() 64 | { 65 | $comparator = implode('', [ 66 | $this->timestamp, 67 | $this->token, 68 | ]); 69 | 70 | return hash_hmac('sha256', $comparator, $this->secret); 71 | } 72 | 73 | /** 74 | * Magically access items from signature array. 75 | * 76 | * @param string $attribute 77 | * @return mixed 78 | */ 79 | public function __get($attribute) 80 | { 81 | return Arr::get($this->signatureArray, $attribute); 82 | } 83 | } 84 | --------------------------------------------------------------------------------