Working on emailers

This commit is contained in:
David Bomba 2023-02-15 11:04:47 +11:00
parent 1e5b96ddad
commit a4f2d40d75
9 changed files with 89 additions and 289 deletions

View File

@ -23,6 +23,7 @@ use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Services\Email\MailObject;
use App\Transformers\CreditTransformer;
use App\Transformers\InvoiceTransformer;
use App\Transformers\PurchaseOrderTransformer;
@ -127,6 +128,8 @@ class EmailController extends BaseController
'body' => $body,
];
$mo = new MailObject;
if(Ninja::isHosted() && !$entity_obj->company->account->account_sms_verified)
return response(['message' => 'Please verify your account to send emails.'], 400);

View File

@ -73,7 +73,6 @@ class TokenAuth
| session
*/
app('queue')->createPayloadUsing(function () use ($company_token) {
nlog("setting DB ". $company_token->company->db);
return ['db' => $company_token->company->db];
});

View File

@ -11,19 +11,14 @@
namespace App\Jobs\Entity;
use App\Events\Invoice\InvoiceReminderWasEmailed;
use App\Events\Invoice\InvoiceWasEmailed;
use App\Events\Invoice\InvoiceWasEmailedAndFailed;
use App\Jobs\Mail\EntityFailedSendMailer;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\TemplateEmail;
use App\Models\Activity;
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\Utils\HtmlEngine;
@ -34,7 +29,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
/*Multi Mailer implemented*/

View File

@ -351,4 +351,16 @@ class Quote extends BaseModel
{
return ctrans('texts.quote');
}
/**
* calculateTemplate
*
* @param string $entity_string
* @return string
*/
public function calculateTemplate(string $entity_string): string
{
return $entity_string;
}
}

View File

@ -37,7 +37,6 @@ class MultiDBProvider extends ServiceProvider
JobProcessing::class,
function ($event) {
if (isset($event->job->payload()['db'])) {
nlog("Settings DB: " . $event->job->payload()['db']);
MultiDB::setDb($event->job->payload()['db']);
}
}

View File

