Merge branch 'v5-develop' into v5-develop

Signed-off-by: David Bomba <turbo124@gmail.com>
This commit is contained in:
David Bomba 2024-03-17 22:37:30 +11:00 committed by GitHub
commit d752f03b82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2597 additions and 1586 deletions

View File

@ -13,5 +13,10 @@ namespace Illuminate\Contracts\Mail
{
return true;
}
public function brevo_config(string $key)
{
return true;
}
}
}

View File

@ -451,6 +451,8 @@ class CompanySettings extends BaseSettings
public $mailgun_endpoint = 'api.mailgun.net'; //api.eu.mailgun.net
public $brevo_secret = '';
public $auto_bill_standard_invoices = false;
public $email_alignment = 'center'; // center , left, right
@ -530,6 +532,7 @@ class CompanySettings extends BaseSettings
'postmark_secret' => 'string',
'mailgun_secret' => 'string',
'mailgun_domain' => 'string',
'brevo_secret' => 'string',
'send_email_on_mark_paid' => 'bool',
'vendor_portal_enable_uploads' => 'bool',
'besr_id' => 'string',

View File

@ -213,7 +213,7 @@ class SettingsData
public bool $show_accept_quote_terms = false; //@TODO ben to confirm
public string $email_sending_method = 'default'; // enum 'default', 'gmail', 'office365', 'client_postmark', 'client_mailgun' //@implemented
public string $email_sending_method = 'default'; // enum 'default', 'gmail', 'office365', 'client_postmark', 'client_mailgun' , 'client_brevo' //@implemented
public string $gmail_sending_user_id = '0'; //@implemented
@ -433,6 +433,8 @@ class SettingsData
public string $mailgun_endpoint = 'api.mailgun.net'; // api.eu.mailgun.net
public string $brevo_secret = '';
public bool $auto_bill_standard_invoices = false;
public string $email_alignment = 'center'; // center, left, right

View File

