From 78f25d007927369257dafa9cbb00dda8fd2d9442 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Tue, 10 Mar 2026 22:46:28 +0200 Subject: [PATCH] =?UTF-8?q?Lis=C3=A4=C3=A4=20SMTP-l=C3=A4hetystuki=20posti?= =?UTF-8?q?laatikoihin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- api.php | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++-- db.php | 24 ++++++++-- index.html | 26 ++++++++++ script.js | 10 ++++ 4 files changed, 191 insertions(+), 6 deletions(-) diff --git a/api.php b/api.php index 373fc48..c5a572c 100644 --- a/api.php +++ b/api.php @@ -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); diff --git a/db.php b/db.php index b7e6c97..2aa5ae5 100644 --- a/db.php +++ b/db.php @@ -309,6 +309,11 @@ function initDatabase(): void { imap_user VARCHAR(255), imap_encryption VARCHAR(10) DEFAULT 'ssl', imap_password VARCHAR(255), + smtp_host VARCHAR(255) DEFAULT '', + smtp_port INT DEFAULT 587, + smtp_user VARCHAR(255) DEFAULT '', + smtp_password VARCHAR(255) DEFAULT '', + smtp_encryption VARCHAR(10) DEFAULT 'tls', smtp_from_email VARCHAR(255), smtp_from_name VARCHAR(255), aktiivinen BOOLEAN DEFAULT TRUE, @@ -431,6 +436,11 @@ function initDatabase(): void { "ALTER TABLE customer_connections ADD COLUMN laite VARCHAR(100) DEFAULT '' AFTER vlan", "ALTER TABLE customer_connections ADD COLUMN portti VARCHAR(100) DEFAULT '' AFTER laite", "ALTER TABLE customer_connections ADD COLUMN ip VARCHAR(100) DEFAULT '' AFTER portti", + "ALTER TABLE mailboxes ADD COLUMN smtp_host VARCHAR(255) DEFAULT '' AFTER smtp_from_name", + "ALTER TABLE mailboxes ADD COLUMN smtp_port INT DEFAULT 587 AFTER smtp_host", + "ALTER TABLE mailboxes ADD COLUMN smtp_user VARCHAR(255) DEFAULT '' AFTER smtp_port", + "ALTER TABLE mailboxes ADD COLUMN smtp_password VARCHAR(255) DEFAULT '' AFTER smtp_user", + "ALTER TABLE mailboxes ADD COLUMN smtp_encryption VARCHAR(10) DEFAULT 'tls' AFTER smtp_password", ]; foreach ($alters as $sql) { try { $db->query($sql); } catch (\Throwable $e) { /* sarake on jo olemassa / jo ajettu */ } @@ -1148,6 +1158,7 @@ function dbLoadMailboxes(string $companyId): array { foreach ($boxes as &$b) { $b['aktiivinen'] = (bool)$b['aktiivinen']; $b['imap_port'] = (int)$b['imap_port']; + $b['smtp_port'] = (int)($b['smtp_port'] ?? 587); unset($b['company_id']); } return $boxes; @@ -1155,13 +1166,15 @@ function dbLoadMailboxes(string $companyId): array { function dbSaveMailbox(string $companyId, array $mailbox): void { _dbExecute(" - INSERT INTO mailboxes (id, company_id, nimi, imap_host, imap_port, imap_user, imap_encryption, imap_password, smtp_from_email, smtp_from_name, aktiivinen) - VALUES (:id, :company_id, :nimi, :imap_host, :imap_port, :imap_user, :imap_encryption, :imap_password, :smtp_from_email, :smtp_from_name, :aktiivinen) + INSERT INTO mailboxes (id, company_id, nimi, imap_host, imap_port, imap_user, imap_encryption, imap_password, smtp_from_email, smtp_from_name, smtp_host, smtp_port, smtp_user, smtp_password, smtp_encryption, aktiivinen) + VALUES (:id, :company_id, :nimi, :imap_host, :imap_port, :imap_user, :imap_encryption, :imap_password, :smtp_from_email, :smtp_from_name, :smtp_host, :smtp_port, :smtp_user, :smtp_password, :smtp_encryption, :aktiivinen) ON DUPLICATE KEY UPDATE nimi = VALUES(nimi), imap_host = VALUES(imap_host), imap_port = VALUES(imap_port), imap_user = VALUES(imap_user), imap_encryption = VALUES(imap_encryption), imap_password = VALUES(imap_password), smtp_from_email = VALUES(smtp_from_email), - smtp_from_name = VALUES(smtp_from_name), aktiivinen = VALUES(aktiivinen) + smtp_from_name = VALUES(smtp_from_name), smtp_host = VALUES(smtp_host), smtp_port = VALUES(smtp_port), + smtp_user = VALUES(smtp_user), smtp_password = VALUES(smtp_password), + smtp_encryption = VALUES(smtp_encryption), aktiivinen = VALUES(aktiivinen) ", [ 'id' => $mailbox['id'], 'company_id' => $companyId, @@ -1173,6 +1186,11 @@ function dbSaveMailbox(string $companyId, array $mailbox): void { 'imap_password' => $mailbox['imap_password'] ?? '', 'smtp_from_email' => $mailbox['smtp_from_email'] ?? '', 'smtp_from_name' => $mailbox['smtp_from_name'] ?? '', + 'smtp_host' => $mailbox['smtp_host'] ?? '', + 'smtp_port' => $mailbox['smtp_port'] ?? 587, + 'smtp_user' => $mailbox['smtp_user'] ?? '', + 'smtp_password' => $mailbox['smtp_password'] ?? '', + 'smtp_encryption' => $mailbox['smtp_encryption'] ?? 'tls', 'aktiivinen' => $mailbox['aktiivinen'] ?? true, ]); } diff --git a/index.html b/index.html index 3a8a1fa..bf56b81 100644 --- a/index.html +++ b/index.html @@ -835,6 +835,32 @@ +

SMTP-lähetysasetukset

+

Jätä tyhjäksi käyttääksesi palvelimen omaa sendmailia

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/script.js b/script.js index 050f1d1..9c1a530 100644 --- a/script.js +++ b/script.js @@ -2527,6 +2527,11 @@ function showMailboxForm(mb = null) { document.getElementById('mailbox-form-encryption').value = mb ? (mb.imap_encryption || 'ssl') : 'ssl'; document.getElementById('mailbox-form-smtp-email').value = mb ? (mb.smtp_from_email || '') : ''; document.getElementById('mailbox-form-smtp-name').value = mb ? (mb.smtp_from_name || '') : ''; + document.getElementById('mailbox-form-smtp-host').value = mb ? (mb.smtp_host || '') : ''; + document.getElementById('mailbox-form-smtp-port').value = mb ? (mb.smtp_port || 587) : 587; + document.getElementById('mailbox-form-smtp-user').value = mb ? (mb.smtp_user || '') : ''; + document.getElementById('mailbox-form-smtp-pass').value = mb ? (mb.smtp_password || '') : ''; + document.getElementById('mailbox-form-smtp-encryption').value = mb ? (mb.smtp_encryption || 'tls') : 'tls'; document.getElementById('mailbox-form-container').style.display = ''; } @@ -2554,6 +2559,11 @@ document.getElementById('btn-save-mailbox').addEventListener('click', async () = imap_encryption: document.getElementById('mailbox-form-encryption').value, smtp_from_email: document.getElementById('mailbox-form-smtp-email').value, smtp_from_name: document.getElementById('mailbox-form-smtp-name').value, + smtp_host: document.getElementById('mailbox-form-smtp-host').value, + smtp_port: parseInt(document.getElementById('mailbox-form-smtp-port').value) || 587, + smtp_user: document.getElementById('mailbox-form-smtp-user').value, + smtp_password: document.getElementById('mailbox-form-smtp-pass').value, + smtp_encryption: document.getElementById('mailbox-form-smtp-encryption').value, aktiivinen: true, }; try {