@ -1,263 +0,0 @@
<?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\Services\Email;
use App\Utils\Ninja;
use App\Models\Company;
use Illuminate\Bus\Queueable;
use App\Services\Email\MailBuild;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Cache;
use Illuminate\Queue\SerializesModels;
use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Contracts\Mail\Mailable;
use Illuminate\Queue\InteractsWithQueue;
use App\DataMapper\Analytics\EmailSuccess;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class BaseMailer implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected Company $company;
public int $tries = 4;
public ?string $client_postmark_secret = null;
public ?string $client_mailgun_secret = null;
public ?string $client_mailgun_domain = null;
public bool $override = false;
public $deleteWhenMissingModels = true;
public Mail $mail;
private string $mailer = 'default';
public function __construct(public mixed $invitation, private ?string $db, public MailObject $mail_object)
{
$this->invitation = $invitation;
$this->company = $invitation->company;
$this->db = $db;
$this->mail_object = $mail_object;
$this->override = $mail_object->override;
}
public function companyCheck(): void
{
/* Handle bad state */
if(!$this->company)
$this->fail();
/* Handle deactivated company */
if($this->company->is_disabled && !$this->override)
$this->fail();
/* To handle spam users we drop all emails from flagged accounts */
if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged)
$this->fail();
}
public function configureMailer(): self
{
$this->setMailDriver();
$this->mail = Mail::mailer($this->mailer);
return $this;
}
/**
* Sets the mail driver to use and applies any specific configuration
* the the mailable
*/
private function setMailDriver(): self
{
switch ($this->mail_object->settings->email_sending_method) {
case 'default':
$this->mailer = config('mail.default');
break;
// case 'gmail':
// $this->mailer = 'gmail';
// $this->setGmailMailer();
// return $this;
// case 'office365':
// $this->mailer = 'office365';
// $this->setOfficeMailer();
// return $this;
// case 'client_postmark':
// $this->mailer = 'postmark';
// $this->setPostmarkMailer();
// return $this;
// case 'client_mailgun':
// $this->mailer = 'mailgun';
// $this->setMailgunMailer();
// return $this;
default:
break;
}
if(Ninja::isSelfHost())
$this->setSelfHostMultiMailer();
return $this;
}
/**
* Allows configuration of multiple mailers
* per company for use by self hosted users
*/
private function setSelfHostMultiMailer(): void
{
if (env($this->email_service->company->id . '_MAIL_HOST'))
{
config([
'mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => env($this->email_service->company->id . '_MAIL_HOST'),
'port' => env($this->email_service->company->id . '_MAIL_PORT'),
'username' => env($this->email_service->company->id . '_MAIL_USERNAME'),
'password' => env($this->email_service->company->id . '_MAIL_PASSWORD'),
],
]);
if(env($this->email_service->company->id . '_MAIL_FROM_ADDRESS'))
{
$this->email_mailable
->from(env($this->email_service->company->id . '_MAIL_FROM_ADDRESS', env('MAIL_FROM_ADDRESS')), env($this->email_service->company->id . '_MAIL_FROM_NAME', env('MAIL_FROM_NAME')));
}
}
}
/**
* Ensure we discard any data that is not required
*
* @return void
*/
private function cleanUpMailers(): void
{
$this->client_postmark_secret = false;
$this->client_mailgun_secret = false;
$this->client_mailgun_domain = false;
//always dump the drivers to prevent reuse
app('mail.manager')->forgetMailers();
}
public function trySending()
{
try {
$mailer
->to($this->mail_object->to_user->email)
->send($this->mail_object->mailable);
/* Count the amount of emails sent across all the users accounts */
Cache::increment($this->company->account->key);
LightLogs::create(new EmailSuccess($this->company->company_key))
->send();
}
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){
nlog("Mailer failed with a Logic Exception {$e->getMessage()}");
$this->fail();
$this->cleanUpMailers();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
return;
}
catch (\Exception | \Google\Service\Exception $e) {
nlog("Mailer failed with {$e->getMessage()}");
$message = $e->getMessage();
/**
* Post mark buries the proper message in a a guzzle response
* this merges a text string with a json object
* need to harvest the ->Message property using the following
*/
if(stripos($e->getMessage(), 'code 406') || stripos($e->getMessage(), 'code 300') || stripos($e->getMessage(), 'code 413'))
{
$message = "Either Attachment too large, or recipient has been suppressed.";
$this->fail();
$this->logMailError($e->getMessage(), $this->company->clients()->first());
$this->cleanUpMailers();
return;
}
//only report once, not on all tries
if($this->attempts() == $this->tries)
{
/* If the is an entity attached to the message send a failure mailer */
if($this->nmo->entity)
$this->entityEmailFailed($message);
/* Don't send postmark failures to Sentry */
if(Ninja::isHosted() && (!$e instanceof ClientException))
app('sentry')->captureException($e);
}
/* Releasing immediately does not add in the backoff */
$this->release($this->backoff()[$this->attempts()-1]);
}
}
public function backoff()
{
return [5, 10, 30, 240];
}
public function failed($exception = null)
{
config(['queue.failed.driver' => null]);
}
}

View File

