Initial commit: OMSORG website + Mitarbeiter-App
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Sends an email with an optional file attachment via SMTP.
|
||||
*
|
||||
* @param array $cfg Config array with smtp_host, smtp_port, smtp_user, smtp_password, mail_from
|
||||
* @param string $to Recipient address
|
||||
* @param string $subject Subject (plain UTF-8, will be encoded)
|
||||
* @param string $body Plain-text body
|
||||
* @param array $attach Optional: ['path' => '/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()];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user