import { useCallback, useEffect, useState } from 'react' import { api } from '../../../api/client.js' import { useSite } from '../../../contexts/SiteContext.jsx' // Email delivery panel (Gmail over OAuth2), rendered as a section on the Settings // page. Sending is authorized by an in-app "Connect Gmail" consent flow that // captures a refresh token server-side — the token is write-only over the API // (stored encrypted, never returned). Reuses the Google SSO OAuth client, so it // requires the Google provider to be configured on the Authentication page first. const STATUS_COLOR = { connected: '#7fd0a4', error: '#d98b84', unconfigured: 'var(--muted)', } // Human-friendly text for the ?email_error= the callback may redirect with. const ERROR_TEXT = { denied: 'Google sign-in was cancelled or denied.', bad_state: 'The connect session expired. Please try again.', no_client: 'The Google OAuth client is not configured.', no_refresh_token: 'Google did not return a refresh token. Remove this app under your Google Account → Security → Third-party access, then reconnect.', no_email: 'Could not read the Gmail address from Google.', error: 'Could not connect the Gmail account. Please try again.', } function StatusPanel({ config }) { const color = STATUS_COLOR[config.status] || 'var(--muted)' return (
{config.status || 'unconfigured'}
{config.senderEmail && (

Sending as {config.senderEmail}

)} {config.statusDetail && (

{config.statusDetail}

)} {config.lastVerifiedAt && (

Last verified: {new Date(config.lastVerifiedAt).toLocaleString()}

)}
) } export default function EmailDelivery() { const { siteTitle } = useSite() const [config, setConfig] = useState(null) const [error, setError] = useState('') const [senderName, setSenderName] = useState('') const [enabled, setEnabled] = useState(false) const [busy, setBusy] = useState('') const [msg, setMsg] = useState('') const [actionError, setActionError] = useState('') const [banner, setBanner] = useState(null) // fields kind ('ok' or 'err') and text const load = useCallback(async (seedForm = false) => { try { const c = await api.admin.getEmailConfig() setConfig(c) if (seedForm) { setSenderName(c.senderName || '') setEnabled(c.enabled) } return c } catch { setError('Could not load email settings.') return null } }, []) // On mount, surface the outcome of a just-completed connect redirect, strip the // query params so a refresh doesn't replay the banner, then load config. useEffect(() => { const params = new URLSearchParams(window.location.search) if (params.has('email_connected')) { setBanner({ kind: 'ok', text: 'Gmail account connected.' }) } else if (params.has('email_error')) { setBanner({ kind: 'err', text: ERROR_TEXT[params.get('email_error')] || 'Could not connect email.' }) } if (params.has('email_connected') || params.has('email_error')) { params.delete('email_connected') params.delete('email_error') const qs = params.toString() window.history.replaceState({}, '', window.location.pathname + (qs ? `?${qs}` : '')) } load(true) }, [load]) async function connect() { setBusy('connect') setActionError('') try { const { url } = await api.admin.emailConnectUrl() window.location.href = url } catch (err) { setActionError(err.message || 'Could not start the connect flow.') setBusy('') } } async function save() { setBusy('save') setMsg('') setActionError('') try { const saved = await api.admin.saveEmailConfig({ senderName, enabled }) setConfig(saved) setMsg('Saved.') } catch (err) { setActionError(err.message || 'Could not save.') } finally { setBusy('') } } async function sendTest() { setBusy('test') setMsg('') setActionError('') try { const r = await api.admin.testEmail() setMsg(`Test email sent to ${r.to}.`) await load() } catch (err) { setActionError(err.message || 'Could not send the test email.') } finally { setBusy('') } } async function disconnect() { setBusy('disconnect') setMsg('') setActionError('') try { const c = await api.admin.disconnectEmail() setConfig(c) setEnabled(false) setMsg('Disconnected.') } catch (err) { setActionError(err.message || 'Could not disconnect.') } finally { setBusy('') } } if (error) return

{error}

if (!config) return null const connected = config.hasRefreshToken return (

Email delivery

Sends the contact form through Gmail over OAuth2, delivered to the Contact email above. Reuses the Google authentication client — configure that on the Authentication page first.

{banner && (
{banner.text}
)} {!config.googleConfigured && (

The Google authentication provider needs a client ID and secret before you can connect a Gmail account.

)} {!connected ? (
) : ( <>
)}
{msg && {msg}} {actionError && {actionError}}
) }