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