Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 250
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResendEmails
0.00% covered (danger)
0.00%
0 / 250
0.00% covered (danger)
0.00%
0 / 4
3306
0.00% covered (danger)
0.00%
0 / 1
 handle
0.00% covered (danger)
0.00%
0 / 149
0.00% covered (danger)
0.00%
0 / 1
1406
 isEmailValid
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getEmailBody
0.00% covered (danger)
0.00%
0 / 78
0.00% covered (danger)
0.00%
0 / 1
132
 calculateEmailRequestSize
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
1<?php
2
3namespace App\Console\Commands;
4
5use App\Models\TblBlockedDomains;
6use App\Models\TblCcBcc;
7use App\Models\TblCompanyEmails;
8use App\Models\TblEmailConfiguration;
9use App\Models\TblEmailFiles;
10use App\Models\TblFiles;
11use App\Models\TblQuotations;
12use App\Models\TblResendEmails;
13use App\Models\TblSendgridWebhook;
14use App\Services\SendgridLogger;
15use Illuminate\Console\Command;
16use Illuminate\Support\Facades\DB;
17use Illuminate\Support\Facades\Log;
18use Illuminate\Support\Facades\Storage;
19use SendGrid\Mail\Attachment;
20use SendGrid\Mail\Mail;
21
22class ResendEmails extends Command
23{
24    /**
25     * The name and signature of the console command.
26     *
27     * @var string
28     */
29    protected $signature = 'command:resend-emails {id?} {type?}';
30
31    private ?Mail $email = null;
32
33    /**
34     * The console command description.
35     *
36     * @var string
37     */
38    protected $description = 'Command description';
39
40    /**
41     * Execute the console command.
42     */
43    public function handle(): void
44    {
45
46        try {
47
48            $this->email = new Mail;
49
50            $id = $this->argument('id') ?? null;
51            $typeId = (int) ($this->argument('type') ?? 0);
52
53            $type = 'followUps';
54
55            if ($typeId == 1) {
56                $type = 'sendToClient';
57            }
58
59            if ($id !== null) {
60
61                $result = TblQuotations::where('id', $id)->first();
62
63                $emailTemplate = TblEmailConfiguration::where('type', $type)->where('is_default', 1)->get()->keyBy('company_id');
64
65                if ($result) {
66
67                    $budgetTypeId = $result->budget_type_id;
68                    $quoteId = $result->id;
69                    $commercialUser = $result->commercial;
70                    $companyId = $result->company_id;
71
72                    $emailContent = $this->getEmailBody(
73                        $emailTemplate[$companyId]->getAttribute('html'),
74                        $emailTemplate[$companyId]->getAttribute('subject'),
75                        $emailTemplate[$companyId]->getAttribute('email_template_id'),
76                        $result
77                    );
78
79                    $html = $emailContent[0];
80                    $subject = $emailContent[1];
81
82                    $blockedDomains = TblBlockedDomains::where('company_id', $result->company_id)->pluck('domain')->toArray();
83
84                    if (config('services.sendgrid.staging')) {
85                        $toEmail = 'christian@ibventur.es';
86                    } else {
87                        $toEmail = $result->email;
88                    }
89
90                    if ($toEmail != null) {
91
92                        $toEmail = explode(',', (string) $toEmail);
93                        $toEmail = array_map(trim(...), $toEmail);
94
95                        $companyEmail = null;
96
97                        $queryUsers = "SELECT 
98                                            sender_email AS from_email, 
99                                            `name` AS from_name 
100                                        FROM tbl_users
101                                        WHERE 
102                                            sender_enabled = 1 
103                                        AND response_id IS NOT NULL 
104                                        AND verified = 1 
105                                        AND `name` = '{$commercialUser}'";
106
107                        $commercialEmail = DB::select($queryUsers);
108
109                        if (count($commercialEmail) > 0) {
110                            $companyEmail = $commercialEmail[0];
111                        } else {
112                            if ($emailTemplate[$companyId]->getAttribute('from_id') != null) {
113                                $companyEmail = TblCompanyEmails::where('id', $emailTemplate[$companyId]->getAttribute('from_id'))->first();
114                            } else {
115                                $companyEmail = TblCompanyEmails::where('is_active', 1)->where('verified', 1)->where('company_id', $result->company_id)->first();
116                            }
117                        }
118
119                        if (! $companyEmail) {
120                            $error = __('language.no_active_verified_sender');
121                            Log::channel('resend_emails')->error("ID:{$quoteId}{$error}");
122                        }
123
124                        $ccBcc = TblCcBcc::where('company_id', $result->company_id)->get();
125
126                        $this->email->setFrom($companyEmail->from_email, $companyEmail->from_name);
127                        $this->email->setSubject($subject);
128
129                        $s = 0;
130                        $addTo = [];
131                        foreach ($toEmail as $clientEmail) {
132                            $isValid = $this->isEmailValid($clientEmail);
133
134                            if ($isValid) {
135                                $domain = substr($clientEmail, strpos($clientEmail, '@') + 1);
136
137                                if (! in_array($domain, $blockedDomains)) {
138                                    $this->email->addTo($clientEmail);
139                                    array_push($addTo, $clientEmail);
140                                    $s++;
141                                }
142                            } else {
143                                Log::channel('resend_emails')->error("ID:{$quoteId}: Invalid email {$clientEmail}");
144                            }
145                        }
146
147                        if ($s == 0) {
148                            $toEmail = json_encode($toEmail);
149                            Log::channel('resend_emails')->error("[NO-VALID-EMAILS:{$s}] ID:{$quoteId}{$toEmail}}");
150                        }
151
152                        if (! config('services.sendgrid.staging')) {
153                            if (count($ccBcc) > 0) {
154                                foreach ($ccBcc as $data) {
155                                    if (! in_array($data->email, $toEmail)) {
156                                        $this->email->addBcc($data->email);
157                                    }
158                                }
159                            }
160                        }
161
162                        $this->email->addContent('text/html', $html);
163
164                        $sendgrid = new \SendGrid(config('services.sendgrid.api_key'));
165
166                        $files = TblFiles::where('quotation_id', $result->id)->where('is_internal', null)->get();
167                        $requestSize = $this->calculateEmailRequestSize($this->email);
168
169                        foreach ($files as $key => $value) {
170                            if ($files[$key]->filename && Storage::disk('s3')->exists('uploads/'.$files[$key]->filename)) {
171
172                                $fileContent = Storage::disk('s3')->get('uploads/'.$files[$key]->filename);
173                                $fileSize = strlen((string) $fileContent) / 1048576;
174                                $fileSize = (int) ceil($fileSize);
175
176                                $originalName = $files[$key]->original_name ?: basename((string) $files[$key]->filename);
177                            } elseif ($files[$key]->file) {
178                                $fileContent = $files[$key]->file;
179
180                                if (is_string($fileContent) && base64_decode($fileContent, true)) {
181                                    $fileContent = base64_decode($fileContent);
182                                }
183
184                                $fileSize = strlen((string) $fileContent) / 1048576;
185                                $fileSize = (int) ceil($fileSize);
186
187                                $originalName = $files[$key]->original_name ?: ($files[$key]->getAttribute('file_name') ?: 'file');
188                            } else {
189                                continue;
190                            }
191
192                            if ($fileSize > 0) {
193                                if ($requestSize + $fileSize < 25) {
194                                    $attachment = new Attachment;
195                                    $attachment->setFilename($originalName);
196                                    $attachment->setDisposition('attachment');
197
198                                    $attachment->setContent(base64_encode((string) $fileContent));
199
200                                    if ($files[$key]->mime_type) {
201                                        $attachment->setType($files[$key]->mime_type);
202                                    }
203
204                                    $this->email->addAttachment($attachment);
205                                    $requestSize = $this->calculateEmailRequestSize($this->email);
206                                } else {
207                                    Log::channel('resend_emails')->error('File omitted due to size limit: '.$originalName);
208                                }
209                            }
210                        }
211
212                        $sentWithBackup = false;
213
214                        try {
215                            $response = $sendgrid->send($this->email);
216                            SendgridLogger::log($this->email, $response);
217                        } catch (\Throwable $sendException) {
218                            SendgridLogger::logException($this->email, $sendException);
219                            throw $sendException;
220                        }
221
222                        if ($response->statusCode() == 202) {
223
224                            $messageId = null;
225
226                            foreach ($response->headers() as $header) {
227                                if (str_starts_with(strtolower((string) $header), 'x-message-id:')) {
228                                    $messageId = trim(substr((string) $header, strpos((string) $header, ':') + 1));
229                                    break;
230                                }
231                            }
232
233                            $typeArray = [
234                                'y_message_id' => $messageId,
235                                'y_status' => 'Processing',
236                            ];
237
238                            if ($type == 'sendToClient') {
239                                $typeArray = [
240                                    'x_message_id' => $messageId,
241                                    'x_status' => 'Processing',
242                                ];
243                            }
244
245                            TblQuotations::where('id', $result->id)->update($typeArray);
246
247                            $jsonBody = [];
248
249                            foreach ($toEmail as $clientEmail) {
250                                $isValid = $this->isEmailValid($clientEmail);
251                                $eventStatus = 'processed';
252                                $eventResponse = '';
253                                if (! $isValid) {
254                                    $eventStatus = 'invalid';
255                                    $eventResponse = 'Invalid email address';
256                                }
257
258                                array_push(
259                                    $jsonBody,
260                                    [
261                                        'email' => $clientEmail,
262                                        'event' => $eventStatus,
263                                        'sg_message_id' => $messageId,
264                                        'smtp-id' => $messageId,
265                                        'timestamp' => strtotime(date('Y-m-d H:i:s')),
266                                        'response' => $eventResponse,
267                                    ]
268                                );
269                            }
270
271                            TblSendgridWebhook::create(
272                                [
273                                    'quotation_id' => $result->id,
274                                    'type' => $type,
275                                    'json_body' => json_encode($jsonBody),
276                                    'x_message_id' => $messageId,
277                                ]
278                            );
279
280                            TblResendEmails::where('quotation_id', $quoteId)->where('type', $typeId)->delete();
281
282                            $toEmail = json_encode($toEmail);
283                            Log::channel('resend_emails')->error("[SENT-OK] ID:{$quoteId}{$toEmail}}");
284                        } else {
285                            Log::channel('resend_emails')->error('[ERROR-SG] ID:'.$result->id.' - '.$response->body());
286                        }
287                    }
288                } else {
289                    Log::channel('resend_emails')->error('[ERROR-ID] ID does not exist');
290                }
291            } else {
292                Log::channel('resend_emails')->error('[ERROR-ID] Missing ID');
293            }
294
295        } catch (\Exception $e) {
296            Log::channel('resend_emails')->error($e);
297        }
298    }
299
300    public function isEmailValid($email): bool
301    {
302        // Regular expression pattern for email validation
303        $pattern = '/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/';
304
305        // Check if the email matches the pattern
306        if (preg_match($pattern, (string) $email)) {
307            return true; // Valid email
308        } else {
309            return false; // Invalid email
310        }
311    }
312
313    public function getEmailBody($body, $subject, $emailTemplateId, $result): array
314    {
315
316        $availableParameters = [
317            'quote_id',
318            'company_id',
319            'client',
320            'client_type',
321            'phone_number',
322            'email',
323            'issue_date',
324            'request_date',
325            'duration',
326            'invoice_number',
327            'type',
328            'acceptance_date',
329            'status',
330            'source',
331            'amount',
332            'reason_for_not_following_up',
333            'last_follow_up_date',
334            'last_follow_up_comment',
335            'reason_for_rejection_id',
336            'reason_for_rejection',
337            'commercial',
338            'created_at',
339            'created_by',
340            'updated_at',
341            'updated_by',
342        ];
343
344        $dateParameters = [
345            'issue_date',
346            'request_date',
347            'acceptance_date',
348            'last_follow_up_date',
349            'created_at',
350            'updated_at',
351        ];
352
353        preg_match_all('/{{(.*?)}}/', (string) $body, $matches);
354
355        $parameters = $matches[1];
356
357        foreach ($parameters as $parameter) {
358
359            if (in_array($parameter, $dateParameters)) {
360                if ($result->{$parameter}) {
361                    $result->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result->{$parameter})));
362                }
363            }
364
365            if (in_array($parameter, $availableParameters)) {
366                $body = str_replace('{{'.$parameter.'}}', $result->{$parameter}, $body);
367            }
368        }
369
370        preg_match_all('/{{(.*?)}}/', (string) $subject, $matches);
371
372        $parameters = $matches[1];
373
374        foreach ($parameters as $parameter) {
375
376            if (in_array($parameter, $dateParameters)) {
377                if ($result->{$parameter}) {
378                    $result->{$parameter} = iconv('ISO-8859-2', 'UTF-8', strftime('%A, %B %d, %Y', strtotime((string) $result->{$parameter})));
379                }
380            }
381
382            if (in_array($parameter, $availableParameters)) {
383                $subject = str_replace('{{'.$parameter.'}}', $result->{$parameter}, $subject);
384            }
385        }
386
387        $templateFiles = TblEmailFiles::where('email_template_id', $emailTemplateId)->orderBy('order', 'asc')->get();
388
389        foreach ($templateFiles as $item) {
390            $f = storage_path('app/public/uploads/'.$item->filename);
391
392            if (file_exists($f)) {
393                $imgpath = file_get_contents($f);
394                $base64 = 'data:image/png;base64,'.base64_encode($imgpath);
395                $mimeType = mime_content_type($f);
396
397                $this->email->addAttachment(
398                    $imgpath,
399                    $mimeType,
400                    str_replace(' ', '', $item->original_name),
401                    'inline',
402                    str_replace(' ', '', $item->original_name),
403                );
404
405                $body .= "<img src='cid:{$item->original_name}' style='height: 45px; padding-right: 6px' />";
406            } else {
407                Log::channel('resend_emails')->error("[{$result->id}] File not found: ".$f);
408            }
409        }
410
411        $html = '<!DOCTYPE html>';
412        $html .= '<html>';
413        $html .= '<head>';
414        $html .= '<meta charset="UTF-8">';
415        $html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
416        $html .= '</head>';
417        $html .= '<body>';
418        $html .= $body;
419        $html .= '</body>';
420        $html .= '</html>';
421
422        return [$html, $subject];
423    }
424
425    public function calculateEmailRequestSize(Mail $email): int
426    {
427
428        $size = 0;
429
430        // Add size of 'from', 'to', 'subject', 'content'
431        $from = $email->getFrom();
432        $size += strlen(json_encode([
433            'from' => $from->getEmail().' '.$from->getName(),
434            'subject' => $email->getSubject(),
435        ]));
436
437        // Add size of 'to' (recipients)
438        $personalizations = $email->getPersonalizations() ?? [];
439        foreach ($personalizations as $personalization) {
440            foreach ($personalization->getTos() as $to) {
441                $size += strlen($to->getEmail().' '.$to->getName());
442            }
443        }
444
445        // Add size of content
446        foreach ($email->getContents() as $content) {
447            $size += strlen((string) $content->getValue());
448        }
449
450        // Add size of attachments (if any)
451
452        if ($email->getAttachments() != null && $email->getAttachments() != '') {
453            foreach ($email->getAttachments() as $attachment) {
454                $size += strlen($attachment->getContent()); // Base64 encoded size
455                $size += strlen($attachment->getFilename());
456                $size += strlen($attachment->getType());
457            }
458        }
459
460        $sizeInMegabytes = $size / 1048576; // 1 MB = 1,048,576 bytes
461
462        return (int) ceil($sizeInMegabytes);
463    }
464}