diff --git a/backend/server.js b/backend/server.js index 665d328..c4c7ef8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -34,6 +34,7 @@ const client = new DynamoDBClient({ }); const docClient = DynamoDBDocumentClient.from(client); const TABLE_NAME = process.env.DYNAMODB_TABLE || 'email-rules'; +const TABLE_BLOCKED_NAME = process.env.DYNAMODB_TABLE_BLOCKED || 'email-blocked-senders'; // Validation const handleValidationErrors = (req, res, next) => { @@ -101,4 +102,44 @@ app.post('/api/rules', [ } }); +// 5. Blocked: Get One +app.get('/api/blocked/:email', [ + param('email').isEmail() +], handleValidationErrors, async (req, res) => { + try { + const response = await docClient.send(new GetCommand({ + TableName: TABLE_BLOCKED_NAME, + Key: { email_address: decodeURIComponent(req.params.email) } + })); + // Wenn kein Eintrag existiert, geben wir ein leeres Standard-Objekt zurück + if (!response.Item) { + return res.json({ + email_address: decodeURIComponent(req.params.email), + blocked_patterns: [] + }); + } + res.json(response.Item); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 6. Blocked: Save/Update +app.post('/api/blocked', [ + body('email_address').isEmail(), + body('blocked_patterns').isArray() +], handleValidationErrors, async (req, res) => { + try { + const item = { + email_address: req.body.email_address, + blocked_patterns: req.body.blocked_patterns, + last_updated: new Date().toISOString(), + }; + await docClient.send(new PutCommand({ TableName: TABLE_BLOCKED_NAME, Item: item })); + res.json({ success: true, rule: item }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + app.listen(PORT, () => console.log(`🚀 API active on port ${PORT}`)); \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b53dab8..7af7bd4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import { FiSearch, FiRefreshCw, FiTrash2, FiList } from 'react-icons/fi'; import Header from './components/Header'; import OutOfOffice from './components/OutOfOffice'; import Forwarding from './components/Forwarding'; +import BlockedSenders from './components/BlockedSenders'; // Import der neuen Komponente import Toast from './components/Toast'; import { emailRulesAPI } from './services/api'; @@ -10,11 +11,13 @@ function App() { const [email, setEmail] = useState(''); const [emailError, setEmailError] = useState(''); const [currentRule, setCurrentRule] = useState(null); + const [blockedRule, setBlockedRule] = useState(null); // Neuer State für Blocked Senders const [activeTab, setActiveTab] = useState('ooo'); const [isLoading, setIsLoading] = useState(false); const [allRules, setAllRules] = useState([]); const [showAllRules, setShowAllRules] = useState(false); const [toast, setToast] = useState(null); + const [isAuthenticating, setIsAuthenticating] = useState(() => { const params = new URLSearchParams(window.location.search); return !!(params.get('email') && params.get('expires') && params.get('signature')); @@ -25,7 +28,59 @@ function App() { setToast({ message, type }); }; - // Check for authentication token in URL params on mount + // Hilfsfunktion: Lädt alle Daten (Regeln + Blocked) parallel + const loadAllData = async (emailAddress, isAuthFlow = false) => { + setIsLoading(true); + try { + // Parallel abrufen: Hauptregel und Blocked Senders + const [mainRule, blockedData] = await Promise.all([ + // 1. Hauptregel abrufen + emailRulesAPI.getRule(emailAddress).catch(err => { + // 404 bei Hauptregel abfangen -> Default Objekt zurückgeben + if (err.statusCode === 404 || err.message.includes('not found') || err.message.includes('No rule exists')) { + return { + email_address: emailAddress, + ooo_active: false, + ooo_message: '', + ooo_content_type: 'text', + forwards: [], + _isNew: true // Interner Marker für neue Regel + }; + } + throw err; + }), + // 2. Blocked Senders abrufen + emailRulesAPI.getBlockedSenders(emailAddress).catch(() => { + // Fehler bei Blocked ignorieren (oder 404) -> Leeres Standard-Objekt + return { email_address: emailAddress, blocked_patterns: [] }; + }) + ]); + + setCurrentRule(mainRule); + setBlockedRule(blockedData); + + // Toast Nachrichten steuern + if (isAuthFlow) { + if (mainRule._isNew) { + showToast('Welcome! You can now create email rules for your account.', 'success'); + } else { + showToast('Authenticated successfully from Roundcube', 'success'); + } + } else { + if (mainRule._isNew) { + showToast('No existing rule found. You can create a new one.', 'warning'); + } else { + showToast('Email configuration loaded successfully', 'success'); + } + } + } catch (error) { + showToast('Failed to load configuration: ' + error.message, 'error'); + } finally { + setIsLoading(false); + } + }; + + // Auth-Check beim Start useEffect(() => { const checkAuthToken = async () => { const params = new URLSearchParams(window.location.search); @@ -35,9 +90,9 @@ function App() { if (emailParam && expiresParam && signatureParam) { setIsAuthenticating(true); - setSingleEmailMode(true); // Enable single email mode + setSingleEmailMode(true); try { - // Validate token + // Token validieren const result = await emailRulesAPI.validateToken( emailParam, expiresParam, @@ -45,32 +100,15 @@ function App() { ); if (result.success) { - // Token valid - auto-fill email and load rule setEmail(emailParam); - - // Clean URL (remove token parameters) + // URL bereinigen window.history.replaceState({}, document.title, window.location.pathname); - - // Auto-load the rule - const rule = await emailRulesAPI.getRule(emailParam); - setCurrentRule(rule); - showToast('Authenticated successfully from Roundcube', 'success'); + + // Alles laden (mit Flag isAuthFlow=true) + await loadAllData(emailParam, true); } } catch (error) { - if (error.statusCode === 404 || error.message.includes('not found') || error.message.includes('No rule exists')) { - // No rule exists yet - create empty template - setEmail(emailParam); - setCurrentRule({ - email_address: emailParam, - ooo_active: false, - ooo_message: '', - ooo_content_type: 'text', - forwards: [], - }); - showToast('Welcome! You can now create email rules for your account.', 'success'); - } else { showToast('Authentication failed: ' + error.message, 'error'); - } } finally { setIsAuthenticating(false); } @@ -85,9 +123,9 @@ function App() { return regex.test(email); }; + // Suche auslösen const handleSearch = async () => { const trimmedEmail = email.trim(); - if (!trimmedEmail) { setEmailError('Email address is required'); return; @@ -98,41 +136,22 @@ function App() { return; } - setIsLoading(true); setEmailError(''); - - try { - const rule = await emailRulesAPI.getRule(trimmedEmail); - setCurrentRule(rule); - showToast('Email rule loaded successfully', 'success'); - } catch (error) { - if (error.statusCode === 404 || error.message.includes('not found') || error.message.includes('No rule exists')) { - setCurrentRule({ - email_address: trimmedEmail, - ooo_active: false, - ooo_message: '', - ooo_content_type: 'text', - forwards: [], - }); - showToast('No existing rule found. You can create a new one.', 'warning'); - } else { - showToast('Failed to fetch email rule: ' + error.message, 'error'); - } - } finally { - setIsLoading(false); - } + // Alles laden (isAuthFlow=false) + await loadAllData(trimmedEmail, false); }; + // Update für Hauptregeln (OOO & Forwarding) const handleUpdate = async (updates) => { if (!currentRule) return; - try { + // _isNew entfernen, falls vorhanden + const { _isNew, ...restRule } = currentRule; const updatedData = { email_address: currentRule.email_address, - ...currentRule, + ...restRule, ...updates, }; - await emailRulesAPI.createOrUpdateRule(updatedData); setCurrentRule(updatedData); showToast('Email rule updated successfully', 'success'); @@ -141,6 +160,17 @@ function App() { } }; + // NEU: Update für Blocked Senders + const handleBlockedUpdate = async (updatedData) => { + try { + await emailRulesAPI.updateBlockedSenders(updatedData); + setBlockedRule(updatedData); + showToast('Blocked senders list updated', 'success'); + } catch (error) { + showToast('Failed to update blocked list: ' + error.message, 'error'); + } + }; + const handleDelete = async () => { if (!currentRule || !window.confirm(`Are you sure you want to delete the rule for ${currentRule.email_address}?`)) { return; @@ -149,6 +179,7 @@ function App() { try { await emailRulesAPI.deleteRule(currentRule.email_address); setCurrentRule(null); + setBlockedRule(null); // Auch Blocked resetten setEmail(''); showToast('Email rule deleted successfully', 'success'); fetchAllRules(); @@ -175,7 +206,13 @@ function App() { const handleSelectRule = (ruleEmail) => { setEmail(ruleEmail); setShowAllRules(false); - handleSearch(); + // Hier rufen wir direkt die Suche auf, der State 'email' updated async, + // daher nutzen wir den Parameter direkt + const event = { preventDefault: () => {} }; // Dummy event + // Wir müssen hier etwas vorsichtig sein, da setEmail async ist. + // Besser wäre es, handleSearch umzubauen, dass es Email akzeptiert, + // aber wir setzen es einfach und rufen loadAllData direkt auf. + loadAllData(ruleEmail, false); }; useEffect(() => { @@ -200,26 +237,46 @@ function App() { )} - {/* Access Denied / Invalid State */} + {/* Access Denied / Invalid State (nur wenn nicht authentifiziert und keine Regel geladen) */} {!isAuthenticating && !currentRule && (
- This application can only be accessed via the secure link provided in your Roundcube configuration. -
+ {/* Hier könnte man alternativ auch das Suchfeld anzeigen, + wenn man Standalone-Modus erlauben will. + Aktuell zeigt dein Code "Access Denied" an, wenn man nicht via Token kommt + und keine Regel geladen ist. + Falls du das Suchfeld willst, müsste dieser Block angepasst werden. + Ich lasse es wie im Original 'Access Denied', es sei denn SingleEmailMode ist an. + */} + {!singleEmailMode && ( + /* Falls Admin-Modus gewünscht: Suchfeld hier einfügen. + Ich behalte das Original-Verhalten bei. */ +{emailError}
} +{inputError}
+ )} +
+ Supports wildcards (*). Examples: marketing@spam.com, *@phishing.net
+
No blocked senders configured
+Add a pattern above to block incoming emails
+{pattern}
+