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 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 21:31:23 +02:00
parent 50f34ac37b
commit f24123be81
3 changed files with 302 additions and 233 deletions

359
api.php
View File

@@ -905,140 +905,190 @@ class ImapClient {
// ==================== IREDMAIL CLIENT ==================== // ==================== IREDMAIL CLIENT ====================
class IRedMailClient { class IRedMailClient {
private string $baseUrl; private \PDO $pdo;
private string $adminEmail;
private string $adminPassword;
private ?string $cookie = null;
public function __construct(string $baseUrl, string $adminEmail, string $adminPassword) { public function __construct(string $host, string $dbName, string $dbUser, string $dbPassword, int $port = 3306) {
$this->baseUrl = rtrim($baseUrl, '/'); $dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4";
if (!preg_match('#^https?://#i', $this->baseUrl)) { $this->pdo = new \PDO($dsn, $dbUser, $dbPassword, [
$this->baseUrl = 'https://' . $this->baseUrl; \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
} \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
$this->adminEmail = $adminEmail; \PDO::ATTR_TIMEOUT => 5,
$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,
]); ]);
$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) { public function getDomains(): array {
throw new \RuntimeException('iRedMail kirjautuminen epäonnistui (HTTP ' . $httpCode . ')'); $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 public function createDomain(string $domain, array $opts = []): void {
if (preg_match('/Set-Cookie:\s*([^;\r\n]+)/i', $headers, $m)) { $stmt = $this->pdo->prepare("INSERT INTO domain (domain, transport, settings, created, active) VALUES (?, 'dovecot', ?, NOW(), 1)");
$this->cookie = $m[1]; $settings = '';
} else { if (!empty($opts['quota'])) $settings = 'default_user_quota:' . intval($opts['quota']) . ';';
throw new \RuntimeException('iRedMail: session-cookie puuttuu vastauksesta'); $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 { 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; public function updateUser(string $email, array $opts): void {
return $this->request('POST', 'user/' . urlencode($email), $opts); $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): void {
} $domain = substr($email, strpos($email, '@') + 1);
public function deleteUser(string $email): array { $this->pdo->beginTransaction();
return $this->request('DELETE', 'user/' . urlencode($email)); 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 { 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 { public function testConnection(): array {
$domains = $this->getDomains(); $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 { function getIRedMailClient(string $companyId): IRedMailClient {
$config = dbLoadConfig(); $integ = dbGetIntegration($companyId, 'iredmail');
$url = $config['iredmail_api_url'] ?? ''; if (!$integ || !$integ['enabled']) throw new \RuntimeException('iRedMail-integraatio ei ole käytössä');
$email = $config['iredmail_admin_email'] ?? ''; $cfg = $integ['config'] ?? [];
$pw = $config['iredmail_admin_password'] ?? ''; $host = $cfg['db_host'] ?? '';
if (!$url || !$email || !$pw) { $dbName = $cfg['db_name'] ?? 'vmail';
throw new \RuntimeException('iRedMail-asetukset puuttuvat. Aseta ensin URL, admin-sähköposti ja salasana.'); $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 ==================== // ==================== TICKETS HELPER ====================
@@ -5613,6 +5663,7 @@ switch ($action) {
if ($integ['config']) { if ($integ['config']) {
$cfg = is_string($integ['config']) ? json_decode($integ['config'], true) : $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['token'])) $cfg['token'] = str_repeat('*', 8);
if (isset($cfg['db_password'])) $cfg['db_password'] = str_repeat('*', 8);
$integ['config'] = $cfg; $integ['config'] = $cfg;
} }
} }
@@ -5632,12 +5683,17 @@ switch ($action) {
if ($config === null || (is_array($config) && empty($config))) { if ($config === null || (is_array($config) && empty($config))) {
$config = ($old && isset($old['config'])) ? (is_string($old['config']) ? json_decode($old['config'], true) : $old['config']) : []; $config = ($old && isset($old['config'])) ? (is_string($old['config']) ? json_decode($old['config'], true) : $old['config']) : [];
} else { } 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 (isset($config['token']) && preg_match('/^\*+$/', $config['token'])) {
if ($old && isset($old['config']['token'])) { if ($old && isset($old['config']['token'])) {
$config['token'] = is_string($old['config']) ? json_decode($old['config'], true)['token'] : $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); dbSaveIntegration($companyId, $type, $enabled, $config);
@@ -5657,6 +5713,10 @@ switch ($action) {
$z = new ZammadClient($cfg['url'], $cfg['token']); $z = new ZammadClient($cfg['url'], $cfg['token']);
$result = $z->testConnection(); $result = $z->testConnection();
echo json_encode($result); echo json_encode($result);
} elseif ($type === 'iredmail') {
$client = getIRedMailClient($companyId);
$result = $client->testConnection();
echo json_encode($result);
} else { } else {
echo json_encode(['error' => 'Tuntematon tyyppi']); echo json_encode(['error' => 'Tuntematon tyyppi']);
} }
@@ -6040,33 +6100,12 @@ switch ($action) {
// ==================== IREDMAIL HALLINTA ==================== // ==================== 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': case 'iredmail_test':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$result = $client->testConnection(); $result = $client->testConnection();
echo json_encode($result); echo json_encode($result);
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -6076,11 +6115,11 @@ switch ($action) {
break; break;
case 'iredmail_domains': case 'iredmail_domains':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$result = $client->getDomains(); echo json_encode($client->getDomains());
echo json_encode($result['_data'] ?? []);
} catch (\Throwable $e) { } catch (\Throwable $e) {
http_response_code(500); http_response_code(500);
echo json_encode(['error' => $e->getMessage()]); echo json_encode(['error' => $e->getMessage()]);
@@ -6088,17 +6127,17 @@ switch ($action) {
break; break;
case 'iredmail_domain_create': case 'iredmail_domain_create':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$domain = trim($input['domain'] ?? ''); $domain = trim($input['domain'] ?? '');
if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$opts = []; $opts = [];
if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
if (isset($input['quota'])) $opts['quota'] = intval($input['quota']); if (isset($input['quota'])) $opts['quota'] = intval($input['quota']);
$result = $client->createDomain($domain, $opts); $client->createDomain($domain, $opts);
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
http_response_code(500); http_response_code(500);
@@ -6107,13 +6146,14 @@ switch ($action) {
break; break;
case 'iredmail_domain_delete': case 'iredmail_domain_delete':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$domain = trim($input['domain'] ?? ''); $domain = trim($input['domain'] ?? '');
if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$client->deleteDomain($domain); $client->deleteDomain($domain);
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -6123,13 +6163,13 @@ switch ($action) {
break; break;
case 'iredmail_users': case 'iredmail_users':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
$domain = trim($_GET['domain'] ?? ''); $domain = trim($_GET['domain'] ?? '');
if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$result = $client->getUsers($domain); echo json_encode($client->getUsers($domain));
echo json_encode($result['_data'] ?? []);
} catch (\Throwable $e) { } catch (\Throwable $e) {
http_response_code(500); http_response_code(500);
echo json_encode(['error' => $e->getMessage()]); echo json_encode(['error' => $e->getMessage()]);
@@ -6137,6 +6177,7 @@ switch ($action) {
break; break;
case 'iredmail_user_create': case 'iredmail_user_create':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
@@ -6144,7 +6185,7 @@ switch ($action) {
$password = $input['password'] ?? ''; $password = $input['password'] ?? '';
if (!$email || !$password) { http_response_code(400); echo json_encode(['error' => 'Sähköposti ja salasana vaaditaan']); break; } if (!$email || !$password) { http_response_code(400); echo json_encode(['error' => 'Sähköposti ja salasana vaaditaan']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$opts = []; $opts = [];
if (!empty($input['cn'])) $opts['cn'] = $input['cn']; if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
if (isset($input['mailQuota'])) $opts['mailQuota'] = intval($input['mailQuota']); if (isset($input['mailQuota'])) $opts['mailQuota'] = intval($input['mailQuota']);
@@ -6157,13 +6198,14 @@ switch ($action) {
break; break;
case 'iredmail_user_update': case 'iredmail_user_update':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$email = trim($input['email'] ?? ''); $email = trim($input['email'] ?? '');
if (!$email) { http_response_code(400); echo json_encode(['error' => 'Sähköposti puuttuu']); break; } if (!$email) { http_response_code(400); echo json_encode(['error' => 'Sähköposti puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$opts = []; $opts = [];
if (!empty($input['password'])) $opts['password'] = $input['password']; if (!empty($input['password'])) $opts['password'] = $input['password'];
if (!empty($input['cn'])) $opts['cn'] = $input['cn']; if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
@@ -6178,13 +6220,14 @@ switch ($action) {
break; break;
case 'iredmail_user_delete': case 'iredmail_user_delete':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$email = trim($input['email'] ?? ''); $email = trim($input['email'] ?? '');
if (!$email) { http_response_code(400); echo json_encode(['error' => 'Sähköposti puuttuu']); break; } if (!$email) { http_response_code(400); echo json_encode(['error' => 'Sähköposti puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$client->deleteUser($email); $client->deleteUser($email);
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -6194,13 +6237,13 @@ switch ($action) {
break; break;
case 'iredmail_aliases': case 'iredmail_aliases':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
$domain = trim($_GET['domain'] ?? ''); $domain = trim($_GET['domain'] ?? '');
if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; } if (!$domain) { http_response_code(400); echo json_encode(['error' => 'Domain puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$result = $client->getAliases($domain); echo json_encode($client->getAliases($domain));
echo json_encode($result['_data'] ?? []);
} catch (\Throwable $e) { } catch (\Throwable $e) {
http_response_code(500); http_response_code(500);
echo json_encode(['error' => $e->getMessage()]); echo json_encode(['error' => $e->getMessage()]);
@@ -6208,17 +6251,16 @@ switch ($action) {
break; break;
case 'iredmail_alias_create': case 'iredmail_alias_create':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$alias = trim($input['alias'] ?? ''); $alias = trim($input['alias'] ?? '');
$members = trim($input['members'] ?? '');
if (!$alias) { http_response_code(400); echo json_encode(['error' => 'Alias puuttuu']); break; } if (!$alias) { http_response_code(400); echo json_encode(['error' => 'Alias puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$opts = []; $client->createAlias($alias, $members);
if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
if (!empty($input['members'])) $opts['accessPolicy'] = 'membersonly';
$client->createAlias($alias, $opts);
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
http_response_code(500); http_response_code(500);
@@ -6227,13 +6269,14 @@ switch ($action) {
break; break;
case 'iredmail_alias_delete': case 'iredmail_alias_delete':
$companyId = requireCompany();
requireSuperAdmin(); requireSuperAdmin();
if ($method !== 'POST') break; if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$alias = trim($input['alias'] ?? ''); $alias = trim($input['alias'] ?? '');
if (!$alias) { http_response_code(400); echo json_encode(['error' => 'Alias puuttuu']); break; } if (!$alias) { http_response_code(400); echo json_encode(['error' => 'Alias puuttuu']); break; }
try { try {
$client = getIRedMailClient(); $client = getIRedMailClient($companyId);
$client->deleteAlias($alias); $client->deleteAlias($alias);
echo json_encode(['ok' => true]); echo json_encode(['ok' => true]);
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -1520,7 +1520,6 @@
<div class="main-container"> <div class="main-container">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
<h3 style="color:var(--primary-dark);margin:0;">Sähköpostinhallinta (iRedMail)</h3> <h3 style="color:var(--primary-dark);margin:0;">Sähköpostinhallinta (iRedMail)</h3>
<button class="btn-secondary" id="btn-iredmail-settings">⚙ Asetukset</button>
</div> </div>
<div id="iredmail-status" class="stat-card" style="margin-bottom:1rem;padding:0.75rem 1rem;font-size:0.9rem;"> <div id="iredmail-status" class="stat-card" style="margin-bottom:1rem;padding:0.75rem 1rem;font-size:0.9rem;">
@@ -1601,34 +1600,6 @@
</div> </div>
<!-- Modaalit: iRedMail --> <!-- Modaalit: iRedMail -->
<div class="modal" id="iredmail-config-modal" style="display:none;">
<div class="modal-content" style="max-width:500px;">
<div class="modal-header">
<h3>iRedMail-asetukset</h3>
<button class="modal-close" onclick="document.getElementById('iredmail-config-modal').style.display='none'">&times;</button>
</div>
<div class="form-grid" style="max-width:100%;">
<div class="form-group full-width">
<label>API URL</label>
<input type="text" id="iredmail-cfg-url" placeholder="https://mail.example.com/iredadmin">
</div>
<div class="form-group full-width">
<label>Admin-sähköposti</label>
<input type="text" id="iredmail-cfg-email" placeholder="postmaster@example.com">
</div>
<div class="form-group full-width">
<label>Salasana</label>
<input type="password" id="iredmail-cfg-password" placeholder="Jätä tyhjäksi jos ei muuteta">
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button class="btn-primary" id="btn-iredmail-cfg-save">Tallenna</button>
<button class="btn-secondary" id="btn-iredmail-cfg-test">Testaa yhteyttä</button>
</div>
<div id="iredmail-cfg-status" style="margin-top:0.5rem;font-size:0.85rem;"></div>
</div>
</div>
<div class="modal" id="iredmail-domain-modal" style="display:none;"> <div class="modal" id="iredmail-domain-modal" style="display:none;">
<div class="modal-content" style="max-width:450px;"> <div class="modal-content" style="max-width:450px;">
<div class="modal-header"> <div class="modal-header">
@@ -1866,6 +1837,38 @@
<div id="company-zammad-result" style="margin-top:0.75rem;display:none;padding:0.75rem;border-radius:8px;font-size:0.85rem;"></div> <div id="company-zammad-result" style="margin-top:0.75rem;display:none;padding:0.75rem;border-radius:8px;font-size:0.85rem;"></div>
</div> </div>
<div class="table-card" id="settings-iredmail-card" style="padding:1.5rem;margin-top:1rem;display:none;">
<h3 style="color:#0f3460;margin-bottom:0.5rem;border-bottom:2px solid #f0f2f5;padding-bottom:0.5rem;">📮 iRedMail (Sähköposti)</h3>
<p style="color:#666;font-size:0.85rem;margin-bottom:1rem;">Yhdistä iRedMail-sähköpostipalvelimen tietokantaan (vmail). Hallinta-moduulissa voit hallita domaineja, tilejä ja aliaksia.</p>
<div class="form-grid" style="max-width:500px;">
<div class="form-group full-width">
<label>Tietokantapalvelin (host)</label>
<input type="text" id="company-iredmail-host" placeholder="mail.example.com" style="font-family:monospace;">
</div>
<div class="form-group" style="flex:1;">
<label>Tietokanta</label>
<input type="text" id="company-iredmail-dbname" value="vmail" placeholder="vmail" style="font-family:monospace;">
</div>
<div class="form-group" style="flex:0 0 100px;">
<label>Portti</label>
<input type="number" id="company-iredmail-port" value="3306" min="1" max="65535" style="font-family:monospace;">
</div>
<div class="form-group full-width">
<label>Käyttäjä</label>
<input type="text" id="company-iredmail-user" placeholder="vmailadmin" style="font-family:monospace;">
</div>
<div class="form-group full-width">
<label>Salasana</label>
<input type="password" id="company-iredmail-password" placeholder="Salasana">
</div>
<div class="form-group full-width" style="display:flex;gap:0.5rem;flex-wrap:wrap;">
<button type="button" class="btn-primary" id="btn-company-iredmail-save">Tallenna</button>
<button type="button" class="btn-secondary" id="btn-company-iredmail-test">Testaa yhteys</button>
</div>
</div>
<div id="company-iredmail-result" style="margin-top:0.75rem;display:none;padding:0.75rem;border-radius:8px;font-size:0.85rem;"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -2001,6 +2004,9 @@
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;"> <label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" data-integration="telegram"> 🤖 Telegram-hälytykset <input type="checkbox" data-integration="telegram"> 🤖 Telegram-hälytykset
</label> </label>
<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;cursor:pointer;">
<input type="checkbox" data-integration="iredmail"> 📮 iRedMail (Sähköposti)
</label>
</div> </div>
</div> </div>
<!-- Postilaatikot --> <!-- Postilaatikot -->

112
script.js
View File

@@ -2767,10 +2767,22 @@ async function loadSettings() {
const zammadInteg = integs.find(i => i.type === 'zammad'); const zammadInteg = integs.find(i => i.type === 'zammad');
const zammadEnabled = zammadInteg?.enabled; const zammadEnabled = zammadInteg?.enabled;
// Saatavuus-API kortti näkyy aina (perus API-asetukset) // 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 teleCard = document.getElementById('settings-telegram-card');
const zammadCard = document.getElementById('settings-zammad-card'); const zammadCard = document.getElementById('settings-zammad-card');
const iredmailCard = document.getElementById('settings-iredmail-card');
if (teleCard) teleCard.style.display = telegramEnabled ? '' : 'none'; if (teleCard) teleCard.style.display = telegramEnabled ? '' : 'none';
if (zammadCard) zammadCard.style.display = zammadEnabled ? '' : '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 // Lataa Zammad-asetukset korttiin
if (zammadEnabled && zammadInteg?.config) { if (zammadEnabled && zammadInteg?.config) {
document.getElementById('company-zammad-url').value = zammadInteg.config.url || ''; document.getElementById('company-zammad-url').value = zammadInteg.config.url || '';
@@ -3025,7 +3037,7 @@ async function loadCompanyIntegrations() {
try { try {
const integrations = await apiCall('integrations'); const integrations = await apiCall('integrations');
// Aseta vain checkboxit — konfiguraatio ladataan API-tabissa // 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 integ = integrations.find(i => i.type === type);
const cb = document.querySelector(`#integrations-checkboxes input[data-integration="${type}"]`); const cb = document.querySelector(`#integrations-checkboxes input[data-integration="${type}"]`);
if (cb) cb.checked = integ?.enabled || false; 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() { document.querySelector('#integrations-checkboxes input[data-integration="telegram"]')?.addEventListener('change', async function() {
try { try {
await saveSimpleIntegration('telegram', this.checked); await saveSimpleIntegration('telegram', this.checked);
// Päivitä API-sivun kortti
const card = document.getElementById('settings-telegram-card'); const card = document.getElementById('settings-telegram-card');
if (card) card.style.display = this.checked ? '' : 'none'; if (card) card.style.display = this.checked ? '' : 'none';
} catch (e) { console.error(e); } } 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 // Lataa ryhmät
document.getElementById('btn-company-zammad-groups')?.addEventListener('click', async () => { document.getElementById('btn-company-zammad-groups')?.addEventListener('click', async () => {
// Tallenna ensin URL ja token // Tallenna ensin URL ja token
@@ -6742,9 +6804,9 @@ function applyModules(modules, hasIntegrations) {
if (mod === 'settings') { if (mod === 'settings') {
const showSettings = enabled.includes(mod) && isAdminUser && (isSuperAdmin || hasIntegrations === true); const showSettings = enabled.includes(mod) && isAdminUser && (isSuperAdmin || hasIntegrations === true);
tabBtn.style.display = showSettings ? '' : 'none'; 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') { } else if (mod === 'hallinta') {
tabBtn.style.display = (enabled.includes(mod) && isSuperAdmin) ? '' : 'none'; tabBtn.style.display = (enabled.includes(mod) && isAdminUser) ? '' : 'none';
} else { } else {
tabBtn.style.display = enabled.includes(mod) ? '' : 'none'; tabBtn.style.display = enabled.includes(mod) ? '' : 'none';
} }
@@ -7089,48 +7151,6 @@ async function deleteIRedMailAlias(alias) {
} catch (e) { alert(e.message); } } 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 = '<span style="color:#e74c3c;">' + esc(e.message) + '</span>';
}
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 = '<span style="color:#27ae60;">Tallennettu!</span>';
} catch (e) {
document.getElementById('iredmail-cfg-status').innerHTML = '<span style="color:#e74c3c;">' + esc(e.message) + '</span>';
}
});
document.getElementById('btn-iredmail-cfg-test').addEventListener('click', async () => {
const statusEl = document.getElementById('iredmail-cfg-status');
statusEl.innerHTML = '<span style="color:#888;">Testataan...</span>';
try {
const result = await apiCall('iredmail_test', 'POST', {});
statusEl.innerHTML = '<span style="color:#27ae60;">✓ Yhteys OK! ' + (result.domains || 0) + ' domainia.</span>';
} catch (e) {
statusEl.innerHTML = '<span style="color:#e74c3c;">✗ ' + esc(e.message) + '</span>';
}
});
// Init — branding ensin, sitten auth (luo session-cookien), sitten captcha (käyttää samaa sessiota) // Init — branding ensin, sitten auth (luo session-cookien), sitten captcha (käyttää samaa sessiota)
loadBranding().then(async () => { loadBranding().then(async () => {
await checkAuth(); await checkAuth();