Lisää SMTP-lähetystuki postilaatikoihin

Aiemmin sähköpostit lähetettiin PHP mail()-funktiolla, mikä
ei toimi kunnolla useimmilla palvelimilla (SPF/DKIM-ongelmat).
Nyt mailboxiin voi konfiguroida SMTP-asetukset (host, port,
user, pass, encryption), ja lähetys tapahtuu suoraan
SMTP-palvelimen kautta socket-yhteydellä. Fallback PHP
mail():iin jos SMTP-asetuksia ei ole asetettu.

- db.php: smtp_host/port/user/password/encryption sarakkeet
- api.php: sendViaSMTP() socket-pohjainen SMTP-client
- index.html: SMTP-kentät mailbox-lomakkeeseen
- script.js: SMTP-kenttien luku/kirjoitus lomakkeessa

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 22:46:28 +02:00
parent a13f3e00a5
commit 78f25d0079
4 changed files with 191 additions and 6 deletions

137
api.php
View File

@@ -572,6 +572,13 @@ function sendTicketMail(string $to, string $subject, string $body, string $inRep
$fromEmail = $mailbox['smtp_from_email'] ?? $mailbox['imap_user'] ?? MAIL_FROM;
$fromName = $mailbox['smtp_from_name'] ?? $mailbox['nimi'] ?? 'Asiakaspalvelu';
// Jos mailboxilla on SMTP-asetukset, käytä SMTP:tä
$smtpHost = $mailbox['smtp_host'] ?? '';
if ($smtpHost !== '') {
return sendViaSMTP($to, $subject, $body, $fromEmail, $fromName, $inReplyTo, $references, $mailbox, $cc);
}
// Fallback: PHP mail()
$headers = "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "From: {$fromName} <{$fromEmail}>\r\n";
@@ -586,6 +593,118 @@ function sendTicketMail(string $to, string $subject, string $body, string $inRep
return mail($to, $subject, $body, $headers, '-f ' . $fromEmail);
}
function sendViaSMTP(string $to, string $subject, string $body, string $fromEmail, string $fromName, string $inReplyTo, string $references, array $mailbox, string $cc): bool {
$host = $mailbox['smtp_host'];
$port = (int)($mailbox['smtp_port'] ?? 587);
$user = $mailbox['smtp_user'] ?? '';
$pass = $mailbox['smtp_password'] ?? '';
$encryption = $mailbox['smtp_encryption'] ?? 'tls';
$timeout = 15;
$errno = 0; $errstr = '';
// Yhteys
if ($encryption === 'ssl') {
$fp = @stream_socket_client("ssl://{$host}:{$port}", $errno, $errstr, $timeout);
} else {
$fp = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, $timeout);
}
if (!$fp) {
error_log("SMTP connect failed: {$errstr} ({$errno})");
return false;
}
stream_set_timeout($fp, $timeout);
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '220') { fclose($fp); error_log("SMTP banner: $resp"); return false; }
// EHLO
fwrite($fp, "EHLO " . gethostname() . "\r\n");
$ehloResp = '';
while ($line = fgets($fp, 512)) {
$ehloResp .= $line;
if (substr($line, 3, 1) === ' ') break;
}
// STARTTLS jos tls
if ($encryption === 'tls') {
fwrite($fp, "STARTTLS\r\n");
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '220') { fclose($fp); error_log("SMTP STARTTLS: $resp"); return false; }
$crypto = stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT);
if (!$crypto) { fclose($fp); error_log("SMTP TLS negotiation failed"); return false; }
// EHLO uudelleen TLS:n jälkeen
fwrite($fp, "EHLO " . gethostname() . "\r\n");
while ($line = fgets($fp, 512)) {
if (substr($line, 3, 1) === ' ') break;
}
}
// AUTH LOGIN
if ($user !== '') {
fwrite($fp, "AUTH LOGIN\r\n");
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '334') { fclose($fp); error_log("SMTP AUTH: $resp"); return false; }
fwrite($fp, base64_encode($user) . "\r\n");
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '334') { fclose($fp); error_log("SMTP AUTH user: $resp"); return false; }
fwrite($fp, base64_encode($pass) . "\r\n");
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '235') { fclose($fp); error_log("SMTP AUTH pass: $resp"); return false; }
}
// MAIL FROM
fwrite($fp, "MAIL FROM:<{$fromEmail}>\r\n");
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '250') { fclose($fp); error_log("SMTP MAIL FROM: $resp"); return false; }
// RCPT TO
$allRecipients = array_filter(array_map('trim', explode(',', $to)));
if ($cc) {
$allRecipients = array_merge($allRecipients, array_filter(array_map('trim', explode(',', $cc))));
}
foreach ($allRecipients as $rcpt) {
fwrite($fp, "RCPT TO:<{$rcpt}>\r\n");
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '250' && substr($resp, 0, 3) !== '251') {
fclose($fp); error_log("SMTP RCPT TO: $resp"); return false;
}
}
// DATA
fwrite($fp, "DATA\r\n");
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '354') { fclose($fp); error_log("SMTP DATA: $resp"); return false; }
// Rakennetaan viesti
$messageId = '<' . uniqid('msg_', true) . '@' . (explode('@', $fromEmail)[1] ?? 'localhost') . '>';
$msg = "From: {$fromName} <{$fromEmail}>\r\n";
$msg .= "To: {$to}\r\n";
if ($cc) $msg .= "Cc: {$cc}\r\n";
$msg .= "Subject: =?UTF-8?B?" . base64_encode($subject) . "?=\r\n";
$msg .= "Message-ID: {$messageId}\r\n";
if ($inReplyTo) {
$msg .= "In-Reply-To: {$inReplyTo}\r\n";
$msg .= "References: " . ($references ? $references . ' ' : '') . $inReplyTo . "\r\n";
}
$msg .= "MIME-Version: 1.0\r\n";
$msg .= "Content-Type: text/plain; charset=UTF-8\r\n";
$msg .= "Content-Transfer-Encoding: base64\r\n";
$msg .= "Date: " . date('r') . "\r\n";
$msg .= "\r\n";
$msg .= chunk_split(base64_encode($body));
$msg .= "\r\n.\r\n";
fwrite($fp, $msg);
$resp = fgets($fp, 512);
if (substr($resp, 0, 3) !== '250') { fclose($fp); error_log("SMTP send: $resp"); return false; }
// QUIT
fwrite($fp, "QUIT\r\n");
fclose($fp);
return true;
}
function parseLiittymat(array $input): array {
$liittymat = [];
foreach (($input['liittymat'] ?? []) as $l) {
@@ -2580,6 +2699,7 @@ switch ($action) {
$result[] = [
'id' => $mb['id'],
'nimi' => $mb['nimi'] ?? $mb['imap_user'] ?? '',
'smtp_from_email' => $mb['smtp_from_email'] ?? $mb['imap_user'] ?? '',
'company_id' => $comp['id'],
'company_nimi' => $comp['nimi'],
];
@@ -2755,6 +2875,7 @@ switch ($action) {
// Palauta postilaatikot ilman salasanoja
$mbs = array_map(function($mb) {
$mb['imap_password'] = !empty($mb['imap_password']) ? '********' : '';
$mb['smtp_password'] = !empty($mb['smtp_password']) ? '********' : '';
return $mb;
}, $mailboxes);
echo json_encode($mbs);
@@ -2775,16 +2896,26 @@ switch ($action) {
'imap_encryption' => trim($input['imap_encryption'] ?? 'ssl'),
'smtp_from_email' => trim($input['smtp_from_email'] ?? ''),
'smtp_from_name' => trim($input['smtp_from_name'] ?? ''),
'smtp_host' => trim($input['smtp_host'] ?? ''),
'smtp_port' => intval($input['smtp_port'] ?? 587),
'smtp_user' => trim($input['smtp_user'] ?? ''),
'smtp_encryption' => trim($input['smtp_encryption'] ?? 'tls'),
'aktiivinen' => $input['aktiivinen'] ?? true,
];
// Salasana: jos ******** -> pidä vanha, muuten päivitä
// Hae vanha mailbox salasanojen vertailua varten
$existingMb = dbGetMailbox($mb['id']);
// IMAP-salasana: jos ******** -> pidä vanha, muuten päivitä
if (isset($input['imap_password']) && $input['imap_password'] !== '********') {
$mb['imap_password'] = $input['imap_password'];
} else {
// Hae vanha salasana
$existingMb = dbGetMailbox($mb['id']);
$mb['imap_password'] = $existingMb ? ($existingMb['imap_password'] ?? '') : '';
}
// SMTP-salasana: jos ******** -> pidä vanha, muuten päivitä
if (isset($input['smtp_password']) && $input['smtp_password'] !== '********') {
$mb['smtp_password'] = $input['smtp_password'];
} else {
$mb['smtp_password'] = $existingMb ? ($existingMb['smtp_password'] ?? '') : '';
}
if (empty($mb['nimi'])) {
http_response_code(400);