Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.65% covered (warning)
58.65%
122 / 208
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Mail
58.65% covered (warning)
58.65%
122 / 208
28.57% covered (danger)
28.57%
2 / 7
234.84
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 initQueueTransmission
40.00% covered (danger)
40.00%
20 / 50
0.00% covered (danger)
0.00%
0 / 1
79.42
 sendQueueItems
73.08% covered (warning)
73.08%
57 / 78
0.00% covered (danger)
0.00%
0 / 1
14.81
 getValidMailer
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
6
 readMailer
70.45% covered (warning)
70.45%
31 / 44
0.00% covered (danger)
0.00%
0 / 1
12.58
 startProcess
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 deleteEntitiesFromQueue
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 *
5* @package Zmsmessaging
6*
7*/
8
9namespace BO\Zmsmessaging;
10
11use BO\Zmsentities\Mimepart;
12use BO\Zmsentities\Mail as MailEntity;
13use PHPMailer\PHPMailer\PHPMailer;
14use PHPMailer\PHPMailer\Exception as PHPMailerException;
15
16class Mail extends BaseController
17{
18    protected $messagesQueue = null;
19    protected $startTime;
20
21    public function __construct($verbose = false, $maxRunTime = 50)
22    {
23        parent::__construct($verbose, $maxRunTime);
24        $this->log("Read Mail QueueList start with limit " . \App::$mails_per_minute . " - " . \App::$now->format('c'));
25        $queueList = \App::$http->readGetResult('/mails/', [
26            'resolveReferences' => 0,
27            'limit' => \App::$mails_per_minute,
28            'onlyIds' => true
29        ])->getCollection();
30        if (null !== $queueList) {
31            $this->messagesQueue = $queueList->sortByCustomKey('createTimestamp');
32        } else {
33            $this->log("QueueList is null - " . \App::$now->format('c'));
34        }
35    }
36
37    public function initQueueTransmission($action = false)
38    {
39        $resultList = [];
40        if ($this->messagesQueue && count($this->messagesQueue)) {
41            if ($this->maxRunTime < $this->getSpendTime()) {
42                $this->log("Max Runtime exceeded before processing started - " . \App::$now->format('c'));
43                return $resultList;
44            }
45            $this->log("Messages queue count - " . count($this->messagesQueue));
46            if (count($this->messagesQueue) <= 50) {
47                $this->log("Less than or equal to 50 items, sending immediately.");
48
49                $itemIds = [];
50                foreach ($this->messagesQueue as $item) {
51                    if ($this->maxRunTime < $this->getSpendTime()) {
52                        $this->log("Max Runtime exceeded during message loop - " . \App::$now->format('c'));
53                        break;
54                    }
55                    $itemIds[] = $item['id'];
56                }
57
58                if (!empty($itemIds)) {
59                    try {
60                        $results = $this->sendQueueItems($action, $itemIds);
61                        foreach ($results as $result) {
62                            $resultList[] = $result;
63                            if (isset($result['errorInfo'])) {
64                                $this->log("Error processing mail item: " . $result['errorInfo']);
65                            }
66                        }
67                    } catch (\Exception $exception) {
68                        $this->log("Error processing mail items: " . $exception->getMessage());
69                        $resultList[] = [
70                            'errorInfo' => $exception->getMessage()
71                        ];
72                    }
73                }
74            } else {
75                $batchSize = min(count($this->messagesQueue), max(1, ceil(count($this->messagesQueue) / 12)));
76                $this->log("More than 50 items, processing in batches of $batchSize.");
77                $batches = array_chunk(iterator_to_array($this->messagesQueue), $batchSize);
78                $this->log("Messages divided into " . count($batches) . " batches.");
79
80                $processHandles = [];
81                foreach ($batches as $batch) {
82                    if ($this->maxRunTime < $this->getSpendTime()) {
83                        $this->log("Max Runtime exceeded during batch processing - " . \App::$now->format('c'));
84                        break;
85                    }
86
87                    $ids = array_map(function ($message) {
88                        return $message['id'];
89                    }, $batch);
90                    $encodedIds = base64_encode(json_encode($ids));
91                    $actionStr = is_array($action) ? json_encode($action) : ($action === false ? 'false' : ($action === true ? 'true' : (string)$action));
92
93                    $idsStr = implode(', ', $ids);
94                    $command = "php " . escapeshellarg(__DIR__ . '/MailProcessor.php') . " " . escapeshellarg($encodedIds) . " " . escapeshellarg($actionStr);
95                    $processHandles[] = $this->startProcess($command, $idsStr);
96                }
97
98                if ($this->maxRunTime >= $this->getSpendTime()) {
99                    $this->monitorProcesses($processHandles);
100                } else {
101                    $this->log("Max Runtime exceeded before process monitoring started - " . \App::$now->format('c'));
102                }
103            }
104        } else {
105            $resultList[] = [
106                'errorInfo' => 'No mail entry found in Database...',
107            ];
108            $this->log('No mail entry found in Database');
109        }
110
111        return $resultList;
112    }
113
114    public function sendQueueItems($action, array $itemIds)
115    {
116        $endpoint = '/mails/';
117        $params = [
118            'resolveReferences' => 2,
119            'ids' => implode(',', $itemIds)
120        ];
121
122        try {
123            $response = \App::$http->readGetResult($endpoint, $params);
124            $mailItems = $response->getCollection();
125        } catch (\Exception $e) {
126            $this->log("Error fetching mail data: " . $e->getMessage() . "\n\n");
127            return ['errorInfo' => 'Failed to fetch mail data'];
128        }
129
130        if (empty($mailItems)) {
131            $this->log("No mail items found for the provided IDs.");
132            return ['errorInfo' => 'No mail items found'];
133        }
134
135        $results = [];
136        $processedMails = [];
137        $successfullySentIds = [];
138
139        foreach ($mailItems as $item) {
140            $entity = new MailEntity($item);
141            $processId = $entity['process']['id'] ?? null;
142            $mailer = $this->getValidMailer($entity);
143            if (!$mailer) {
144                $this->log("No valid mailer for mail ID: " . $entity->id);
145                continue;
146            }
147
148            try {
149                $result = $this->sendMailer($entity, $mailer, $action);
150                if ($result instanceof PHPMailer) {
151                    $mailResult = [
152                        'id' => ($result->getLastMessageID()) ? $result->getLastMessageID() : $entity->id,
153                        'mailId' => $entity->id,
154                        'processId' => $processId,
155                        'createTimestamp' => $entity->createTimestamp,
156                        'recipients' => $result->getAllRecipientAddresses(),
157                        'mime' => $result->getMailMIME(),
158                        'attachments' => $result->getAttachments(),
159                        'customHeaders' => $result->getCustomHeaders(),
160                    ];
161                    $results[] = $mailResult;
162                    $processedMails[] = [
163                        'mailId' => $entity->id,
164                        'processId' => $processId,
165                        'createTimestamp' => $entity->createTimestamp,
166                    ];
167                    \App::$log->info('Mail processed from queue', [
168                        'mailId' => $entity->id,
169                        'processId' => $processId,
170                        'createTimestamp' => $entity->createTimestamp,
171                    ]);
172                    $successfullySentIds[] = $entity->id;
173                } else {
174                    $errorInfo = $result->ErrorInfo ?? 'Unknown mailer error';
175                    $results[] = [
176                        'errorInfo' => $errorInfo,
177                        'mailId' => $entity->id,
178                        'processId' => $processId,
179                    ];
180                    $this->log('Mail send failed with error: ' . $errorInfo);
181                }
182            } catch (\Exception $e) {
183                $this->log("Exception while sending mail ID " . $entity->id . ": " . $e->getMessage());
184                $results[] = [
185                    'errorInfo' => $e->getMessage(),
186                    'mailId' => $entity->id,
187                    'processId' => $processId,
188                ];
189            }
190        }
191
192        if ($action && !empty($successfullySentIds)) {
193            try {
194                $this->deleteEntitiesFromQueue($successfullySentIds);
195            } catch (\Exception $e) {
196                $this->log("Error deleting processed mails: " . $e->getMessage());
197            }
198        }
199
200        if (!empty($processedMails)) {
201            \App::$log->info('Mail queue batch finished', [
202                'count' => count($processedMails),
203                'mails' => $processedMails,
204            ]);
205            $this->log(
206                'Processing finished for IDs [emailId, processId, createdTimestamp)]: '
207                . implode(', ', array_map(
208                    static fn (array $mail) => '[' . $mail['mailId'] . ', ' . $mail['processId'] . ', ' . $mail['createTimestamp'] . ']',
209                    $processedMails
210                ))
211            );
212        }
213
214        return $results;
215    }
216
217    protected function getValidMailer(MailEntity $entity)
218    {
219        $message = '';
220        $messageId = $entity['id'];
221        try {
222            $mailer = $this->readMailer($entity);
223        // @codeCoverageIgnoreStart
224        } catch (PHPMailerException $exception) {
225            $message = "Message #$messageId PHPMailer Failure: " . $exception->getMessage();
226            $code = $exception->getCode();
227            \App::$log->warning($message, []);
228        } catch (\Exception $exception) {
229            $message = "Message #$messageId Exception Failure: " . $exception->getMessage();
230            $code = $exception->getCode();
231            \App::$log->warning($message, []);
232        }
233        if ($message) {
234            if (428 == $code || 422 == $code) {
235                $this->log("Build Mailer Failure " . $code . ": deleteEntityFromQueue() - " . \App::$now->format('c'));
236                $this->deleteEntityFromQueue($entity);
237            } else {
238                $this->log(
239                    "Build Mailer Failure " . $code . ": removeEntityOlderThanOneHour() - " . \App::$now->format('c')
240                );
241                $this->removeEntityOlderThanOneHour($entity);
242            }
243
244            $log = new Mimepart(['mime' => 'text/plain']);
245            $log->content = $message;
246            $this->log("Build Mailer Exception log message: " . $message);
247            \App::$http->readPostResult('/log/process/' . $entity->process['id'] . '/', $log, ['error' => 1]);
248            return false;
249        }
250
251        // @codeCoverageIgnoreEnd
252        return $mailer;
253    }
254
255    /**
256     * @SuppressWarnings("CyclomaticComplexity")
257     * @SuppressWarnings("NPathComplexity")
258     */
259    protected function readMailer(MailEntity $entity)
260    {
261        $this->testEntity($entity);
262        $encoding = 'base64';
263        foreach ($entity->multipart as $part) {
264            $mimepart = new Mimepart($part);
265            if ($mimepart->isText()) {
266                $textPart = $mimepart->getContent();
267            }
268            if ($mimepart->isHtml()) {
269                $htmlPart = $mimepart->getContent();
270            }
271            if ($mimepart->isIcs()) {
272                $icsPart = $mimepart->getContent();
273            }
274        }
275        $mailer = new PHPMailer(true);
276        $mailer->CharSet = 'UTF-8';
277        $mailer->SMTPDebug = \App::$smtp_debug;
278        $mailer->SetLanguage("de");
279        $mailer->Encoding = $encoding;
280        $mailer->IsHTML(true);
281        $mailer->XMailer = \App::IDENTIFIER;
282        $mailer->Subject = $entity['subject'];
283        $mailer->AltBody = (isset($textPart)) ? $textPart : '';
284        $mailer->Body = (isset($htmlPart)) ? $htmlPart : '';
285        $mailer->SetFrom($entity['department']['email'], $entity['department']['name']);
286        $mailer->AddAddress($entity->getRecipient(), $entity->client['familyName']);
287
288        if (null !== $entity->getIcsPart()) {
289            $mailer->AddStringAttachment(
290                $icsPart,
291                "Termin.ics",
292                $encoding,
293                "text/calendar; charset=utf-8; method=REQUEST"
294            );
295        }
296
297        if (\App::$smtp_enabled) {
298            $mailer->IsSMTP();
299            $mailer->SMTPAuth = \App::$smtp_auth_enabled;
300            $mailer->SMTPSecure = \App::$smtp_auth_method;
301            $mailer->Port = \App::$smtp_port;
302            $mailer->Host = \App::$smtp_host;
303            $mailer->Username = \App::$smtp_username;
304            $mailer->Password = \App::$smtp_password;
305            if (\App::$smtp_skip_tls_verify) {
306                $mailer->SMTPOptions['ssl'] = [
307                    'verify_peer' => false,
308                    'verify_peer_name' => false,
309                    'allow_self_signed' => true,
310                ];
311            }
312        }
313
314        return $mailer;
315    }
316
317    private function startProcess($command, $ids)
318    {
319        $descriptorSpec = [
320            0 => ["pipe", "r"], // stdin
321            1 => ["pipe", "w"], // stdout
322            2 => ["pipe", "w"]  // stderr
323        ];
324
325        $process = proc_open($command . ' 2>&1', $descriptorSpec, $pipes); // Redirect stderr to stdout
326        if (is_resource($process)) {
327            return [
328                'process' => $process,
329                'pipes' => $pipes,
330                'ids' => $ids
331            ];
332        } else {
333            return null;
334        }
335    }
336
337    private function deleteEntitiesFromQueue(array $itemIds)
338    {
339        $endpoint = '/mails/';
340        $params = [
341            'ids' => implode(',', $itemIds)
342        ];
343
344        try {
345            $response = \App::$http->readDeleteResult($endpoint, $params);
346            return $response;
347        } catch (\Exception $e) {
348            $this->log("Error deleting mail data: " . $e->getMessage() . "\n\n");
349            throw new \Exception("Failed to delete mail data");
350        }
351    }
352}