Files
omsorg/omsorgWeb/mitarbeiter-app-legacy/lib/mail.php
T
Felix KemmlerandClaude Sonnet 5 b6c1389c55 Reorganize into monorepo layout, move mitarbeiter-app to legacy reference
Consolidates the previously separate omsorgapp and omsorgCore repos
(each had their own nested .git with GitHub history) plus the old
root-level website/mitarbeiter-app into a single monorepo, matching
the structure already documented in the root CLAUDE.md. Also moves
the PHP employee app aside as omsorgWeb/mitarbeiter-app-legacy/ to
serve as a template for a ground-up rewrite.

Fixes .gitignore in the same pass: the config-secrets/uploads/data
patterns were unanchored (relative to repo root, not depth-agnostic),
so they silently stopped matching once the app moved under omsorgWeb/.
Patterns are now **/-prefixed and cover both mitarbeiter-app and
mitarbeiter-app-legacy, keeping DB/SMTP credentials and uploaded
employee documents out of version control.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 14:21:37 +02:00

138 lines
5.4 KiB
PHP

<?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()];
}
}