From f24123be818f514de44604d560b9d0a21942c768 Mon Sep 17 00:00:00 2001 From: Jukka Lampikoski Date: Fri, 13 Mar 2026 21:31:23 +0200 Subject: [PATCH] Convert iRedMail from REST API to direct MySQL (vmail DB) + per-company integration - Replace IRedMailClient REST API class with direct MySQL/PDO connection to vmail database - Move iRedMail config from global config table to per-company integrations (like Zammad) - Add iRedMail integration card to API settings with DB host/name/user/password/port fields - Add iRedMail checkbox to integrations section in company settings - Change Hallinta tab visibility: show for admins (not just superadmins) when module enabled - API endpoints now use requireCompany() + requireSuperAdmin() and get config from integrations - Password hashing uses SSHA512 (iRedMail default) - Mask db_password in API responses (like token masking for Zammad) Co-Authored-By: Claude Opus 4.6 --- api.php | 359 ++++++++++++++++++++++++++++++----------------------- index.html | 64 +++++----- script.js | 112 ++++++++++------- 3 files changed, 302 insertions(+), 233 deletions(-) diff --git a/api.php b/api.php index 949447f..aef15d2 100644 --- a/api.php +++ b/api.php @@ -905,140 +905,190 @@ class ImapClient { // ==================== IREDMAIL CLIENT ==================== class IRedMailClient { - private string $baseUrl; - private string $adminEmail; - private string $adminPassword; - private ?string $cookie = null; + private \PDO $pdo; - public function __construct(string $baseUrl, string $adminEmail, string $adminPassword) { - $this->baseUrl = rtrim($baseUrl, '/'); - if (!preg_match('#^https?://#i', $this->baseUrl)) { - $this->baseUrl = 'https://' . $this->baseUrl; - } - $this->adminEmail = $adminEmail; - $this->adminPassword = $adminPassword; - } - - public function login(): void { - $ch = curl_init($this->baseUrl . '/api/login'); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => http_build_query([ - 'username' => $this->adminEmail, - 'password' => $this->adminPassword, - ]), - CURLOPT_TIMEOUT => 10, - CURLOPT_CONNECTTIMEOUT => 5, - CURLOPT_HEADER => true, - CURLOPT_SSL_VERIFYPEER => false, + public function __construct(string $host, string $dbName, string $dbUser, string $dbPassword, int $port = 3306) { + $dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4"; + $this->pdo = new \PDO($dsn, $dbUser, $dbPassword, [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_TIMEOUT => 5, ]); - $response = curl_exec($ch); - $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $headers = substr($response, 0, $headerSize); - curl_close($ch); + } - if ($httpCode >= 400) { - throw new \RuntimeException('iRedMail kirjautuminen epäonnistui (HTTP ' . $httpCode . ')'); - } + public function getDomains(): array { + $stmt = $this->pdo->query("SELECT d.domain, d.active, d.created, + (SELECT COUNT(*) FROM mailbox m WHERE m.domain = d.domain) AS mailboxes, + (SELECT COUNT(*) FROM alias a WHERE a.domain = d.domain AND a.address != a.goto AND a.address NOT IN (SELECT username FROM mailbox WHERE domain = d.domain)) AS aliases, + d.settings + FROM domain d WHERE d.domain != 'localhost' ORDER BY d.domain"); + return $stmt->fetchAll(); + } - // Parse Set-Cookie header - if (preg_match('/Set-Cookie:\s*([^;\r\n]+)/i', $headers, $m)) { - $this->cookie = $m[1]; - } else { - throw new \RuntimeException('iRedMail: session-cookie puuttuu vastauksesta'); + public function createDomain(string $domain, array $opts = []): void { + $stmt = $this->pdo->prepare("INSERT INTO domain (domain, transport, settings, created, active) VALUES (?, 'dovecot', ?, NOW(), 1)"); + $settings = ''; + if (!empty($opts['quota'])) $settings = 'default_user_quota:' . intval($opts['quota']) . ';'; + $stmt->execute([$domain, $settings]); + } + + public function deleteDomain(string $domain): void { + $this->pdo->beginTransaction(); + try { + $this->pdo->prepare("DELETE FROM alias WHERE domain = ?")->execute([$domain]); + $this->pdo->prepare("DELETE FROM forwardings WHERE domain = ?")->execute([$domain]); + $this->pdo->prepare("DELETE FROM mailbox WHERE domain = ?")->execute([$domain]); + $this->pdo->prepare("DELETE FROM domain WHERE domain = ?")->execute([$domain]); + $this->pdo->commit(); + } catch (\Throwable $e) { + $this->pdo->rollBack(); + throw $e; } } - public function request(string $method, string $endpoint, ?array $data = null, bool $retried = false): array { - if (!$this->cookie) $this->login(); - - $url = $this->baseUrl . '/api/' . ltrim($endpoint, '/'); - $ch = curl_init($url); - $opts = [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 15, - CURLOPT_CONNECTTIMEOUT => 5, - CURLOPT_HTTPHEADER => ['Cookie: ' . $this->cookie], - CURLOPT_SSL_VERIFYPEER => false, - ]; - - if ($method === 'POST') { - $opts[CURLOPT_POST] = true; - if ($data) $opts[CURLOPT_POSTFIELDS] = http_build_query($data); - } elseif ($method === 'PUT') { - $opts[CURLOPT_CUSTOMREQUEST] = 'PUT'; - if ($data) $opts[CURLOPT_POSTFIELDS] = http_build_query($data); - } elseif ($method === 'DELETE') { - $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE'; - } - - curl_setopt_array($ch, $opts); - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - // Retry login on 401 - if ($httpCode === 401 && !$retried) { - $this->cookie = null; - return $this->request($method, $endpoint, $data, true); - } - - $result = json_decode($response, true) ?: []; - if ($httpCode >= 400 || (isset($result['_success']) && !$result['_success'])) { - throw new \RuntimeException('iRedMail API: ' . ($result['_msg'] ?? "HTTP $httpCode")); - } - return $result; - } - - public function getDomains(): array { return $this->request('GET', 'domains'); } - public function createDomain(string $domain, array $opts = []): array { - return $this->request('POST', 'domain/' . urlencode($domain), $opts); - } - public function deleteDomain(string $domain): array { - return $this->request('DELETE', 'domain/' . urlencode($domain)); - } public function getUsers(string $domain): array { - return $this->request('GET', 'users/' . urlencode($domain)); + $stmt = $this->pdo->prepare("SELECT username AS email, name AS cn, quota AS mailQuota, active, + CASE WHEN active = 1 THEN 'active' ELSE 'disabled' END AS accountStatus + FROM mailbox WHERE domain = ? ORDER BY username"); + $stmt->execute([$domain]); + return $stmt->fetchAll(); } - public function getUser(string $email): array { - return $this->request('GET', 'user/' . urlencode($email)); + + public function createUser(string $email, string $password, array $opts = []): void { + $parts = explode('@', $email, 2); + if (count($parts) !== 2) throw new \RuntimeException('Virheellinen sähköpostiosoite'); + [$local, $domain] = $parts; + + // Tarkista onko domain olemassa + $stmt = $this->pdo->prepare("SELECT domain FROM domain WHERE domain = ?"); + $stmt->execute([$domain]); + if (!$stmt->fetch()) throw new \RuntimeException("Domain {$domain} ei ole olemassa"); + + // Tarkista duplikaatti + $stmt = $this->pdo->prepare("SELECT username FROM mailbox WHERE username = ?"); + $stmt->execute([$email]); + if ($stmt->fetch()) throw new \RuntimeException("Tili {$email} on jo olemassa"); + + $hash = $this->hashPassword($password); + $quota = intval($opts['mailQuota'] ?? 1024) * 1048576; // MB → bytes + $name = $opts['cn'] ?? ''; + $maildir = $domain . '/' . $local . '/'; + + $this->pdo->beginTransaction(); + try { + $stmt = $this->pdo->prepare("INSERT INTO mailbox (username, password, name, storagebasedirectory, storagenode, maildir, quota, domain, active, enablesmtp, enablepop3, enableimap, enabledeliver, enablelda, created) VALUES (?, ?, ?, '/var/vmail', 'vmail1', ?, ?, ?, 1, 1, 1, 1, 1, 1, NOW())"); + $stmt->execute([$email, $hash, $name, $maildir, $quota, $domain]); + + // Lisää myös alias-rivi (iRedMail vaatii) + $stmt = $this->pdo->prepare("INSERT INTO alias (address, goto, domain, active) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE goto = VALUES(goto)"); + $stmt->execute([$email, $email, $domain]); + + // Lisää forwardings-rivi + $stmt = $this->pdo->prepare("INSERT INTO forwardings (address, forwarding, domain, dest_domain, is_forwarding, active) VALUES (?, ?, ?, ?, 0, 1) ON DUPLICATE KEY UPDATE forwarding = VALUES(forwarding)"); + $stmt->execute([$email, $email, $domain, $domain]); + + $this->pdo->commit(); + } catch (\Throwable $e) { + $this->pdo->rollBack(); + throw $e; + } } - public function createUser(string $email, string $password, array $opts = []): array { - $opts['password'] = $password; - return $this->request('POST', 'user/' . urlencode($email), $opts); + + public function updateUser(string $email, array $opts): void { + $sets = []; + $params = []; + if (!empty($opts['password'])) { + $sets[] = 'password = ?'; + $params[] = $this->hashPassword($opts['password']); + $sets[] = 'passwordlastchange = NOW()'; + } + if (isset($opts['cn'])) { + $sets[] = 'name = ?'; + $params[] = $opts['cn']; + } + if (isset($opts['mailQuota'])) { + $sets[] = 'quota = ?'; + $params[] = intval($opts['mailQuota']) * 1048576; + } + if (isset($opts['accountStatus'])) { + $sets[] = 'active = ?'; + $params[] = $opts['accountStatus'] === 'disabled' ? 0 : 1; + } + if (empty($sets)) return; + $sets[] = 'modified = NOW()'; + $params[] = $email; + $sql = "UPDATE mailbox SET " . implode(', ', $sets) . " WHERE username = ?"; + $this->pdo->prepare($sql)->execute($params); } - public function updateUser(string $email, array $opts): array { - return $this->request('PUT', 'user/' . urlencode($email), $opts); - } - public function deleteUser(string $email): array { - return $this->request('DELETE', 'user/' . urlencode($email)); + + public function deleteUser(string $email): void { + $domain = substr($email, strpos($email, '@') + 1); + $this->pdo->beginTransaction(); + try { + $this->pdo->prepare("DELETE FROM mailbox WHERE username = ?")->execute([$email]); + $this->pdo->prepare("DELETE FROM alias WHERE address = ?")->execute([$email]); + $this->pdo->prepare("DELETE FROM forwardings WHERE address = ? OR forwarding = ?")->execute([$email, $email]); + $this->pdo->commit(); + } catch (\Throwable $e) { + $this->pdo->rollBack(); + throw $e; + } } + public function getAliases(string $domain): array { - return $this->request('GET', 'aliases/' . urlencode($domain)); + // Aliakset = alias-rivit jotka eivät ole mailbox-käyttäjien omia + $stmt = $this->pdo->prepare("SELECT a.address, a.goto, a.domain, a.active + FROM alias a WHERE a.domain = ? + AND a.address NOT IN (SELECT username FROM mailbox WHERE domain = ?) + ORDER BY a.address"); + $stmt->execute([$domain, $domain]); + $results = $stmt->fetchAll(); + return array_map(function($a) { + return [ + 'address' => $a['address'], + 'goto' => $a['goto'], + 'members' => $a['goto'], + 'active' => $a['active'], + ]; + }, $results); } - public function createAlias(string $alias, array $opts = []): array { - return $this->request('POST', 'alias/' . urlencode($alias), $opts); + + public function createAlias(string $alias, string $goto): void { + $domain = substr($alias, strpos($alias, '@') + 1); + $stmt = $this->pdo->prepare("INSERT INTO alias (address, goto, domain, active, created) VALUES (?, ?, ?, 1, NOW()) ON DUPLICATE KEY UPDATE goto = VALUES(goto), modified = NOW()"); + $stmt->execute([$alias, $goto, $domain]); } - public function deleteAlias(string $alias): array { - return $this->request('DELETE', 'alias/' . urlencode($alias)); + + public function deleteAlias(string $alias): void { + $this->pdo->prepare("DELETE FROM alias WHERE address = ?")->execute([$alias]); } + public function testConnection(): array { $domains = $this->getDomains(); - return ['ok' => true, 'domains' => count($domains['_data'] ?? [])]; + return ['ok' => true, 'domains' => count($domains)]; + } + + private function hashPassword(string $password): string { + // SSHA512 — iRedMail default + $salt = random_bytes(8); + $hash = hash('sha512', $password . $salt, true); + return '{SSHA512}' . base64_encode($hash . $salt); } } -function getIRedMailClient(): IRedMailClient { - $config = dbLoadConfig(); - $url = $config['iredmail_api_url'] ?? ''; - $email = $config['iredmail_admin_email'] ?? ''; - $pw = $config['iredmail_admin_password'] ?? ''; - if (!$url || !$email || !$pw) { - throw new \RuntimeException('iRedMail-asetukset puuttuvat. Aseta ensin URL, admin-sähköposti ja salasana.'); +function getIRedMailClient(string $companyId): IRedMailClient { + $integ = dbGetIntegration($companyId, 'iredmail'); + if (!$integ || !$integ['enabled']) throw new \RuntimeException('iRedMail-integraatio ei ole käytössä'); + $cfg = $integ['config'] ?? []; + $host = $cfg['db_host'] ?? ''; + $dbName = $cfg['db_name'] ?? 'vmail'; + $dbUser = $cfg['db_user'] ?? ''; + $dbPassword = $cfg['db_password'] ?? ''; + $port = intval($cfg['db_port'] ?? 3306); + if (!$host || !$dbUser || !$dbPassword) { + throw new \RuntimeException('iRedMail-tietokanta-asetukset puuttuvat. Aseta host, käyttäjä ja salasana.'); } - return new IRedMailClient($url, $email, $pw); + return new IRedMailClient($host, $dbName, $dbUser, $dbPassword, $port); } // ==================== TICKETS HELPER ==================== @@ -5613,6 +5663,7 @@ switch ($action) { if ($integ['config']) { $cfg = is_string($integ['config']) ? json_decode($integ['config'], true) : $integ['config']; if (isset($cfg['token'])) $cfg['token'] = str_repeat('*', 8); + if (isset($cfg['db_password'])) $cfg['db_password'] = str_repeat('*', 8); $integ['config'] = $cfg; } } @@ -5632,12 +5683,17 @@ switch ($action) { if ($config === null || (is_array($config) && empty($config))) { $config = ($old && isset($old['config'])) ? (is_string($old['config']) ? json_decode($old['config'], true) : $old['config']) : []; } else { - // Jos token on maskattua (********), säilytä vanha + // Jos token/password on maskattua (********), säilytä vanha if (isset($config['token']) && preg_match('/^\*+$/', $config['token'])) { if ($old && isset($old['config']['token'])) { $config['token'] = is_string($old['config']) ? json_decode($old['config'], true)['token'] : $old['config']['token']; } } + if (isset($config['db_password']) && preg_match('/^\*+$/', $config['db_password'])) { + if ($old && isset($old['config']['db_password'])) { + $config['db_password'] = is_string($old['config']) ? json_decode($old['config'], true)['db_password'] : $old['config']['db_password']; + } + } } dbSaveIntegration($companyId, $type, $enabled, $config); @@ -5657,6 +5713,10 @@ switch ($action) { $z = new ZammadClient($cfg['url'], $cfg['token']); $result = $z->testConnection(); echo json_encode($result); + } elseif ($type === 'iredmail') { + $client = getIRedMailClient($companyId); + $result = $client->testConnection(); + echo json_encode($result); } else { echo json_encode(['error' => 'Tuntematon tyyppi']); } @@ -6040,33 +6100,12 @@ switch ($action) { // ==================== IREDMAIL HALLINTA ==================== - case 'iredmail_config': - requireSuperAdmin(); - $config = dbLoadConfig(); - echo json_encode([ - 'url' => $config['iredmail_api_url'] ?? '', - 'admin_email' => $config['iredmail_admin_email'] ?? '', - 'has_password' => !empty($config['iredmail_admin_password']), - ]); - break; - - case 'iredmail_config_save': - requireSuperAdmin(); - if ($method !== 'POST') break; - $input = json_decode(file_get_contents('php://input'), true); - $config = dbLoadConfig(); - if (isset($input['url'])) $config['iredmail_api_url'] = trim($input['url']); - if (isset($input['admin_email'])) $config['iredmail_admin_email'] = trim($input['admin_email']); - if (!empty($input['password'])) $config['iredmail_admin_password'] = $input['password']; - dbSaveConfig($config); - echo json_encode(['ok' => true]); - break; - case 'iredmail_test': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; try { - $client = getIRedMailClient(); + $client = getIRedMailClient($companyId); $result = $client->testConnection(); echo json_encode($result); } catch (\Throwable $e) { @@ -6076,11 +6115,11 @@ switch ($action) { break; case 'iredmail_domains': + $companyId = requireCompany(); requireSuperAdmin(); try { - $client = getIRedMailClient(); - $result = $client->getDomains(); - echo json_encode($result['_data'] ?? []); + $client = getIRedMailClient($companyId); + echo json_encode($client->getDomains()); } catch (\Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); @@ -6088,17 +6127,17 @@ switch ($action) { break; case 'iredmail_domain_create': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $domain = trim($input['domain'] ?? ''); if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } try { - $client = getIRedMailClient(); + $client = getIRedMailClient($companyId); $opts = []; - if (!empty($input['cn'])) $opts['cn'] = $input['cn']; if (isset($input['quota'])) $opts['quota'] = intval($input['quota']); - $result = $client->createDomain($domain, $opts); + $client->createDomain($domain, $opts); echo json_encode(['ok' => true]); } catch (\Throwable $e) { http_response_code(500); @@ -6107,13 +6146,14 @@ switch ($action) { break; case 'iredmail_domain_delete': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $domain = trim($input['domain'] ?? ''); if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } try { - $client = getIRedMailClient(); + $client = getIRedMailClient($companyId); $client->deleteDomain($domain); echo json_encode(['ok' => true]); } catch (\Throwable $e) { @@ -6123,13 +6163,13 @@ switch ($action) { break; case 'iredmail_users': + $companyId = requireCompany(); requireSuperAdmin(); $domain = trim($_GET['domain'] ?? ''); if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } try { - $client = getIRedMailClient(); - $result = $client->getUsers($domain); - echo json_encode($result['_data'] ?? []); + $client = getIRedMailClient($companyId); + echo json_encode($client->getUsers($domain)); } catch (\Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); @@ -6137,6 +6177,7 @@ switch ($action) { break; case 'iredmail_user_create': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); @@ -6144,7 +6185,7 @@ switch ($action) { $password = $input['password'] ?? ''; if (!$email || !$password) { http_response_code(400); echo json_encode(['error' => 'Sähköposti ja salasana vaaditaan']); break; } try { - $client = getIRedMailClient(); + $client = getIRedMailClient($companyId); $opts = []; if (!empty($input['cn'])) $opts['cn'] = $input['cn']; if (isset($input['mailQuota'])) $opts['mailQuota'] = intval($input['mailQuota']); @@ -6157,13 +6198,14 @@ switch ($action) { break; case 'iredmail_user_update': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $email = trim($input['email'] ?? ''); if (!$email) { http_response_code(400); echo json_encode(['error' => 'Sähköposti puuttuu']); break; } try { - $client = getIRedMailClient(); + $client = getIRedMailClient($companyId); $opts = []; if (!empty($input['password'])) $opts['password'] = $input['password']; if (!empty($input['cn'])) $opts['cn'] = $input['cn']; @@ -6178,13 +6220,14 @@ switch ($action) { break; case 'iredmail_user_delete': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $email = trim($input['email'] ?? ''); if (!$email) { http_response_code(400); echo json_encode(['error' => 'Sähköposti puuttuu']); break; } try { - $client = getIRedMailClient(); + $client = getIRedMailClient($companyId); $client->deleteUser($email); echo json_encode(['ok' => true]); } catch (\Throwable $e) { @@ -6194,13 +6237,13 @@ switch ($action) { break; case 'iredmail_aliases': + $companyId = requireCompany(); requireSuperAdmin(); $domain = trim($_GET['domain'] ?? ''); if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } try { - $client = getIRedMailClient(); - $result = $client->getAliases($domain); - echo json_encode($result['_data'] ?? []); + $client = getIRedMailClient($companyId); + echo json_encode($client->getAliases($domain)); } catch (\Throwable $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]); @@ -6208,17 +6251,16 @@ switch ($action) { break; case 'iredmail_alias_create': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $alias = trim($input['alias'] ?? ''); + $members = trim($input['members'] ?? ''); if (!$alias) { http_response_code(400); echo json_encode(['error' => 'Alias puuttuu']); break; } try { - $client = getIRedMailClient(); - $opts = []; - if (!empty($input['cn'])) $opts['cn'] = $input['cn']; - if (!empty($input['members'])) $opts['accessPolicy'] = 'membersonly'; - $client->createAlias($alias, $opts); + $client = getIRedMailClient($companyId); + $client->createAlias($alias, $members); echo json_encode(['ok' => true]); } catch (\Throwable $e) { http_response_code(500); @@ -6227,13 +6269,14 @@ switch ($action) { break; case 'iredmail_alias_delete': + $companyId = requireCompany(); requireSuperAdmin(); if ($method !== 'POST') break; $input = json_decode(file_get_contents('php://input'), true); $alias = trim($input['alias'] ?? ''); if (!$alias) { http_response_code(400); echo json_encode(['error' => 'Alias puuttuu']); break; } try { - $client = getIRedMailClient(); + $client = getIRedMailClient($companyId); $client->deleteAlias($alias); echo json_encode(['ok' => true]); } catch (\Throwable $e) { diff --git a/index.html b/index.html index 3df35ca..f9ff678 100644 --- a/index.html +++ b/index.html @@ -1520,7 +1520,6 @@

Sähköpostinhallinta (iRedMail)

-
@@ -1601,34 +1600,6 @@
- -
@@ -2001,6 +2004,9 @@ + diff --git a/script.js b/script.js index 47d1dcc..28be95d 100644 --- a/script.js +++ b/script.js @@ -2767,10 +2767,22 @@ async function loadSettings() { const zammadInteg = integs.find(i => i.type === 'zammad'); const zammadEnabled = zammadInteg?.enabled; // Saatavuus-API kortti näkyy aina (perus API-asetukset) + const iredmailInteg = integs.find(i => i.type === 'iredmail'); + const iredmailEnabled = iredmailInteg?.enabled; const teleCard = document.getElementById('settings-telegram-card'); const zammadCard = document.getElementById('settings-zammad-card'); + const iredmailCard = document.getElementById('settings-iredmail-card'); if (teleCard) teleCard.style.display = telegramEnabled ? '' : 'none'; if (zammadCard) zammadCard.style.display = zammadEnabled ? '' : 'none'; + if (iredmailCard) iredmailCard.style.display = iredmailEnabled ? '' : 'none'; + // Lataa iRedMail-asetukset korttiin + if (iredmailEnabled && iredmailInteg?.config) { + document.getElementById('company-iredmail-host').value = iredmailInteg.config.db_host || ''; + document.getElementById('company-iredmail-dbname').value = iredmailInteg.config.db_name || 'vmail'; + document.getElementById('company-iredmail-port').value = iredmailInteg.config.db_port || '3306'; + document.getElementById('company-iredmail-user').value = iredmailInteg.config.db_user || ''; + document.getElementById('company-iredmail-password').value = iredmailInteg.config.db_password || ''; + } // Lataa Zammad-asetukset korttiin if (zammadEnabled && zammadInteg?.config) { document.getElementById('company-zammad-url').value = zammadInteg.config.url || ''; @@ -3025,7 +3037,7 @@ async function loadCompanyIntegrations() { try { const integrations = await apiCall('integrations'); // Aseta vain checkboxit — konfiguraatio ladataan API-tabissa - ['zammad', 'saatavuus_api', 'telegram'].forEach(type => { + ['zammad', 'saatavuus_api', 'telegram', 'iredmail'].forEach(type => { const integ = integrations.find(i => i.type === type); const cb = document.querySelector(`#integrations-checkboxes input[data-integration="${type}"]`); if (cb) cb.checked = integ?.enabled || false; @@ -3082,12 +3094,62 @@ document.querySelector('#integrations-checkboxes input[data-integration="saatavu document.querySelector('#integrations-checkboxes input[data-integration="telegram"]')?.addEventListener('change', async function() { try { await saveSimpleIntegration('telegram', this.checked); - // Päivitä API-sivun kortti const card = document.getElementById('settings-telegram-card'); if (card) card.style.display = this.checked ? '' : 'none'; } catch (e) { console.error(e); } }); +// iRedMail checkbox toggle +document.querySelector('#integrations-checkboxes input[data-integration="iredmail"]')?.addEventListener('change', async function() { + try { + await saveSimpleIntegration('iredmail', this.checked); + const card = document.getElementById('settings-iredmail-card'); + if (card) card.style.display = this.checked ? '' : 'none'; + } catch (e) { console.error(e); } +}); + +// iRedMail tallenna +async function saveCompanyIRedMail() { + const config = { + db_host: document.getElementById('company-iredmail-host').value.trim(), + db_name: document.getElementById('company-iredmail-dbname').value.trim() || 'vmail', + db_port: parseInt(document.getElementById('company-iredmail-port').value) || 3306, + db_user: document.getElementById('company-iredmail-user').value.trim(), + db_password: document.getElementById('company-iredmail-password').value, + }; + await apiCall('integration_save', 'POST', { type: 'iredmail', enabled: true, config }); +} + +document.getElementById('btn-company-iredmail-save')?.addEventListener('click', async () => { + const result = document.getElementById('company-iredmail-result'); + try { + await saveCompanyIRedMail(); + result.style.display = 'block'; + result.style.background = '#d4edda'; + result.textContent = '✅ Tallennettu!'; + } catch (e) { + result.style.display = 'block'; + result.style.background = '#f8d7da'; + result.textContent = '❌ ' + e.message; + } +}); + +document.getElementById('btn-company-iredmail-test')?.addEventListener('click', async () => { + const result = document.getElementById('company-iredmail-result'); + result.style.display = 'block'; + result.style.background = '#f8f9fb'; + result.textContent = 'Testataan...'; + try { + await saveCompanyIRedMail(); + const res = await apiCall('integration_test', 'POST', { type: 'iredmail' }); + result.style.background = '#d4edda'; + result.textContent = `✅ Yhteys OK! ${res.domains || 0} domainia.`; + } catch (e) { + result.style.background = '#f8d7da'; + result.textContent = '❌ ' + e.message; + } +}); + // Lataa ryhmät document.getElementById('btn-company-zammad-groups')?.addEventListener('click', async () => { // Tallenna ensin URL ja token @@ -6742,9 +6804,9 @@ function applyModules(modules, hasIntegrations) { if (mod === 'settings') { const showSettings = enabled.includes(mod) && isAdminUser && (isSuperAdmin || hasIntegrations === true); tabBtn.style.display = showSettings ? '' : 'none'; - // hallinta: vain superadmineille + pitää olla moduulina päällä + // hallinta: adminille/superadminille + pitää olla moduulina päällä } else if (mod === 'hallinta') { - tabBtn.style.display = (enabled.includes(mod) && isSuperAdmin) ? '' : 'none'; + tabBtn.style.display = (enabled.includes(mod) && isAdminUser) ? '' : 'none'; } else { tabBtn.style.display = enabled.includes(mod) ? '' : 'none'; } @@ -7089,48 +7151,6 @@ async function deleteIRedMailAlias(alias) { } catch (e) { alert(e.message); } } -// --- iRedMail asetukset --- - -document.getElementById('btn-iredmail-settings').addEventListener('click', async () => { - try { - const cfg = await apiCall('iredmail_config'); - document.getElementById('iredmail-cfg-url').value = cfg.url || ''; - document.getElementById('iredmail-cfg-email').value = cfg.admin_email || ''; - document.getElementById('iredmail-cfg-password').value = ''; - document.getElementById('iredmail-cfg-password').placeholder = cfg.has_password ? 'Asetettu — jätä tyhjäksi jos ei muuteta' : 'Anna salasana'; - document.getElementById('iredmail-cfg-status').innerHTML = ''; - } catch (e) { - document.getElementById('iredmail-cfg-status').innerHTML = '' + esc(e.message) + ''; - } - document.getElementById('iredmail-config-modal').style.display = 'flex'; -}); - -document.getElementById('btn-iredmail-cfg-save').addEventListener('click', async () => { - const data = { - url: document.getElementById('iredmail-cfg-url').value.trim(), - admin_email: document.getElementById('iredmail-cfg-email').value.trim(), - }; - const pw = document.getElementById('iredmail-cfg-password').value; - if (pw) data.password = pw; - try { - await apiCall('iredmail_config_save', 'POST', data); - document.getElementById('iredmail-cfg-status').innerHTML = 'Tallennettu!'; - } catch (e) { - document.getElementById('iredmail-cfg-status').innerHTML = '' + esc(e.message) + ''; - } -}); - -document.getElementById('btn-iredmail-cfg-test').addEventListener('click', async () => { - const statusEl = document.getElementById('iredmail-cfg-status'); - statusEl.innerHTML = 'Testataan...'; - try { - const result = await apiCall('iredmail_test', 'POST', {}); - statusEl.innerHTML = '✓ Yhteys OK! ' + (result.domains || 0) + ' domainia.'; - } catch (e) { - statusEl.innerHTML = '✗ ' + esc(e.message) + ''; - } -}); - // Init — branding ensin, sitten auth (luo session-cookien), sitten captcha (käyttää samaa sessiota) loadBranding().then(async () => { await checkAuth();