@ -0,0 +1,72 @@
<?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\Http\Controllers;
use App\Jobs\Brevo\ProcessBrevoWebhook;
use Illuminate\Http\Request;
/**
* Class PostMarkController.
*/
class BrevoController extends BaseController
{
private $invitation;
public function __construct()
{
}
/**
* Process Postmark Webhook.
*
*
* @OA\Post(
* path="/api/v1/postmark_webhook",
* operationId="postmarkWebhook",
* tags={"postmark"},
* summary="Processing webhooks from PostMark",
* description="Adds an credit to the system",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="Returns the saved credit object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Credit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function webhook(Request $request)
{
if ($request->has('token') && $request->get('token') == config('services.brevo.key')) {
ProcessBrevoWebhook::dispatch($request->all())->delay(10);
return response()->json(['message' => 'Success'], 200);
}
return response()->json(['message' => 'Unauthorized'], 403);
}
}

View File

@ -0,0 +1,492 @@
<?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\Brevo;
use App\DataMapper\Analytics\Mail\EmailBounce;
use App\DataMapper\Analytics\Mail\EmailSpam;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Models\SystemLog;
use App\Notifications\Ninja\EmailBounceNotification;
use App\Notifications\Ninja\EmailSpamNotification;
use Brevo\Client\Configuration;
use Brevo\Client\Model\GetTransacEmailContentEvents;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Brevo\Client\Api\TransactionalEmailsApi;
use Turbo124\Beacon\Facades\LightLogs;
class ProcessBrevoWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $invitation;
private $entity;
private array $default_response = [
'recipients' => '',
'subject' => 'Message not found.',
'entity' => '',
'entity_id' => '',
'events' => [],
];
private ?Company $company = null;
/**
* 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', ['message-id' => $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()
{
MultiDB::findAndSetDbByCompanyKey($this->request['tags'][0]);
$this->company = Company::where('company_key', $this->request['tags'][0])->first();
$this->invitation = $this->discoverInvitation($this->request['message-id']);
if ($this->company && $this->request['event'] == 'spam' && config('ninja.notification.slack')) {
$this->company->notification(new EmailSpamNotification($this->company))->ninja();
}
if (!$this->invitation) {
return;
}
if (array_key_exists('reason', $this->request)) {
$this->invitation->email_error = $this->request['reason'];
}
switch ($this->request['event']) {
case 'delivered':
return $this->processDelivery();
case 'soft_bounce':
case 'hard_bounce':
case 'invalid_email':
case 'blocked':
if ($this->request['subject'] == ctrans('texts.confirmation_subject')) {
$this->company->notification(new EmailBounceNotification($this->request['email']))->ninja();
}
return $this->processBounce();
case 'spam':
return $this->processSpamComplaint();
case 'unique_opened':
case 'opened':
case 'click':
return $this->processOpen();
default:
# code...
break;
}
}
// {
// "id": 948562,
// "email": "test@example.com",
// "message-id": "<202312211546.94160606300@smtp-relay.mailin.fr>",
// "date": "2023-12-21 18:34:42",
// "tags": [
// "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV"
// ],
// "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]",
// "event": "unique_opened",
// "subject": "Reminder: Invoice 0002 from Untitled Company",
// "sending_ip": "74.125.208.8",
// "ts": 1703180082,
// "ts_epoch": 1703180082286,
// "ts_event": 1703180082,
// "link": "",
// "sender_email": "user@example.com"
// }
// {
// "id": 948562,
// "email": "test@example.com",
// "message-id": "<202312211555.14720890391@smtp-relay.mailin.fr>",
// "date": "2023-12-21 18:34:53",
// "tags": [
// "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV"
// ],
// "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]",
// "event": "opened",
// "subject": "Reminder: Invoice 0002 from Untitled Company",
// "sending_ip": "74.125.208.8",
// "ts": 1703180093,
// "ts_epoch": 1703180093075,
// "ts_event": 1703180093,
// "link": "",
// "sender_email": "user@example.com"
// }
// {
// "id": 948562,
// "email": "paul@wer-ner.de",
// "message-id": "<202312280812.10968711117@smtp-relay.mailin.fr>",
// "date": "2023-12-28 09:20:18",
// "tags": [
// "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV"
// ],
// "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]",
// "event": "click",
// "subject": "Reminder: Invoice 0002 from Untitled Company",
// "sending_ip": "79.235.133.157",
// "ts": 1703751618,
// "ts_epoch": 1703751618831,
// "ts_event": 1703751618,
// "link": "http://localhost/client/invoice/CssCvqOcKsenMCgYJ7EUNRZwxSDGUkau",
// "sender_email": "user@example.com"
// }
private function processOpen()
{
$this->invitation->opened_date = now();
$this->invitation->save();
$data = array_merge($this->request, ['history' => $this->fetchMessage()]);
$sl = $this->getSystemLog($this->request['message-id']);
if ($sl) {
$this->updateSystemLog($sl, $data);
return;
}
(
new SystemLogger(
$data,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_OPENED,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
)
)->handle();
}
// {
// "id": 948562,
// "email": "test@example",
// "message-id": "<202312211742.12697514322@smtp-relay.mailin.fr>",
// "date": "2023-12-21 18:42:31",
// "tags": [
// "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV"
// ],
// "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]",
// "event": "delivered",
// "subject": "Reminder: Invoice 0002 from Untitled Company",
// "sending_ip": "77.32.148.26",
// "ts_event": 1703180551,
// "ts": 1703180551,
// "reason": "sent",
// "ts_epoch": 1703180551324,
// "sender_email": "user@example.com"
// }
private function processDelivery()
{
$this->invitation->email_status = 'delivered';
$this->invitation->save();
$data = array_merge($this->request, ['history' => $this->fetchMessage()]);
$sl = $this->getSystemLog($this->request['message-id']);
if ($sl) {
$this->updateSystemLog($sl, $data);
return;
}
(
new SystemLogger(
$data,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_DELIVERY,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
)
)->handle();
}
// {
// "id": 948562,
// "email": "ryder36@example.net",
// "message-id": "<202312211744.55168080257@smtp-relay.mailin.fr>",
// "date": "2023-12-21 18:44:52",
// "tags": [
// "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV"
// ],
// "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]",
// "event": "soft_bounce",
// "subject": "Reminder: Invoice 0001 from Untitled Company",
// "sending_ip": "77.32.148.26",
// "ts_event": 1703180692,
// "ts": 1703180692,
// "reason": "Unable to find MX of domain example.net",
// "ts_epoch": 1703180692382,
// "sender_email": "user@example.com"
// }
// {
// "id": 948562,
// "email": "gloria46@example.com",
// "message-id": "<202312211744.57456703957@smtp-relay.mailin.fr>",
// "date": "2023-12-21 18:44:54",
// "tags": [
// "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV"
// ],
// "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]",
// "event": "hard_bounce",
// "subject": "Reminder: Invoice 0001 from Untitled Company",
// "sending_ip": "77.32.148.25",
// "ts_event": 1703180694,
// "ts": 1703180694,
// "reason": "blocked by Admin",
// "ts_epoch": 1703180694175,
// "sender_email": "user@example.com"
// }
// {
// "event" : "invalid_email",
// "email" : "example@example.com",
// "id" : 1,
// "date" : "yyyy-mm-dd hh:i:s",
// "message-id" : "<xxx@msgid.domain>",
// "subject" : "Test subject",
// "tag" : "<defined-tag>",//json of array
// "tags": [
// "company_key"
// ],
// "sending_ip" : "xxx.xx.xxx.xx",
// "ts_epoch" : 1534486682000,
// "template_id" : 1,
// "sender_email": "user@example.com",
// }
// {
// "id": 948562,
// "email": "neoma.langosh@example.com",
// "message-id": "<202312211745.65538701430@smtp-relay.mailin.fr>",
// "date": "2023-12-21 18:45:48",
// "tags": [
// "gMtwiTIJtJxklXCj1OUFANgY6YYynQxV"
// ],
// "tag": "[\"gMtwiTIJtJxklXCj1OUFANgY6YYynQxV\"]",
// "event": "blocked",
// "subject": "Reminder: Invoice 0001 from Untitled Company",
// "ts_event": 1703180748,
// "ts": 1703180748,
// "reason": "blocked : due to blacklist user",
// "ts_epoch": 1703180748987,
// "sender_email": "user@example.com"
// }
private function processBounce()
{
$this->invitation->email_status = 'bounced';
$this->invitation->save();
$bounce = new EmailBounce(
$this->request['tags'][0],
$this->request['sender_email'], // TODO: @turbo124 is this the recipent?
$this->request['message-id']
);
LightLogs::create($bounce)->send();
$data = array_merge($this->request, ['history' => $this->fetchMessage()]);
$sl = $this->getSystemLog($this->request['message-id']);
if ($sl) {
$this->updateSystemLog($sl, $data);
return;
}
(new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle();
// if(config('ninja.notification.slack'))
// $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja();
}
// {
// "event" : "spam",
// "email" : "example@example.com",
// "id" : 1,
// "date" : "yyyy-mm-dd hh:i:s",
// "message-id" : "<xxx@msgid.domain>",
// "tag" : "<defined-tag>",//json of array
// "tags": [
// "company_key"
// ],
// "sending_ip" : "xxx.xx.xxx.xx",
// "sender_email": "user@example.com",
// }
private function processSpamComplaint()
{
$this->invitation->email_status = 'spam';
$this->invitation->save();
$spam = new EmailSpam(
$this->request['tags'][0],
$this->request['sender_email'],
$this->request['message-id']
);
LightLogs::create($spam)->send();
$data = array_merge($this->request, ['history' => $this->fetchMessage()]);
$sl = $this->getSystemLog($this->request['message-id']);
if ($sl) {
$this->updateSystemLog($sl, $data);
return;
}
(new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle();
if (config('ninja.notification.slack')) {
$this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja();
}
}
private function discoverInvitation(string $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;
}
}
public function getRawMessage(string $message_id)
{
$brevo_secret = !empty($this->company->settings->brevo_secret) ? $this->company->settings->brevo_secret : config('services.brevo.key');
$brevo = new TransactionalEmailsApi(null, Configuration::getDefaultConfiguration()->setApiKey('api-key', $brevo_secret));
$messageDetail = $brevo->getTransacEmailContent($message_id);
return $messageDetail;
}
public function getBounceId(string $message_id): ?int
{
$messageDetail = $this->getRawMessage($message_id);
$event = collect($messageDetail->getEvents())->first(function ($event) {
return $event?->Details?->BounceID ?? false;
});
return $event?->Details?->BounceID ?? null;
}
// TODO
private function fetchMessage(): array
{
if (strlen($this->request['message-id']) < 1) {
return $this->default_response;
}
try {
$messageDetail = $this->getRawMessage($this->request['message-id']);
$recipient = array_key_exists("email", $this->request) ? $this->request["email"] : '';
$server_ip = array_key_exists("sending_ip", $this->request) ? $this->request["sending_ip"] : '';
$delivery_message = array_key_exists("reason", $this->request) ? $this->request["reason"] : '';
$subject = $messageDetail->getSubject() ?? '';
$events = collect($messageDetail->getEvents())->map(function (GetTransacEmailContentEvents $event) use ($recipient, $server_ip, $delivery_message) { // @turbo124 event does only contain name & time property, how to handle transformation?!
return [
'bounce_id' => '',
'recipient' => $recipient,
'status' => $event->name ?? '',
'delivery_message' => $delivery_message, // TODO: @turbo124 this results in all cases for the history in the string, which may be incorrect
'server' => '',
'server_ip' => $server_ip,
'date' => \Carbon\Carbon::parse($event->getTime())->format('Y-m-d H:i:s') ?? '',
];
})->toArray();
return [
'recipients' => $recipient,
'subject' => $subject,
'entity' => $this->entity ?? '',
'entity_id' => $this->invitation->{$this->entity}->hashed_id ?? '',
'events' => $events,
];
} catch (\Exception $e) {
return $this->default_response;
}
}
}

View File

@ -62,6 +62,7 @@ class NinjaMailerJob implements ShouldQueue
protected $client_mailgun_domain = false;
protected $client_brevo_secret = false;
public function __construct(public ?NinjaMailerObject $nmo, public bool $override = false)
{
@ -133,6 +134,10 @@ class NinjaMailerJob implements ShouldQueue
$mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain, $this->nmo->settings->mailgun_endpoint);
}
if ($this->client_brevo_secret) {
$mailer->brevo_config($this->client_brevo_secret);
}
$mailable = $this->nmo->mailable;
/** May need to re-build it here */
@ -331,6 +336,10 @@ class NinjaMailerJob implements ShouldQueue
$this->mailer = 'mailgun';
$this->setMailgunMailer();
return $this;
case 'client_brevo':
$this->mailer = 'brevo';
$this->setBrevoMailer();
return $this;
case 'smtp':
$this->mailer = 'smtp';
$this->configureSmtpMailer();
@ -426,6 +435,8 @@ class NinjaMailerJob implements ShouldQueue
$this->client_mailgun_domain = false;
$this->client_brevo_secret = false;
//always dump the drivers to prevent reuse
app('mail.manager')->forgetMailers();
}
@ -504,6 +515,29 @@ class NinjaMailerJob implements ShouldQueue
->from($sending_email, $sending_user);
}
/**
* Configures Brevo using client supplied secret
* as the Mailer
*/
private function setBrevoMailer()
{
if (strlen($this->nmo->settings->brevo_secret) > 2) {
$this->client_brevo_secret = $this->nmo->settings->brevo_secret;
} else {
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$user = $this->resolveSendingUser();
$sending_email = (isset ($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email;
$sending_user = (isset ($this->nmo->settings->email_from_name) && strlen($this->nmo->settings->email_from_name) > 2) ? $this->nmo->settings->email_from_name : $user->name();
$this->nmo
->mailable
->from($sending_email, $sending_user);
}
/**
* Configures Postmark using client supplied secret
* as the Mailer
@ -646,7 +680,7 @@ class NinjaMailerJob implements ShouldQueue
}
/* GMail users are uncapped */
if (Ninja::isHosted() && (in_array($this->nmo->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun']))) {
if (Ninja::isHosted() && (in_array($this->nmo->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun', 'client_brevo']))) {
return false;
}
@ -690,14 +724,16 @@ class NinjaMailerJob implements ShouldQueue
*/
private function logMailError($errors, $recipient_object): void
{
(new SystemLogger(
(
new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->nmo->company
))->handle();
)
)->handle();
$job_failure = new EmailFailure($this->nmo->company->company_key);
$job_failure->string_metric5 = 'failed_email';

View File

@ -53,6 +53,8 @@ class ProcessPostmarkWebhook implements ShouldQueue
'events' => [],
];
private ?Company $company = null;
/**
* Create a new job instance.
*
@ -87,12 +89,12 @@ class ProcessPostmarkWebhook implements ShouldQueue
public function handle()
{
MultiDB::findAndSetDbByCompanyKey($this->request['Tag']);
$company = Company::where('company_key', $this->request['Tag'])->first();
$this->company = Company::where('company_key', $this->request['Tag'])->first();
$this->invitation = $this->discoverInvitation($this->request['MessageID']);
if ($company && $this->request['RecordType'] == 'SpamComplaint' && config('ninja.notification.slack')) {
$company->notification(new EmailSpamNotification($company))->ninja();
if ($this->company && $this->request['RecordType'] == 'SpamComplaint' && config('ninja.notification.slack')) {
$this->company->notification(new EmailSpamNotification($this->company))->ninja();
}
if (!$this->invitation) {
@ -109,7 +111,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
case 'Bounce':
if ($this->request['Subject'] == ctrans('texts.confirmation_subject')) {
$company->notification(new EmailBounceNotification($this->request['Email']))->ninja();
$this->company->notification(new EmailBounceNotification($this->request['Email']))->ninja();
}
return $this->processBounce();
@ -174,14 +176,16 @@ class ProcessPostmarkWebhook implements ShouldQueue
return;
}
(new SystemLogger(
(
new SystemLogger(
$data,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_OPENED,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
))->handle();
)
)->handle();
}
// {
@ -212,14 +216,16 @@ class ProcessPostmarkWebhook implements ShouldQueue
return;
}
(new SystemLogger(
(
new SystemLogger(
$data,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_DELIVERY,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
))->handle();
)
)->handle();
}
// {
@ -349,7 +355,9 @@ class ProcessPostmarkWebhook implements ShouldQueue
public function getRawMessage(string $message_id)
{
$postmark = new PostmarkClient(config('services.postmark.token'));
$postmark_secret = !empty($this->company->settings->postmark_secret) ? $this->company->settings->postmark_secret : config('services.postmark.token');
$postmark = new PostmarkClient($postmark_secret);
$messageDetail = $postmark->getOutboundMessageDetails($message_id);
return $messageDetail;
@ -380,7 +388,9 @@ class ProcessPostmarkWebhook implements ShouldQueue
try {
$postmark = new PostmarkClient(config('services.postmark.token'));
$postmark_secret = !empty($this->company->settings->postmark_secret) ? $this->company->settings->postmark_secret : config('services.postmark.token');
$postmark = new PostmarkClient($postmark_secret);
$messageDetail = $postmark->getOutboundMessageDetails($this->request['MessageID']);
$recipients = collect($messageDetail['recipients'])->flatten()->implode(',');

View File

@ -29,6 +29,8 @@ use App\Http\Middleware\SetDomainNameDb;
use Illuminate\Queue\Events\JobProcessing;
use App\Helpers\Mail\Office365MailTransport;
use Illuminate\Database\Eloquent\Relations\Relation;
use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
use Symfony\Component\Mailer\Transport\Dsn;
class AppServiceProvider extends ServiceProvider
{
@ -119,6 +121,30 @@ class AppServiceProvider extends ServiceProvider
return $this;
});
Mail::extend('brevo', function () {
return (new BrevoTransportFactory)->create(
new Dsn(
'brevo+api',
'default',
config('services.brevo.key')
)
);
});
Mailer::macro('brevo_config', function (string $brevo_key) {
// @phpstan-ignore /** @phpstan-ignore-next-line **/
Mailer::setSymfonyTransport(
(new BrevoTransportFactory)->create(
new Dsn(
'brevo+api',
'default',
$brevo_key
)
)
);
return $this;
});
}
public function register(): void

View File

@ -59,6 +59,8 @@ class AdminEmail implements ShouldQueue
protected ?string $client_mailgun_endpoint = null;
protected ?string $client_brevo_secret = null;
private string $mailer = 'default';
public Mailable $mailable;
@ -137,6 +139,10 @@ class AdminEmail implements ShouldQueue
$mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain, $this->client_mailgun_endpoint);
}
if ($this->client_brevo_secret) {
$mailer->brevo_config($this->client_brevo_secret);
}
/* Attempt the send! */
try {
nlog("Using mailer => " . $this->mailer . " " . now()->toDateTimeString());
@ -248,7 +254,7 @@ class AdminEmail implements ShouldQueue
}
/* GMail users are uncapped */
if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) {
if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun', 'client_brevo'])) {
return false;
}
@ -337,6 +343,10 @@ class AdminEmail implements ShouldQueue
$this->mailer = 'mailgun';
$this->setMailgunMailer();
return $this;
case 'client_brevo':
$this->mailer = 'brevo';
$this->setBrevoMailer();
return $this;
default:
$this->mailer = config('mail.default');
@ -390,6 +400,8 @@ class AdminEmail implements ShouldQueue
$this->client_mailgun_endpoint = null;
$this->client_brevo_secret = null;
//always dump the drivers to prevent reuse
app('mail.manager')->forgetMailers();
}
@ -454,6 +466,28 @@ class AdminEmail implements ShouldQueue
$this->mailable
->from($sending_email, $sending_user);
}
/**
* Configures Brevo using client supplied secret
* as the Mailer
*/
private function setBrevoMailer()
{
if (strlen($this->email_object->settings->brevo_secret) > 2) {
$this->client_brevo_secret = $this->email_object->settings->brevo_secret;
} else {
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$user = $this->resolveSendingUser();
$sending_email = (isset($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable
->from($sending_email, $sending_user);
}
/**
* Configures Postmark using client supplied secret
@ -573,14 +607,16 @@ class AdminEmail implements ShouldQueue
*/
private function logMailError($errors, $recipient_object): void
{
(new SystemLogger(
(
new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->company
))->handle();
)
)->handle();
$job_failure = new EmailFailure($this->company->company_key);
$job_failure->string_metric5 = 'failed_email';

View File

@ -41,6 +41,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Mail;
use Log;
use Turbo124\Beacon\Facades\LightLogs;
class Email implements ShouldQueue
@ -72,6 +73,9 @@ class Email implements ShouldQueue
/** MailGun endpoint */
protected ?string $client_mailgun_endpoint = null;
/** Brevo endpoint */
protected ?string $client_brevo_secret = null;
/** Default mailer */
private string $mailer = 'default';
@ -261,6 +265,7 @@ class Email implements ShouldQueue
/* Init the mailer*/
$mailer = Mail::mailer($this->mailer);
/* Additional configuration if using a client third party mailer */
if ($this->client_postmark_secret) {
$mailer->postmark_config($this->client_postmark_secret);
@ -270,6 +275,10 @@ class Email implements ShouldQueue
$mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain, $this->client_mailgun_endpoint);
}
if ($this->client_brevo_secret) {
$mailer->brevo_config($this->client_brevo_secret);
}
/* Attempt the send! */
try {
nlog("Using mailer => " . $this->mailer . " " . now()->toDateTimeString());
@ -400,7 +409,7 @@ class Email implements ShouldQueue
}
/* GMail users are uncapped */
if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) {
if (in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun', 'client_brevo'])) {
return false;
}
@ -555,6 +564,10 @@ class Email implements ShouldQueue
$this->mailer = 'mailgun';
$this->setMailgunMailer();
return $this;
case 'client_brevo':
$this->mailer = 'brevo';
$this->setBrevoMailer();
return $this;
case 'smtp':
$this->mailer = 'smtp';
$this->configureSmtpMailer();
@ -648,6 +661,8 @@ class Email implements ShouldQueue
$this->client_mailgun_endpoint = null;
$this->client_brevo_secret = null;
//always dump the drivers to prevent reuse
app('mail.manager')->forgetMailers();
}
@ -712,6 +727,28 @@ class Email implements ShouldQueue
$this->mailable
->from($sending_email, $sending_user);
}
/**
* Configures Brevo using client supplied secret
* as the Mailer
*/
private function setBrevoMailer()
{
if (strlen($this->email_object->settings->brevo_secret) > 2) {
$this->client_brevo_secret = $this->email_object->settings->brevo_secret;
} else {
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
$user = $this->resolveSendingUser();
$sending_email = (isset ($this->email_object->settings->custom_sending_email) && stripos($this->email_object->settings->custom_sending_email, "@")) ? $this->email_object->settings->custom_sending_email : $user->email;
$sending_user = (isset ($this->email_object->settings->email_from_name) && strlen($this->email_object->settings->email_from_name) > 2) ? $this->email_object->settings->email_from_name : $user->name();
$this->mailable
->from($sending_email, $sending_user);
}
/**
* Configures Postmark using client supplied secret
@ -831,14 +868,16 @@ class Email implements ShouldQueue
*/
private function logMailError($errors, $recipient_object): void
{
(new SystemLogger(
(
new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->company
))->handle();
)
)->handle();
$job_failure = new EmailFailure($this->company->company_key);
$job_failure->string_metric5 = 'failed_email';

View File

@ -47,6 +47,7 @@
"doctrine/dbal": "^3.0",
"eway/eway-rapid-php": "^1.3",
"fakerphp/faker": "^1.14",
"getbrevo/brevo-php": "^1.0",
"gocardless/gocardless-pro": "^4.12",
"google/apiclient": "^2.7",
"guzzlehttp/guzzle": "^7.2",
@ -92,6 +93,7 @@
"sprain/swiss-qr-bill": "^4.3",
"square/square": "30.0.0.*",
"stripe/stripe-php": "^12",
"symfony/brevo-mailer": "6.4",
"symfony/http-client": "^6.0",
"symfony/mailgun-mailer": "^6.1",
"symfony/postmark-mailer": "^6.1",

646
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,7 +28,7 @@ return [
| sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required.
|
| Supported: "smtp", "sendmail", "mailgun", "ses",
| Supported: "smtp", "sendmail", "mailgun", "brevo", "ses",
| "postmark", "log", "array", "failover"
|
*/
@ -54,6 +54,10 @@ return [
'transport' => 'mailgun',
],
'brevo' => [
'transport' => 'brevo',
],
'postmark' => [
'transport' => 'postmark',
],

View File

@ -12,7 +12,7 @@ return [
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| as Mailgun, Brevo, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
@ -30,6 +30,10 @@ return [
],
],
'brevo' => [
'key' => env('BREVO_SECRET', ''),
],
'postmark' => [
'token' => env('POSTMARK_SECRET', ''),
],

View File

@ -2197,6 +2197,8 @@ $lang = array(
'encryption' => 'Encryption',
'mailgun_domain' => 'Mailgun Domain',
'mailgun_private_key' => 'Mailgun Private Key',
'brevo_domain' => 'Brevo Domain',
'brevo_private_key' => 'Brevo Private Key',
'send_test_email' => 'Send test email',
'select_label' => 'Select Label',
'label' => 'Label',
@ -4847,6 +4849,7 @@ $lang = array(
'email_alignment' => 'Email Alignment',
'pdf_preview_location' => 'PDF Preview Location',
'mailgun' => 'Mailgun',
'brevo' => 'Brevo',
'postmark' => 'Postmark',
'microsoft' => 'Microsoft',
'click_plus_to_create_record' => 'Click + to create a record',

View File

@ -17,8 +17,7 @@ info:
url: 'https://www.elastic.co/licensing/elastic-license'
version: 5.8.34
servers:
-
url: 'https://demo.invoiceninja.com'
- url: "https://demo.invoiceninja.com"
description: |
## Demo API Server InvoiceNinja.
You can use the demo API key `TOKEN` to test the endpoints from within this API spec
@ -14705,8 +14704,148 @@ components:
422:
$ref: "#/components/responses/422"
default:
$ref: "#/components/responses/default"
"/api/v1/companies/{id}/edit":
get:
tags:
- companies
summary: "Shows an company for editting"
description: "Displays an company by id"
operationId: editCompany
parameters:
- $ref: "#/components/parameters/X-API-TOKEN"
- $ref: "#/components/parameters/X-Requested-With"
- $ref: "#/components/parameters/include"
- name: id
in: path
description: "The Company Hashed ID"
required: true
schema:
type: string
format: string
example: D2J234DFA
responses:
200:
description: "Returns the company object"
headers:
X-MINIMUM-CLIENT-VERSION:
$ref: "#/components/headers/X-MINIMUM-CLIENT-VERSION"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
content:
application/json:
schema:
$ref: "#/components/schemas/Company"
401:
$ref: "#/components/responses/401"
403:
$ref: "#/components/responses/403"
422:
$ref: "#/components/responses/422"
default:
$ref: "#/components/responses/default"
"/api/v1/companies/{id}/upload":
post:
tags:
- companies
summary: "Uploads a document to a company"
description: "Handles the uploading of a document to a company"
operationId: uploadCompanies
parameters:
- $ref: "#/components/parameters/X-API-TOKEN"
- $ref: "#/components/parameters/X-Requested-With"
- $ref: "#/components/parameters/include"
- name: id
in: path
description: "The Company Hashed ID"
required: true
schema:
type: string
format: string
example: D2J234DFA
requestBody:
description: "File Upload Body"
required: true
content:
multipart/form-data:
schema:
type: object
properties:
_method:
type: string
example: PUT
documents:
type: array
items:
description: "Array of binary documents for upload"
type: string
format: binary
responses:
200:
description: "Returns the client object"
headers:
X-MINIMUM-CLIENT-VERSION:
$ref: "#/components/headers/X-MINIMUM-CLIENT-VERSION"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
content:
application/json:
schema:
$ref: "#/components/schemas/Company"
401:
$ref: "#/components/responses/401"
403:
$ref: "#/components/responses/403"
422:
$ref: "#/components/responses/422"
default:
$ref: "#/components/responses/default"
"/api/v1/companies/{company}/default":
post:
tags:
- companies
summary: "Sets the company as the default company."
description: "Sets the company as the default company."
operationId: setDefaultCompany
parameters:
- $ref: "#/components/parameters/X-API-TOKEN"
- $ref: "#/components/parameters/X-Requested-With"
- $ref: "#/components/parameters/include"
- name: company
in: path
description: "The Company Hashed ID"
required: true
schema:
type: string
format: string
example: D2J234DFA
responses:
200:
description: "Returns the company object"
headers:
X-MINIMUM-CLIENT-VERSION:
$ref: "#/components/headers/X-MINIMUM-CLIENT-VERSION"
X-RateLimit-Remaining:
$ref: "#/components/headers/X-RateLimit-Remaining"
X-RateLimit-Limit:
$ref: "#/components/headers/X-RateLimit-Limit"
content:
application/json:
schema:
$ref: "#/components/schemas/Company"
401:
$ref: "#/components/responses/401"
403:
$ref: "#/components/responses/403"
TaskSchedulerSchema:
properties:

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
*/
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\BrevoController;
use App\Http\Controllers\PingController;
use App\Http\Controllers\SmtpController;
use App\Http\Controllers\TaskController;
@ -426,6 +427,7 @@ Route::match(['get', 'post'], 'payment_notification_webhook/{company_key}/{compa
Route::post('api/v1/postmark_webhook', [PostMarkController::class, 'webhook'])->middleware('throttle:1000,1');
Route::post('api/v1/brevo_webhook', [BrevoController::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');