22 private $fromname =
'';
26 $this->headers = array();
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 =
'';
36 $this->spam_protection =
true;
42 return filter_var((
string) $inAddress, FILTER_VALIDATE_EMAIL) !==
false;
51 foreach ($this->headers as $key => $value) {
52 $retval .= $key .
': ' . $value .
"\n";
58 $name = trim((
string) $name);
59 $value = trim((
string) $value);
61 if (strcasecmp($name,
'From') === 0) {
62 $from = $this->parse_address($value);
63 $this->from = $from[
'email'];
64 $this->fromname = $from[
'name'];
67 $this->headers[$name] = $value;
71 $this->from = trim((
string) $email);
72 $this->fromname = trim((
string) $name);
73 $this->headers[
'From'] = $this->format_address($this->from, $this->fromname);
76 public function attachfile($file, $disp =
'attachment', $contentType =
'', $cid =
'') {
77 $file = trim((
string) $file);
79 if ($file ===
'' || !is_file($file) || !is_readable($file)) {
80 $this->errstr =
'Attachment nicht lesbar: ' . $file;
84 $this->attachments[] = array(
86 'name' => basename($file),
87 'disposition' => $disp ?:
'attachment',
89 'cid' => (
string) $cid,
96 $file = (string) $inFileName;
98 if (function_exists(
'mime_content_type') && is_file($file)) {
99 $type = @mime_content_type($file);
105 $extension = strtolower((
string) strrchr(basename($file),
'.'));
107 switch ($extension) {
108 case '.gz':
return 'application/gzip';
109 case '.zip':
return 'application/zip';
110 case '.tar':
return 'application/x-tar';
112 case '.html':
return 'text/html';
113 case '.txt':
return 'text/plain';
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';
126 if (strlen(trim((
string) $text)) > 0) {
127 $this->textbody = (string) $text;
132 if (strlen(trim((
string) $text)) > 0) {
133 $this->htmlbody = (string) $text;
138 if (strlen(trim((
string) $text)) > 0) {
139 $this->htmlbody .= (string) $text;
147 public function send($to =
'root@localhost',
$subject =
'Default Subject', $options = array()) {
152 $mail =
new PHPMailer(
true);
153 $mail->CharSet =
'UTF-8';
154 $mail->Encoding =
'base64';
156 $config = $this->mail_config($options);
157 $this->configure_transport($mail,
$config);
158 $this->configure_sender($mail,
$config, $options);
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');
166 $this->configure_body($mail, $options);
167 $this->configure_headers($mail);
168 $this->configure_attachments($mail);
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);
177 $ok = $mail->send() ? 1 : 0;
178 $this->sys_msg($ok ?
'info' :
'error', $to,
$subject, $ok ?
'sent' : $mail->ErrorInfo);
181 }
catch (PHPMailerException $e) {
182 $this->errstr = $e->getMessage();
183 }
catch (Throwable $e) {
184 $this->errstr = $e->getMessage();
187 $this->sys_msg(
'error', $to,
$subject, $this->errstr);
191 private function mail_config($options) {
194 if (function_exists(
'dbx')) {
195 $cfg =
dbx()->get_config(
'dbx',
'mail');
196 if (is_array(
$cfg)) {
200 $defaultMail = dbx()->get_config(
'dbx',
'default_mail');
201 if ($defaultMail !==
'undef' && $defaultMail !==
'') {
202 $config[
'default'] = (string) $defaultMail;
206 if (is_string($options[
'mail'] ??
null)) {
207 $options[
'mail_profile'] = $options[
'mail'];
212 if (is_array($options[
'mail'] ??
null)) {
219 private function select_mail_profile(array
$config, array $options) {
220 $profiles = $this->mail_profiles_from_config(
$config);
225 $profileName = (string) ($options[
'mail_profile'] ?? $options[
'profile'] ??
'');
226 $from = $this->normalize_from($options[
'from'] ??
null);
228 if ($profileName ===
'' && $from[
'email'] !==
'') {
229 $profileName = $this->profile_for_sender($profiles, $from[
'email']);
232 if ($profileName ===
'') {
233 $profileName = (string) (
$config[
'default'] ??
'');
236 if ($profileName ===
'' || !isset($profiles[$profileName]) || !is_array($profiles[$profileName])) {
237 $profileName = (string) (
$config[
'default'] ??
'');
240 if (($profileName ===
'' || !isset($profiles[$profileName])) && count($profiles)) {
241 $keys = array_keys($profiles);
242 $profileName = (string) $keys[0];
247 if ($profileName !==
'' && isset($profiles[$profileName]) && is_array($profiles[$profileName])) {
248 return array_replace_recursive(
$base, $profiles[$profileName], array(
'profile' => $profileName));
254 private function mail_profiles_from_config(array
$config) {
255 if (is_array(
$config[
'profiles'] ??
null)) {
260 foreach (
$config as $key => $value) {
261 if ($key ===
'default' || $key ===
'profiles') {
264 if (is_array($value)) {
265 $profiles[(string) $key] = $value;
272 private function mail_base_config(array
$config) {
276 foreach (
$base as $key => $value) {
277 if (is_array($value)) {
285 private function profile_for_sender(array $profiles, $email) {
286 $domain = strtolower((
string) substr(strrchr((
string) $email,
'@') ?:
'', 1));
287 if ($domain ===
'') {
291 foreach ($profiles as $name => $profile) {
292 if ($this->profile_matches_domain($profile, $domain,
false)) {
293 return (
string) $name;
297 foreach ($profiles as $name => $profile) {
298 if ($this->profile_matches_domain($profile, $domain,
true)) {
299 return (
string) $name;
306 private function profile_matches_domain($profile, $domain, $allowWildcard) {
307 if (!is_array($profile)) {
311 $domains = $profile[
'from_domains'] ?? $profile[
'from_domain'] ?? array();
312 if (is_string($domains)) {
313 $domains = preg_split(
'/[;,]+/', $domains);
316 foreach ((array) $domains as $allowed) {
317 $allowed = strtolower(trim((
string) $allowed));
318 if ($allowed ===
'') {
321 if ($allowed === $domain) {
324 if ($allowWildcard && ($allowed ===
'*' || (substr($allowed, 0, 2) ===
'*.' && str_ends_with($domain, substr($allowed, 1))))) {
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'] ??
''));
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 !==
''));
341 if ($auth && ($username ===
'' || $password ===
'')) {
342 throw new PHPMailerException(
'SMTP auth configuration incomplete.');
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;
353 $secure = strtolower((
string) (
$config[
'secure'] ??
$config[
'smtp_secure'] ??
''));
354 if (in_array($secure, array(
'tls',
'ssl'),
true)) {
355 $mail->SMTPSecure = $secure;
360 if ($transport ===
'sendmail' || $this->sendmail_path !==
'' || !empty(
$config[
'sendmail_path'])) {
362 $path = $this->sendmail_path ?: (string) (
$config[
'sendmail_path'] ??
'');
364 $mail->Sendmail = $path;
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);
376 if ($from[
'email'] ===
'') {
377 $from = $this->normalize_from($this->from ? array(
'email' => $this->from,
'name' => $this->fromname) :
null);
380 if ($from[
'email'] ===
'') {
381 $from = $this->normalize_from(
$config[
'from'] ?? array(
382 'email' =>
$config[
'from_email'] ??
'',
383 'name' =>
$config[
'from_name'] ??
'',
387 if ($from[
'email'] ===
'') {
388 throw new PHPMailerException(
'Absender fehlt.');
391 $mail->setFrom($from[
'email'], $from[
'name']);
393 $sender = trim((
string) (
$config[
'sender'] ??
$config[
'return_path'] ??
$config[
'envelope_from'] ??
''));
394 if ($sender !==
'') {
395 $mail->Sender = $sender;
399 private function configure_body(PHPMailer $mail, array $options) {
400 $html = (string) ($options[
'html'] ?? $this->htmlbody);
401 $text = (string) ($options[
'text'] ?? $this->textbody);
405 $mail->Body = $this->embed_html_images($mail, $html);
406 $mail->AltBody = $text !==
'' ? $text : $this->html_to_text($html);
410 $mail->isHTML(
false);
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)) {
419 $mail->addCustomHeader((
string) $key, (
string) $value);
423 private function configure_attachments(PHPMailer $mail) {
424 foreach ($this->attachments as $attachment) {
425 if (!is_array($attachment)) {
429 $path = (string) ($attachment[
'path'] ??
'');
430 if ($path ===
'' || !is_file($path) || !is_readable($path)) {
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'] ??
'');
440 if ($disp ===
'inline' && $cid !==
'') {
441 $mail->addEmbeddedImage($path, $cid, $name, $encoding, $type);
443 $mail->addAttachment($path, $name, $encoding, $type, $disp);
448 private function embed_html_images(PHPMailer $mail, $html) {
449 if (!$this->auto_embbed_images && !$this->auto_embed_images) {
453 preg_match_all(
'/<img[^>]+src=["\']([^"\']+)["\']/i', $html, $matches);
454 $images = array_unique($matches[1] ?? array());
456 foreach ($images as $src) {
457 $file = $this->html_image_path($src);
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;
471 private function html_image_path($src) {
472 $src = trim((
string) $src);
473 if ($src ===
'' || preg_match(
'/^(https?:|data:|cid:)/i', $src)) {
478 if (!preg_match(
'/^[A-Za-z]:[\\\\\\/]/', $path) && substr($path, 0, 1) !==
'/') {
482 $path = function_exists(
'dbx_os_path_file') ?
dbx_os_path_file($path) : $path;
483 return is_file($path) && is_readable($path) ? $path :
'';
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']);
494 private function normalize_addresses($addresses) {
495 if ($addresses ===
null || $addresses ===
'') {
499 if (is_string($addresses)) {
500 $parts = preg_split(
'/[;,]+/', $addresses);
501 return array_map(array($this,
'parse_address'), array_filter(array_map(
'trim', $parts)));
504 if (is_array($addresses)) {
505 if (isset($addresses[
'email'])) {
506 return array($this->normalize_from($addresses));
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);
514 $out[] = is_array($value) ? $this->normalize_from($value) : $this->parse_address((string) $value);
523 private function normalize_from($from) {
524 if (is_array($from)) {
526 'email' => trim((
string) ($from[
'email'] ?? $from[
'mail'] ?? $from[0] ??
'')),
527 'name' => trim((
string) ($from[
'name'] ?? $from[1] ??
'')),
531 if (is_string($from)) {
532 return $this->parse_address($from);
535 return array(
'email' =>
'',
'name' =>
'');
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]),
'"\' '));
543 return array(
'email' => $value,
'name' =>
'');
546 private function format_address($email, $name =
'') {
547 $email = trim((
string) $email);
548 $name = trim((
string) $name);
549 return $name !==
'' ? $name .
' <' . $email .
'>' : $email;
552 private function html_to_text($html) {
553 return trim(html_entity_decode(strip_tags((
string) $html), ENT_QUOTES | ENT_HTML5,
'UTF-8'));
556 private function sys_msg($status, $to,
$subject, $message) {
557 if (!function_exists(
'dbx')) {
562 dbx()->sys_msg($status,
'mail',
'', (
string)
$subject,
'to=' . $this->address_debug($to) .
' ' . (
string) $message);
563 }
catch (Throwable $e) {
568 private function address_debug($addresses) {
570 foreach ($this->normalize_addresses($addresses) as $address) {
571 if ($address[
'email'] !==
'') {
572 $list[] = $address[
'email'];
575 return implode(
',', $list);
578 private function spam_guard_reason(PHPMailer $mail, $to,
string $subject, array $options): string {
579 if (!$this->spam_protection || !empty($options[
'skip_spam_guard'])) {
587 if (!$this->spam_protection) {
591 return $this->spam_content_reason($text);
594 private function spam_guard_text(PHPMailer $mail, $to,
string $subject, array $options): string {
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()),
605 foreach ($this->headers as $key => $value) {
606 $parts[] = (string) $key .
': ' . (
string) $value;
609 return html_entity_decode(strip_tags(implode(
"\n", $parts)), ENT_QUOTES | ENT_HTML5,
'UTF-8');
612 private function spam_content_reason(
string $text): string {
613 $normalized = strtolower($text);
614 $normalized = preg_replace(
'/\s+/u',
' ', $normalized) ?: $normalized;
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',
623 foreach ($hardPatterns as $pattern => $reason) {
624 if (preg_match($pattern, $text)) {
629 if (preg_match(
'/\b(?:transaction|transfer|top\s*up|crypto|bitcoin|wallet|usdt|profit|investment)\b/i', $text)) {
632 if (preg_match(
'/(?:\$|usd|eur)\s*\d{3,}|\d{3,}\s*(?:\$|usd|eur)/i', $text)) {
635 if (preg_match(
'/(?:https?:\/\/|www\.|[a-z0-9-]+\.(?:ph|ru|top|xyz|click|link|icu|buzz|cfd|quest)\b)/i', $text)) {
638 if (preg_match(
'/\bget\s*(?:-|>|>|to|:)/i', $text)) {
641 if (preg_match(
'/[\x{1F300}-\x{1FAFF}]/u', $text)) {
645 $urlCount = preg_match_all(
'/(?:https?:\/\/|www\.|[a-z0-9-]+\.[a-z]{2,}\/)/i', $text);
646 if ($urlCount >= 2) {
650 return $score >= 5 ?
'spam score ' . $score :
'';