diff --git a/app/Http/Requests/Setup/CheckMailRequest.php b/app/Http/Requests/Setup/CheckMailRequest.php index 7df6368212c9..dd6e7b1413e5 100644 --- a/app/Http/Requests/Setup/CheckMailRequest.php +++ b/app/Http/Requests/Setup/CheckMailRequest.php @@ -32,8 +32,6 @@ class CheckMailRequest extends Request */ public function rules() { - nlog($this->driver); - return [ 'mail_driver' => 'required', 'encryption' => 'required_unless:mail_driver,log', diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 6faeadccfc31..3648a63dd329 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -24,14 +24,36 @@ use Illuminate\Mail\Attachment; class EmailDefaults { + /** + * The settings object for this email + * @var CompanySettings $settings + */ protected $settings; + /** + * The HTML / Template to use for this email + * @var string $template + */ private string $template; + /** + * The locale to use for + * translations for this email + */ private string $locale; + /** + * @param EmailService $email_service The email service class + * @param EmailObject $email_object the email object class + */ public function __construct(protected EmailService $email_service, public EmailObject $email_object){} + /** + * Entry point for generating + * the defaults for the email object + * + * @return EmailObject $email_object The email object + */ public function run() { $this->settings = $this->email_object->settings; @@ -51,6 +73,9 @@ class EmailDefaults } + /** + * Sets the meta data for the Email object + */ private function setMetaData(): self { @@ -66,6 +91,9 @@ class EmailDefaults } + /** + * Sets the locale + */ private function setLocale(): self { @@ -84,6 +112,9 @@ class EmailDefaults return $this; } + /** + * Sets the template + */ private function setTemplate(): self { $this->template = $this->email_object->settings->email_style; @@ -100,6 +131,9 @@ class EmailDefaults return $this; } + /** + * Sets the FROM address + */ private function setFrom(): self { if($this->email_object->from) @@ -111,7 +145,9 @@ class EmailDefaults } - //think about where we do the string replace for variables.... + /** + * Sets the body of the email + */ private function setBody(): self { @@ -133,7 +169,9 @@ class EmailDefaults } - //think about where we do the string replace for variables.... + /** + * Sets the subject of the email + */ private function setSubject(): self { @@ -148,6 +186,25 @@ class EmailDefaults } + /** + * Sets the reply to of the email + */ + private function setReplyTo(): self + { + + $reply_to_email = str_contains($this->email_object->settings->reply_to_email, "@") ? $this->email_object->settings->reply_to_email : $this->email_service->company->owner()->email; + + $reply_to_name = strlen($this->email_object->settings->reply_to_name) > 3 ? $this->email_object->settings->reply_to_name : $this->email_service->company->owner()->present()->name(); + + $this->email_object->reply_to = array_merge($this->email_object->reply_to, [new Address($reply_to_email, $reply_to_name)]); + + return $this; + } + + /** + * Replaces the template placeholders + * with variable values. + */ public function setVariables(): self { @@ -161,18 +218,9 @@ class EmailDefaults return $this; } - private function setReplyTo(): self - { - - $reply_to_email = str_contains($this->email_object->settings->reply_to_email, "@") ? $this->email_object->settings->reply_to_email : $this->email_service->company->owner()->email; - - $reply_to_name = strlen($this->email_object->settings->reply_to_name) > 3 ? $this->email_object->settings->reply_to_name : $this->email_service->company->owner()->present()->name(); - - $this->email_object->reply_to = array_merge($this->email_object->reply_to, [new Address($reply_to_email, $reply_to_name)]); - - return $this; - } - + /** + * Sets the BCC of the email + */ private function setBcc(): self { $bccs = []; @@ -197,6 +245,10 @@ class EmailDefaults return $this; } + /** + * Sets the CC of the email + * @todo at some point.... + */ private function buildCc() { return [ @@ -204,6 +256,14 @@ class EmailDefaults ]; } + /** + * Sets the attachments for the email + * + * Note that we base64 encode these, as they + * sometimes may not survive serialization. + * + * We decode these in the Mailable later + */ private function setAttachments(): self { $attachments = []; @@ -224,6 +284,9 @@ class EmailDefaults } + /** + * Sets the headers for the email + */ private function setHeaders(): self { if($this->email_object->invitation_key) @@ -232,7 +295,13 @@ class EmailDefaults return $this; } - public function parseMarkdownToHtml(string $markdown): ?string + /** + * Converts any markdown to HTML in the email + * + * @param string $markdown The body to convert + * @return string The parsed markdown response + */ + private function parseMarkdownToHtml(string $markdown): ?string { $converter = new CommonMarkConverter([ 'allow_unsafe_links' => false, diff --git a/app/Services/Email/EmailMailable.php b/app/Services/Email/EmailMailable.php index 9dc0e38d5295..aa05bc23b272 100644 --- a/app/Services/Email/EmailMailable.php +++ b/app/Services/Email/EmailMailable.php @@ -59,7 +59,7 @@ class EmailMailable extends Mailable view: $this->email_object->template, text: $this->email_object->text_template, with: [ - 'text_body' => strip_tags($this->email_object->body), + 'text_body' => strip_tags($this->email_object->body), //@todo this is a bit hacky here. 'body' => $this->email_object->body, 'settings' => $this->email_object->settings, 'whitelabel' => $this->email_object->whitelabel, diff --git a/app/Services/Email/EmailMailer.php b/app/Services/Email/EmailMailer.php index f6e4653bfcd8..a5be905327c3 100644 --- a/app/Services/Email/EmailMailer.php +++ b/app/Services/Email/EmailMailer.php @@ -71,26 +71,28 @@ class EmailMailer implements ShouldQueue { MultiDB::setDb($this->email_service->company->db); - //decode all attachments + /* Perform final checks */ + if($this->email_service->preFlightChecksFail()) + return; + + /* Boot the required driver*/ $this->setMailDriver(); + /* Init the mailer*/ $mailer = Mail::mailer($this->mailer); - if($this->client_postmark_secret){ - nlog("inside postmark config"); - nlog($this->client_postmark_secret); + /* Additional configuration if using a client third party mailer */ + if($this->client_postmark_secret) $mailer->postmark_config($this->client_postmark_secret); - } - if($this->client_mailgun_secret){ + if($this->client_mailgun_secret) $mailer->mailgun_config($this->client_mailgun_secret, $this->client_mailgun_domain); - } - - //send email + /* Attempt the send! */ try { nlog("Using mailer => ". $this->mailer. " ". now()->toDateTimeString()); + $mailer->send($this->email_mailable); Cache::increment($this->email_service->company->account->key); @@ -131,16 +133,15 @@ class EmailMailer implements ShouldQueue app('sentry')->captureException($e); $message = null; - // $this->email_service = null; - // $this->email_mailable = null; - + } } /** * Entity notification when an email fails to send - * + * + * @todo - rewrite this * @param string $message * @return void */ @@ -171,6 +172,10 @@ class EmailMailer implements ShouldQueue } + /** + * Sets the mail driver to use and applies any specific configuration + * the the mailable + */ private function setMailDriver(): self { @@ -263,9 +268,10 @@ class EmailMailer implements ShouldQueue private function checkValidSendingUser($user) { /* Always ensure the user is set on the correct account */ - if($user->account_id != $this->email_service->company->account_id){ - + if($user->account_id != $this->email_service->company->account_id) + { $this->email_service->email_object->settings->email_sending_method = 'default'; + return $this->setMailDriver(); } } @@ -281,11 +287,13 @@ class EmailMailer implements ShouldQueue { $sending_user = $this->email_service->email_object->settings->gmail_sending_user_id; - $user = User::find($this->decodePrimaryKey($sending_user)); + if($sending_user == "0") + $user = $this->email_service->company->owner(); + else + $user = User::find($this->decodePrimaryKey($sending_user)); return $user; } - /** * Configures Mailgun using client supplied secret * as the Mailer @@ -301,7 +309,6 @@ class EmailMailer implements ShouldQueue return $this->setMailDriver(); } - $user = $this->resolveSendingUser(); $this->mailable @@ -462,9 +469,9 @@ class EmailMailer implements ShouldQueue * Attempts to refresh the Microsoft refreshToken * * @param App\Models\User - * @return string | bool + * @return mixed */ - private function refreshOfficeToken($user) + private function refreshOfficeToken(User $user): mixed { $expiry = $user->oauth_user_token_expiry ?: now()->subDay(); diff --git a/app/Services/Email/EmailService.php b/app/Services/Email/EmailService.php index 9e66d97c5716..7bc635d9d71b 100644 --- a/app/Services/Email/EmailService.php +++ b/app/Services/Email/EmailService.php @@ -13,18 +13,28 @@ namespace App\Services\Email; use App\Models\Company; use App\Services\Email\EmailObject; +use App\Utils\Ninja; use Illuminate\Mail\Mailable; class EmailService { - protected string $mailer; + /** + * Used to flag whether we force send the email regardless + * + * @var bool $override; + */ protected bool $override; public Mailable $mailable; public function __construct(public EmailObject $email_object, public Company $company){} + /** + * Sends the email via a dispatched job + * @param boolean $override Whether the email should send regardless + * @return void + */ public function send($override = false) :void { $this->override = $override; @@ -34,11 +44,11 @@ class EmailService ->email(); } - public function sendNow($override = false) :void + public function sendNow($force = false) :void { $this->setDefaults() ->updateMailable() - ->email(true); + ->email($force); } private function email($force = false): void @@ -65,9 +75,81 @@ class EmailService return $this; } - private function emailQualityCheck() + /** + * 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 { - + + /* If we are migrating data we don't want to fire any emails */ + if($this->company->is_disabled && !$this->override) + return true; + + /* To handle spam users we drop all emails from flagged accounts */ + if(Ninja::isHosted() && $this->company->account && $this->company->account->is_flagged) + return true; + + /* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */ + if(Ninja::isHosted() && $this->hasValidEmails()) + return true; + + /* GMail users are uncapped */ + if(Ninja::isHosted() && in_array($this->email_object->settings->email_sending_method, ['gmail', 'office365', 'client_postmark', 'client_mailgun'])) + return false; + + /* On the hosted platform, if the user is over the email quotas, we do not send the email. */ + if(Ninja::isHosted() && $this->company->account && $this->company->account->emailQuotaExceeded()) + return true; + + /* If the account is verified, we allow emails to flow */ + if(Ninja::isHosted() && $this->company->account && $this->company->account->is_verified_account) { + + //11-01-2022 + + /* Continue to analyse verified accounts in case they later start sending poor quality emails*/ + // if(class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class)) + // (new \Modules\Admin\Jobs\Account\EmailQuality($this->nmo, $this->company))->run(); + + return false; + } + + /* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */ + if(Ninja::isHosted() && $this->company->account && !$this->company->account->account_sms_verified){ + + if(class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) + return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run(); + + return true; + } + + /* On the hosted platform we actively scan all outbound emails to ensure outbound email quality remains high */ + if(class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class)) + return (new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->company))->run(); + + return false; } + private function hasValidEmails(): bool + { + + foreach($this->email_object->to as $address_object) + { + + if(strpos($address_object->address, '@example.com') !== false) + return true; + + if(!str_contains($address_object->address, "@")) + return true; + + } + + + return false; + } + + } \ No newline at end of file