Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
54.60% |
95 / 174 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
|
54.60% |
95 / 174 |
|
28.57% |
2 / 7 |
283.98 | |
0.00% |
0 / 1 |
|
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
initQueueTransmission | |
37.50% |
18 / 48 |
|
0.00% |
0 / 1 |
87.56 | |||
sendQueueItems | |
69.57% |
32 / 46 |
|
0.00% |
0 / 1 |
14.41 | |||
getValidMailer | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
6 | |||
readMailer | |
70.45% |
31 / 44 |
|
0.00% |
0 / 1 |
12.58 | |||
startProcess | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
deleteEntitiesFromQueue | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * |
5 | * @package Zmsmessaging |
6 | * |
7 | */ |
8 | |
9 | namespace BO\Zmsmessaging; |
10 | |
11 | use BO\Zmsentities\Mimepart; |
12 | use PHPMailer\PHPMailer\PHPMailer; |
13 | use PHPMailer\PHPMailer\Exception as PHPMailerException; |
14 | |
15 | class 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 | } |