Add configurable SMTP email settings and contact-form email notifications
New admin panel "E-Mail" tab (Einstellungen -> E-Mail) lets the shop
owner configure their SMTP server, test the connection with a real
test email, set which address receives new-inquiry notifications, and
edit both the customer confirmation email and the shop notification
email as templates with {{placeholder}} variables.
Previously SMTP config only lived in a non-DB config.php file (with a
blank host, so mail sending was effectively off) and there was no
customer confirmation email at all - only a hardcoded owner
notification. ContactController now sends both emails using the
DB-configured (or config.php-fallback) settings; mail sending stays
best-effort so a contact form submission never fails because of it.
Backend: new Mailer support class (config resolution, PHPMailer setup,
placeholder rendering) and MailSettingsController (show/update/test),
following the existing WebsiteSettings/DatabaseSettings conventions
(password never returned in plaintext, empty password on save keeps
the existing one). New app_settings columns wired into Schema.php so
the "Datenbankstruktur aktualisieren" admin button picks them up on
existing installs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
967eac0563
commit
0de35ef692
10 changed files with 675 additions and 39 deletions
|
|
@ -37,6 +37,7 @@ import AdminUsers from './pages/admin/AdminUsers.jsx';
|
||||||
import PermissionGroups from './pages/admin/PermissionGroups.jsx';
|
import PermissionGroups from './pages/admin/PermissionGroups.jsx';
|
||||||
import SettingsLayout from './pages/admin/settings/SettingsLayout.jsx';
|
import SettingsLayout from './pages/admin/settings/SettingsLayout.jsx';
|
||||||
import WebsiteSettings from './pages/admin/settings/WebsiteSettings.jsx';
|
import WebsiteSettings from './pages/admin/settings/WebsiteSettings.jsx';
|
||||||
|
import EmailSettings from './pages/admin/settings/EmailSettings.jsx';
|
||||||
import DatabaseSettings from './pages/admin/settings/DatabaseSettings.jsx';
|
import DatabaseSettings from './pages/admin/settings/DatabaseSettings.jsx';
|
||||||
import ExportImportSettings from './pages/admin/settings/ExportImportSettings.jsx';
|
import ExportImportSettings from './pages/admin/settings/ExportImportSettings.jsx';
|
||||||
import MaintenanceSettings from './pages/admin/settings/MaintenanceSettings.jsx';
|
import MaintenanceSettings from './pages/admin/settings/MaintenanceSettings.jsx';
|
||||||
|
|
@ -89,6 +90,7 @@ export default function App() {
|
||||||
>
|
>
|
||||||
<Route index element={<Navigate to="website" replace />} />
|
<Route index element={<Navigate to="website" replace />} />
|
||||||
<Route path="website" element={<WebsiteSettings />} />
|
<Route path="website" element={<WebsiteSettings />} />
|
||||||
|
<Route path="email" element={<EmailSettings />} />
|
||||||
<Route path="database" element={<DatabaseSettings />} />
|
<Route path="database" element={<DatabaseSettings />} />
|
||||||
<Route path="export-import" element={<ExportImportSettings />} />
|
<Route path="export-import" element={<ExportImportSettings />} />
|
||||||
<Route path="maintenance" element={<MaintenanceSettings />} />
|
<Route path="maintenance" element={<MaintenanceSettings />} />
|
||||||
|
|
|
||||||
297
hifi-src/src/pages/admin/settings/EmailSettings.jsx
Normal file
297
hifi-src/src/pages/admin/settings/EmailSettings.jsx
Normal file
|
|
@ -0,0 +1,297 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { api } from '../../../api/client.js';
|
||||||
|
|
||||||
|
const emptyForm = {
|
||||||
|
mail_host: '',
|
||||||
|
mail_port: 587,
|
||||||
|
mail_username: '',
|
||||||
|
mail_password: '',
|
||||||
|
mail_encryption: 'tls',
|
||||||
|
mail_from_email: '',
|
||||||
|
mail_from_name: '',
|
||||||
|
mail_notify_email: '',
|
||||||
|
mail_customer_subject: '',
|
||||||
|
mail_customer_body: '',
|
||||||
|
mail_owner_subject: '',
|
||||||
|
mail_owner_body: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLACEHOLDERS = [
|
||||||
|
['{{name}}', 'Name des Kunden'],
|
||||||
|
['{{email}}', 'E-Mail des Kunden'],
|
||||||
|
['{{phone}}', 'Telefonnummer'],
|
||||||
|
['{{vin}}', 'Fahrgestellnummer (FIN)'],
|
||||||
|
['{{message}}', 'Nachricht des Kunden'],
|
||||||
|
['{{brand}}', 'Marke'],
|
||||||
|
['{{model}}', 'Modell'],
|
||||||
|
['{{package}}', 'Paket'],
|
||||||
|
['{{product}}', 'Produkt'],
|
||||||
|
['{{upgrades}}', 'Gewählte Upgrades'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function PlaceholderLegend() {
|
||||||
|
return (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||||
|
{PLACEHOLDERS.map(([token, label]) => (
|
||||||
|
<span
|
||||||
|
key={token}
|
||||||
|
title={label}
|
||||||
|
className="rounded-md bg-neutral-100 px-2 py-1 font-mono text-xs text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300"
|
||||||
|
>
|
||||||
|
{token}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmailSettings() {
|
||||||
|
const [form, setForm] = useState(emptyForm);
|
||||||
|
const [hasPassword, setHasPassword] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const [saveBusy, setSaveBusy] = useState(false);
|
||||||
|
const [saveError, setSaveError] = useState('');
|
||||||
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
|
|
||||||
|
const [testBusy, setTestBusy] = useState(false);
|
||||||
|
const [testError, setTestError] = useState('');
|
||||||
|
const [testSuccess, setTestSuccess] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api
|
||||||
|
.get('/settings/mail')
|
||||||
|
.then((res) => {
|
||||||
|
setForm({ ...res, mail_password: '' });
|
||||||
|
setHasPassword(res.has_password);
|
||||||
|
})
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const update = (field, value) => {
|
||||||
|
setForm((f) => ({ ...f, [field]: value }));
|
||||||
|
setSaveSuccess(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSaveBusy(true);
|
||||||
|
setSaveError('');
|
||||||
|
setSaveSuccess(false);
|
||||||
|
try {
|
||||||
|
const res = await api.post('/settings/mail', form);
|
||||||
|
setForm({ ...res, mail_password: '' });
|
||||||
|
setHasPassword(res.has_password);
|
||||||
|
setSaveSuccess(true);
|
||||||
|
} catch (err) {
|
||||||
|
setSaveError(err.message);
|
||||||
|
} finally {
|
||||||
|
setSaveBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
setTestBusy(true);
|
||||||
|
setTestError('');
|
||||||
|
setTestSuccess('');
|
||||||
|
try {
|
||||||
|
const res = await api.post('/settings/mail/test', form);
|
||||||
|
setTestSuccess(`Testmail gesendet an ${res.sent_to}.`);
|
||||||
|
} catch (err) {
|
||||||
|
setTestError(err.message);
|
||||||
|
} finally {
|
||||||
|
setTestBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <p className="text-sm text-neutral-400">Lädt…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSave} className="max-w-2xl space-y-6">
|
||||||
|
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">SMTP-Server</h2>
|
||||||
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Zugangsdaten deines Mail-Postfachs oder Transactional-Mail-Anbieters. Solange kein Server eingetragen ist,
|
||||||
|
werden keine E-Mails verschickt (Kontaktanfragen werden trotzdem in der Datenbank gespeichert).
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-[1fr_120px]">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Server (Host)</label>
|
||||||
|
<input
|
||||||
|
value={form.mail_host}
|
||||||
|
onChange={(e) => update('mail_host', e.target.value)}
|
||||||
|
placeholder="smtp.beispiel.de"
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Port</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={form.mail_port}
|
||||||
|
onChange={(e) => update('mail_port', e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Verschlüsselung</label>
|
||||||
|
<select
|
||||||
|
value={form.mail_encryption}
|
||||||
|
onChange={(e) => update('mail_encryption', e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<option value="tls">STARTTLS</option>
|
||||||
|
<option value="ssl">SSL</option>
|
||||||
|
<option value="none">Keine</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Benutzername</label>
|
||||||
|
<input
|
||||||
|
value={form.mail_username}
|
||||||
|
onChange={(e) => update('mail_username', e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">
|
||||||
|
Passwort {hasPassword ? '(leer lassen = unverändert)' : ''}
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={form.mail_password}
|
||||||
|
onChange={(e) => update('mail_password', e.target.value)}
|
||||||
|
placeholder={hasPassword ? '••••••••' : ''}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword((s) => !s)}
|
||||||
|
className="shrink-0 rounded-md border border-neutral-300 px-3 text-sm dark:border-neutral-700"
|
||||||
|
>
|
||||||
|
{showPassword ? 'Verbergen' : 'Anzeigen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Absender-E-Mail</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={form.mail_from_email}
|
||||||
|
onChange={(e) => update('mail_from_email', e.target.value)}
|
||||||
|
placeholder="info@hifi-planet-amorbach.de"
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Absender-Name</label>
|
||||||
|
<input
|
||||||
|
value={form.mail_from_name}
|
||||||
|
onChange={(e) => update('mail_from_name', e.target.value)}
|
||||||
|
placeholder="HifiPlanet"
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 border-t border-neutral-200 pt-4 dark:border-neutral-800">
|
||||||
|
{testError && <p className="mb-2 text-sm text-red-600">{testError}</p>}
|
||||||
|
{testSuccess && <p className="mb-2 text-sm text-green-600 dark:text-green-400">{testSuccess}</p>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleTest}
|
||||||
|
disabled={testBusy}
|
||||||
|
className="rounded-md border border-neutral-300 px-4 py-2 text-sm font-medium hover:bg-neutral-50 disabled:opacity-50 dark:border-neutral-700 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{testBusy ? 'Sende Testmail…' : 'Verbindung testen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">Empfänger</h2>
|
||||||
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
An diese Adresse geht die Benachrichtigung bei jeder neuen Kontaktanfrage.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={form.mail_notify_email}
|
||||||
|
onChange={(e) => update('mail_notify_email', e.target.value)}
|
||||||
|
placeholder="info@hifi-planet-amorbach.de"
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">E-Mail an den Kunden (Bestätigung)</h2>
|
||||||
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Geht automatisch an den Kunden, sobald er eine Kontaktanfrage abschickt.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Betreff</label>
|
||||||
|
<input
|
||||||
|
value={form.mail_customer_subject}
|
||||||
|
onChange={(e) => update('mail_customer_subject', e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Text</label>
|
||||||
|
<textarea
|
||||||
|
rows={8}
|
||||||
|
value={form.mail_customer_body}
|
||||||
|
onChange={(e) => update('mail_customer_body', e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PlaceholderLegend />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
|
||||||
|
<h2 className="mb-1 font-semibold text-neutral-900 dark:text-white">E-Mail an den Shop (Benachrichtigung)</h2>
|
||||||
|
<p className="mb-4 text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
Geht an die oben eingestellte Empfänger-Adresse, sobald eine neue Kontaktanfrage eingeht.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Betreff</label>
|
||||||
|
<input
|
||||||
|
value={form.mail_owner_subject}
|
||||||
|
onChange={(e) => update('mail_owner_subject', e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-neutral-700 dark:text-neutral-300">Text</label>
|
||||||
|
<textarea
|
||||||
|
rows={8}
|
||||||
|
value={form.mail_owner_body}
|
||||||
|
onChange={(e) => update('mail_owner_body', e.target.value)}
|
||||||
|
className="w-full rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm dark:border-neutral-700 dark:bg-neutral-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PlaceholderLegend />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{saveError && <p className="text-sm text-red-600">{saveError}</p>}
|
||||||
|
{saveSuccess && <p className="text-sm text-green-600 dark:text-green-400">Gespeichert.</p>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saveBusy}
|
||||||
|
className="rounded-md bg-brand-500 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{saveBusy ? 'Speichere…' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { NavLink, Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ to: '/admin/settings/website', label: 'Website' },
|
{ to: '/admin/settings/website', label: 'Website' },
|
||||||
|
{ to: '/admin/settings/email', label: 'E-Mail' },
|
||||||
{ to: '/admin/settings/database', label: 'Datenbank' },
|
{ to: '/admin/settings/database', label: 'Datenbank' },
|
||||||
{ to: '/admin/settings/export-import', label: 'Export & Import' },
|
{ to: '/admin/settings/export-import', label: 'Export & Import' },
|
||||||
{ to: '/admin/settings/maintenance', label: 'Wartungsmodus' },
|
{ to: '/admin/settings/maintenance', label: 'Wartungsmodus' },
|
||||||
|
|
|
||||||
18
hifi/api/database/migration_mail_settings.sql
Normal file
18
hifi/api/database/migration_mail_settings.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- SMTP-Einstellungen und E-Mail-Vorlagen (Kundenbestaetigung + Shop-Benachrichtigung)
|
||||||
|
-- admin-konfigurierbar machen. Additiv, nichts wird geloescht.
|
||||||
|
-- Hinweis: Auf der Live-Seite reicht stattdessen ein Klick auf "Datenbankstruktur aktualisieren"
|
||||||
|
-- unter Admin-Panel -> Einstellungen -> Datenbank.
|
||||||
|
|
||||||
|
ALTER TABLE app_settings
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_host VARCHAR(255) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_port INT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_username VARCHAR(255) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_password VARCHAR(255) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_encryption VARCHAR(10) NOT NULL DEFAULT 'tls',
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_from_email VARCHAR(150) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_from_name VARCHAR(150) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_notify_email VARCHAR(150) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_customer_subject VARCHAR(255) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_customer_body TEXT NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_owner_subject VARCHAR(255) NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS mail_owner_body TEXT NULL;
|
||||||
|
|
@ -24,6 +24,18 @@ CREATE TABLE app_settings (
|
||||||
whatsapp VARCHAR(50) NULL,
|
whatsapp VARCHAR(50) NULL,
|
||||||
contact_email VARCHAR(150) NULL,
|
contact_email VARCHAR(150) NULL,
|
||||||
hero_image_path VARCHAR(255) NULL,
|
hero_image_path VARCHAR(255) NULL,
|
||||||
|
mail_host VARCHAR(255) NULL,
|
||||||
|
mail_port INT NULL,
|
||||||
|
mail_username VARCHAR(255) NULL,
|
||||||
|
mail_password VARCHAR(255) NULL,
|
||||||
|
mail_encryption VARCHAR(10) NOT NULL DEFAULT 'tls',
|
||||||
|
mail_from_email VARCHAR(150) NULL,
|
||||||
|
mail_from_name VARCHAR(150) NULL,
|
||||||
|
mail_notify_email VARCHAR(150) NULL,
|
||||||
|
mail_customer_subject VARCHAR(255) NULL,
|
||||||
|
mail_customer_body TEXT NULL,
|
||||||
|
mail_owner_subject VARCHAR(255) NULL,
|
||||||
|
mail_owner_body TEXT NULL,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use App\Controllers\DatabaseConfigController;
|
||||||
use App\Controllers\GalleryBrandController;
|
use App\Controllers\GalleryBrandController;
|
||||||
use App\Controllers\GalleryPhotoController;
|
use App\Controllers\GalleryPhotoController;
|
||||||
use App\Controllers\GalleryProjectController;
|
use App\Controllers\GalleryProjectController;
|
||||||
|
use App\Controllers\MailSettingsController;
|
||||||
use App\Controllers\MaintenanceController;
|
use App\Controllers\MaintenanceController;
|
||||||
use App\Controllers\ModelController;
|
use App\Controllers\ModelController;
|
||||||
use App\Controllers\PackageController;
|
use App\Controllers\PackageController;
|
||||||
|
|
@ -174,6 +175,10 @@ $router->post('/settings/schema-migrate', $perm('settings.manage', fn($p) => Sch
|
||||||
$router->get('/settings/database', $perm('settings.manage', fn($p) => DatabaseConfigController::show()));
|
$router->get('/settings/database', $perm('settings.manage', fn($p) => DatabaseConfigController::show()));
|
||||||
$router->post('/settings/database', $perm('settings.manage', fn($p) => DatabaseConfigController::update()));
|
$router->post('/settings/database', $perm('settings.manage', fn($p) => DatabaseConfigController::update()));
|
||||||
|
|
||||||
|
$router->get('/settings/mail', $perm('settings.manage', fn($p) => MailSettingsController::show()));
|
||||||
|
$router->post('/settings/mail', $perm('settings.manage', fn($p) => MailSettingsController::update()));
|
||||||
|
$router->post('/settings/mail/test', $perm('settings.manage', fn($p) => MailSettingsController::test()));
|
||||||
|
|
||||||
$router->get('/maintenance', fn($p) => MaintenanceController::status());
|
$router->get('/maintenance', fn($p) => MaintenanceController::status());
|
||||||
$router->post('/maintenance', $perm('settings.manage', fn($p) => MaintenanceController::update()));
|
$router->post('/maintenance', $perm('settings.manage', fn($p) => MaintenanceController::update()));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ namespace App\Controllers;
|
||||||
|
|
||||||
use App\Config\Database;
|
use App\Config\Database;
|
||||||
use App\Support\Http;
|
use App\Support\Http;
|
||||||
use PHPMailer\PHPMailer\PHPMailer;
|
use App\Support\Mailer;
|
||||||
|
|
||||||
class ContactController
|
class ContactController
|
||||||
{
|
{
|
||||||
|
|
@ -43,7 +43,9 @@ class ContactController
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$id = (int) $db->lastInsertId();
|
$id = (int) $db->lastInsertId();
|
||||||
self::sendNotificationMail($body, $name, $email, $selectedUpgrades);
|
$vars = self::templateVars($body, $name, $email, $selectedUpgrades);
|
||||||
|
self::sendOwnerNotification($vars);
|
||||||
|
self::sendCustomerConfirmation($vars, $email);
|
||||||
|
|
||||||
Http::send(['ok' => true, 'id' => $id], 201);
|
Http::send(['ok' => true, 'id' => $id], 201);
|
||||||
}
|
}
|
||||||
|
|
@ -105,51 +107,65 @@ class ContactController
|
||||||
Http::send(['ok' => true]);
|
Http::send(['ok' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function sendNotificationMail(array $body, string $name, string $email, array $selectedUpgrades = []): void
|
/**
|
||||||
|
* Baut die Platzhalter-Werte fuer beide Vorlagen (Kundenbestaetigung + Shop-Benachrichtigung)
|
||||||
|
* einmal zentral, statt sie in jeder Mail-Methode einzeln zusammenzusetzen.
|
||||||
|
*/
|
||||||
|
private static function templateVars(array $body, string $name, string $email, array $selectedUpgrades): array
|
||||||
{
|
{
|
||||||
$config = require __DIR__ . '/../../config/config.php';
|
$upgradesText = '-';
|
||||||
$mailConfig = $config['mail'];
|
if ($selectedUpgrades) {
|
||||||
|
$upgradesText = implode("\n", array_map(
|
||||||
|
fn($u) => sprintf('- %s (%.2f €)', $u['name'], $u['price']),
|
||||||
|
$selectedUpgrades
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($mailConfig['host'])) {
|
return [
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email,
|
||||||
|
'phone' => $body['phone'] ?? null,
|
||||||
|
'vin' => $body['vin'] ?? null,
|
||||||
|
'message' => $body['message'] ?? null,
|
||||||
|
'brand' => $body['brand_name'] ?? null,
|
||||||
|
'model' => $body['model_name'] ?? null,
|
||||||
|
'package' => $body['package_name'] ?? null,
|
||||||
|
'product' => $body['product_name'] ?? null,
|
||||||
|
'upgrades' => $upgradesText,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function sendOwnerNotification(array $vars): void
|
||||||
|
{
|
||||||
|
$config = Mailer::resolveConfig();
|
||||||
|
if (empty($config['host']) || empty($config['notify_email'])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$mail = new PHPMailer(true);
|
$mail = Mailer::build($config);
|
||||||
$mail->isSMTP();
|
$mail->addAddress($config['notify_email']);
|
||||||
$mail->Host = $mailConfig['host'];
|
$mail->addReplyTo($vars['email'], $vars['name']);
|
||||||
$mail->Port = $mailConfig['port'];
|
$mail->Subject = Mailer::render($config['owner_subject'], $vars);
|
||||||
$mail->SMTPAuth = true;
|
$mail->Body = Mailer::render($config['owner_body'], $vars);
|
||||||
$mail->Username = $mailConfig['username'];
|
$mail->send();
|
||||||
$mail->Password = $mailConfig['password'];
|
} catch (\Throwable $e) {
|
||||||
$mail->SMTPSecure = $mailConfig['encryption'];
|
// Mailversand ist best effort – die Anfrage ist bereits in der DB gespeichert.
|
||||||
$mail->CharSet = 'UTF-8';
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$mail->setFrom($mailConfig['from_email'], $mailConfig['from_name']);
|
private static function sendCustomerConfirmation(array $vars, string $email): void
|
||||||
$mail->addAddress($mailConfig['to_email']);
|
{
|
||||||
$mail->addReplyTo($email, $name);
|
$config = Mailer::resolveConfig();
|
||||||
|
if (empty($config['host'])) {
|
||||||
$context = trim(sprintf(
|
return;
|
||||||
"Marke: %s\nModell: %s\nPaket: %s\nProdukt: %s",
|
}
|
||||||
$body['brand_name'] ?? '-',
|
|
||||||
$body['model_name'] ?? '-',
|
|
||||||
$body['package_name'] ?? '-',
|
|
||||||
$body['product_name'] ?? '-'
|
|
||||||
));
|
|
||||||
|
|
||||||
$upgradesText = '-';
|
|
||||||
if ($selectedUpgrades) {
|
|
||||||
$upgradesText = implode("\n", array_map(
|
|
||||||
fn($u) => sprintf('- %s (%.2f €)', $u['name'], $u['price']),
|
|
||||||
$selectedUpgrades
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
$mail->Subject = 'Neue Kontaktanfrage von ' . $name;
|
|
||||||
$mail->Body = "Name: {$name}\nE-Mail: {$email}\nTelefon: " . ($body['phone'] ?? '-')
|
|
||||||
. "\nFahrgestellnummer (FIN): " . ($body['vin'] ?? '-') . "\n\n"
|
|
||||||
. $context . "\n\nGewünschte Upgrades:\n" . $upgradesText . "\n\nNachricht:\n" . ($body['message'] ?? '-');
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mail = Mailer::build($config);
|
||||||
|
$mail->addAddress($email, $vars['name']);
|
||||||
|
$mail->Subject = Mailer::render($config['customer_subject'], $vars);
|
||||||
|
$mail->Body = Mailer::render($config['customer_body'], $vars);
|
||||||
$mail->send();
|
$mail->send();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Mailversand ist best effort – die Anfrage ist bereits in der DB gespeichert.
|
// Mailversand ist best effort – die Anfrage ist bereits in der DB gespeichert.
|
||||||
|
|
|
||||||
134
hifi/api/src/Controllers/MailSettingsController.php
Normal file
134
hifi/api/src/Controllers/MailSettingsController.php
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Config\Database;
|
||||||
|
use App\Support\Http;
|
||||||
|
use App\Support\Mailer;
|
||||||
|
|
||||||
|
class MailSettingsController
|
||||||
|
{
|
||||||
|
// Greifen, solange in den Einstellungen noch nichts eingetragen wurde. Nur die Vorlagen
|
||||||
|
// haben sinnvolle Default-Texte - die SMTP-Zugangsdaten starten bewusst leer.
|
||||||
|
private const DEFAULTS = [
|
||||||
|
'mail_host' => '',
|
||||||
|
'mail_port' => 587,
|
||||||
|
'mail_username' => '',
|
||||||
|
'mail_encryption' => 'tls',
|
||||||
|
'mail_from_email' => '',
|
||||||
|
'mail_from_name' => 'HifiPlanet',
|
||||||
|
'mail_notify_email' => '',
|
||||||
|
'mail_customer_subject' => Mailer::DEFAULT_CUSTOMER_SUBJECT,
|
||||||
|
'mail_customer_body' => Mailer::DEFAULT_CUSTOMER_BODY,
|
||||||
|
'mail_owner_subject' => Mailer::DEFAULT_OWNER_SUBJECT,
|
||||||
|
'mail_owner_body' => Mailer::DEFAULT_OWNER_BODY,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static function currentRow(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$stmt = Database::connection()->query(
|
||||||
|
'SELECT mail_host, mail_port, mail_username, mail_password, mail_encryption,
|
||||||
|
mail_from_email, mail_from_name, mail_notify_email,
|
||||||
|
mail_customer_subject, mail_customer_body, mail_owner_subject, mail_owner_body
|
||||||
|
FROM app_settings WHERE id = 1'
|
||||||
|
);
|
||||||
|
return $stmt->fetch() ?: [];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function show(): void
|
||||||
|
{
|
||||||
|
$row = self::currentRow();
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach (self::DEFAULTS as $key => $default) {
|
||||||
|
$result[$key] = ($row[$key] ?? null) !== null && $row[$key] !== '' ? $row[$key] : $default;
|
||||||
|
}
|
||||||
|
unset($result['mail_password']);
|
||||||
|
|
||||||
|
Http::send(array_merge($result, [
|
||||||
|
'has_password' => !empty($row['mail_password']),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function update(): void
|
||||||
|
{
|
||||||
|
$body = Http::jsonBody();
|
||||||
|
$db = Database::connection();
|
||||||
|
$current = self::currentRow();
|
||||||
|
|
||||||
|
// Leeres Passwort im Formular = aktuelles Passwort beibehalten (wird beim Laden nie
|
||||||
|
// im Klartext an den Browser geschickt, siehe show()).
|
||||||
|
$password = ($body['mail_password'] ?? '') !== '' ? $body['mail_password'] : ($current['mail_password'] ?? null);
|
||||||
|
|
||||||
|
$db->exec('INSERT IGNORE INTO app_settings (id) VALUES (1)');
|
||||||
|
$stmt = $db->prepare(
|
||||||
|
'UPDATE app_settings SET
|
||||||
|
mail_host = ?, mail_port = ?, mail_username = ?, mail_password = ?, mail_encryption = ?,
|
||||||
|
mail_from_email = ?, mail_from_name = ?, mail_notify_email = ?,
|
||||||
|
mail_customer_subject = ?, mail_customer_body = ?, mail_owner_subject = ?, mail_owner_body = ?
|
||||||
|
WHERE id = 1'
|
||||||
|
);
|
||||||
|
$stmt->execute([
|
||||||
|
trim($body['mail_host'] ?? '') ?: null,
|
||||||
|
!empty($body['mail_port']) ? (int) $body['mail_port'] : null,
|
||||||
|
trim($body['mail_username'] ?? '') ?: null,
|
||||||
|
$password ?: null,
|
||||||
|
trim($body['mail_encryption'] ?? '') ?: 'tls',
|
||||||
|
trim($body['mail_from_email'] ?? '') ?: null,
|
||||||
|
trim($body['mail_from_name'] ?? '') ?: null,
|
||||||
|
trim($body['mail_notify_email'] ?? '') ?: null,
|
||||||
|
trim($body['mail_customer_subject'] ?? '') ?: null,
|
||||||
|
$body['mail_customer_body'] ?? null,
|
||||||
|
trim($body['mail_owner_subject'] ?? '') ?: null,
|
||||||
|
$body['mail_owner_body'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schickt eine echte Testmail mit den (moeglicherweise noch ungespeicherten) Formularwerten,
|
||||||
|
* damit der Admin sich nicht erst durchklicken muss, um zu sehen, ob die SMTP-Daten stimmen.
|
||||||
|
*/
|
||||||
|
public static function test(): void
|
||||||
|
{
|
||||||
|
$body = Http::jsonBody();
|
||||||
|
$current = self::currentRow();
|
||||||
|
|
||||||
|
$host = trim($body['mail_host'] ?? '');
|
||||||
|
$fromEmail = trim($body['mail_from_email'] ?? '');
|
||||||
|
$target = trim($body['mail_notify_email'] ?? '') ?: $fromEmail;
|
||||||
|
$password = ($body['mail_password'] ?? '') !== '' ? $body['mail_password'] : ($current['mail_password'] ?? '');
|
||||||
|
|
||||||
|
if ($host === '' || $fromEmail === '') {
|
||||||
|
Http::error('Server und Absender-E-Mail werden fuer den Test benoetigt', 422);
|
||||||
|
}
|
||||||
|
if (!filter_var($target, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
Http::error('Empfaenger-E-Mail (Benachrichtigungs- oder Absenderadresse) ist ungueltig', 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mail = Mailer::build([
|
||||||
|
'host' => $host,
|
||||||
|
'port' => $body['mail_port'] ?? 587,
|
||||||
|
'username' => trim($body['mail_username'] ?? ''),
|
||||||
|
'password' => $password,
|
||||||
|
'encryption' => trim($body['mail_encryption'] ?? '') ?: 'tls',
|
||||||
|
'from_email' => $fromEmail,
|
||||||
|
'from_name' => trim($body['mail_from_name'] ?? '') ?: 'HifiPlanet',
|
||||||
|
]);
|
||||||
|
$mail->addAddress($target);
|
||||||
|
$mail->Subject = 'Testmail von HifiPlanet';
|
||||||
|
$mail->Body = "Diese Testmail bestaetigt, dass deine SMTP-Einstellungen funktionieren.";
|
||||||
|
$mail->send();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Http::error('Test fehlgeschlagen: ' . $e->getMessage(), 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
Http::send(['ok' => true, 'sent_to' => $target]);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
hifi/api/src/Support/Mailer.php
Normal file
127
hifi/api/src/Support/Mailer.php
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Config\Database;
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buendelt alles rund um SMTP-Konfiguration und E-Mail-Vorlagen an einer Stelle,
|
||||||
|
* damit ContactController (echter Versand) und MailSettingsController (Verbindungstest)
|
||||||
|
* dieselbe PHPMailer-Aufbau- und Platzhalter-Logik nutzen statt sie zu duplizieren.
|
||||||
|
*/
|
||||||
|
class Mailer
|
||||||
|
{
|
||||||
|
public const DEFAULT_CUSTOMER_SUBJECT = 'Deine Anfrage bei HifiPlanet ist eingegangen';
|
||||||
|
|
||||||
|
public const DEFAULT_CUSTOMER_BODY = <<<'TXT'
|
||||||
|
Hallo {{name}},
|
||||||
|
|
||||||
|
vielen Dank fuer deine Anfrage bei HifiPlanet! Wir haben sie erhalten und melden uns so schnell wie moeglich bei dir.
|
||||||
|
|
||||||
|
Deine Angaben:
|
||||||
|
Marke: {{brand}}
|
||||||
|
Modell: {{model}}
|
||||||
|
Paket: {{package}}
|
||||||
|
Produkt: {{product}}
|
||||||
|
|
||||||
|
Nachricht:
|
||||||
|
{{message}}
|
||||||
|
|
||||||
|
Viele Gruesse
|
||||||
|
Dein HifiPlanet-Team
|
||||||
|
TXT;
|
||||||
|
|
||||||
|
public const DEFAULT_OWNER_SUBJECT = 'Neue Kontaktanfrage von {{name}}';
|
||||||
|
|
||||||
|
public const DEFAULT_OWNER_BODY = <<<'TXT'
|
||||||
|
Name: {{name}}
|
||||||
|
E-Mail: {{email}}
|
||||||
|
Telefon: {{phone}}
|
||||||
|
Fahrgestellnummer (FIN): {{vin}}
|
||||||
|
|
||||||
|
Marke: {{brand}}
|
||||||
|
Modell: {{model}}
|
||||||
|
Paket: {{package}}
|
||||||
|
Produkt: {{product}}
|
||||||
|
|
||||||
|
Gewuenschte Upgrades:
|
||||||
|
{{upgrades}}
|
||||||
|
|
||||||
|
Nachricht:
|
||||||
|
{{message}}
|
||||||
|
TXT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest die SMTP-/Vorlagen-Einstellungen aus app_settings. Leere Felder fallen auf
|
||||||
|
* die statische config.php zurueck (Zweck: Bestandsinstallationen, bei denen host/from_email
|
||||||
|
* dort schon manuell eingetragen wurden, funktionieren weiter, bis im Admin-Panel gespeichert wird).
|
||||||
|
*/
|
||||||
|
public static function resolveConfig(): array
|
||||||
|
{
|
||||||
|
$fallback = (require __DIR__ . '/../../config/config.php')['mail'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = Database::connection()->query(
|
||||||
|
'SELECT mail_host, mail_port, mail_username, mail_password, mail_encryption,
|
||||||
|
mail_from_email, mail_from_name, mail_notify_email,
|
||||||
|
mail_customer_subject, mail_customer_body, mail_owner_subject, mail_owner_body
|
||||||
|
FROM app_settings WHERE id = 1'
|
||||||
|
);
|
||||||
|
$row = $stmt->fetch() ?: [];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$row = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$str = fn($value) => $value !== null && $value !== '' ? $value : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'host' => $str($row['mail_host'] ?? null) ?? $fallback['host'],
|
||||||
|
'port' => $str($row['mail_port'] ?? null) ?? $fallback['port'],
|
||||||
|
'username' => $str($row['mail_username'] ?? null) ?? $fallback['username'],
|
||||||
|
'password' => $str($row['mail_password'] ?? null) ?? $fallback['password'],
|
||||||
|
'encryption' => $str($row['mail_encryption'] ?? null) ?? $fallback['encryption'],
|
||||||
|
'from_email' => $str($row['mail_from_email'] ?? null) ?? $fallback['from_email'],
|
||||||
|
'from_name' => $str($row['mail_from_name'] ?? null) ?? $fallback['from_name'],
|
||||||
|
'notify_email' => $str($row['mail_notify_email'] ?? null) ?? $fallback['to_email'],
|
||||||
|
'customer_subject' => $str($row['mail_customer_subject'] ?? null) ?? self::DEFAULT_CUSTOMER_SUBJECT,
|
||||||
|
'customer_body' => $str($row['mail_customer_body'] ?? null) ?? self::DEFAULT_CUSTOMER_BODY,
|
||||||
|
'owner_subject' => $str($row['mail_owner_subject'] ?? null) ?? self::DEFAULT_OWNER_SUBJECT,
|
||||||
|
'owner_body' => $str($row['mail_owner_body'] ?? null) ?? self::DEFAULT_OWNER_BODY,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen fertig konfigurierten, aber noch nicht abgeschickten PHPMailer aus den
|
||||||
|
* uebergebenen SMTP-Feldern (host/port/username/password/encryption/from_email/from_name).
|
||||||
|
*/
|
||||||
|
public static function build(array $config): PHPMailer
|
||||||
|
{
|
||||||
|
$mail = new PHPMailer(true);
|
||||||
|
$mail->isSMTP();
|
||||||
|
$mail->Host = $config['host'];
|
||||||
|
$mail->Port = (int) $config['port'];
|
||||||
|
$mail->SMTPAuth = true;
|
||||||
|
$mail->Username = $config['username'];
|
||||||
|
$mail->Password = $config['password'];
|
||||||
|
$mail->SMTPSecure = ($config['encryption'] ?? '') !== 'none' ? $config['encryption'] : false;
|
||||||
|
$mail->CharSet = 'UTF-8';
|
||||||
|
$mail->setFrom($config['from_email'], $config['from_name'] ?: $config['from_email']);
|
||||||
|
|
||||||
|
return $mail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersetzt {{platzhalter}} in einer Vorlage. Unbekannte Platzhalter bleiben unveraendert
|
||||||
|
* stehen (auffaelliger als sie einfach zu leeren, falls sich im Text ein Tippfehler einschleicht).
|
||||||
|
*/
|
||||||
|
public static function render(string $template, array $vars): string
|
||||||
|
{
|
||||||
|
$replacements = [];
|
||||||
|
foreach ($vars as $key => $value) {
|
||||||
|
$replacements['{{' . $key . '}}'] = $value ?? '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtr($template, $replacements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,18 @@ class Schema
|
||||||
whatsapp VARCHAR(50) NULL,
|
whatsapp VARCHAR(50) NULL,
|
||||||
contact_email VARCHAR(150) NULL,
|
contact_email VARCHAR(150) NULL,
|
||||||
hero_image_path VARCHAR(255) NULL,
|
hero_image_path VARCHAR(255) NULL,
|
||||||
|
mail_host VARCHAR(255) NULL,
|
||||||
|
mail_port INT NULL,
|
||||||
|
mail_username VARCHAR(255) NULL,
|
||||||
|
mail_password VARCHAR(255) NULL,
|
||||||
|
mail_encryption VARCHAR(10) NOT NULL DEFAULT 'tls',
|
||||||
|
mail_from_email VARCHAR(150) NULL,
|
||||||
|
mail_from_name VARCHAR(150) NULL,
|
||||||
|
mail_notify_email VARCHAR(150) NULL,
|
||||||
|
mail_customer_subject VARCHAR(255) NULL,
|
||||||
|
mail_customer_body TEXT NULL,
|
||||||
|
mail_owner_subject VARCHAR(255) NULL,
|
||||||
|
mail_owner_body TEXT NULL,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
) ENGINE=InnoDB",
|
) ENGINE=InnoDB",
|
||||||
|
|
||||||
|
|
@ -230,6 +242,18 @@ class Schema
|
||||||
'whatsapp' => 'VARCHAR(50) NULL',
|
'whatsapp' => 'VARCHAR(50) NULL',
|
||||||
'contact_email' => 'VARCHAR(150) NULL',
|
'contact_email' => 'VARCHAR(150) NULL',
|
||||||
'hero_image_path' => 'VARCHAR(255) NULL',
|
'hero_image_path' => 'VARCHAR(255) NULL',
|
||||||
|
'mail_host' => 'VARCHAR(255) NULL',
|
||||||
|
'mail_port' => 'INT NULL',
|
||||||
|
'mail_username' => 'VARCHAR(255) NULL',
|
||||||
|
'mail_password' => 'VARCHAR(255) NULL',
|
||||||
|
'mail_encryption' => "VARCHAR(10) NOT NULL DEFAULT 'tls'",
|
||||||
|
'mail_from_email' => 'VARCHAR(150) NULL',
|
||||||
|
'mail_from_name' => 'VARCHAR(150) NULL',
|
||||||
|
'mail_notify_email' => 'VARCHAR(150) NULL',
|
||||||
|
'mail_customer_subject' => 'VARCHAR(255) NULL',
|
||||||
|
'mail_customer_body' => 'TEXT NULL',
|
||||||
|
'mail_owner_subject' => 'VARCHAR(255) NULL',
|
||||||
|
'mail_owner_body' => 'TEXT NULL',
|
||||||
'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
'updated_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
|
||||||
],
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue