import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Mail, Users, Wifi, Server, Copy, CheckCheck, Settings as SettingsIcon, CheckCircle, XCircle } from 'lucide-react';
import { emailAPI, cellAPI, peerAPI } from '../../services/api';
import { useConfig } from '../../contexts/ConfigContext';
import { useDraftConfig } from '../../contexts/DraftConfigContext';
import { useAuth } from '../../contexts/AuthContext';
import { Field, TextInput, NumberInput } from '../../components/FormFields';
import { detectPortConflicts, validateEmailConfig, PORT_CONFLICT_FIELDS } from '../../utils/serviceConfig';
const EMAIL_DEFAULTS = { domain: '', smtp_port: 25, submission_port: 587, imap_port: 993, webmail_port: 8888 };
function CopyButton({ text }) {
const [copied, setCopied] = useState(false);
return (
);
}
function InfoRow({ label, value }) {
return (
);
}
function AdminConfigSection({ emailCfg, onChange, errors, portConflicts, saving }) {
const conflictFor = (f) => portConflicts[`email|${f}`];
return (
Service Configuration
{saving && Saving…}
onChange({ ...emailCfg, domain: v })} placeholder="mail.example.com" />
onChange({ ...emailCfg, smtp_port: v })} min={1} max={65535} />
onChange({ ...emailCfg, submission_port: v })} min={1} max={65535} />
onChange({ ...emailCfg, imap_port: v })} min={1} max={65535} />
onChange({ ...emailCfg, webmail_port: v })} min={1} max={65535} />
);
}
function Toast({ msg, type }) {
if (!msg) return null;
return (
{type === 'error' ? : }
{msg}
);
}
export default function EmailPage() {
const { user } = useAuth();
const isAdmin = user?.role === 'admin';
const { domain = 'cell', effective_domain, domain_mode = 'lan', service_ips = {}, service_configs = {}, refresh: refreshConfig } = useConfig();
const draftConfig = useDraftConfig();
const svcDomain = (domain_mode !== 'lan' && effective_domain) ? effective_domain : domain;
const proto = domain_mode === 'lan' ? 'http' : 'https';
const cellHost = `mail.${svcDomain}`;
const mailIp = service_ips.vip_mail || '172.20.0.23';
const dnsIp = service_ips.dns || '172.20.0.3';
const emailCfgServer = service_configs.email || {};
const imapPort = emailCfgServer.imap_port ?? 993;
const smtpPort = emailCfgServer.smtp_port ?? 25;
const webmailPort = emailCfgServer.webmail_port ?? 8888;
// Admin state
const [emailCfg, setEmailCfg] = useState({ ...EMAIL_DEFAULTS });
const [dirty, setDirty] = useState(false);
const [saving, setSaving] = useState(false);
const [users, setUsers] = useState([]);
const [status, setStatus] = useState(null);
const [toast, setToast] = useState(null);
// Peer state
const [peerData, setPeerData] = useState(null);
useEffect(() => {
if (service_configs.email) setEmailCfg({ ...EMAIL_DEFAULTS, ...service_configs.email });
}, [service_configs.email]);
useEffect(() => {
if (!isAdmin) {
peerAPI.services().then(r => setPeerData(r.data)).catch(() => {});
return;
}
emailAPI.getUsers().then(r => setUsers(r.data)).catch(() => {});
emailAPI.getStatus().then(r => setStatus(r.data)).catch(() => {});
}, [isAdmin]);
const showToast = (msg, type = 'success') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 3000);
};
const errors = useMemo(() => validateEmailConfig(emailCfg), [emailCfg]);
const portConflicts = useMemo(
() => detectPortConflicts({ ...service_configs, email: emailCfg }),
[emailCfg, service_configs]
);
const hasErrors = useMemo(
() => Object.keys(errors).length > 0 ||
PORT_CONFLICT_FIELDS.email.some(f => portConflicts[`email|${f}`]),
[errors, portConflicts]
);
// Refs so flusher closure always sees current values after navigation
const emailCfgRef = useRef(emailCfg);
useEffect(() => { emailCfgRef.current = emailCfg; }, [emailCfg]);
const dirtyRef = useRef(dirty);
useEffect(() => { dirtyRef.current = dirty; }, [dirty]);
const hasErrorsRef = useRef(hasErrors);
useEffect(() => { hasErrorsRef.current = hasErrors; }, [hasErrors]);
const save = useCallback(async () => {
if (!dirtyRef.current || hasErrorsRef.current) return;
setSaving(true);
try {
await cellAPI.updateConfig({ email: emailCfgRef.current });
setDirty(false);
draftConfig?.setDirty('email', false);
refreshConfig();
} catch (err) {
showToast(err?.response?.data?.error || 'Failed to save email config', 'error');
} finally {
setSaving(false);
}
}, [draftConfig, refreshConfig]);
const saveRef = useRef(save);
useEffect(() => { saveRef.current = save; }, [save]);
// Register flusher without cleanup so it persists when user navigates away mid-edit
useEffect(() => {
if (!draftConfig) return;
draftConfig.registerFlusher('email', () => saveRef.current());
}, [draftConfig]); // eslint-disable-line react-hooks/exhaustive-deps
// Debounced auto-save
useEffect(() => {
if (!dirty || hasErrors) return;
const t = setTimeout(() => saveRef.current(), 800);
return () => clearTimeout(t);
}, [emailCfg, dirty, hasErrors]); // eslint-disable-line react-hooks/exhaustive-deps
const handleChange = (cfg) => {
setEmailCfg(cfg);
setDirty(true);
draftConfig?.setDirty('email', true);
};
return (
Email Services
Postfix (SMTP) + Dovecot (IMAP)
{/* IMAP */}
{/* SMTP */}
{/* Webmail */}
Webmail
Requires VPN + DNS set to {dnsIp}.
{/* Status — admin only */}
{isAdmin && (
Service Status
{status ? (
Postfix (SMTP):
Running
Dovecot (IMAP):
Running
) : (
Status unavailable
)}
)}
{/* Peer credentials */}
{!isAdmin && peerData?.email && (
Your Account
Authenticate with your dashboard username and password.
)}
{/* Admin users list */}
{isAdmin && users.length > 0 && (
Email Accounts
{users.map((u, i) => (
{u.username}
{u.domain}
))}
)}
{/* Admin config form */}
{isAdmin && (
)}
);
}