Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/actions/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
URLParams,
DRAWER_DOMAINS_REVEICED,
} from './types';
import { domains, addDomain, editDomain, deleteDomain, domain, defaultSyncPolicy, dns, drawerDomains, disabledPlugins, editDomainPlugins, dkimKeygen } from '../api';
import { domains, addDomain, editDomain, deleteDomain, domain, defaultSyncPolicy, dns, drawerDomains, disabledPlugins, editDomainPlugins, dkimKeygen, editSmtpGateway, smtpGateway } from '../api';
import { defaultDeleteHandler, defaultDetailsHandler, defaultListHandler2, defaultPatchHandler, defaultPostHandler } from './handlers';
import { Dispatch } from 'redux';
import { CreateDomainParams, DeleteDomainProps, NewDomain, UpdateDomain } from '@/types/domains';
import { CreateDomainParams, DeleteDomainProps, NewDomain, SmtpGatewayData, UpdateDomain } from '@/types/domains';
import { ApiError } from '@/types/common';


Expand Down Expand Up @@ -65,6 +65,14 @@ export function editDomainPluginData(domainID: number, pluginList: string[]) {
return defaultPatchHandler(editDomainPlugins, domainID, pluginList);
}

export function fetchDomainSmtpGateway(id: number) {
return defaultDetailsHandler(smtpGateway, id);
}

export function editDomainSmtpGateway(domainID: number, gateway: SmtpGatewayData) {
return defaultPatchHandler(editSmtpGateway, domainID, gateway);
}

export function deleteDomainData(id: number, params: DeleteDomainProps) {
return defaultDeleteHandler(deleteDomain, DOMAIN_DATA_DELETE, {id, params});
}
Expand Down
17 changes: 16 additions & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: 2020-2026 grommunio GmbH
import { URLParams } from './actions/types';
import store from './store';
import { NewDomain, UpdateDomain } from './types/domains';
import { NewDomain, SmtpGatewayData, UpdateDomain } from './types/domains';
import { NewFolder, UpdateFolder } from './types/folders';
import { NewGroup, UpdateGroup } from './types/groups';
import { CheckLdapUsersParams, FetchLdapParams, ImportLdapParams, LdapConfigData, LdapDumpParams, SyncLdapParams } from './types/ldap';
Expand Down Expand Up @@ -279,6 +279,21 @@ export function editDomainPlugins(id: number, pluginList: string[]) {
}
}

export function smtpGateway(id: number) {
return async () => {
const resp = await fetch(`${baseUrl}/domains/${id}/smtpGateway`);
// 404 means no gateway has been configured for this domain yet
if(resp.status === 404) return { data: null };
return await handleErrors(resp);
}
}

export function editSmtpGateway(id: number, gateway: SmtpGatewayData) {
return async () => {
return await put(`/domains/${id}/smtpGateway`, gateway)
}
}

