diff --git a/app/Http/Controllers/MailgunWebhookController.php b/app/Http/Controllers/MailgunWebhookController.php new file mode 100644 index 000000000000..812501d3eb27 --- /dev/null +++ b/app/Http/Controllers/MailgunWebhookController.php @@ -0,0 +1,40 @@ +all(); + + if (\abs(\time() - $request['signature']['timestamp']) > 15) + return response()->json(['message' => 'Success'], 200); + + if(\hash_equals(\hash_hmac('sha256', $input['signature']['timestamp'] . $input['signature']['token'], config('services.mailgun.webhook_signing_key')), $input['signature']['signature'])) + ProcessMailgunWebhook::dispatch($request->all())->delay(10); + + return response()->json(['message' => 'Success.'], 200); + } +} diff --git a/app/Jobs/Mailgun/ProcessMailgunWebhook.php b/app/Jobs/Mailgun/ProcessMailgunWebhook.php new file mode 100644 index 000000000000..1411635e417f --- /dev/null +++ b/app/Jobs/Mailgun/ProcessMailgunWebhook.php @@ -0,0 +1,484 @@ + '', + 'subject' => 'Message not found.', + 'entity' => '', + 'entity_id' => '', + 'events' => [], + ]; + + /** + * Create a new job instance. + * + */ + public function __construct(private array $request) + { + } + + private function getSystemLog(string $message_id): ?SystemLog + { + return SystemLog::query() + ->where('company_id', $this->invitation->company_id) + ->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE) + ->whereJsonContains('log', ['MessageID' => $this->message_id]) + ->orderBy('id', 'desc') + ->first(); + + } + + private function updateSystemLog(SystemLog $system_log, array $data): void + { + $system_log->log = $data; + $system_log->save(); + } + + + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + nlog($this->request); + + if(!$this->request['event-data']['tags'][0]) + return; + + MultiDB::findAndSetDbByCompanyKey($this->request['event-data']['tags'][0]); + $company = Company::where('company_key', $this->request['event-data']['tags'][0])->first(); + + if ($company && $this->request['event-data']['event'] == 'complained' && config('ninja.notification.slack')) { + $company->notification(new EmailSpamNotification($company))->ninja(); + } + + $this->message_id = $this->request['event-data']['message']['headers']['message-id']; + + $this->request['MessageID'] = $this->message_id; + + $this->invitation = $this->discoverInvitation($this->message_id); + + if (!$this->invitation) { + return; + } + + if (isset($this->request['event-details']['delivery-status']['message'])) { + $this->invitation->email_error = $this->request['event-details']['delivery-status']['message']; + } + + switch ($this->request['event-data']['event']) { + case 'delivered': + return $this->processDelivery(); + case 'Bounce': + return $this->processBounce(); + case 'complained': + return $this->processSpamComplaint(); + case 'Open': + return $this->processOpen(); + default: + # code... + break; + } + } + +/* +{ + "signature": { + "token": "7f388cf8096aa0bca1477aee9d91e156c61f8fa8282c7f1c0c", + "timestamp": "1705376308", + "signature": "a22b7c3dd4861e27a1664cef3611a1954c0665cfcaca9b8f35ee216243a4ce3f" + }, + "event-data": { + "id": "Ase7i2zsRYeDXztHGENqRA", + "timestamp": 1521243339.873676, + "log-level": "info", + "event": "opened", + "message": { + "headers": { + "message-id": "20130503182626.18666.16540@mail.invoicing.co" + } + }, + "recipient": "alice@example.com", + "recipient-domain": "example.com", + "ip": "50.56.129.169", + "geolocation": { + "country": "US", + "region": "CA", + "city": "San Francisco" + }, + "client-info": { + "client-os": "Linux", + "device-type": "desktop", + "client-name": "Chrome", + "client-type": "browser", + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31" + }, + "campaigns": [], + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "user-variables": { + "my_var_1": "Mailgun Variable #1", + "my-var-2": "awesome" + } + } +} +*/ + private function processOpen() + { + $this->invitation->opened_date = now(); + $this->invitation->save(); + + SystemLogger::dispatch( + $this->request, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_OPENED, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ); + } + +/* +{ + "signature": { + "token": "70b91a64ed0f1bdf90fb9c6ea7e3c31d5792a3d0945ffc20fe", + "timestamp": "1705376276", + "signature": "ba96f841fc236e1bf5840b02fad512d0bd15b0731b5e6b154764c7a05f7ee999" + }, + "event-data": { + "id": "CPgfbmQMTCKtHW6uIWtuVe", + "timestamp": 1521472262.908181, + "log-level": "info", + "event": "delivered", + "delivery-status": { + "tls": true, + "mx-host": "smtp-in.example.com", + "code": 250, + "description": "", + "session-seconds": 0.4331989288330078, + "utf8": true, + "attempt-no": 1, + "message": "OK", + "certificate-verified": true + }, + "flags": { + "is-routed": false, + "is-authenticated": true, + "is-system-test": false, + "is-test-mode": false + }, + "envelope": { + "transport": "smtp", + "sender": "bob@mail.invoicing.co", + "sending-ip": "209.61.154.250", + "targets": "alice@example.com" + }, + "message": { + "headers": { + "to": "Alice ", + "message-id": "20130503182626.18666.16540@mail.invoicing.co", + "from": "Bob ", + "subject": "Test delivered webhook" + }, + "attachments": [], + "size": 111 + }, + "recipient": "alice@example.com", + "recipient-domain": "example.com", + "storage": { + "url": "https://se.api.mailgun.net/v3/domains/mail.invoicing.co/messages/message_key", + "key": "message_key" + }, + "campaigns": [], + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "user-variables": { + "my_var_1": "Mailgun Variable #1", + "my-var-2": "awesome" + } + } +} +*/ + private function processDelivery() + { + $this->invitation->email_status = 'delivered'; + $this->invitation->save(); + + $sl = $this->getSystemLog($this->request['MessageID']); + + if($sl) { + $data = $sl->log; + $data['history']['events'][] = $this->getEvent(); + $this->updateSystemLog($sl, $data); + return; + } + + $data = array_merge($this->request, ['history' => $this->fetchMessage()]); + + SystemLogger::dispatch( + $data, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_DELIVERY, + SystemLog::TYPE_WEBHOOK_RESPONSE, + $this->invitation->contact->client, + $this->invitation->company + ); + } + + /* + { + "signature": { + "token": "7494a9089874cda8c478ba7608d15158229d5b8de41ddfdae8", + "timestamp": "1705376357", + "signature": "a8ba107ac919626526b76e46e43ba40e629833fafab8728d402f28476bad0c7b" + }, + "event-data": { + "id": "G9Bn5sl1TC6nu79C8C0bwg", + "timestamp": 1521233195.375624, + "log-level": "error", + "event": "failed", + "severity": "permanent", + "reason": "suppress-bounce", + "delivery-status": { + "attempt-no": 1, + "message": "", + "code": 605, + "enhanced-code": "", + "description": "Not delivering to previously bounced address", + "session-seconds": 0 + }, + "flags": { + "is-routed": false, + "is-authenticated": true, + "is-system-test": false, + "is-test-mode": false + }, + "envelope": { + "sender": "bob@mail.invoicing.co", + "transport": "smtp", + "targets": "alice@example.com" + }, + "message": { + "headers": { + "to": "Alice ", + "message-id": "20130503192659.13651.20287@mail.invoicing.co", + "from": "Bob ", + "subject": "Test permanent_fail webhook" + }, + "attachments": [], + "size": 111 + }, + "recipient": "alice@example.com", + "recipient-domain": "example.com", + "storage": { + "url": "https://se.api.mailgun.net/v3/domains/mail.invoicing.co/messages/message_key", + "key": "message_key" + }, + "campaigns": [], + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "user-variables": { + "my_var_1": "Mailgun Variable #1", + "my-var-2": "awesome" + } + } +} +*/ + private function processBounce() + { + $this->invitation->email_status = 'bounced'; + $this->invitation->save(); + + $bounce = new EmailBounce( + $this->request['event-data']['tags'][0], + $this->request['From'], + $this->request['MessageID'] + ); + + LightLogs::create($bounce)->queue(); + + SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + } + +/* +{ + "signature": { + "token": "d7be371deef49c8b187119df295e3eb17fd1974d513a4be2cb", + "timestamp": "1705376380", + "signature": "52f31c75b492d67be906423279e0effe563e28790ee65ba23a1b30006df649df" + }, + "event-data": { + "id": "-Agny091SquKnsrW2NEKUA", + "timestamp": 1521233123.501324, + "log-level": "warn", + "event": "complained", + "envelope": { + "sending-ip": "173.193.210.33" + }, + "flags": { + "is-test-mode": false + }, + "message": { + "headers": { + "to": "Alice ", + "message-id": "20110215055645.25246.63817@mail.invoicing.co", + "from": "Bob ", + "subject": "Test complained webhook" + }, + "attachments": [], + "size": 111 + }, + "recipient": "alice@example.com", + "campaigns": [], + "tags": [ + "my_tag_1", + "my_tag_2" + ], + "user-variables": { + "my_var_1": "Mailgun Variable #1", + "my-var-2": "awesome" + } + } +} +*/ + private function processSpamComplaint() + { + $this->invitation->email_status = 'spam'; + $this->invitation->save(); + + $spam = new EmailSpam( + $this->request['event-data']['tags'][0], + $this->request['From'], + $this->request['MessageID'] + ); + + LightLogs::create($spam)->queue(); + + SystemLogger::dispatch($this->request, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company); + + if (config('ninja.notification.slack')) { + $this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja(); + } + } + + private function discoverInvitation($message_id) + { + $invitation = false; + + if ($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'invoice'; + return $invitation; + } elseif ($invitation = QuoteInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'quote'; + return $invitation; + } elseif ($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'recurring_invoice'; + return $invitation; + } elseif ($invitation = CreditInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'credit'; + return $invitation; + } elseif ($invitation = PurchaseOrderInvitation::where('message_id', $message_id)->first()) { + $this->entity = 'purchase_order'; + return $invitation; + } else { + return $invitation; + } + } + + private function fetchMessage(): array + { + if(strlen($this->message_id) < 2) { + return $this->default_response; + } + + try { + + $recipients = $this->request['event-data']['recipient'] ?? ''; + $subject = $this->request['event-data']['message']['headers']['subject'] ?? ''; + + return [ + 'recipients' => $recipients, + 'subject' => $subject, + 'entity' => $this->entity ?? '', + 'entity_id' => $this->invitation->{$this->entity}->hashed_id ?? '', + 'events' => [$this->getEvent()], + ]; + + } catch (\Exception $e) { + + return $this->default_response; + + } + } + + private function getEvent(): array + { + $recipients = $this->request['event-data']['recipient'] ?? ''; + + return [ + 'bounce_id' => $this->request['event-data']['id'] ?? '', + 'recipient' => $recipients, + 'status' => $this->request['event-data']['event'] ?? '', + 'delivery_message' => $this->request['event-details']['delivery-status']['description'] ?? $this->request['event-details']['delivery-status']['message'] ?? '', + 'server' => $this->request['event-data']['recipient-domain'] ?? '', + 'server_ip' => $this->request['event-data']['envelope']['sending-ip'] ?? '', + 'date' => \Carbon\Carbon::parse($this->request['event-data']['timestamp'])->format('Y-m-d H:i:s') ?? '', + ]; + + } + +} diff --git a/app/Listeners/Mail/MailSentListener.php b/app/Listeners/Mail/MailSentListener.php index 0f55dee35f27..5bd570922606 100644 --- a/app/Listeners/Mail/MailSentListener.php +++ b/app/Listeners/Mail/MailSentListener.php @@ -61,7 +61,7 @@ class MailSentListener implements ShouldQueue return; } - $invitation->message_id = $message_id; + $invitation->message_id = str_replace(["<",">"], "", $message_id); $invitation->save(); } } catch (\Exception $e) { @@ -76,18 +76,14 @@ class MailSentListener implements ShouldQueue foreach (MultiDB::$dbs as $db) { if ($invitation = InvoiceInvitation::on($db)->where('key', $key)->first()) { - // $invitation->invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client"); return $invitation; } elseif ($invitation = QuoteInvitation::on($db)->where('key', $key)->first()) { - // $invitation->quote->sendEvent(Webhook::EVENT_SENT_QUOTE, "client"); return $invitation; } elseif ($invitation = RecurringInvoiceInvitation::on($db)->where('key', $key)->first()) { return $invitation; } elseif ($invitation = CreditInvitation::on($db)->where('key', $key)->first()) { - // $invitation->credit->sendEvent(Webhook::EVENT_SENT_CREDIT, "client"); return $invitation; } elseif ($invitation = PurchaseOrderInvitation::on($db)->where('key', $key)->first()) { - // $invitation->purchase_order->sendEvent(Webhook::EVENT_SENT_PURCHASE_ORDER, "vendor"); return $invitation; } } diff --git a/config/services.php b/config/services.php index f5e6067c0337..0b933611ac62 100644 --- a/config/services.php +++ b/config/services.php @@ -22,6 +22,7 @@ return [ 'domain' => env('MAILGUN_DOMAIN', ''), 'secret' => env('MAILGUN_SECRET', ''), 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + 'webhook_signing_key' => env('MAILGUN_WEBHOOK_SIGNING_KEY', ''), 'scheme' => 'https', ], diff --git a/routes/api.php b/routes/api.php index eb5e8963d846..7024c67d3131 100644 --- a/routes/api.php +++ b/routes/api.php @@ -47,6 +47,7 @@ use App\Http\Controllers\InAppPurchase\AppleController; use App\Http\Controllers\InvoiceController; use App\Http\Controllers\LicenseController; use App\Http\Controllers\LogoutController; +use App\Http\Controllers\MailgunWebhookController; use App\Http\Controllers\MigrationController; use App\Http\Controllers\OneTimeTokenController; use App\Http\Controllers\PaymentController; @@ -417,7 +418,9 @@ Route::match(['get', 'post'], 'payment_notification_webhook/{company_key}/{compa ->middleware('throttle:1000,1') ->name('payment_notification_webhook'); + Route::post('api/v1/postmark_webhook', [PostMarkController::class, 'webhook'])->middleware('throttle:1000,1'); +Route::post('api/v1/mailgun_webhook', [MailgunWebhookController::class, 'webhook'])->middleware('throttle:1000,1'); Route::get('token_hash_router', [OneTimeTokenController::class, 'router'])->middleware('throttle:500,1'); Route::get('webcron', [WebCronController::class, 'index'])->middleware('throttle:100,1'); Route::post('api/v1/get_migration_account', [HostedMigrationController::class, 'getAccount'])->middleware('guest')->middleware('throttle:100,1');