@ -13,13 +13,18 @@ namespace App\Services\Email;
use App\Models\Task;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Account;
use App\Models\Expense;
use App\Models\Invoice;
use App\Utils\HtmlEngine;
use App\Models\ClientContact;
use App\Models\PurchaseOrder;
use App\Models\VendorContact;
use App\Utils\Traits\MakesHash;
use App\Utils\VendorHtmlEngine;
use App\Jobs\Entity\CreateRawPdf;
use Illuminate\Support\Facades\App;
@ -30,7 +35,6 @@ use Illuminate\Contracts\Mail\Mailable;
use App\DataMapper\EmailTemplateDefaults;
use League\CommonMark\CommonMarkConverter;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Utils\Traits\MakesHash;
class MailBuild
{
@ -118,6 +122,30 @@ class MailBuild
$this->vendor = $vendor_contact?->vendor;
if($this->mail_entity?->invitation){
if($this->mail_entity->invitation?->invoice){
$this->mail_entity->mail_object->entity_string = 'invoice';
$this->mail_entity->mail_object->entity_class = Invoice::class;
}
if($this->mail_entity->invitation?->quote){
$this->mail_entity->mail_object->entity_string = 'quote';
$this->mail_entity->mail_object->entity_class = Quote::class;
}
if($this->mail_entity->invitation?->credit){
$this->mail_entity->mail_object->entity_string = 'credit';
$this->mail_entity->mail_object->entity_class = Credit::class;
}
if($this->mail_entity->invitation?->puchase_order){
$this->mail_entity->mail_object->entity_string = 'purchase_order';
$this->mail_entity->mail_object->entity_class = PurchaseOrder::class;
}
}
return $this;
}
@ -261,7 +289,7 @@ class MailBuild
elseif(is_string($this->mail_entity->mail_object->email_template_subject) && strlen($this->settings->{$this->mail_entity->mail_object->email_template_subject}) > 3)
$this->mail_entity->mail_object->subject = $this->settings->{$this->mail_entity->mail_object->email_template_subject};
else
$this->mail_entity->mail_object->subject = EmailTemplateDefaults::getDefaultTemplate($this->mail_entity->mail_object->email_template_subject, $this->locale);
$this->mail_entity->mail_object->subject = EmailTemplateDefaults::getDefaultTemplate($this->resolveBaseEntityTemplate(), $this->locale);
return $this;
@ -282,7 +310,7 @@ class MailBuild
$this->mail_entity->mail_object->body = $this->settings->{$this->mail_entity->mail_object->email_template_body};
}
else{
$this->mail_entity->mail_object->body = EmailTemplateDefaults::getDefaultTemplate($this->mail_entity->mail_object->email_template_body, $this->locale);
$this->mail_entity->mail_object->body = EmailTemplateDefaults::getDefaultTemplate($this->resolveBaseEntityTemplate('body'), $this->locale);
}
if($this->template == 'email.template.custom'){
@ -292,6 +320,29 @@ class MailBuild
return $this;
}
/**
* Where no template is explicitly passed, we need to infer by the entity type -
* which is hopefully resolvable.
*
* @param string $type
* @return string
*/
private function resolveBaseEntityTemplate(string $type = 'subject'): string
{
//handle statements being emailed
//handle custom templates these types won't have a resolvable entity_string
if(!$this->mail_entity->mail_object->entity_string)
return 'email_template_invoice';
match($type){
'subject' => $template = "email_subject_{$this->mail_entity->mail_object->entity_string}",
'body' => $template = "email_template_{$this->mail_entity->mail_object->entity_string}",
default => $template = "email_template_invoice",
};
return $template;
}
/**
* Sets the attachments for the email
@ -365,24 +416,18 @@ class MailBuild
}
if($this->mail_entity->invitation?->invoice)
$entity = 'invoice';
if($this->mail_entity->invitation?->quote)
$entity = 'quote';
if($this->mail_entity->invitation?->credit)
$entity = 'credit';
if(!$this->mail_entity->mail_object->entity_string)
return $this;
$pdf = ((new CreateRawPdf($this->mail_entity->invitation, $this->mail_entity->invitation->company->db))->handle());
nlog($this->mail_entity->mail_object->attachments);
$this->mail_entity->mail_object->attachments = array_merge($this->mail_entity->mail_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->mail_entity->invitation->{$entity}->numberFormatter().'.pdf']]);
$this->mail_entity->mail_object->attachments = array_merge($this->mail_entity->mail_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->mail_entity->invitation->{$this->mail_entity->mail_object->entity_string}->numberFormatter().'.pdf']]);
if ($this->client->getSetting('document_email_attachment') !== false && $this->mail_entity->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) {
$this->attachDocuments($this->mail_entity->invitation->{$entity}->documents);
$this->attachDocuments($this->mail_entity->invitation->{$this->mail_entity->mail_object->entity_string}->documents);
}
@ -391,12 +436,12 @@ class MailBuild
if($this->settings->ubl_email_attachment && $entity == 'invoice')
if($this->settings->ubl_email_attachment && $this->mail_entity->mail_object->entity_string == 'invoice')
{
}
if($entity == 'invoice')
if($this->mail_entity->mail_object->entity_string == 'invoice')
{
$line_items = $this->mail_entity->invitation->invoice->line_items;

View File

@ -78,6 +78,8 @@ class MailObject
public ?string $entity_class = null;
public ?string $entity_string = null;
public array $variables = [];
public ?string $template = null;

View File

@ -13,6 +13,8 @@ namespace App\Services\Quote;
use App\Jobs\Entity\EmailEntity;
use App\Models\ClientContact;
use App\Services\Email\MailEntity;
use App\Services\Email\MailObject;
class SendEmail
{
@ -37,15 +39,22 @@ class SendEmail
*/
public function run()
{
nlog($this->reminder_template);
nlog("is there a template");
if (! $this->reminder_template) {
$this->reminder_template = $this->quote->calculateTemplate('quote');
}
$mo = new MailObject();
$this->quote->service()->markSent()->save();
$this->quote->invitations->each(function ($invitation) {
$this->quote->invitations->each(function ($invitation) use ($mo){
if (! $invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template);
// EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template);
MailEntity::dispatch($invitation, $invitation->company->db, $mo);
}
});