Add Hallinta module with iRedMail email management

New "Hallinta" main tab (superadmin only) with "Sähköposti" sub-tab for
managing email via iRedAdmin-Pro REST API. Features:
- IRedMailClient PHP class with cookie-based session auth + auto-retry
- Domain CRUD (list, create, delete)
- Mailbox CRUD (list, create, delete, password change)
- Alias CRUD (list, create, delete)
- Configuration modal (API URL, admin credentials, connection test)
- Search/filter for mailboxes
- 13 new API endpoints, all requireSuperAdmin()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 19:58:10 +02:00
parent b3d8b7e067
commit 6ea62b075f
4 changed files with 908 additions and 0 deletions

343
api.php
View File

@@ -902,6 +902,145 @@ class ImapClient {
}
}
// ==================== IREDMAIL CLIENT ====================
class IRedMailClient {
private string $baseUrl;
private string $adminEmail;
private string $adminPassword;
private ?string $cookie = null;
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,
]);
$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 . ')');
}
// 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 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));
}
public function getUser(string $email): array {
return $this->request('GET', 'user/' . urlencode($email));
}
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): array {
return $this->request('PUT', 'user/' . urlencode($email), $opts);
}
public function deleteUser(string $email): array {
return $this->request('DELETE', 'user/' . urlencode($email));
}
public function getAliases(string $domain): array {
return $this->request('GET', 'aliases/' . urlencode($domain));
}
public function createAlias(string $alias, array $opts = []): array {
return $this->request('POST', 'alias/' . urlencode($alias), $opts);
}
public function deleteAlias(string $alias): array {
return $this->request('DELETE', 'alias/' . urlencode($alias));
}
public function testConnection(): array {
$domains = $this->getDomains();
return ['ok' => true, 'domains' => count($domains['_data'] ?? [])];
}
}
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.');
}
return new IRedMailClient($url, $email, $pw);
}
// ==================== TICKETS HELPER ====================
function sendTelegramAlert(string $companyId, array $ticket): void {
@@ -5872,6 +6011,210 @@ switch ($action) {
}
break;
// ==================== 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':
requireSuperAdmin();
if ($method !== 'POST') break;
try {
$client = getIRedMailClient();
$result = $client->testConnection();
echo json_encode($result);
} catch (\Throwable $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_domains':
requireSuperAdmin();
try {
$client = getIRedMailClient();
$result = $client->getDomains();
echo json_encode($result['_data'] ?? []);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_domain_create':
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();
$opts = [];
if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
if (isset($input['quota'])) $opts['quota'] = intval($input['quota']);
$result = $client->createDomain($domain, $opts);
echo json_encode(['ok' => true]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_domain_delete':
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->deleteDomain($domain);
echo json_encode(['ok' => true]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_users':
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'] ?? []);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_user_create':
requireSuperAdmin();
if ($method !== 'POST') break;
$input = json_decode(file_get_contents('php://input'), true);
$email = trim($input['email'] ?? '');
$password = $input['password'] ?? '';
if (!$email || !$password) { http_response_code(400); echo json_encode(['error' => 'Sähköposti ja salasana vaaditaan']); break; }
try {
$client = getIRedMailClient();
$opts = [];
if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
if (isset($input['mailQuota'])) $opts['mailQuota'] = intval($input['mailQuota']);
$client->createUser($email, $password, $opts);
echo json_encode(['ok' => true]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_user_update':
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();
$opts = [];
if (!empty($input['password'])) $opts['password'] = $input['password'];
if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
if (isset($input['mailQuota'])) $opts['mailQuota'] = intval($input['mailQuota']);
if (isset($input['accountStatus'])) $opts['accountStatus'] = $input['accountStatus'];
$client->updateUser($email, $opts);
echo json_encode(['ok' => true]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_user_delete':
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->deleteUser($email);
echo json_encode(['ok' => true]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_aliases':
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'] ?? []);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_alias_create':
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();
$opts = [];
if (!empty($input['cn'])) $opts['cn'] = $input['cn'];
if (!empty($input['members'])) $opts['accessPolicy'] = 'membersonly';
$client->createAlias($alias, $opts);
echo json_encode(['ok' => true]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
case 'iredmail_alias_delete':
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->deleteAlias($alias);
echo json_encode(['ok' => true]);
} catch (\Throwable $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Tuntematon toiminto']);

View File

@@ -89,6 +89,7 @@
<button class="tab" data-tab="documents">Dokumentit</button>
<button class="tab" data-tab="netadmin">NetAdmin</button>
<button class="tab" data-tab="changelog">Muutosloki</button>
<button class="tab" data-tab="hallinta" id="tab-hallinta" style="display:none">Hallinta</button>
<button class="tab" data-tab="settings" id="tab-settings" style="display:none">API</button>
</div>
@@ -1509,6 +1510,238 @@
</div>
</div>
<!-- Tab: Hallinta (vain superadmin) -->
<div class="tab-content" id="tab-content-hallinta">
<div class="sub-tab-bar" id="hallinta-sub-tab-bar">
<button class="sub-tab active" data-hallinta-subtab="hallinta-email">Sähköposti</button>
</div>
<div id="subtab-hallinta-email" class="sub-tab-content active">
<div class="main-container">
<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>
<button class="btn-secondary" id="btn-iredmail-settings">⚙ Asetukset</button>
</div>
<div id="iredmail-status" class="stat-card" style="margin-bottom:1rem;padding:0.75rem 1rem;font-size:0.9rem;">
<span id="iredmail-status-text">Yhteyttä ei ole määritetty</span>
</div>
<!-- Domain-listaus -->
<div id="iredmail-domain-section">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem;">
<h4 style="margin:0;color:var(--primary-dark);">Domainit</h4>
<button class="btn-primary" id="btn-iredmail-add-domain">+ Lisää domain</button>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>Domain</th>
<th>Tilejä</th>
<th>Aliaksia</th>
<th>Kiintiö (MB)</th>
<th style="width:100px;">Toiminnot</th>
</tr>
</thead>
<tbody id="iredmail-domain-tbody"></tbody>
</table>
<div id="no-iredmail-domains" style="text-align:center;padding:2rem;color:#aaa;display:none;">
Ei domaineja. Määritä ensin iRedMail-yhteys asetuksista.
</div>
</div>
</div>
<!-- Käyttäjät (näkyy kun domain valittu) -->
<div id="iredmail-users-section" style="display:none;margin-top:1.5rem;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem;flex-wrap:wrap;gap:0.5rem;">
<h4 style="margin:0;color:var(--primary-dark);">
<a href="#" id="iredmail-back-to-domains" style="text-decoration:none;color:var(--primary-color);">← Domainit</a>
/ <span id="iredmail-current-domain"></span> — Tilit
</h4>
<div style="display:flex;gap:0.5rem;">
<input type="text" id="iredmail-user-search" placeholder="Hae tilejä..." style="width:200px;">
<button class="btn-primary" id="btn-iredmail-add-user">+ Lisää tili</button>
</div>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>Sähköposti</th>
<th>Nimi</th>
<th>Kiintiö (MB)</th>
<th>Tila</th>
<th style="width:160px;">Toiminnot</th>
</tr>
</thead>
<tbody id="iredmail-user-tbody"></tbody>
</table>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin:1.5rem 0 0.5rem;">
<h4 style="margin:0;color:var(--primary-dark);">Aliakset</h4>
<button class="btn-primary" id="btn-iredmail-add-alias">+ Lisää alias</button>
</div>
<div class="table-card">
<table>
<thead>
<tr>
<th>Alias</th>
<th>Kohde(t)</th>
<th style="width:80px;">Toiminnot</th>
</tr>
</thead>
<tbody id="iredmail-alias-tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- 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-content" style="max-width:450px;">
<div class="modal-header">
<h3>Lisää domain</h3>
<button class="modal-close" onclick="document.getElementById('iredmail-domain-modal').style.display='none'">&times;</button>
</div>
<div class="form-grid" style="max-width:100%;">
<div class="form-group full-width">
<label>Domain *</label>
<input type="text" id="iredmail-domain-name" placeholder="esim. yritys.fi">
</div>
<div class="form-group full-width">
<label>Kuvaus</label>
<input type="text" id="iredmail-domain-cn" placeholder="Yrityksen nimi">
</div>
<div class="form-group full-width">
<label>Kiintiö (MB, 0 = rajaton)</label>
<input type="number" id="iredmail-domain-quota" value="0" min="0">
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button class="btn-primary" id="btn-iredmail-domain-save">Lisää</button>
<button class="btn-secondary" onclick="document.getElementById('iredmail-domain-modal').style.display='none'">Peruuta</button>
</div>
</div>
</div>
<div class="modal" id="iredmail-user-modal" style="display:none;">
<div class="modal-content" style="max-width:450px;">
<div class="modal-header">
<h3 id="iredmail-user-modal-title">Lisää tili</h3>
<button class="modal-close" onclick="document.getElementById('iredmail-user-modal').style.display='none'">&times;</button>
</div>
<div class="form-grid" style="max-width:100%;">
<div class="form-group full-width" id="iredmail-user-email-group">
<label>Sähköpostiosoite *</label>
<div style="display:flex;gap:0.3rem;align-items:center;">
<input type="text" id="iredmail-user-local" placeholder="kayttaja" style="flex:1;">
<span>@</span>
<span id="iredmail-user-domain-label" style="font-weight:600;"></span>
</div>
</div>
<div class="form-group full-width">
<label>Nimi</label>
<input type="text" id="iredmail-user-cn" placeholder="Etunimi Sukunimi">
</div>
<div class="form-group full-width">
<label>Salasana *</label>
<input type="password" id="iredmail-user-password" placeholder="Vähintään 8 merkkiä">
</div>
<div class="form-group full-width">
<label>Kiintiö (MB, 0 = rajaton)</label>
<input type="number" id="iredmail-user-quota" value="1024" min="0">
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button class="btn-primary" id="btn-iredmail-user-save">Tallenna</button>
<button class="btn-secondary" onclick="document.getElementById('iredmail-user-modal').style.display='none'">Peruuta</button>
</div>
</div>
</div>
<div class="modal" id="iredmail-password-modal" style="display:none;">
<div class="modal-content" style="max-width:400px;">
<div class="modal-header">
<h3>Vaihda salasana</h3>
<button class="modal-close" onclick="document.getElementById('iredmail-password-modal').style.display='none'">&times;</button>
</div>
<p style="color:#666;font-size:0.9rem;margin-bottom:1rem;" id="iredmail-pw-email-label"></p>
<div class="form-grid" style="max-width:100%;">
<div class="form-group full-width">
<label>Uusi salasana *</label>
<input type="password" id="iredmail-pw-new" placeholder="Vähintään 8 merkkiä">
</div>
<div class="form-group full-width">
<label>Vahvista salasana *</label>
<input type="password" id="iredmail-pw-confirm" placeholder="Sama salasana uudelleen">
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button class="btn-primary" id="btn-iredmail-pw-save">Vaihda</button>
<button class="btn-secondary" onclick="document.getElementById('iredmail-password-modal').style.display='none'">Peruuta</button>
</div>
</div>
</div>
<div class="modal" id="iredmail-alias-modal" style="display:none;">
<div class="modal-content" style="max-width:450px;">
<div class="modal-header">
<h3>Lisää alias</h3>
<button class="modal-close" onclick="document.getElementById('iredmail-alias-modal').style.display='none'">&times;</button>
</div>
<div class="form-grid" style="max-width:100%;">
<div class="form-group full-width">
<label>Alias-osoite *</label>
<div style="display:flex;gap:0.3rem;align-items:center;">
<input type="text" id="iredmail-alias-local" placeholder="info" style="flex:1;">
<span>@</span>
<span id="iredmail-alias-domain-label" style="font-weight:600;"></span>
</div>
</div>
<div class="form-group full-width">
<label>Kohdeosoitteet (yksi per rivi)</label>
<textarea id="iredmail-alias-members" rows="4" placeholder="user1@example.com&#10;user2@example.com"></textarea>
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button class="btn-primary" id="btn-iredmail-alias-save">Tallenna</button>
<button class="btn-secondary" onclick="document.getElementById('iredmail-alias-modal').style.display='none'">Peruuta</button>
</div>
</div>
</div>
<!-- Tab: Asetukset (vain admin) -->
<div class="tab-content" id="tab-content-settings">
<div class="main-container">

327
script.js
View File

@@ -329,6 +329,10 @@ function switchToTab(target, subTab, extra) {
if (target === 'users') loadUsers();
if (target === 'settings') loadSettings();
if (target === 'companies') loadCompaniesTab();
if (target === 'hallinta') {
const hallintaSubMap = { email: 'hallinta-email' };
switchHallintaSubTab(hallintaSubMap[subTab] || 'hallinta-email');
}
}
document.querySelectorAll('.tab').forEach(tab => {
@@ -6739,6 +6743,9 @@ function applyModules(modules, hasIntegrations) {
}
}
});
// Hallinta-tabi: aina näkyvä superadmineille, ei moduuliriippuvuutta
const hallintaTab = document.getElementById('tab-hallinta');
if (hallintaTab) hallintaTab.style.display = isSuperAdmin ? '' : 'none';
// Jos aktiivinen tabi on piilotettu → vaihda ensimmäiseen näkyvään
const activeTab = document.querySelector('.tab.active');
if (activeTab && activeTab.style.display === 'none') {
@@ -6800,6 +6807,326 @@ async function loadBranding() {
}
}
// ==================== HALLINTA: IREDMAIL SÄHKÖPOSTI ====================
let iredmailCurrentDomain = '';
function switchHallintaSubTab(target) {
document.querySelectorAll('#hallinta-sub-tab-bar .sub-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('#tab-content-hallinta > .sub-tab-content').forEach(c => c.classList.remove('active'));
const btn = document.querySelector(`[data-hallinta-subtab="${target}"]`);
if (btn) btn.classList.add('active');
const content = document.getElementById('subtab-' + target);
if (content) content.classList.add('active');
if (target === 'hallinta-email') loadIRedMailDomains();
window.location.hash = 'hallinta/' + target.replace('hallinta-', '');
}
document.querySelectorAll('#hallinta-sub-tab-bar .sub-tab').forEach(btn => {
btn.addEventListener('click', () => switchHallintaSubTab(btn.dataset.hallintaSubtab));
});
// --- Domainit ---
async function loadIRedMailDomains() {
const tbody = document.getElementById('iredmail-domain-tbody');
const noData = document.getElementById('no-iredmail-domains');
const statusEl = document.getElementById('iredmail-status-text');
try {
const domains = await apiCall('iredmail_domains');
if (!domains || domains.length === 0) {
tbody.innerHTML = '';
noData.style.display = '';
statusEl.innerHTML = '⚠ Yhteys OK, mutta ei domaineja';
statusEl.parentElement.style.background = '#fff3cd';
return;
}
noData.style.display = 'none';
statusEl.innerHTML = '✓ Yhteys OK — ' + domains.length + ' domainia';
statusEl.parentElement.style.background = '#d4edda';
renderIRedMailDomains(domains);
} catch (e) {
tbody.innerHTML = '';
noData.style.display = '';
statusEl.innerHTML = '✗ ' + (e.message || 'Yhteysvirhe');
statusEl.parentElement.style.background = '#f8d7da';
}
}
function renderIRedMailDomains(domains) {
const tbody = document.getElementById('iredmail-domain-tbody');
tbody.innerHTML = domains.map(d => {
const domain = d.domain || d.primaryDomain || d.domainName || (typeof d === 'string' ? d : JSON.stringify(d));
const users = d.mailboxes || d.numberOfUsers || d.aliases_count || '';
const aliases = d.aliases || d.numberOfAliases || '';
const quota = d.maxQuotaSize || d.quota || '0';
return `<tr>
<td><a href="#" onclick="openIRedMailDomain('${esc(domain)}');return false;" style="font-weight:600;color:var(--primary-color);">${esc(domain)}</a></td>
<td>${esc(String(users))}</td>
<td>${esc(String(aliases))}</td>
<td>${esc(String(quota))}</td>
<td>
<button onclick="deleteIRedMailDomain('${esc(domain)}')" class="btn-danger" style="font-size:0.8rem;padding:3px 8px;">Poista</button>
</td>
</tr>`;
}).join('');
}
async function openIRedMailDomain(domain) {
iredmailCurrentDomain = domain;
document.getElementById('iredmail-domain-section').style.display = 'none';
document.getElementById('iredmail-users-section').style.display = '';
document.getElementById('iredmail-current-domain').textContent = domain;
document.getElementById('iredmail-user-domain-label').textContent = domain;
document.getElementById('iredmail-alias-domain-label').textContent = domain;
await Promise.all([loadIRedMailUsers(domain), loadIRedMailAliases(domain)]);
}
document.getElementById('iredmail-back-to-domains').addEventListener('click', (e) => {
e.preventDefault();
iredmailCurrentDomain = '';
document.getElementById('iredmail-users-section').style.display = 'none';
document.getElementById('iredmail-domain-section').style.display = '';
});
async function deleteIRedMailDomain(domain) {
if (!confirm('Poistetaanko domain ' + domain + ' ja KAIKKI sen tilit? Tätä ei voi perua!')) return;
try {
await apiCall('iredmail_domain_delete', 'POST', { domain });
await loadIRedMailDomains();
} catch (e) { alert(e.message); }
}
// Lisää domain
document.getElementById('btn-iredmail-add-domain').addEventListener('click', () => {
document.getElementById('iredmail-domain-name').value = '';
document.getElementById('iredmail-domain-cn').value = '';
document.getElementById('iredmail-domain-quota').value = '0';
document.getElementById('iredmail-domain-modal').style.display = 'flex';
});
document.getElementById('btn-iredmail-domain-save').addEventListener('click', async () => {
const domain = document.getElementById('iredmail-domain-name').value.trim();
if (!domain) { alert('Domain puuttuu'); return; }
try {
await apiCall('iredmail_domain_create', 'POST', {
domain,
cn: document.getElementById('iredmail-domain-cn').value.trim(),
quota: parseInt(document.getElementById('iredmail-domain-quota').value) || 0,
});
document.getElementById('iredmail-domain-modal').style.display = 'none';
await loadIRedMailDomains();
} catch (e) { alert(e.message); }
});
// --- Käyttäjät ---
async function loadIRedMailUsers(domain) {
const tbody = document.getElementById('iredmail-user-tbody');
try {
const users = await apiCall('iredmail_users&domain=' + encodeURIComponent(domain));
renderIRedMailUsers(users || []);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="5" style="color:#e74c3c;text-align:center;">${esc(e.message)}</td></tr>`;
}
}
function renderIRedMailUsers(users) {
const tbody = document.getElementById('iredmail-user-tbody');
if (users.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#aaa;">Ei tilejä</td></tr>';
return;
}
tbody.innerHTML = users.map(u => {
const email = u.mail || u.email || u.username || '';
const name = u.cn || u.name || u.displayName || '';
const quota = u.mailQuota || u.quota || '0';
const status = u.accountStatus === 'disabled' ? '<span style="color:#e74c3c;">Ei käytössä</span>' : '<span style="color:#27ae60;">Aktiivinen</span>';
return `<tr data-email="${esc(email.toLowerCase())}">
<td style="font-weight:500;">${esc(email)}</td>
<td>${esc(name)}</td>
<td>${esc(String(quota))}</td>
<td>${status}</td>
<td>
<button onclick="showIRedMailPasswordModal('${esc(email)}')" class="btn-secondary" style="font-size:0.75rem;padding:2px 6px;">Salasana</button>
<button onclick="deleteIRedMailUser('${esc(email)}')" class="btn-danger" style="font-size:0.75rem;padding:2px 6px;">Poista</button>
</td>
</tr>`;
}).join('');
}
// Haku
document.getElementById('iredmail-user-search').addEventListener('input', function() {
const q = this.value.toLowerCase();
document.querySelectorAll('#iredmail-user-tbody tr[data-email]').forEach(row => {
const email = row.dataset.email || '';
const name = row.children[1]?.textContent?.toLowerCase() || '';
row.style.display = (email.includes(q) || name.includes(q)) ? '' : 'none';
});
});
// Lisää tili
document.getElementById('btn-iredmail-add-user').addEventListener('click', () => {
document.getElementById('iredmail-user-modal-title').textContent = 'Lisää tili';
document.getElementById('iredmail-user-local').value = '';
document.getElementById('iredmail-user-cn').value = '';
document.getElementById('iredmail-user-password').value = '';
document.getElementById('iredmail-user-quota').value = '1024';
document.getElementById('iredmail-user-modal').style.display = 'flex';
});
document.getElementById('btn-iredmail-user-save').addEventListener('click', async () => {
const local = document.getElementById('iredmail-user-local').value.trim();
const password = document.getElementById('iredmail-user-password').value;
if (!local) { alert('Käyttäjänimi puuttuu'); return; }
if (!password || password.length < 8) { alert('Salasana vähintään 8 merkkiä'); return; }
const email = local + '@' + iredmailCurrentDomain;
try {
await apiCall('iredmail_user_create', 'POST', {
email,
password,
cn: document.getElementById('iredmail-user-cn').value.trim(),
mailQuota: parseInt(document.getElementById('iredmail-user-quota').value) || 0,
});
document.getElementById('iredmail-user-modal').style.display = 'none';
await loadIRedMailUsers(iredmailCurrentDomain);
} catch (e) { alert(e.message); }
});
async function deleteIRedMailUser(email) {
if (!confirm('Poistetaanko tili ' + email + '? Kaikki viestit menetetään!')) return;
try {
await apiCall('iredmail_user_delete', 'POST', { email });
await loadIRedMailUsers(iredmailCurrentDomain);
} catch (e) { alert(e.message); }
}
// Salasanan vaihto
function showIRedMailPasswordModal(email) {
document.getElementById('iredmail-pw-email-label').textContent = email;
document.getElementById('iredmail-pw-new').value = '';
document.getElementById('iredmail-pw-confirm').value = '';
document.getElementById('iredmail-password-modal').style.display = 'flex';
document.getElementById('iredmail-password-modal').dataset.email = email;
}
document.getElementById('btn-iredmail-pw-save').addEventListener('click', async () => {
const pw1 = document.getElementById('iredmail-pw-new').value;
const pw2 = document.getElementById('iredmail-pw-confirm').value;
if (!pw1 || pw1.length < 8) { alert('Salasana vähintään 8 merkkiä'); return; }
if (pw1 !== pw2) { alert('Salasanat eivät täsmää'); return; }
const email = document.getElementById('iredmail-password-modal').dataset.email;
try {
await apiCall('iredmail_user_update', 'POST', { email, password: pw1 });
document.getElementById('iredmail-password-modal').style.display = 'none';
alert('Salasana vaihdettu!');
} catch (e) { alert(e.message); }
});
// --- Aliakset ---
async function loadIRedMailAliases(domain) {
const tbody = document.getElementById('iredmail-alias-tbody');
try {
const aliases = await apiCall('iredmail_aliases&domain=' + encodeURIComponent(domain));
renderIRedMailAliases(aliases || []);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="3" style="color:#e74c3c;text-align:center;">${esc(e.message)}</td></tr>`;
}
}
function renderIRedMailAliases(aliases) {
const tbody = document.getElementById('iredmail-alias-tbody');
if (aliases.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#aaa;">Ei aliaksia</td></tr>';
return;
}
tbody.innerHTML = aliases.map(a => {
const alias = a.mail || a.address || a.alias || '';
const members = a.members || a.goto || a.accessPolicy || '';
const membersStr = Array.isArray(members) ? members.join(', ') : String(members);
return `<tr>
<td style="font-weight:500;">${esc(alias)}</td>
<td style="font-size:0.85rem;color:#666;">${esc(membersStr)}</td>
<td>
<button onclick="deleteIRedMailAlias('${esc(alias)}')" class="btn-danger" style="font-size:0.75rem;padding:2px 6px;">Poista</button>
</td>
</tr>`;
}).join('');
}
document.getElementById('btn-iredmail-add-alias').addEventListener('click', () => {
document.getElementById('iredmail-alias-local').value = '';
document.getElementById('iredmail-alias-members').value = '';
document.getElementById('iredmail-alias-modal').style.display = 'flex';
});
document.getElementById('btn-iredmail-alias-save').addEventListener('click', async () => {
const local = document.getElementById('iredmail-alias-local').value.trim();
if (!local) { alert('Alias puuttuu'); return; }
const alias = local + '@' + iredmailCurrentDomain;
const members = document.getElementById('iredmail-alias-members').value.trim();
try {
await apiCall('iredmail_alias_create', 'POST', {
alias,
cn: alias,
members: members,
});
document.getElementById('iredmail-alias-modal').style.display = 'none';
await loadIRedMailAliases(iredmailCurrentDomain);
} catch (e) { alert(e.message); }
});
async function deleteIRedMailAlias(alias) {
if (!confirm('Poistetaanko alias ' + alias + '?')) return;
try {
await apiCall('iredmail_alias_delete', 'POST', { alias });
await loadIRedMailAliases(iredmailCurrentDomain);
} 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)
loadBranding().then(async () => {
await checkAuth();

View File

@@ -2431,3 +2431,8 @@ span.empty {
.integration-config-card {
border-left: 3px solid var(--primary-color);
}
/* iRedMail */
#iredmail-status { border-radius: 8px; transition: background 0.3s; }
.btn-danger { background: #e74c3c; color: #fff; border: none; border-radius: 4px; cursor: pointer; padding: 4px 10px; font-size: 0.82rem; }
.btn-danger:hover { background: #c0392b; }