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