mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-10-31 00:07:31 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			533 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			533 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * Invoice Ninja (https://invoiceninja.com).
 | |
|  *
 | |
|  * @link https://github.com/invoiceninja/invoiceninja source repository
 | |
|  *
 | |
|  * @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
 | |
|  *
 | |
|  * @license https://www.elastic.co/licensing/elastic-license
 | |
|  */
 | |
| 
 | |
| namespace App\Jobs\Mailgun;
 | |
| 
 | |
| use App\Utils\Ninja;
 | |
| use App\Models\Company;
 | |
| use App\Models\SystemLog;
 | |
| use App\Libraries\MultiDB;
 | |
| use Illuminate\Bus\Queueable;
 | |
| use App\Jobs\Util\SystemLogger;
 | |
| use App\Models\QuoteInvitation;
 | |
| use App\Models\CreditInvitation;
 | |
| use App\Models\InvoiceInvitation;
 | |
| use Illuminate\Queue\SerializesModels;
 | |
| use Turbo124\Beacon\Facades\LightLogs;
 | |
| use App\Models\PurchaseOrderInvitation;
 | |
| use Illuminate\Queue\InteractsWithQueue;
 | |
| use App\Models\RecurringInvoiceInvitation;
 | |
| use Illuminate\Contracts\Queue\ShouldQueue;
 | |
| use Illuminate\Foundation\Bus\Dispatchable;
 | |
| use App\DataMapper\Analytics\Mail\EmailSpam;
 | |
| use App\DataMapper\Analytics\Mail\EmailBounce;
 | |
| use App\Notifications\Ninja\EmailSpamNotification;
 | |
| use App\Notifications\Ninja\EmailBounceNotification;
 | |
| 
 | |
| class ProcessMailgunWebhook implements ShouldQueue
 | |
| {
 | |
|     use Dispatchable;
 | |
|     use InteractsWithQueue;
 | |
|     use Queueable;
 | |
|     use SerializesModels;
 | |
| 
 | |
|     public $tries = 1;
 | |
| 
 | |
|     public $invitation;
 | |
| 
 | |
|     private $entity;
 | |
| 
 | |
|     private string $message_id = '';
 | |
| 
 | |
|     private array $default_response =  [
 | |
|         'recipients' => '',
 | |
|         '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 'failed':
 | |
|                 return $this->processBounce();
 | |
|             case 'complained':
 | |
|                 return $this->processSpamComplaint();
 | |
|             case 'opened':
 | |
|                 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();
 | |
| 
 | |
|         $sl = $this->getSystemLog($this->request['MessageID']);
 | |
| 
 | |
|         /** Prevents Gmail tracking from firing inappropriately */
 | |
|         if($this->request['signature']['timestamp'] < $sl->log['signature']['timestamp'] + 3) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         $event = [
 | |
|             'bounce_id' => '',
 | |
|             'recipient' => $this->request['event-data']['recipient'] ?? '',
 | |
|             'status' => $this->request['event-data']['event'] ?? '',
 | |
|             'delivery_message' => collect($this->request['event-data']['client-info'])->implode(" ") ?? '',
 | |
|             'server' => collect($this->request['event-data']['geolocation'])->implode(" - ") ??  '',
 | |
|             'server_ip' => $this->request['event-data']['ip'] ?? '',
 | |
|             'date' => \Carbon\Carbon::parse($this->request['event-data']['timestamp'])->format('Y-m-d H:i:s') ?? '',
 | |
|         ];
 | |
| 
 | |
|         if($sl) {
 | |
|             $data = $sl->log;
 | |
|             $data['history']['events'][] = $event;
 | |
|             $this->updateSystemLog($sl, $data);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|     {
 | |
|       "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 <alice@example.com>",
 | |
|             "message-id": "20130503182626.18666.16540@mail.invoicing.co",
 | |
|             "from": "Bob <bob@mail.invoicing.co>",
 | |
|             "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 <alice@example.com>",
 | |
|         "message-id": "20130503192659.13651.20287@mail.invoicing.co",
 | |
|         "from": "Bob <bob@mail.invoicing.co>",
 | |
|         "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['event-data']['envelope']['sender'],
 | |
|             $this->message_id
 | |
|         );
 | |
| 
 | |
|         LightLogs::create($bounce)->queue();
 | |
| 
 | |
|         $sl = $this->getSystemLog($this->message_id);
 | |
| 
 | |
|         $event = [
 | |
|             'bounce_id' => $this->request['event-data']['id'],
 | |
|             'recipient' => $this->request['event-data']['recipient'] ?? '',
 | |
|             'status' => $this->request['event-data']['event'] ?? '',
 | |
|             'delivery_message' => $this->request['event-data']['delivery-status']['description'] ?? $this->request['event-data']['delivery-status']['message'] ?? '',
 | |
|             'server' => $this->request['event-data']['delivery-status']['mx-host'] ??  '',
 | |
|             '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') ?? '',
 | |
|         ];
 | |
| 
 | |
|         if($sl) {
 | |
|             $data = $sl->log;
 | |
|             $data['history']['events'][] = $event;
 | |
|             $this->updateSystemLog($sl, $data);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /*
 | |
|     {
 | |
|       "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 <alice@example.com>",
 | |
|             "message-id": "20110215055645.25246.63817@mail.invoicing.co",
 | |
|             "from": "Bob <bob@mail.invoicing.co>",
 | |
|             "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['event-data']['message']['headers']['from'],
 | |
|             $this->message_id
 | |
|         );
 | |
| 
 | |
|         LightLogs::create($spam)->queue();
 | |
| 
 | |
|         $sl = $this->getSystemLog($this->message_id);
 | |
| 
 | |
|         $event = [
 | |
|             'bounce_id' => '',
 | |
|             'recipient' => $this->request['event-data']['recipient'] ?? '',
 | |
|             'status' => $this->request['event-data']['event'] ?? '',
 | |
|             'delivery_message' => 'Spam Complaint',
 | |
|             'server' => '',
 | |
|             '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') ?? '',
 | |
|         ];
 | |
| 
 | |
|         if($sl) {
 | |
|             $data = $sl->log;
 | |
|             $data['history']['events'][] = $event;
 | |
|             $this->updateSystemLog($sl, $data);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     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' => '',
 | |
|             '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') ?? '',
 | |
|         ];
 | |
| 
 | |
|     }
 | |
| 
 | |
| }
 |