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:
343
api.php
343
api.php
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user