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']);