export async function changeDomainPassword(id: number, newPw: string) {
try {
return await put('/system/domains/' + id + '/password', { new: newPw });
Expand Down
117 changes: 110 additions & 7 deletions src/containers/DomainDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
Grid2,
TextField,
FormControl,
InputLabel,
MenuItem,
Select,
Button,
Tabs,
Tab,
Expand All @@ -23,7 +25,7 @@ import {
ListItemIcon,
ListItemText,
} from '@mui/material';
import { editDomainData, editDomainPluginData, fetchDomainDetails, fetchDomainPlugins } from '../actions/domains';
import { editDomainData, editDomainPluginData, editDomainSmtpGateway, fetchDomainDetails, fetchDomainPlugins, fetchDomainSmtpGateway } from '../actions/domains';
import { getStringAfterLastSlash, getPolicyDiff } from '../utils';
import { fetchOrgsData } from '../actions/orgs';
import SyncPolicies from '../components/SyncPolicies';
Expand All @@ -32,13 +34,13 @@ import { CapabilityContext } from '../CapabilityContext';
import ViewWrapper from '../components/ViewWrapper';
import { fetchServersData } from '../actions/servers';
import MagnitudeAutocomplete from '../components/MagnitudeAutocomplete';
import { AppSettingsAlt, Dns, Extension } from '@mui/icons-material';
import { AppSettingsAlt, Dns, Extension, Send } from '@mui/icons-material';
import { useNavigate } from 'react-router';
import { useAppDispatch, useAppSelector } from '../store';
import { ChangeEvent } from '@/types/common';
import { Org } from '@/types/orgs';
import { Server } from '@/types/servers';
import { DOMAIN_STATUS, UpdateDomain } from '../types/domains';
import { DOMAIN_STATUS, SmtpGatewayData, UpdateDomain } from '../types/domains';
import { SyncPolicy } from '@/types/sync';


Expand Down Expand Up @@ -66,6 +68,17 @@ const useStyles = makeStyles()((theme: Theme) => ({
}
}));

const EMPTY_SMTP_GATEWAY: SmtpGatewayData = {
host: '',
port: 25,
encryption: 'none',
username: '',
password: '',
passwordSet: false,
enabled: true,
description: '',
};

type DomainDetailsState = {
ID: number;
domainname: string;
Expand Down Expand Up @@ -115,6 +128,7 @@ const DomainDetails = () => {
unsaved: false,
});
const [disabledPlugins, setDisabledPlugins] = useState<string[]>([]);
const [smtpGateway, setSmtpGateway] = useState<SmtpGatewayData>(EMPTY_SMTP_GATEWAY);
const [loading, setLoading] = useState(true);
const [snackbar, setSnackbar] = useState("");
const context = useContext(CapabilityContext);
Expand All @@ -129,6 +143,10 @@ const DomainDetails = () => {
const fetchServers = async () =>
await dispatch(fetchServersData({ sort: 'hostname,asc', limit: 1000000, level: 0 }));

const putSmtpGateway = async (domainID: number, gateway: SmtpGatewayData) =>
await dispatch(editDomainSmtpGateway(domainID, gateway));
const fetchSmtpGateway = async (domainID: number) => await dispatch(fetchDomainSmtpGateway(domainID));

useEffect(() => {
(async () => {
if(capabilities.includes(SYSTEM_ADMIN_READ)) {
Expand Down Expand Up @@ -164,11 +182,17 @@ const DomainDetails = () => {

const handleInput = (field: string) => (event: ChangeEvent) => {
setState({
...state,
...state,
[field]: event.target.value,
});
}

const updateSmtp = <K extends keyof SmtpGatewayData>(field: K, value: SmtpGatewayData[K]) =>
setSmtpGateway(gw => ({
...gw,
[field]: value,
}));

const handleCheckbox = (field: string) => (event: ChangeEvent) => setState({
...state,
[field]: event.target.checked,
Expand All @@ -179,15 +203,23 @@ const DomainDetails = () => {
const { ID, domainname, domainStatus, org, chat, homeserver,
maxUser, title, address, adminName, tel, defaultPolicy, syncPolicy } = state;

// Save plugins
if(tab === 2) {
putPlugins(ID)
.then(() => setSnackbar('Success!'))
.catch(message => setSnackbar(message || 'Unknown error'));
return;
}

// Save domain
if(tab === 3) {
const payload: any = { ...smtpGateway };
if(!smtpGateway.password)
delete payload.password;
putSmtpGateway(ID, payload)
.then(() => setSnackbar('Success!'))
.catch(message => setSnackbar(message || 'Unknown error'));
return;
}

edit({
ID,
domainname,
Expand All @@ -213,7 +245,6 @@ const DomainDetails = () => {
const handleTab = (_: unknown, tab: number) => {
setState({ ...state, tab });

// Plugins tab
if(tab === 2) {
(async () => {
setLoading(true);
Expand All @@ -225,6 +256,16 @@ const DomainDetails = () => {
setLoading(false);
})();
}

if(tab === 3) {
(async () => {
setLoading(true);
const gw = await fetchSmtpGateway(parseInt(getStringAfterLastSlash()))
.catch(message => setSnackbar(message || 'Unknown error'));
setSmtpGateway(gw?.data ? { ...EMPTY_SMTP_GATEWAY, ...gw.data } : EMPTY_SMTP_GATEWAY);
setLoading(false);
})();
}
}

const handleSyncChange = (field: string) => (event: ChangeEvent) => {
Expand Down Expand Up @@ -327,7 +368,69 @@ const DomainDetails = () => {
<Tab label={t("Domain")} sx={{ minHeight: 48 }} iconPosition='start' icon={<Dns />}/>
<Tab label={t("Sync policy")} sx={{ minHeight: 48 }} iconPosition='start' icon={<AppSettingsAlt />}/>
<Tab label={t("Disabled plugins")} sx={{ minHeight: 48 }} iconPosition='start' icon={<Extension />}/>
<Tab label={t("SMTP gateway")} sx={{ minHeight: 48 }} iconPosition='start' icon={<Send />}/>
</Tabs>
{tab === 3 && <div className={classes.form}>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 8 }}>
{t("Route outgoing mail from this domain through a specific SMTP server. "
+ "Leave empty to use the global default.")}
</Typography>
<TextField
fullWidth
className={classes.input}
label={t("Host")}
value={smtpGateway.host}
onChange={e => updateSmtp('host', e.target.value)}
placeholder="smtp.example.com"
required
/>
<TextField
fullWidth
type="number"
className={classes.input}
label={t("Port")}
value={smtpGateway.port}
onChange={e => updateSmtp('port', Number(e.target.value) || 25)}
/>
<FormControl fullWidth className={classes.input}>
<InputLabel id="smtp-encryption-label">{t("Encryption")}</InputLabel>
<Select
labelId="smtp-encryption-label"
label={t("Encryption")}
value={smtpGateway.encryption}
onChange={e => updateSmtp('encryption', e.target.value as SmtpGatewayData['encryption'])}
>
<MenuItem value="none">{t("None (plain SMTP)")}</MenuItem>
<MenuItem value="starttls">{t("STARTTLS (verify certificate)")}</MenuItem>
<MenuItem value="starttls_unverified">{t("STARTTLS (do not verify certificate)")}</MenuItem>
<MenuItem value="tls">{t("TLS (implicit, port 465)")}</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
className={classes.input}
label={t("Username")}
value={smtpGateway.username}
onChange={e => updateSmtp('username', e.target.value)}
autoComplete="off"
/>
<TextField
fullWidth
type="password"
className={classes.input}
label={t("Password") + (smtpGateway.passwordSet ? ` (${t("set — leave empty to keep")})` : '')}
value={smtpGateway.password}
onChange={e => updateSmtp('password', e.target.value)}
autoComplete="new-password"
/>
<TextField
fullWidth
className={classes.input}
label={t("Description")}
value={smtpGateway.description}
onChange={e => updateSmtp('description', e.target.value)}
/>
</div>}
{tab === 0 && <FormControl className={classes.form}>
<Grid2 container className={classes.input}>
<TextField
Expand Down
11 changes: 11 additions & 0 deletions src/types/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,15 @@ export type DeleteDomainProps = {

export type CreateDomainParams = {
createRole?: boolean;
}

export type SmtpGatewayData = {
host: string;
port: number;
encryption: 'none' | 'starttls' | 'starttls_unverified' | 'tls';
username: string;
password: string;
passwordSet: boolean;
enabled: boolean;
description: string;
}