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
@ -520,262 +522,263 @@ class CompanySettings extends BaseSettings
'show_task_item_description' => 'bool',
'allow_billable_task_items' => 'bool',
'accept_client_input_quote_approval' => 'bool',
'custom_sending_email' => 'string',
'show_paid_stamp' => 'bool',
'show_shipping_address' => 'bool',
'company_logo_size' => 'string',
'show_email_footer' => 'bool',
'email_alignment' => 'string',
'auto_bill_standard_invoices' => 'bool',
'postmark_secret' => 'string',
'mailgun_secret' => 'string',
'mailgun_domain' => 'string',
'send_email_on_mark_paid' => 'bool',
'vendor_portal_enable_uploads' => 'bool',
'besr_id' => 'string',
'qr_iban' => 'string',
'email_subject_purchase_order' => 'string',
'email_template_purchase_order' => 'string',
'require_purchase_order_signature' => 'bool',
'purchase_order_public_notes' => 'string',
'purchase_order_terms' => 'string',
'purchase_order_design_id' => 'string',
'purchase_order_footer' => 'string',
'purchase_order_number_pattern' => 'string',
'page_numbering_alignment' => 'string',
'page_numbering' => 'bool',
'auto_archive_invoice_cancelled' => 'bool',
'email_from_name' => 'string',
'show_all_tasks_client_portal' => 'string',
'entity_send_time' => 'int',
'shared_invoice_credit_counter' => 'bool',
'reply_to_name' => 'string',
'hide_empty_columns_on_pdf' => 'bool',
'enable_reminder_endless' => 'bool',
'use_credits_payment' => 'string',
'recurring_invoice_number_pattern' => 'string',
'recurring_invoice_number_counter' => 'int',
'custom_sending_email' => 'string',
'show_paid_stamp' => 'bool',
'show_shipping_address' => 'bool',
'company_logo_size' => 'string',
'show_email_footer' => 'bool',
'email_alignment' => 'string',
'auto_bill_standard_invoices' => 'bool',
'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',
'qr_iban' => 'string',
'email_subject_purchase_order' => 'string',
'email_template_purchase_order' => 'string',
'require_purchase_order_signature' => 'bool',
'purchase_order_public_notes' => 'string',
'purchase_order_terms' => 'string',
'purchase_order_design_id' => 'string',
'purchase_order_footer' => 'string',
'purchase_order_number_pattern' => 'string',
'page_numbering_alignment' => 'string',
'page_numbering' => 'bool',
'auto_archive_invoice_cancelled' => 'bool',
'email_from_name' => 'string',
'show_all_tasks_client_portal' => 'string',
'entity_send_time' => 'int',
'shared_invoice_credit_counter' => 'bool',
'reply_to_name' => 'string',
'hide_empty_columns_on_pdf' => 'bool',
'enable_reminder_endless' => 'bool',
'use_credits_payment' => 'string',
'recurring_invoice_number_pattern' => 'string',
'recurring_invoice_number_counter' => 'int',
'client_portal_under_payment_minimum' => 'float',
'auto_bill_date' => 'string',
'primary_color' => 'string',
'secondary_color' => 'string',
'client_portal_allow_under_payment' => 'bool',
'client_portal_allow_over_payment' => 'bool',
'auto_bill' => 'string',
'lock_invoices' => 'string',
'client_portal_terms' => 'string',
'client_portal_privacy_policy' => 'string',
'client_can_register' => 'bool',
'portal_design_id' => 'string',
'late_fee_endless_percent' => 'float',
'late_fee_endless_amount' => 'float',
'auto_email_invoice' => 'bool',
'reminder_send_time' => 'int',
'email_sending_method' => 'string',
'gmail_sending_user_id' => 'string',
'counter_number_applied' => 'string',
'quote_number_applied' => 'string',
'email_subject_custom1' => 'string',
'email_subject_custom2' => 'string',
'email_subject_custom3' => 'string',
'email_template_custom1' => 'string',
'email_template_custom2' => 'string',
'email_template_custom3' => 'string',
'enable_reminder1' => 'bool',
'enable_reminder2' => 'bool',
'enable_reminder3' => 'bool',
'num_days_reminder1' => 'int',
'num_days_reminder2' => 'int',
'num_days_reminder3' => 'int',
'schedule_reminder1' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date)
'schedule_reminder2' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date)
'schedule_reminder3' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date)
'late_fee_amount1' => 'float',
'late_fee_amount2' => 'float',
'late_fee_amount3' => 'float',
'late_fee_percent1' => 'float',
'late_fee_percent2' => 'float',
'late_fee_percent3' => 'float',
'endless_reminder_frequency_id' => 'integer',
'auto_bill_date' => 'string',
'primary_color' => 'string',
'secondary_color' => 'string',
'client_portal_allow_under_payment' => 'bool',
'client_portal_allow_over_payment' => 'bool',
'auto_bill' => 'string',
'lock_invoices' => 'string',
'client_portal_terms' => 'string',
'client_portal_privacy_policy' => 'string',
'client_can_register' => 'bool',
'portal_design_id' => 'string',
'late_fee_endless_percent' => 'float',
'late_fee_endless_amount' => 'float',
'auto_email_invoice' => 'bool',
'reminder_send_time' => 'int',
'email_sending_method' => 'string',
'gmail_sending_user_id' => 'string',
'counter_number_applied' => 'string',
'quote_number_applied' => 'string',
'email_subject_custom1' => 'string',
'email_subject_custom2' => 'string',
'email_subject_custom3' => 'string',
'email_template_custom1' => 'string',
'email_template_custom2' => 'string',
'email_template_custom3' => 'string',
'enable_reminder1' => 'bool',
'enable_reminder2' => 'bool',
'enable_reminder3' => 'bool',
'num_days_reminder1' => 'int',
'num_days_reminder2' => 'int',
'num_days_reminder3' => 'int',
'schedule_reminder1' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date)
'schedule_reminder2' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date)
'schedule_reminder3' => 'string', // (enum: after_invoice_date, before_due_date, after_due_date)
'late_fee_amount1' => 'float',
'late_fee_amount2' => 'float',
'late_fee_amount3' => 'float',
'late_fee_percent1' => 'float',
'late_fee_percent2' => 'float',
'late_fee_percent3' => 'float',
'endless_reminder_frequency_id' => 'integer',
'client_online_payment_notification' => 'bool',
'client_manual_payment_notification' => 'bool',
'document_email_attachment' => 'bool',
'enable_client_portal_password' => 'bool',
'enable_email_markup' => 'bool',
'enable_client_portal_dashboard' => 'bool',
'enable_client_portal' => 'bool',
'email_template_statement' => 'string',
'email_subject_statement' => 'string',
'signature_on_pdf' => 'bool',
'quote_footer' => 'string',
'page_size' => 'string',
'page_layout' => 'string',
'font_size' => 'int',
'primary_font' => 'string',
'secondary_font' => 'string',
'hide_paid_to_date' => 'bool',
'embed_documents' => 'bool',
'all_pages_header' => 'bool',
'all_pages_footer' => 'bool',
'project_number_pattern' => 'string',
'project_number_counter' => 'int',
'task_number_pattern' => 'string',
'task_number_counter' => 'int',
'expense_number_pattern' => 'string',
'expense_number_counter' => 'int',
'recurring_expense_number_pattern' => 'string',
'recurring_expense_number_counter' => 'int',
'recurring_quote_number_pattern' => 'string',
'recurring_quote_number_counter' => 'int',
'vendor_number_pattern' => 'string',
'vendor_number_counter' => 'int',
'ticket_number_pattern' => 'string',
'ticket_number_counter' => 'int',
'payment_number_pattern' => 'string',
'payment_number_counter' => 'int',
'reply_to_email' => 'string',
'bcc_email' => 'string',
'pdf_email_attachment' => 'bool',
'ubl_email_attachment' => 'bool',
'email_style' => 'string',
'email_style_custom' => 'string',
'company_gateway_ids' => 'string',
'address1' => 'string',
'address2' => 'string',
'city' => 'string',
'company_logo' => 'string',
'country_id' => 'string',
'client_number_pattern' => 'string',
'client_number_counter' => 'integer',
'credit_number_pattern' => 'string',
'credit_number_counter' => 'integer',
'currency_id' => 'string',
'custom_value1' => 'string',
'custom_value2' => 'string',
'custom_value3' => 'string',
'custom_value4' => 'string',
'custom_message_dashboard' => 'string',
'custom_message_unpaid_invoice' => 'string',
'custom_message_paid_invoice' => 'string',
'custom_message_unapproved_quote' => 'string',
'default_task_rate' => 'float',
'email_signature' => 'string',
'email_subject_invoice' => 'string',
'email_subject_quote' => 'string',
'email_subject_credit' => 'string',
'email_subject_payment' => 'string',
'email_subject_payment_partial' => 'string',
'email_template_invoice' => 'string',
'email_template_quote' => 'string',
'email_template_credit' => 'string',
'email_template_payment' => 'string',
'email_template_payment_partial' => 'string',
'email_subject_reminder1' => 'string',
'email_subject_reminder2' => 'string',
'email_subject_reminder3' => 'string',
'email_subject_reminder_endless' => 'string',
'email_template_reminder1' => 'string',
'email_template_reminder2' => 'string',
'email_template_reminder3' => 'string',
'email_template_reminder_endless' => 'string',
'inclusive_taxes' => 'bool',
'invoice_number_pattern' => 'string',
'invoice_number_counter' => 'integer',
'invoice_design_id' => 'string',
'document_email_attachment' => 'bool',
'enable_client_portal_password' => 'bool',
'enable_email_markup' => 'bool',
'enable_client_portal_dashboard' => 'bool',
'enable_client_portal' => 'bool',
'email_template_statement' => 'string',
'email_subject_statement' => 'string',
'signature_on_pdf' => 'bool',
'quote_footer' => 'string',
'page_size' => 'string',
'page_layout' => 'string',
'font_size' => 'int',
'primary_font' => 'string',
'secondary_font' => 'string',
'hide_paid_to_date' => 'bool',
'embed_documents' => 'bool',
'all_pages_header' => 'bool',
'all_pages_footer' => 'bool',
'project_number_pattern' => 'string',
'project_number_counter' => 'int',
'task_number_pattern' => 'string',
'task_number_counter' => 'int',
'expense_number_pattern' => 'string',
'expense_number_counter' => 'int',
'recurring_expense_number_pattern' => 'string',
'recurring_expense_number_counter' => 'int',
'recurring_quote_number_pattern' => 'string',
'recurring_quote_number_counter' => 'int',
'vendor_number_pattern' => 'string',
'vendor_number_counter' => 'int',
'ticket_number_pattern' => 'string',
'ticket_number_counter' => 'int',
'payment_number_pattern' => 'string',
'payment_number_counter' => 'int',
'reply_to_email' => 'string',
'bcc_email' => 'string',
'pdf_email_attachment' => 'bool',
'ubl_email_attachment' => 'bool',
'email_style' => 'string',
'email_style_custom' => 'string',
'company_gateway_ids' => 'string',
'address1' => 'string',
'address2' => 'string',
'city' => 'string',
'company_logo' => 'string',
'country_id' => 'string',
'client_number_pattern' => 'string',
'client_number_counter' => 'integer',
'credit_number_pattern' => 'string',
'credit_number_counter' => 'integer',
'currency_id' => 'string',
'custom_value1' => 'string',
'custom_value2' => 'string',
'custom_value3' => 'string',
'custom_value4' => 'string',
'custom_message_dashboard' => 'string',
'custom_message_unpaid_invoice' => 'string',
'custom_message_paid_invoice' => 'string',
'custom_message_unapproved_quote' => 'string',
'default_task_rate' => 'float',
'email_signature' => 'string',
'email_subject_invoice' => 'string',
'email_subject_quote' => 'string',
'email_subject_credit' => 'string',
'email_subject_payment' => 'string',
'email_subject_payment_partial' => 'string',
'email_template_invoice' => 'string',
'email_template_quote' => 'string',
'email_template_credit' => 'string',
'email_template_payment' => 'string',
'email_template_payment_partial' => 'string',
'email_subject_reminder1' => 'string',
'email_subject_reminder2' => 'string',
'email_subject_reminder3' => 'string',
'email_subject_reminder_endless' => 'string',
'email_template_reminder1' => 'string',
'email_template_reminder2' => 'string',
'email_template_reminder3' => 'string',
'email_template_reminder_endless' => 'string',
'inclusive_taxes' => 'bool',
'invoice_number_pattern' => 'string',
'invoice_number_counter' => 'integer',
'invoice_design_id' => 'string',
// 'invoice_fields' => 'string',
'invoice_taxes' => 'int',
'invoice_taxes' => 'int',
//'enabled_item_tax_rates' => 'int',
'invoice_footer' => 'string',
'invoice_labels' => 'string',
'invoice_terms' => 'string',
'credit_footer' => 'string',
'credit_terms' => 'string',
'name' => 'string',
'payment_terms' => 'string',
'payment_type_id' => 'string',
'phone' => 'string',
'postal_code' => 'string',
'quote_design_id' => 'string',
'credit_design_id' => 'string',
'quote_number_pattern' => 'string',
'quote_number_counter' => 'integer',
'quote_terms' => 'string',
'recurring_number_prefix' => 'string',
'reset_counter_frequency_id' => 'integer',
'reset_counter_date' => 'string',
'require_invoice_signature' => 'bool',
'require_quote_signature' => 'bool',
'state' => 'string',
'email' => 'string',
'vat_number' => 'string',
'id_number' => 'string',
'tax_name1' => 'string',
'tax_name2' => 'string',
'tax_name3' => 'string',
'tax_rate1' => 'float',
'tax_rate2' => 'float',
'tax_rate3' => 'float',
'show_accept_quote_terms' => 'bool',
'show_accept_invoice_terms' => 'bool',
'timezone_id' => 'string',
'valid_until' => 'string',
'date_format_id' => 'string',
'military_time' => 'bool',
'language_id' => 'string',
'show_currency_code' => 'bool',
'send_reminders' => 'bool',
'enable_client_portal_tasks' => 'bool',
'auto_archive_invoice' => 'bool',
'auto_archive_quote' => 'bool',
'auto_convert_quote' => 'bool',
'shared_invoice_quote_counter' => 'bool',
'counter_padding' => 'integer',
'invoice_footer' => 'string',
'invoice_labels' => 'string',
'invoice_terms' => 'string',
'credit_footer' => 'string',
'credit_terms' => 'string',
'name' => 'string',
'payment_terms' => 'string',
'payment_type_id' => 'string',
'phone' => 'string',
'postal_code' => 'string',
'quote_design_id' => 'string',
'credit_design_id' => 'string',
'quote_number_pattern' => 'string',
'quote_number_counter' => 'integer',
'quote_terms' => 'string',
'recurring_number_prefix' => 'string',
'reset_counter_frequency_id' => 'integer',
'reset_counter_date' => 'string',
'require_invoice_signature' => 'bool',
'require_quote_signature' => 'bool',
'state' => 'string',
'email' => 'string',
'vat_number' => 'string',
'id_number' => 'string',
'tax_name1' => 'string',
'tax_name2' => 'string',
'tax_name3' => 'string',
'tax_rate1' => 'float',
'tax_rate2' => 'float',
'tax_rate3' => 'float',
'show_accept_quote_terms' => 'bool',
'show_accept_invoice_terms' => 'bool',
'timezone_id' => 'string',
'valid_until' => 'string',
'date_format_id' => 'string',
'military_time' => 'bool',
'language_id' => 'string',
'show_currency_code' => 'bool',
'send_reminders' => 'bool',
'enable_client_portal_tasks' => 'bool',
'auto_archive_invoice' => 'bool',
'auto_archive_quote' => 'bool',
'auto_convert_quote' => 'bool',
'shared_invoice_quote_counter' => 'bool',
'counter_padding' => 'integer',
//'design' => 'string',
'website' => 'string',
'pdf_variables' => 'object',
'portal_custom_head' => 'string',
'portal_custom_css' => 'string',
'portal_custom_footer' => 'string',
'portal_custom_js' => 'string',
'client_portal_enable_uploads' => 'bool',
'purchase_order_number_counter' => 'integer',
'website' => 'string',
'pdf_variables' => 'object',
'portal_custom_head' => 'string',
'portal_custom_css' => 'string',
'portal_custom_footer' => 'string',
'portal_custom_js' => 'string',
'client_portal_enable_uploads' => 'bool',
'purchase_order_number_counter' => 'integer',
];
public static $free_plan_casts = [
'currency_id' => 'string',
'company_gateway_ids' => 'string',
'address1' => 'string',
'address2' => 'string',
'city' => 'string',
'company_logo' => 'string',
'country_id' => 'string',
'custom_value1' => 'string',
'custom_value2' => 'string',
'custom_value3' => 'string',
'custom_value4' => 'string',
'inclusive_taxes' => 'bool',
'name' => 'string',
'payment_terms' => 'string',
'payment_type_id' => 'string',
'phone' => 'string',
'postal_code' => 'string',
'state' => 'string',
'email' => 'string',
'vat_number' => 'string',
'id_number' => 'string',
'tax_name1' => 'string',
'tax_name2' => 'string',
'tax_name3' => 'string',
'tax_rate1' => 'float',
'tax_rate2' => 'float',
'tax_rate3' => 'float',
'timezone_id' => 'string',
'date_format_id' => 'string',
'military_time' => 'bool',
'language_id' => 'string',
'show_currency_code' => 'bool',
'website' => 'string',
'default_task_rate' => 'float',
'currency_id' => 'string',
'company_gateway_ids' => 'string',
'address1' => 'string',
'address2' => 'string',
'city' => 'string',
'company_logo' => 'string',
'country_id' => 'string',
'custom_value1' => 'string',
'custom_value2' => 'string',
'custom_value3' => 'string',
'custom_value4' => 'string',
'inclusive_taxes' => 'bool',
'name' => 'string',
'payment_terms' => 'string',
'payment_type_id' => 'string',
'phone' => 'string',
'postal_code' => 'string',
'state' => 'string',
'email' => 'string',
'vat_number' => 'string',
'id_number' => 'string',
'tax_name1' => 'string',
'tax_name2' => 'string',
'tax_name3' => 'string',
'tax_rate1' => 'float',
'tax_rate2' => 'float',
'tax_rate3' => 'float',
'timezone_id' => 'string',
'date_format_id' => 'string',
'military_time' => 'bool',
'language_id' => 'string',
'show_currency_code' => 'bool',
'website' => 'string',
'default_task_rate' => 'float',
];
/**
@ -851,7 +854,7 @@ class CompanySettings extends BaseSettings
$company_settings = (object) get_class_vars(self::class);
foreach ($company_settings as $key => $value) {
if (! property_exists($settings, $key)) {
if (!property_exists($settings, $key)) {
$settings->{$key} = self::castAttribute($key, $company_settings->{$key});
}
}
@ -884,7 +887,7 @@ class CompanySettings extends BaseSettings
{
$notification = new stdClass();
$notification->email = [];
$notification->email = ['invoice_sent_all','payment_success_all','payment_manual_all'];
$notification->email = ['invoice_sent_all', 'payment_success_all', 'payment_manual_all'];
return $notification;
}

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
@ -469,8 +471,8 @@ class SettingsData
public function cast(mixed $object)
{
if(is_array($object)) {
$object = (object)$object;
if (is_array($object)) {
$object = (object) $object;
}
if (is_object($object)) {
@ -478,9 +480,9 @@ class SettingsData
try {
settype($object->{$key}, gettype($this->{$key}));
} catch(\Exception | \Error | \Throwable $e) {
} catch (\Exception | \Error | \Throwable $e) {
if(property_exists($this, $key)) {
if (property_exists($this, $key)) {
$object->{$key} = $this->{$key};
} else {
unset($object->{$key});
@ -506,11 +508,11 @@ class SettingsData
public function toObject(): object
{
return (object)$this->object;
return (object) $this->object;
}
public function toArray(): array
{
return (array)$this->object;
return (array) $this->object;
}
}

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)
{
@ -99,7 +100,7 @@ class NinjaMailerJob implements ShouldQueue
}
$this->nmo->mailable->replyTo($this->nmo->settings->reply_to_email, $reply_to_name);
} elseif(isset($this->nmo->invitation->user)) {
} elseif (isset ($this->nmo->invitation->user)) {
$this->nmo->mailable->replyTo($this->nmo->invitation->user->email, $this->nmo->invitation->user->present()->name());
} else {
$this->nmo->mailable->replyTo($this->company->owner()->email, $this->company->owner()->present()->name());
@ -112,16 +113,16 @@ class NinjaMailerJob implements ShouldQueue
/* If we have an invitation present, we pass the invitation key into the email headers*/
if ($this->nmo->invitation) {
$this->nmo
->mailable
->withSymfonyMessage(function ($message) {
$message->getHeaders()->addTextHeader('x-invitation', $this->nmo->invitation->key);
});
->mailable
->withSymfonyMessage(function ($message) {
$message->getHeaders()->addTextHeader('x-invitation', $this->nmo->invitation->key);
});
}
//send email
try {
nlog("Trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString());
nlog("Using mailer => ". $this->mailer);
nlog("Trying to send to {$this->nmo->to_user->email} " . now()->toDateTimeString());
nlog("Using mailer => " . $this->mailer);
$mailer = Mail::mailer($this->mailer);
@ -133,10 +134,14 @@ 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 */
if(Ninja::isHosted() && method_exists($mailable, 'build')) {
if (Ninja::isHosted() && method_exists($mailable, 'build')) {
$mailable->build();
}
@ -149,15 +154,15 @@ class NinjaMailerJob implements ShouldQueue
$this->incrementEmailCounter();
LightLogs::create(new EmailSuccess($this->nmo->company->company_key, $this->nmo->mailable->subject))
->send();
->send();
} catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
} catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch(\Symfony\Component\Mime\Exception\LogicException $e) {
} catch (\Symfony\Component\Mime\Exception\LogicException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
@ -273,7 +278,7 @@ class NinjaMailerJob implements ShouldQueue
// return $this;
// }
if(Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') {
if (Ninja::isHosted() && $this->company->account->isPaid() && $this->nmo->settings->email_sending_method == 'default') {
//check if outlook.
try {
@ -281,7 +286,7 @@ class NinjaMailerJob implements ShouldQueue
$domain = explode("@", $email)[1] ?? "";
$dns = dns_get_record($domain, DNS_MX);
$server = $dns[0]["target"];
if(stripos($server, "outlook.com") !== false) {
if (stripos($server, "outlook.com") !== false) {
$this->mailer = 'postmark';
$this->client_postmark_secret = config('services.postmark-outlook.token');
@ -293,13 +298,13 @@ class NinjaMailerJob implements ShouldQueue
}
$this->nmo
->mailable
->from(config('services.postmark-outlook.from.address'), $email_from_name);
->mailable
->from(config('services.postmark-outlook.from.address'), $email_from_name);
return $this;
}
} catch(\Exception $e) {
} catch (\Exception $e) {
nlog("problem switching outlook driver - hosted");
nlog($e->getMessage());
}
@ -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();
@ -380,11 +389,11 @@ class NinjaMailerJob implements ShouldQueue
}
$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_email = (isset ($this->nmo->settings->custom_sending_email) && stripos($this->nmo->settings->custom_sending_email, "@")) ? $this->nmo->settings->custom_sending_email : $user->email;
$this->nmo
->mailable
->from($sending_email, $email_from_name);
->mailable
->from($sending_email, $email_from_name);
}
@ -407,8 +416,8 @@ class NinjaMailerJob implements ShouldQueue
if (env($this->company->id . '_MAIL_FROM_ADDRESS')) {
$this->nmo
->mailable
->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
->mailable
->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
}
}
}
@ -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();
}
@ -475,8 +486,8 @@ class NinjaMailerJob implements ShouldQueue
}
$this->nmo
->mailable
->from(config('services.mailgun.from.address'), $email_from_name);
->mailable
->from(config('services.mailgun.from.address'), $email_from_name);
}
@ -496,12 +507,35 @@ class NinjaMailerJob implements ShouldQueue
$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();
$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);
->mailable
->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);
}
/**
@ -519,12 +553,12 @@ class NinjaMailerJob implements ShouldQueue
$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();
$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);
->mailable
->from($sending_email, $sending_user);
}
/**
@ -550,11 +584,11 @@ class NinjaMailerJob implements ShouldQueue
}
$this->nmo
->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
@ -578,7 +612,7 @@ class NinjaMailerJob implements ShouldQueue
}
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
} catch(\Exception $e) {
} catch (\Exception $e) {
$this->logMailError('Gmail Token Invalid', $this->company->clients()->first());
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
@ -598,7 +632,7 @@ class NinjaMailerJob implements ShouldQueue
* Now that our token is refreshed and valid we can boot the
* mail driver at runtime and also set the token which will persist
* just for this request.
*/
*/
$token = $user->oauth_user_token->access_token;
@ -609,11 +643,11 @@ class NinjaMailerJob implements ShouldQueue
}
$this->nmo
->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
@ -626,7 +660,7 @@ class NinjaMailerJob implements ShouldQueue
private function preFlightChecksFail(): bool
{
/* Always send regardless */
if($this->override) {
if ($this->override) {
return false;
}
@ -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,21 +724,23 @@ class NinjaMailerJob implements ShouldQueue
*/
private function logMailError($errors, $recipient_object): void
{
(new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->nmo->company
))->handle();
(
new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->nmo->company
)
)->handle();
$job_failure = new EmailFailure($this->nmo->company->company_key);
$job_failure->string_metric5 = 'failed_email';
$job_failure->string_metric6 = substr($errors, 0, 150);
LightLogs::create($job_failure)
->send();
->send();
$job_failure = null;
}
@ -729,8 +765,8 @@ class NinjaMailerJob implements ShouldQueue
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id') ,
'client_secret' => config('ninja.o365.client_secret') ,
'client_id' => config('ninja.o365.client_id'),
'client_secret' => config('ninja.o365.client_secret'),
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token

