dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
dbxMail.class.php
Go to the documentation of this file.
1<?php
2
3use PHPMailer\PHPMailer\Exception as PHPMailerException;
4use PHPMailer\PHPMailer\PHPMailer;
5
6class dbxMail {
7
8 public $errstr = '';
9 public $headers = array();
10 public $textbody = '';
11 public $htmlbody = '';
12 public $attachments = array();
13 public $html_images = array();
14 public $auto_embbed_images = true;
15 public $auto_embed_images = true;
16 public $boundary = '';
17 public $sendmail_path = '';
18 public $subject = '';
19 public $spam_protection = true;
20
21 private $from = '';
22 private $fromname = '';
23
24 public function init() {
25 $this->errstr = '';
26 $this->headers = array();
27 $this->textbody = '';
28 $this->htmlbody = '';
29 $this->attachments = array();
30 $this->html_images = array();
31 $this->auto_embbed_images = true;
32 $this->auto_embed_images = true;
33 $this->boundary = '==Multipart_Boundary_x' . md5((string) microtime(true)) . 'x';
34 $this->sendmail_path = '';
35 $this->subject = '';
36 $this->spam_protection = true;
37 $this->from = '';
38 $this->fromname = '';
39 }
40
41 public function checkEmail($inAddress) {
42 return filter_var((string) $inAddress, FILTER_VALIDATE_EMAIL) !== false;
43 }
44
45 public function get_body() {
46 return $this->textbody . $this->htmlbody;
47 }
48
49 public function get_header() {
50 $retval = '';
51 foreach ($this->headers as $key => $value) {
52 $retval .= $key . ': ' . $value . "\n";
53 }
54 return $retval;
55 }
56
57 public function set_header($name, $value) {
58 $name = trim((string) $name);
59 $value = trim((string) $value);
60
61 if (strcasecmp($name, 'From') === 0) {
62 $from = $this->parse_address($value);
63 $this->from = $from['email'];
64 $this->fromname = $from['name'];
65 }
66
67 $this->headers[$name] = $value;
68 }
69
70 public function set_from($email, $name = '') {
71 $this->from = trim((string) $email);
72 $this->fromname = trim((string) $name);
73 $this->headers['From'] = $this->format_address($this->from, $this->fromname);
74 }
75
76 public function attachfile($file, $disp = 'attachment', $contentType = '', $cid = '') {
77 $file = trim((string) $file);
78
79 if ($file === '' || !is_file($file) || !is_readable($file)) {
80 $this->errstr = 'Attachment nicht lesbar: ' . $file;
81 return 0;
82 }
83
84 $this->attachments[] = array(
85 'path' => $file,
86 'name' => basename($file),
87 'disposition' => $disp ?: 'attachment',
88 'content_type' => $contentType ?: $this->getContentType($file),
89 'cid' => (string) $cid,
90 );
91
92 return 1;
93 }
94
95 public function getContentType($inFileName) {
96 $file = (string) $inFileName;
97
98 if (function_exists('mime_content_type') && is_file($file)) {
99 $type = @mime_content_type($file);
100 if ($type) {
101 return $type;
102 }
103 }
104
105 $extension = strtolower((string) strrchr(basename($file), '.'));
106
107 switch ($extension) {
108 case '.gz': return 'application/gzip';
109 case '.zip': return 'application/zip';
110 case '.tar': return 'application/x-tar';
111 case '.htm':
112 case '.html': return 'text/html';
113 case '.txt': return 'text/plain';
114 case '.jpg':
115 case '.jpeg': return 'image/jpeg';
116 case '.gif': return 'image/gif';
117 case '.png': return 'image/png';
118 case '.pdf': return 'application/pdf';
119 case '.csv': return 'text/csv';
120 case '.json': return 'application/json';
121 default: return 'application/octet-stream';
122 }
123 }
124
125 public function bodytext($text) {
126 if (strlen(trim((string) $text)) > 0) {
127 $this->textbody = (string) $text;
128 }
129 }
130
131 public function bodyhtml($text) {
132 if (strlen(trim((string) $text)) > 0) {
133 $this->htmlbody = (string) $text;
134 }
135 }
136
137 public function htmltext($text) {
138 if (strlen(trim((string) $text)) > 0) {
139 $this->htmlbody .= (string) $text;
140 }
141 }
142
143 public function clear_bodytext() { $this->textbody = ''; }
144 public function clear_htmltext() { $this->htmlbody = ''; }
145 public function get_error() { return $this->errstr; }
146
147 public function send($to = 'root@localhost', $subject = 'Default Subject', $options = array()) {
148 $this->errstr = '';
149 $subject = (string) ($subject !== '' ? $subject : $this->subject);
150
151 try {
152 $mail = new PHPMailer(true);
153 $mail->CharSet = 'UTF-8';
154 $mail->Encoding = 'base64';
155
156 $config = $this->mail_config($options);
157 $this->configure_transport($mail, $config);
158 $this->configure_sender($mail, $config, $options);
159
160 $this->add_addresses($mail, $to, 'addAddress');
161 $this->add_addresses($mail, $options['cc'] ?? $this->headers['Cc'] ?? array(), 'addCC');
162 $this->add_addresses($mail, $options['bcc'] ?? $this->headers['Bcc'] ?? array(), 'addBCC');
163 $this->add_addresses($mail, $options['reply_to'] ?? $this->headers['Reply-To'] ?? array(), 'addReplyTo');
164
165 $mail->Subject = $subject;
166 $this->configure_body($mail, $options);
167 $this->configure_headers($mail);
168 $this->configure_attachments($mail);
169
170 $spamReason = $this->spam_guard_reason($mail, $to, $subject, $options);
171 if ($spamReason !== '') {
172 $this->errstr = 'Mail wurde durch den Spam-Schutz blockiert: ' . $spamReason;
173 $this->sys_msg('security', $to, $subject, $this->errstr);
174 return 0;
175 }
176
177 $ok = $mail->send() ? 1 : 0;
178 $this->sys_msg($ok ? 'info' : 'error', $to, $subject, $ok ? 'sent' : $mail->ErrorInfo);
179
180 return $ok;
181 } catch (PHPMailerException $e) {
182 $this->errstr = $e->getMessage();
183 } catch (Throwable $e) {
184 $this->errstr = $e->getMessage();
185 }
186
187 $this->sys_msg('error', $to, $subject, $this->errstr);
188 return 0;
189 }
190
191 private function mail_config($options) {
192 $config = array();
193
194 if (function_exists('dbx')) {
195 $cfg = dbx()->get_config('dbx', 'mail');
196 if (is_array($cfg)) {
197 $config = $cfg;
198 }
199
200 $defaultMail = dbx()->get_config('dbx', 'default_mail');
201 if ($defaultMail !== 'undef' && $defaultMail !== '') {
202 $config['default'] = (string) $defaultMail;
203 }
204 }
205
206 if (is_string($options['mail'] ?? null)) {
207 $options['mail_profile'] = $options['mail'];
208 }
209
210 $config = $this->select_mail_profile($config, $options);
211
212 if (is_array($options['mail'] ?? null)) {
213 $config = array_replace_recursive($config, $options['mail']);
214 }
215
216 return $config;
217 }
218
219 private function select_mail_profile(array $config, array $options) {
220 $profiles = $this->mail_profiles_from_config($config);
221 if (!$profiles) {
222 return $config;
223 }
224
225 $profileName = (string) ($options['mail_profile'] ?? $options['profile'] ?? '');
226 $from = $this->normalize_from($options['from'] ?? null);
227
228 if ($profileName === '' && $from['email'] !== '') {
229 $profileName = $this->profile_for_sender($profiles, $from['email']);
230 }
231
232 if ($profileName === '') {
233 $profileName = (string) ($config['default'] ?? '');
234 }
235
236 if ($profileName === '' || !isset($profiles[$profileName]) || !is_array($profiles[$profileName])) {
237 $profileName = (string) ($config['default'] ?? '');
238 }
239
240 if (($profileName === '' || !isset($profiles[$profileName])) && count($profiles)) {
241 $keys = array_keys($profiles);
242 $profileName = (string) $keys[0];
243 }
244
245 $base = $this->mail_base_config($config);
246
247 if ($profileName !== '' && isset($profiles[$profileName]) && is_array($profiles[$profileName])) {
248 return array_replace_recursive($base, $profiles[$profileName], array('profile' => $profileName));
249 }
250
251 return $base;
252 }
253
254 private function mail_profiles_from_config(array $config) {
255 if (is_array($config['profiles'] ?? null)) {
256 return $config['profiles'];
257 }
258
259 $profiles = array();
260 foreach ($config as $key => $value) {
261 if ($key === 'default' || $key === 'profiles') {
262 continue;
263 }
264 if (is_array($value)) {
265 $profiles[(string) $key] = $value;
266 }
267 }
268
269 return $profiles;
270 }
271
272 private function mail_base_config(array $config) {
273 $base = $config;
274 unset($base['profiles'], $base['default']);
275
276 foreach ($base as $key => $value) {
277 if (is_array($value)) {
278 unset($base[$key]);
279 }
280 }
281
282 return $base;
283 }
284
285 private function profile_for_sender(array $profiles, $email) {
286 $domain = strtolower((string) substr(strrchr((string) $email, '@') ?: '', 1));
287 if ($domain === '') {
288 return '';
289 }
290
291 foreach ($profiles as $name => $profile) {
292 if ($this->profile_matches_domain($profile, $domain, false)) {
293 return (string) $name;
294 }
295 }
296
297 foreach ($profiles as $name => $profile) {
298 if ($this->profile_matches_domain($profile, $domain, true)) {
299 return (string) $name;
300 }
301 }
302
303 return '';
304 }
305
306 private function profile_matches_domain($profile, $domain, $allowWildcard) {
307 if (!is_array($profile)) {
308 return false;
309 }
310
311 $domains = $profile['from_domains'] ?? $profile['from_domain'] ?? array();
312 if (is_string($domains)) {
313 $domains = preg_split('/[;,]+/', $domains);
314 }
315
316 foreach ((array) $domains as $allowed) {
317 $allowed = strtolower(trim((string) $allowed));
318 if ($allowed === '') {
319 continue;
320 }
321 if ($allowed === $domain) {
322 return true;
323 }
324 if ($allowWildcard && ($allowed === '*' || (substr($allowed, 0, 2) === '*.' && str_ends_with($domain, substr($allowed, 1))))) {
325 return true;
326 }
327 }
328
329 return false;
330 }
331
332 private function configure_transport(PHPMailer $mail, array $config) {
333 $transport = strtolower((string) ($config['transport'] ?? $config['type'] ?? ''));
334 $host = trim((string) ($config['host'] ?? $config['smtp_host'] ?? ''));
335
336 if ($transport === 'smtp' || $host !== '') {
337 $username = (string) ($config['user'] ?? $config['username'] ?? '');
338 $password = (string) ($config['pass'] ?? $config['password'] ?? '');
339 $auth = (bool) ($config['auth'] ?? $config['smtp_auth'] ?? ($username !== ''));
340
341 if ($auth && ($username === '' || $password === '')) {
342 throw new PHPMailerException('SMTP auth configuration incomplete.');
343 }
344
345 $mail->isSMTP();
346 $mail->Host = $host;
347 $mail->Port = (int) ($config['port'] ?? $config['smtp_port'] ?? 587);
348 $mail->Timeout = max(1, (int) ($config['timeout'] ?? $config['smtp_timeout'] ?? 3));
349 $mail->SMTPAuth = $auth;
350 $mail->Username = $username;
351 $mail->Password = $password;
352
353 $secure = strtolower((string) ($config['secure'] ?? $config['smtp_secure'] ?? ''));
354 if (in_array($secure, array('tls', 'ssl'), true)) {
355 $mail->SMTPSecure = $secure;
356 }
357 return;
358 }
359
360 if ($transport === 'sendmail' || $this->sendmail_path !== '' || !empty($config['sendmail_path'])) {
361 $mail->isSendmail();
362 $path = $this->sendmail_path ?: (string) ($config['sendmail_path'] ?? '');
363 if ($path !== '') {
364 $mail->Sendmail = $path;
365 }
366 return;
367 }
368
369 $mail->isMail();
370 }
371
372 private function configure_sender(PHPMailer $mail, array $config, array $options) {
373 $forceFrom = !empty($config['force_from']);
374 $from = $forceFrom ? array('email' => '', 'name' => '') : $this->normalize_from($options['from'] ?? null);
375
376 if ($from['email'] === '') {
377 $from = $this->normalize_from($this->from ? array('email' => $this->from, 'name' => $this->fromname) : null);
378 }
379
380 if ($from['email'] === '') {
381 $from = $this->normalize_from($config['from'] ?? array(
382 'email' => $config['from_email'] ?? '',
383 'name' => $config['from_name'] ?? '',
384 ));
385 }
386
387 if ($from['email'] === '') {
388 throw new PHPMailerException('Absender fehlt.');
389 }
390
391 $mail->setFrom($from['email'], $from['name']);
392
393 $sender = trim((string) ($config['sender'] ?? $config['return_path'] ?? $config['envelope_from'] ?? ''));
394 if ($sender !== '') {
395 $mail->Sender = $sender;
396 }
397 }
398
399 private function configure_body(PHPMailer $mail, array $options) {
400 $html = (string) ($options['html'] ?? $this->htmlbody);
401 $text = (string) ($options['text'] ?? $this->textbody);
402
403 if ($html !== '') {
404 $mail->isHTML(true);
405 $mail->Body = $this->embed_html_images($mail, $html);
406 $mail->AltBody = $text !== '' ? $text : $this->html_to_text($html);
407 return;
408 }
409
410 $mail->isHTML(false);
411 $mail->Body = $text;
412 }
413
414 private function configure_headers(PHPMailer $mail) {
415 foreach ($this->headers as $key => $value) {
416 if (in_array(strtolower((string) $key), array('from', 'to', 'cc', 'bcc', 'reply-to', 'content-type', 'mime-version'), true)) {
417 continue;
418 }
419 $mail->addCustomHeader((string) $key, (string) $value);
420 }
421 }
422
423 private function configure_attachments(PHPMailer $mail) {
424 foreach ($this->attachments as $attachment) {
425 if (!is_array($attachment)) {
426 continue;
427 }
428
429 $path = (string) ($attachment['path'] ?? '');
430 if ($path === '' || !is_file($path) || !is_readable($path)) {
431 continue;
432 }
433
434 $name = (string) ($attachment['name'] ?? basename($path));
435 $encoding = PHPMailer::ENCODING_BASE64;
436 $type = (string) ($attachment['content_type'] ?? $this->getContentType($path));
437 $disp = (string) ($attachment['disposition'] ?? 'attachment');
438 $cid = (string) ($attachment['cid'] ?? '');
439
440 if ($disp === 'inline' && $cid !== '') {
441 $mail->addEmbeddedImage($path, $cid, $name, $encoding, $type);
442 } else {
443 $mail->addAttachment($path, $name, $encoding, $type, $disp);
444 }
445 }
446 }
447
448 private function embed_html_images(PHPMailer $mail, $html) {
449 if (!$this->auto_embbed_images && !$this->auto_embed_images) {
450 return $html;
451 }
452
453 preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $matches);
454 $images = array_unique($matches[1] ?? array());
455
456 foreach ($images as $src) {
457 $file = $this->html_image_path($src);
458 if (!$file) {
459 continue;
460 }
461
462 $cid = 'part.' . md5($file);
463 $mail->addEmbeddedImage($file, $cid, basename($file), PHPMailer::ENCODING_BASE64, $this->getContentType($file));
464 $html = str_replace($src, 'cid:' . $cid, $html);
465 $this->html_images[] = $file;
466 }
467
468 return $html;
469 }
470
471 private function html_image_path($src) {
472 $src = trim((string) $src);
473 if ($src === '' || preg_match('/^(https?:|data:|cid:)/i', $src)) {
474 return '';
475 }
476
477 $path = $src;
478 if (!preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) && substr($path, 0, 1) !== '/') {
479 $path = dbx_get_base_dir() . ltrim($path, '/\\');
480 }
481
482 $path = function_exists('dbx_os_path_file') ? dbx_os_path_file($path) : $path;
483 return is_file($path) && is_readable($path) ? $path : '';
484 }
485
486 private function add_addresses(PHPMailer $mail, $addresses, $method) {
487 foreach ($this->normalize_addresses($addresses) as $address) {
488 if ($address['email'] !== '') {
489 $mail->$method($address['email'], $address['name']);
490 }
491 }
492 }
493
494 private function normalize_addresses($addresses) {
495 if ($addresses === null || $addresses === '') {
496 return array();
497 }
498
499 if (is_string($addresses)) {
500 $parts = preg_split('/[;,]+/', $addresses);
501 return array_map(array($this, 'parse_address'), array_filter(array_map('trim', $parts)));
502 }
503
504 if (is_array($addresses)) {
505 if (isset($addresses['email'])) {
506 return array($this->normalize_from($addresses));
507 }
508
509 $out = array();
510 foreach ($addresses as $key => $value) {
511 if (is_string($key) && is_string($value) && filter_var($key, FILTER_VALIDATE_EMAIL)) {
512 $out[] = array('email' => $key, 'name' => $value);
513 } else {
514 $out[] = is_array($value) ? $this->normalize_from($value) : $this->parse_address((string) $value);
515 }
516 }
517 return $out;
518 }
519
520 return array();
521 }
522
523 private function normalize_from($from) {
524 if (is_array($from)) {
525 return array(
526 'email' => trim((string) ($from['email'] ?? $from['mail'] ?? $from[0] ?? '')),
527 'name' => trim((string) ($from['name'] ?? $from[1] ?? '')),
528 );
529 }
530
531 if (is_string($from)) {
532 return $this->parse_address($from);
533 }
534
535 return array('email' => '', 'name' => '');
536 }
537
538 private function parse_address($value) {
539 $value = trim((string) $value);
540 if (preg_match('/^(.*)<([^>]+)>$/', $value, $m)) {
541 return array('email' => trim($m[2]), 'name' => trim(trim($m[1]), '"\' '));
542 }
543 return array('email' => $value, 'name' => '');
544 }
545
546 private function format_address($email, $name = '') {
547 $email = trim((string) $email);
548 $name = trim((string) $name);
549 return $name !== '' ? $name . ' <' . $email . '>' : $email;
550 }
551
552 private function html_to_text($html) {
553 return trim(html_entity_decode(strip_tags((string) $html), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
554 }
555
556 private function sys_msg($status, $to, $subject, $message) {
557 if (!function_exists('dbx')) {
558 return;
559 }
560
561 try {
562 dbx()->sys_msg($status, 'mail', '', (string) $subject, 'to=' . $this->address_debug($to) . ' ' . (string) $message);
563 } catch (Throwable $e) {
564 // Mailversand darf nicht an der Protokollierung scheitern.
565 }
566 }
567
568 private function address_debug($addresses) {
569 $list = array();
570 foreach ($this->normalize_addresses($addresses) as $address) {
571 if ($address['email'] !== '') {
572 $list[] = $address['email'];
573 }
574 }
575 return implode(',', $list);
576 }
577
578 private function spam_guard_reason(PHPMailer $mail, $to, string $subject, array $options): string {
579 if (!$this->spam_protection || !empty($options['skip_spam_guard'])) {
580 return '';
581 }
582
583 return $this->spam_reason_for_text($this->spam_guard_text($mail, $to, $subject, $options));
584 }
585
586 public function spam_reason_for_text(string $text): string {
587 if (!$this->spam_protection) {
588 return '';
589 }
590
591 return $this->spam_content_reason($text);
592 }
593
594 private function spam_guard_text(PHPMailer $mail, $to, string $subject, array $options): string {
595 $parts = array(
596 $subject,
597 $mail->Body,
598 $mail->AltBody,
599 (string) ($options['text'] ?? ''),
600 (string) ($options['html'] ?? ''),
601 $this->address_debug($to),
602 $this->address_debug($options['reply_to'] ?? $this->headers['Reply-To'] ?? array()),
603 );
604
605 foreach ($this->headers as $key => $value) {
606 $parts[] = (string) $key . ': ' . (string) $value;
607 }
608
609 return html_entity_decode(strip_tags(implode("\n", $parts)), ENT_QUOTES | ENT_HTML5, 'UTF-8');
610 }
611
612 private function spam_content_reason(string $text): string {
613 $normalized = strtolower($text);
614 $normalized = preg_replace('/\s+/u', ' ', $normalized) ?: $normalized;
615 $score = 0;
616
617 $hardPatterns = array(
618 '/(?:https?:\/\/)?(?:www\.)?telegra\.ph\//i' => 'telegra.ph link',
619 '/\btransaction[-\s_]*\d{2}-\d{2}-/i' => 'transaction spam code',
620 '/\b(?:transaction|transfer|top\s*up)\b.{0,80}\b(?:get|claim|bonus|payment)\b/i' => 'finance spam phrase',
621 );
622
623 foreach ($hardPatterns as $pattern => $reason) {
624 if (preg_match($pattern, $text)) {
625 return $reason;
626 }
627 }
628
629 if (preg_match('/\b(?:transaction|transfer|top\s*up|crypto|bitcoin|wallet|usdt|profit|investment)\b/i', $text)) {
630 $score += 2;
631 }
632 if (preg_match('/(?:\$|usd|eur)\s*\d{3,}|\d{3,}\s*(?:\$|usd|eur)/i', $text)) {
633 $score += 2;
634 }
635 if (preg_match('/(?:https?:\/\/|www\.|[a-z0-9-]+\.(?:ph|ru|top|xyz|click|link|icu|buzz|cfd|quest)\b)/i', $text)) {
636 $score += 2;
637 }
638 if (preg_match('/\bget\s*(?:-|>|&gt;|to|:)/i', $text)) {
639 $score += 1;
640 }
641 if (preg_match('/[\x{1F300}-\x{1FAFF}]/u', $text)) {
642 $score += 1;
643 }
644
645 $urlCount = preg_match_all('/(?:https?:\/\/|www\.|[a-z0-9-]+\.[a-z]{2,}\/)/i', $text);
646 if ($urlCount >= 2) {
647 $score += 2;
648 }
649
650 return $score >= 5 ? 'spam score ' . $score : '';
651 }
652}
653
654?>
$cfg
spam_reason_for_text(string $text)
bodytext($text)
htmltext($text)
set_header($name, $value)
bodyhtml($text)
set_from($email, $name='')
getContentType($inFileName)
attachfile($file, $disp='attachment', $contentType='', $cid='')
send($to='root @localhost', $subject='Default Subject', $options=array())
checkEmail($inAddress)
$config['version']
Definition config.php:2
dbx_os_path_file($path_file)
Definition index.php:164
if( $syncRequest)
Definition index.php:520
dbx_get_base_dir($cut_Data=0)
Definition index.php:157
DBX schema administration.