'/abs/path', 'name' => 'display.pdf', 'mime' => 'application/pdf'] * @return array ['ok' => bool, 'log' => string] */ function smtp_send(array $cfg, string $to, string $subject, string $body, array $attach = []): array { $log = ''; $read = function ($conn) use (&$log) { $line = ''; while ($chunk = fgets($conn, 512)) { $log .= '<< ' . $chunk; $line = $chunk; if ($chunk[3] === ' ') break; // last line of multi-line response } return $line; }; $write = function ($conn, string $cmd) use (&$log) { $log .= '>> ' . $cmd; fwrite($conn, $cmd); }; $code = fn(string $line) => (int)substr($line, 0, 3); try { $host = trim($cfg['smtp_host']); $port = (int)$cfg['smtp_port']; $ssl = ($port === 465); $conn = @fsockopen(($ssl ? 'ssl://' : '') . $host, $port, $errno, $errstr, 10); if (!$conn) { return ['ok' => false, 'log' => "Connect failed: $errstr ($errno)"]; } stream_set_timeout($conn, 10); $line = $read($conn); if ($code($line) !== 220) throw new RuntimeException("Unexpected greeting: $line"); $write($conn, "EHLO omsorg-connect\r\n"); $capabilities = ''; while ($chunk = fgets($conn, 512)) { $log .= '<< ' . $chunk; $capabilities .= $chunk; if ($chunk[3] === ' ') break; } if ($code($capabilities) !== 250) throw new RuntimeException("EHLO failed"); if (!$ssl && str_contains($capabilities, 'STARTTLS')) { $write($conn, "STARTTLS\r\n"); $line = $read($conn); if ($code($line) !== 220) throw new RuntimeException("STARTTLS failed: $line"); if (!stream_socket_enable_crypto($conn, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { throw new RuntimeException("TLS handshake failed"); } $write($conn, "EHLO omsorg-connect\r\n"); while ($chunk = fgets($conn, 512)) { $log .= '<< ' . $chunk; if ($chunk[3] === ' ') break; } } $write($conn, "AUTH LOGIN\r\n"); $line = $read($conn); if ($code($line) !== 334) throw new RuntimeException("AUTH LOGIN failed: $line"); $write($conn, base64_encode($cfg['smtp_user']) . "\r\n"); $line = $read($conn); if ($code($line) !== 334) throw new RuntimeException("Username rejected: $line"); $write($conn, base64_encode($cfg['smtp_password']) . "\r\n"); $line = $read($conn); if ($code($line) !== 235) throw new RuntimeException("Password rejected: $line"); $write($conn, "MAIL FROM:<{$cfg['mail_from']}>\r\n"); $line = $read($conn); if ($code($line) !== 250) throw new RuntimeException("MAIL FROM failed: $line"); $write($conn, "RCPT TO:<$to>\r\n"); $line = $read($conn); if ($code($line) !== 250) throw new RuntimeException("RCPT TO failed: $line"); $write($conn, "DATA\r\n"); $line = $read($conn); if ($code($line) !== 354) throw new RuntimeException("DATA failed: $line"); $enc_subject = '=?UTF-8?B?' . base64_encode($subject) . '?='; $boundary = 'omsorg_' . md5(uniqid((string)mt_rand(), true)); $headers = "From: OMSORG Connect <{$cfg['mail_from']}>\r\n"; $headers .= "To: $to\r\n"; $headers .= "Subject: $enc_subject\r\n"; $headers .= "MIME-Version: 1.0\r\n"; if ($attach) { $headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n"; $msg = $headers . "\r\n"; $msg .= "--$boundary\r\n"; $msg .= "Content-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n"; $msg .= $body . "\r\n\r\n"; $safe_name = preg_replace('/[^A-Za-z0-9._-]/', '_', $attach['name']); $file_data = chunk_split(base64_encode(file_get_contents($attach['path']))); $msg .= "--$boundary\r\n"; $msg .= "Content-Type: {$attach['mime']}; name=\"$safe_name\"\r\n"; $msg .= "Content-Transfer-Encoding: base64\r\n"; $msg .= "Content-Disposition: attachment; filename=\"$safe_name\"\r\n\r\n"; $msg .= $file_data . "\r\n"; $msg .= "--$boundary--"; } else { $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; $msg = $headers . "\r\n" . $body; } // Dot-stuffing: lines starting with '.' must be doubled $msg = str_replace("\n.", "\n..", $msg); $write($conn, $msg . "\r\n.\r\n"); $line = $read($conn); if ($code($line) !== 250) throw new RuntimeException("Message rejected: $line"); $write($conn, "QUIT\r\n"); $read($conn); fclose($conn); return ['ok' => true, 'log' => $log]; } catch (RuntimeException $e) { if (isset($conn) && is_resource($conn)) fclose($conn); return ['ok' => false, 'log' => $log . "\nError: " . $e->getMessage()]; } }