View File

@ -45,7 +45,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
private $entity;
private array $default_response = [
private array $default_response = [
'recipients' => '',
'subject' => 'Message not found.',
'entity' => '',
@ -53,6 +53,8 @@ class ProcessPostmarkWebhook implements ShouldQueue
'events' => [],
];
private ?Company $company = null;
/**
* Create a new job instance.
*
@ -64,11 +66,11 @@ class ProcessPostmarkWebhook implements ShouldQueue
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' => $message_id])
->orderBy('id', 'desc')
->first();
->where('company_id', $this->invitation->company_id)
->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE)
->whereJsonContains('log', ['MessageID' => $message_id])
->orderBy('id', 'desc')
->first();
}
@ -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) {
@ -108,8 +110,8 @@ class ProcessPostmarkWebhook implements ShouldQueue
return $this->processDelivery();
case 'Bounce':
if($this->request['Subject'] == ctrans('texts.confirmation_subject')) {
$company->notification(new EmailBounceNotification($this->request['Email']))->ninja();
if ($this->request['Subject'] == ctrans('texts.confirmation_subject')) {
$this->company->notification(new EmailBounceNotification($this->request['Email']))->ninja();
}
return $this->processBounce();
@ -169,19 +171,21 @@ class ProcessPostmarkWebhook implements ShouldQueue
$sl = $this->getSystemLog($this->request['MessageID']);
if($sl) {
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();
(
new SystemLogger(
$data,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_OPENED,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
)
)->handle();
}
// {
@ -207,19 +211,21 @@ class ProcessPostmarkWebhook implements ShouldQueue
$sl = $this->getSystemLog($this->request['MessageID']);
if($sl) {
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();
(
new SystemLogger(
$data,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_DELIVERY,
SystemLog::TYPE_WEBHOOK_RESPONSE,
$this->invitation->contact->client,
$this->invitation->company
)
)->handle();
}
// {
@ -265,7 +271,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
$sl = $this->getSystemLog($this->request['MessageID']);
if($sl) {
if ($sl) {
$this->updateSystemLog($sl, $data);
return;
}
@ -316,7 +322,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
$sl = $this->getSystemLog($this->request['MessageID']);
if($sl) {
if ($sl) {
$this->updateSystemLog($sl, $data);
}
@ -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;
@ -362,7 +370,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
$messageDetail = $this->getRawMessage($message_id);
$event = collect($messageDetail->messageevents)->first(function ($event) {
$event = collect($messageDetail->messageevents)->first(function ($event) {
return $event?->Details?->BounceID ?? false;
@ -374,29 +382,31 @@ class ProcessPostmarkWebhook implements ShouldQueue
private function fetchMessage(): array
{
if(strlen($this->request['MessageID']) < 1) {
if (strlen($this->request['MessageID']) < 1) {
return $this->default_response;
}
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(',');
$subject = $messageDetail->subject ?? '';
$events = collect($messageDetail->messageevents)->map(function ($event) {
$events = collect($messageDetail->messageevents)->map(function ($event) {
return [
'bounce_id' => $event?->Details?->BounceID ?? '',
'recipient' => $event->Recipient ?? '',
'status' => $event->Type ?? '',
'delivery_message' => $event->Details->DeliveryMessage ?? $event->Details->Summary ?? '',
'server' => $event->Details->DestinationServer ?? '',
'server_ip' => $event->Details->DestinationIP ?? '',
'date' => \Carbon\Carbon::parse($event->ReceivedAt)->format('Y-m-d H:i:s') ?? '',
];
'bounce_id' => $event?->Details?->BounceID ?? '',
'recipient' => $event->Recipient ?? '',
'status' => $event->Type ?? '',
'delivery_message' => $event->Details->DeliveryMessage ?? $event->Details->Summary ?? '',
'server' => $event->Details->DestinationServer ?? '',
'server_ip' => $event->Details->DestinationIP ?? '',
'date' => \Carbon\Carbon::parse($event->ReceivedAt)->format('Y-m-d H:i:s') ?? '',
];
})->toArray();

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
{
@ -55,7 +57,7 @@ class AppServiceProvider extends ServiceProvider
/* Defines the name used in polymorphic tables */
Relation::morphMap([
'invoices' => Invoice::class,
'invoices' => Invoice::class,
'proposals' => Proposal::class,
]);
@ -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;
@ -82,7 +84,7 @@ class AdminEmail implements ShouldQueue
MultiDB::setDb($this->company->db);
$this->setOverride()
->buildMailable();
->buildMailable();
if ($this->preFlightChecksFail()) {
return;
@ -137,24 +139,28 @@ 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());
nlog("Using mailer => " . $this->mailer . " " . now()->toDateTimeString());
$mailer->send($this->mailable);
Cache::increment("email_quota".$this->company->account->key);
Cache::increment("email_quota" . $this->company->account->key);
LightLogs::create(new EmailSuccess($this->company->company_key, $this->mailable->subject))
->send();
->send();
} catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
} catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch(\Symfony\Component\Mime\Exception\LogicException $e) {
} catch (\Symfony\Component\Mime\Exception\LogicException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
@ -215,16 +221,16 @@ class AdminEmail implements ShouldQueue
}
/**
* On the hosted platform we scan all outbound email for
* spam. This sequence processes the filters we use on all
* emails.
*
* @return bool
*/
* On the hosted platform we scan all outbound email for
* spam. This sequence processes the filters we use on all
* emails.
*
* @return bool
*/
public function preFlightChecksFail(): bool
{
/* Always send if disabled */
if($this->override) {
if ($this->override) {
return false;
}
@ -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');
@ -369,7 +379,7 @@ class AdminEmail implements ShouldQueue
if (env($this->company->id . '_MAIL_FROM_ADDRESS')) {
$this->mailable
->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
}
}
}
@ -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();
}
@ -452,7 +464,29 @@ class AdminEmail implements ShouldQueue
$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);
->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);
}
/**
@ -474,7 +508,7 @@ class AdminEmail implements ShouldQueue
$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);
->from($sending_email, $sending_user);
}
/**
@ -500,10 +534,10 @@ class AdminEmail implements ShouldQueue
}
$this->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
@ -527,7 +561,7 @@ class AdminEmail implements ShouldQueue
}
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
} catch(\Exception $e) {
} catch (\Exception $e) {
$this->logMailError('Gmail Token Invalid', $this->company->clients()->first());
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
@ -547,7 +581,7 @@ class AdminEmail implements ShouldQueue
* Now that our token is refreshed and valid we can boot the
* mail driver at runtime and also set the token which will persist
* just for this request.
*/
*/
$token = $user->oauth_user_token->access_token;
@ -558,10 +592,10 @@ class AdminEmail implements ShouldQueue
}
$this->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
@ -573,21 +607,23 @@ class AdminEmail implements ShouldQueue
*/
private function logMailError($errors, $recipient_object): void
{
(new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->company
))->handle();
(
new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->company
)
)->handle();
$job_failure = new EmailFailure($this->company->company_key);
$job_failure->string_metric5 = 'failed_email';
$job_failure->string_metric6 = substr($errors, 0, 150);
LightLogs::create($job_failure)
->send();
->send();
$job_failure = null;
}
@ -608,8 +644,8 @@ class AdminEmail implements ShouldQueue
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id') ,
'client_secret' => config('ninja.o365.client_secret') ,
'client_id' => config('ninja.o365.client_id'),
'client_secret' => config('ninja.o365.client_secret'),
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token

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';
@ -100,9 +104,9 @@ class Email implements ShouldQueue
MultiDB::setDb($this->company->db);
$this->setOverride()
->initModels()
->setDefaults()
->buildMailable();
->initModels()
->setDefaults()
->buildMailable();
/** Ensure quota's on hosted platform are respected. :) */
$this->setMailDriver();
@ -249,7 +253,7 @@ class Email implements ShouldQueue
if(in_array($this->mailer, ['default','mailgun','postmark']))
Cache::increment("email_quota".$this->company->account->key);
}
/**
* Attempts to send the email
*
@ -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,24 +275,28 @@ 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());
nlog("Using mailer => " . $this->mailer . " " . now()->toDateTimeString());
$mailer->send($this->mailable);
$this->incrementEmailCounter();
LightLogs::create(new EmailSuccess($this->company->company_key, $this->mailable->subject))
->send();
->send();
} catch(\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
} catch (\Symfony\Component\Mime\Exception\RfcComplianceException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
} catch(\Symfony\Component\Mime\Exception\LogicException $e) {
} catch (\Symfony\Component\Mime\Exception\LogicException $e) {
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
@ -367,16 +376,16 @@ class Email implements ShouldQueue
}
/**
* On the hosted platform we scan all outbound email for
* spam. This sequence processes the filters we use on all
* emails.
*
* @return bool
*/
* On the hosted platform we scan all outbound email for
* spam. This sequence processes the filters we use on all
* emails.
*
* @return bool
*/
public function preFlightChecksFail(): bool
{
/* Always send if disabled */
if($this->override) {
if ($this->override) {
return false;
}
@ -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;
}
@ -461,7 +470,7 @@ class Email implements ShouldQueue
return true;
}
if($address_object->name == " " || $address_object->name == "") {
if ($address_object->name == " " || $address_object->name == "") {
return true;
}
}
@ -479,7 +488,7 @@ class Email implements ShouldQueue
}
$this->mailable
->from(config('services.mailgun.from.address'), $email_from_name);
->from(config('services.mailgun.from.address'), $email_from_name);
}
@ -498,7 +507,7 @@ class Email implements ShouldQueue
// return $this;
// }
if(Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') {
if (Ninja::isHosted() && $this->company->account->isPaid() && $this->email_object->settings->email_sending_method == 'default') {
try {
@ -507,7 +516,7 @@ class Email implements ShouldQueue
$domain = explode("@", $email)[1] ?? "";
$dns = dns_get_record($domain, DNS_MX);
$server = $dns[0]["target"];
if(stripos($server, "outlook.com") !== false) {
if (stripos($server, "outlook.com") !== false) {
if (property_exists($this->email_object->settings, 'email_from_name') && strlen($this->email_object->settings->email_from_name) > 1) {
$email_from_name = $this->email_object->settings->email_from_name;
@ -518,12 +527,12 @@ class Email implements ShouldQueue
$this->mailer = 'postmark';
$this->client_postmark_secret = config('services.postmark-outlook.token');
$this->mailable
->from(config('services.postmark-outlook.from.address'), $email_from_name);
->from(config('services.postmark-outlook.from.address'), $email_from_name);
return $this;
}
} catch(\Exception $e) {
} catch (\Exception $e) {
nlog("problem switching outlook driver - hosted");
nlog($e->getMessage());
}
@ -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();
@ -600,11 +613,11 @@ class Email implements ShouldQueue
$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();
$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);
->from($sending_email, $sending_user);
}
@ -627,7 +640,7 @@ class Email implements ShouldQueue
if (env($this->company->id . '_MAIL_FROM_ADDRESS')) {
$this->mailable
->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
->from(env($this->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
}
}
}
@ -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();
}
@ -706,11 +721,33 @@ class Email implements ShouldQueue
$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();
$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);
->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);
}
/**
@ -728,11 +765,11 @@ class Email implements ShouldQueue
$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();
$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);
->from($sending_email, $sending_user);
}
/**
@ -758,10 +795,10 @@ class Email implements ShouldQueue
}
$this->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
@ -785,7 +822,7 @@ class Email implements ShouldQueue
}
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
} catch(\Exception $e) {
} catch (\Exception $e) {
$this->logMailError('Gmail Token Invalid', $this->company->clients()->first());
$this->email_object->settings->email_sending_method = 'default';
return $this->setMailDriver();
@ -805,7 +842,7 @@ class Email implements ShouldQueue
* Now that our token is refreshed and valid we can boot the
* mail driver at runtime and also set the token which will persist
* just for this request.
*/
*/
$token = $user->oauth_user_token->access_token;
@ -816,10 +853,10 @@ class Email implements ShouldQueue
}
$this->mailable
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
->from($user->email, $user->name())
->withSymfonyMessage(function ($message) use ($token) {
$message->getHeaders()->addTextHeader('gmailtoken', $token);
});
}
/**
@ -831,21 +868,23 @@ class Email implements ShouldQueue
*/
private function logMailError($errors, $recipient_object): void
{
(new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->company
))->handle();
(
new SystemLogger(
$errors,
SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND,
SystemLog::TYPE_FAILURE,
$recipient_object,
$this->company
)
)->handle();
$job_failure = new EmailFailure($this->company->company_key);
$job_failure->string_metric5 = 'failed_email';
$job_failure->string_metric6 = substr($errors, 0, 150);
LightLogs::create($job_failure)
->send();
->send();
$job_failure = null;
}
@ -866,8 +905,8 @@ class Email implements ShouldQueue
$token = json_decode($guzzle->post($url, [
'form_params' => [
'client_id' => config('ninja.o365.client_id') ,
'client_secret' => config('ninja.o365.client_secret') ,
'client_id' => config('ninja.o365.client_id'),
'client_secret' => config('ninja.o365.client_secret'),
'scope' => 'email Mail.Send offline_access profile User.Read openid',
'grant_type' => 'refresh_token',
'refresh_token' => $user->oauth_user_refresh_token

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', ''),
],
@ -67,8 +71,8 @@ return [
],
'stripe' => [
'model' => App\Models\User::class,
'key' => env('STRIPE_KEY'),
'model' => App\Models\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_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',

File diff suppressed because it is too large Load Diff

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');