Compare commits
238 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
97cc0091c0 | |
|
|
eeae972d8b | |
|
|
16fa6d3f4e | |
|
|
04e28903ea | |
|
|
4efc014a15 | |
|
|
2cdcb5ec38 | |
|
|
c313582b0b | |
|
|
eff76f6568 | |
|
|
c48a5c7e02 | |
|
|
d190beadcf | |
|
|
f1c1cf55da | |
|
|
c94f90bcde | |
|
|
0fedff759c | |
|
|
dec95d5803 | |
|
|
1ae3c7c54a | |
|
|
fce4f43833 | |
|
|
6710827b81 | |
|
|
f20a30839f | |
|
|
3bb9afefc3 | |
|
|
5c8a03cc45 | |
|
|
05cc37271c | |
|
|
7a89493efd | |
|
|
afce3d9687 | |
|
|
0beb12569b | |
|
|
be3502f197 | |
|
|
315608a108 | |
|
|
30d2c28504 | |
|
|
b7e411be82 | |
|
|
93699108eb | |
|
|
ff28b98555 | |
|
|
3e157f4e56 | |
|
|
d84537b343 | |
|
|
21015e69a3 | |
|
|
fc0ede4840 | |
|
|
aaeea2bb86 | |
|
|
f266228fcf | |
|
|
41de455826 | |
|
|
49463bf34d | |
|
|
eb77ec3342 | |
|
|
ff6002ca0b | |
|
|
1f013b6dcd | |
|
|
dcb4456ba6 | |
|
|
2fb66df19e | |
|
|
f756789cc4 | |
|
|
32cf27bcd2 | |
|
|
74600185ba | |
|
|
919c2013f9 | |
|
|
e23d9058ee | |
|
|
b48c74b676 | |
|
|
e49b1f6c4c | |
|
|
f473ec66d4 | |
|
|
22b6a9e0f2 | |
|
|
93bcb56605 | |
|
|
b6475fea75 | |
|
|
14901a40e3 | |
|
|
20c9100226 | |
|
|
5dc55a9d1c | |
|
|
fc7e1ed0f9 | |
|
|
5f71a8d5e9 | |
|
|
ea7dc3b94d | |
|
|
4671909e14 | |
|
|
653bf55415 | |
|
|
978fad64f4 | |
|
|
23d6d9cb46 | |
|
|
d3012792bc | |
|
|
bbc7cc6c34 | |
|
|
8ea925e4ae | |
|
|
c276a17a98 | |
|
|
597773123f | |
|
|
afad3c3471 | |
|
|
8a546f4f3d | |
|
|
486166c05a | |
|
|
f375ea0215 | |
|
|
fa8e9e7e88 | |
|
|
742c39f91a | |
|
|
2ec10a8a81 | |
|
|
fb6573598d | |
|
|
6bb2d09621 | |
|
|
cdfdcc0756 | |
|
|
c6d6798898 | |
|
|
496912f5ca | |
|
|
2c82555235 | |
|
|
eb17d056b7 | |
|
|
96477ede1c | |
|
|
8ff63dd966 | |
|
|
50a68047fc | |
|
|
a2ba9a7c03 | |
|
|
ea37abacf8 | |
|
|
32a3b51b2a | |
|
|
0375aed2ce | |
|
|
fd8d81fecb | |
|
|
fcbe944e32 | |
|
|
598f0edacf | |
|
|
00f715480f | |
|
|
9673e717d1 | |
|
|
bf76e93103 | |
|
|
dbd7848f68 | |
|
|
cae269fae1 | |
|
|
77fad53655 | |
|
|
de314adebe | |
|
|
7e7d5c1069 | |
|
|
aaccb4bb32 | |
|
|
7ccc7921c1 | |
|
|
a96d8f690a | |
|
|
b0daf839d9 | |
|
|
ce66a84c8d | |
|
|
fa65bc1c2e | |
|
|
5f627dcb0e | |
|
|
3f6f787fd3 | |
|
|
2331ef74f6 | |
|
|
029d959d51 | |
|
|
d0e1d5613c | |
|
|
68e7635f6d | |
|
|
673e9ec1df | |
|
|
0fc6ee81d6 | |
|
|
79b1eeb93c | |
|
|
dc5d8582e1 | |
|
|
a9ad26757b | |
|
|
1bcd93908e | |
|
|
7d738392b2 | |
|
|
6fa77f9f9b | |
|
|
b721b7b0df | |
|
|
ad85bc0326 | |
|
|
7b1274f349 | |
|
|
cbdab12c19 | |
|
|
b83af57e56 | |
|
|
0635d1d9fe | |
|
|
a771f97516 | |
|
|
9d40597842 | |
|
|
140d737231 | |
|
|
09f9cf3553 | |
|
|
1d18e80749 | |
|
|
190b627fd4 | |
|
|
a119686bfa | |
|
|
b107e9a580 | |
|
|
a7f6d8e242 | |
|
|
3e12d7fd70 | |
|
|
c16315711d | |
|
|
3c6325db32 | |
|
|
a51f271069 | |
|
|
e0128312cf | |
|
|
c834b5f85d | |
|
|
7454638506 | |
|
|
f49b92074e | |
|
|
29566499f9 | |
|
|
81f1c9512a | |
|
|
1e053c2bb6 | |
|
|
2369a29161 | |
|
|
26eb3abdcd | |
|
|
4fc31726be | |
|
|
8fa28a4d84 | |
|
|
60ebd0ed16 | |
|
|
093b5a9eea | |
|
|
becc1bd737 | |
|
|
b59e4c53b4 | |
|
|
29ef2b6a2c | |
|
|
1b79454df5 | |
|
|
38d6fdba6f | |
|
|
996c9c19fe | |
|
|
871d69cc2d | |
|
|
f12c412bbe | |
|
|
7f56926b70 | |
|
|
904d60ed2b | |
|
|
8fd9fccc70 | |
|
|
227c623578 | |
|
|
d60d228012 | |
|
|
96e2b4d5ab | |
|
|
5ec7084cbd | |
|
|
240c672198 | |
|
|
bd7bb50a52 | |
|
|
17ed5b18d9 | |
|
|
096e983313 | |
|
|
880b99f1e0 | |
|
|
2688b2528b | |
|
|
ae3b1e6b14 | |
|
|
f740ffc753 | |
|
|
c4a2f33293 | |
|
|
831f656b54 | |
|
|
5511498200 | |
|
|
3b0ebce1df | |
|
|
22eee053a3 | |
|
|
05cc53ef49 | |
|
|
8a654bef89 | |
|
|
72973e3ca5 | |
|
|
8ff1aeac2a | |
|
|
d50aedeafb | |
|
|
bc2810eb8a | |
|
|
894f753b81 | |
|
|
9d3cbd88b6 | |
|
|
1547302297 | |
|
|
68a31e894d | |
|
|
7833257126 | |
|
|
216e46311b | |
|
|
f9f7433b98 | |
|
|
73bda08244 | |
|
|
660402a32d | |
|
|
32f43020d3 | |
|
|
ed176ec243 | |
|
|
4fd37985b3 | |
|
|
7d30faa7d7 | |
|
|
4f3066e225 | |
|
|
a30c21a1a9 | |
|
|
085f27d67c | |
|
|
3869f6e67f | |
|
|
a322aa17ac | |
|
|
0b415c6862 | |
|
|
31f486c753 | |
|
|
af045b21d5 | |
|
|
5b9e486b98 | |
|
|
fbce5dc8ba | |
|
|
f8f30d57f7 | |
|
|
eb16f7d6ad | |
|
|
814776d1ff | |
|
|
01e7db589a | |
|
|
3c8eaa16df | |
|
|
e833ab72c6 | |
|
|
d5d5fd819c | |
|
|
19618746ba | |
|
|
8551a00414 | |
|
|
94cddb7987 | |
|
|
7c3376bfbc | |
|
|
75d1f136a3 | |
|
|
b9c2eb5eef | |
|
|
a07a0d1a98 | |
|
|
8e8dff39c9 | |
|
|
d271c96828 | |
|
|
b8d121f251 | |
|
|
45e762be7f | |
|
|
38d22c85ed | |
|
|
fc8dbf894a | |
|
|
acab5d4c84 | |
|
|
c1d5ca1988 | |
|
|
79548c5aa0 | |
|
|
52887e2dd5 | |
|
|
3064c0b186 | |
|
|
949cdd1e5b | |
|
|
5158d7b3b3 | |
|
|
6796ff3859 |
|
|
@ -33,7 +33,7 @@ SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
# For cross-subdomain session sharing (e.g. webmail on mail.example.com):
|
# For cross-subdomain session sharing (e.g. webmail on mail.example.com):
|
||||||
# SESSION_DOMAIN=.example.com
|
# SESSION_DOMAIN=.example.com
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=
|
||||||
|
|
||||||
#BROADCAST_CONNECTION=log
|
#BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
|
/storage/backups
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/vendor
|
/vendor
|
||||||
Homestead.json
|
Homestead.json
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development (starts PHP server + queue + logs + Vite concurrently)
|
||||||
|
composer dev
|
||||||
|
|
||||||
|
# Build assets for production
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Vite dev server only
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
composer test
|
||||||
|
php artisan test
|
||||||
|
|
||||||
|
# Run a single test file
|
||||||
|
php artisan test tests/Feature/ExampleTest.php
|
||||||
|
|
||||||
|
# Run a single test by name
|
||||||
|
php artisan test --filter=TestName
|
||||||
|
|
||||||
|
# Migrations
|
||||||
|
php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Design
|
||||||
|
- Tailwind CSS v4
|
||||||
|
- Modals sind immer vom Livewire Modals
|
||||||
|
- Kein Inline-Style nur wenn wirklich notwendig.
|
||||||
|
- Immer prüfen das das Style nicht kaputt ist und wenn Icons in Buttons mit Text sind, sind diese immer nebeneinadner Nie übereinander.
|
||||||
|
|
||||||
|
### What this is
|
||||||
|
Mailwolt is a self-hosted mail server administration panel (German UI). It manages domains, mailboxes, aliases, DNS records (DKIM/DMARC/SPF/TLSA), TLS, Fail2ban, IMAP, and email quarantine/queues. It also has a mail sandbox for testing Postfix transports.
|
||||||
|
|
||||||
|
### Layout & Routing
|
||||||
|
- **`resources/views/layouts/dvx.blade.php`** is the actual app layout (not `app.blade.php`). Livewire full-page components use `#[Layout('layouts.dvx')]`.
|
||||||
|
- Routes are in `routes/web.php` — each page maps directly to a Livewire full-page component via `->name()`.
|
||||||
|
- Navigation structure is driven by `config/ui-menu.php`.
|
||||||
|
- Für den Style wird Tailwind CSS v4
|
||||||
|
|
||||||
|
### Livewire component structure
|
||||||
|
```
|
||||||
|
app/Livewire/Ui/
|
||||||
|
├── Nx/ — Current production UI (Dashboard, DomainList, MailboxList, AliasList, etc.)
|
||||||
|
├── Domain/ — Domain modals and DKIM/DNS views
|
||||||
|
├── Mail/ — Mailbox/alias modals, queue, quarantine
|
||||||
|
├── Security/ — Fail2ban, SSL, RSpamd, TLS, audit logs
|
||||||
|
├── System/ — Settings, users, API keys, webhooks, sandbox, backups
|
||||||
|
├── Search/ — Global search palette modal
|
||||||
|
└── Webmail/ — Webmail-specific components
|
||||||
|
```
|
||||||
|
|
||||||
|
Full-page components live in `Ui/Nx/` and `Ui/System/`, `Ui/Security/`, etc. Modals are always in a `Modal/` subfolder and extend `LivewireUI\Modal\ModalComponent`.
|
||||||
|
|
||||||
|
### Modals (wire-elements/modal v2)
|
||||||
|
- Open from **outside** a Livewire component: `onclick="Livewire.dispatch('openModal', {component:'ui.system.modal.my-modal', arguments:{key:value}})"`
|
||||||
|
- Open from **inside** a Livewire component: `$this->dispatch('openModal', component: '...', arguments: [...])`
|
||||||
|
- **Never** use `wire:click="$dispatch('openModal',...)"` outside a Livewire component context — it won't work.
|
||||||
|
- Modal argument keys must match the `mount(int $keyName)` parameter names exactly.
|
||||||
|
- To prevent closing on backdrop/Escape, override in the modal class:
|
||||||
|
```php
|
||||||
|
public static function closeModalOnClickAway(): bool { return false; }
|
||||||
|
public static function closeModalOnEscape(): bool { return false; }
|
||||||
|
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
||||||
|
```
|
||||||
|
- To force-close the entire modal stack: `$this->forceClose()->closeModal()`
|
||||||
|
|
||||||
|
### Livewire dependency injection
|
||||||
|
- **Never** use constructor injection in Livewire components — Livewire calls `new Component()` with no args.
|
||||||
|
- Use `boot(MyService $service)` instead: this is called on every request and supports DI.
|
||||||
|
|
||||||
|
### CSS design system
|
||||||
|
The app uses a custom `mw-*` variable and class system defined in `resources/css/app.css` (Tailwind CSS v4, no `tailwind.config.js`):
|
||||||
|
|
||||||
|
**CSS variables:**
|
||||||
|
- `--mw-bg`, `--mw-bg3`, `--mw-bg4` — background layers
|
||||||
|
- `--mw-b1`, `--mw-b2`, `--mw-b3` — border shades
|
||||||
|
- `--mw-t1` through `--mw-t5` — text shades (t1 = primary, t4/t5 = muted)
|
||||||
|
- `--mw-v`, `--mw-v2`, `--mw-vbg` — purple accent (primary brand color)
|
||||||
|
- `--mw-gr` — green (success)
|
||||||
|
|
||||||
|
**Reusable component classes:** `.mw-btn-primary`, `.mw-btn-secondary`, `.mw-btn-cancel`, `.mw-btn-save`, `.mw-btn-del`, `.mbx-act-btn`, `.mbx-act-danger`, `.mw-modal-frame`, `.mw-modal-head`, `.mw-modal-body`, `.mw-modal-foot`, `.mw-modal-label`, `.mw-modal-input`, `.mw-modal-error`, `.mbx-badge-mute`, `.mbx-badge-ok`, `.mbx-badge-warn`.
|
||||||
|
|
||||||
|
### Services
|
||||||
|
`app/Services/` contains: `DkimService`, `DnsRecordService`, `ImapService`, `MailStorage`, `SandboxMailParser`, `SandboxService`, `TlsaService`, `TotpService`, `WebhookService`.
|
||||||
|
|
||||||
|
### API
|
||||||
|
REST API under `/api/v1/` uses Laravel Sanctum. Token abilities map to scopes like `mailboxes:read`, `domains:write`, etc. Defined in `routes/api.php`.
|
||||||
|
|
||||||
|
### Sandbox mail system
|
||||||
|
The mail sandbox intercepts Postfix mail via a pipe transport (`php artisan sandbox:receive`). `SandboxRoute` model controls which domains/addresses are intercepted. `SandboxService::syncTransportFile()` writes `/etc/postfix/transport.sandbox` and runs `postmap`.
|
||||||
|
|
||||||
|
### Queue & real-time
|
||||||
|
- Queue driver: database (configurable). Jobs in `app/Jobs/`.
|
||||||
|
- Real-time updates use Laravel Reverb + Pusher.js. Livewire polls (`wire:poll.5s`) are used as fallback on some pages.
|
||||||
|
|
@ -17,7 +17,7 @@ class BackupRun extends Command
|
||||||
$job->update(['status' => 'running']);
|
$job->update(['status' => 'running']);
|
||||||
|
|
||||||
$policy = $job->policy;
|
$policy = $job->policy;
|
||||||
$tmpDir = sys_get_temp_dir() . '/mailwolt_backup_' . $job->id;
|
$tmpDir = sys_get_temp_dir() . '/clubird_backup_' . $job->id;
|
||||||
mkdir($tmpDir, 0700, true);
|
mkdir($tmpDir, 0700, true);
|
||||||
|
|
||||||
$sources = [];
|
$sources = [];
|
||||||
|
|
@ -28,7 +28,7 @@ class BackupRun extends Command
|
||||||
$script = '/usr/local/sbin/mailwolt-backup';
|
$script = '/usr/local/sbin/mailwolt-backup';
|
||||||
if (file_exists($script)) {
|
if (file_exists($script)) {
|
||||||
$output = [];
|
$output = [];
|
||||||
exec("bash " . escapeshellarg($script) . ' 2>&1', $output, $exitCode);
|
exec('sudo -n ' . escapeshellarg($script) . ' 2>&1', $output, $exitCode);
|
||||||
|
|
||||||
$artifact = null;
|
$artifact = null;
|
||||||
foreach ($output as $line) {
|
foreach ($output as $line) {
|
||||||
|
|
@ -37,7 +37,7 @@ class BackupRun extends Command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->finalize($job, $exitCode, implode("\n", array_slice($output, -30)), $artifact);
|
$this->finalize($job, $exitCode, implode("\n", array_slice($output, -30)), $artifact);
|
||||||
$this->cleanTmp($tmpDir);
|
$this->cleanTmp($tmpDir);
|
||||||
return $exitCode === 0 ? self::SUCCESS : self::FAILURE;
|
return $exitCode === 0 ? self::SUCCESS : self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
@ -97,12 +97,12 @@ class BackupRun extends Command
|
||||||
$outDir = storage_path('app/backups');
|
$outDir = storage_path('app/backups');
|
||||||
if (!is_dir($outDir) && !mkdir($outDir, 0750, true) && !is_dir($outDir)) {
|
if (!is_dir($outDir) && !mkdir($outDir, 0750, true) && !is_dir($outDir)) {
|
||||||
// Final fallback: /tmp (always writable)
|
// Final fallback: /tmp (always writable)
|
||||||
$outDir = sys_get_temp_dir() . '/mailwolt_backups';
|
$outDir = sys_get_temp_dir() . '/clubird_backups';
|
||||||
mkdir($outDir, 0750, true);
|
mkdir($outDir, 0750, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stamp = now()->format('Y-m-d_H-i-s');
|
$stamp = now()->format('Y-m-d_H-i-s');
|
||||||
$outFile = "{$outDir}/mailwolt_{$stamp}.tar.gz";
|
$outFile = "{$outDir}/clubird_{$stamp}.tar.gz";
|
||||||
$srcArgs = implode(' ', array_map('escapeshellarg', $sources));
|
$srcArgs = implode(' ', array_map('escapeshellarg', $sources));
|
||||||
|
|
||||||
$tarOutput = [];
|
$tarOutput = [];
|
||||||
|
|
|
||||||
|
|
@ -6,50 +6,125 @@ use Illuminate\Console\Command;
|
||||||
|
|
||||||
class CheckUpdates extends Command
|
class CheckUpdates extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'mailwolt:check-updates';
|
protected $signature = 'clubird:check-updates';
|
||||||
protected $description = 'Check for newer MailWolt releases via git tags';
|
protected $aliases = ['mailwolt:check-updates'];
|
||||||
|
protected $description = 'Check for newer CluBird releases via git tags';
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$currentNorm = $this->readInstalledVersionNorm();
|
$currentNorm = $this->readInstalledVersionNorm();
|
||||||
$currentRaw = $this->readInstalledVersionRaw() ?? ($currentNorm ? 'v'.$currentNorm : null);
|
$currentRaw = $this->readInstalledVersionRaw() ?? ($currentNorm ? 'v'.$currentNorm : null);
|
||||||
|
|
||||||
$appPath = base_path();
|
$appPath = base_path();
|
||||||
$cmd = <<<BASH
|
$remoteFile = '/var/lib/mailwolt/version_remote';
|
||||||
set -e
|
|
||||||
git config --global --add safe.directory {$appPath} 2>/dev/null || true
|
|
||||||
cd {$appPath}
|
|
||||||
git fetch --tags --force --quiet origin +refs/tags/*:refs/tags/*
|
|
||||||
(git tag -l 'v*' --sort=-v:refname | head -n1) || true
|
|
||||||
BASH;
|
|
||||||
|
|
||||||
$latestTagRaw = trim((string) shell_exec($cmd));
|
// Fallback-Kette für Remote-Tags (erste funktionierende Methode gewinnt):
|
||||||
if ($latestTagRaw === '') {
|
// 1. mailwolt-update --check-only (sudoers: mailwolt-update)
|
||||||
$latestTagRaw = trim((string) shell_exec("cd {$appPath} && git tag -l --sort=-v:refname | head -n1"));
|
// 2. mailwolt-fetch-tags (sudoers: mailwolt-fetch-tags)
|
||||||
|
// 3. Gitea/GitHub API (kein Auth nötig wenn Repo öffentlich)
|
||||||
|
// 4. git ls-remote (klappt wenn Credentials im git-Config)
|
||||||
|
// 5. git fetch --tags direkt
|
||||||
|
$updateBin = '/usr/local/sbin/mailwolt-update';
|
||||||
|
$fetchHelper = '/usr/local/sbin/mailwolt-fetch-tags';
|
||||||
|
|
||||||
|
$fetched = false;
|
||||||
|
|
||||||
|
if (file_exists($updateBin) && str_contains((string) @file_get_contents($updateBin), '--check-only')) {
|
||||||
|
@exec('sudo -n ' . escapeshellarg($updateBin) . ' --check-only 2>/dev/null', $_, $rc);
|
||||||
|
$fetched = ($rc === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$fetched && file_exists($fetchHelper)) {
|
||||||
|
@exec('sudo -n ' . escapeshellarg($fetchHelper) . ' 2>/dev/null', $_, $rc);
|
||||||
|
$fetched = ($rc === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gitea/GitHub API – funktioniert ohne Credentials wenn Repo öffentlich
|
||||||
|
if (!$fetched) {
|
||||||
|
$remoteUrl = trim((string) @shell_exec('git -C ' . escapeshellarg($appPath) . ' remote get-url origin 2>/dev/null'));
|
||||||
|
if (preg_match('~https?://([^/]+)/([^/]+/[^/]+?)(?:\.git)?$~', $remoteUrl, $rm)) {
|
||||||
|
$host = $rm[1];
|
||||||
|
$project = $rm[2];
|
||||||
|
// Gitea API
|
||||||
|
$apiUrl = "https://{$host}/api/v1/repos/{$project}/tags?limit=50";
|
||||||
|
$ctx = stream_context_create(['http' => ['timeout' => 5, 'ignore_errors' => true]]);
|
||||||
|
$json = @file_get_contents($apiUrl, false, $ctx);
|
||||||
|
if ($json) {
|
||||||
|
$tags = json_decode($json, true);
|
||||||
|
if (is_array($tags)) {
|
||||||
|
$versions = [];
|
||||||
|
foreach ($tags as $t) {
|
||||||
|
$name = $t['name'] ?? '';
|
||||||
|
if (preg_match('/^v[\d.]+$/', $name)) {
|
||||||
|
$versions[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usort($versions, 'version_compare');
|
||||||
|
$latest = end($versions);
|
||||||
|
if ($latest) {
|
||||||
|
@mkdir(dirname($remoteFile), 0755, true);
|
||||||
|
file_put_contents($remoteFile, $latest);
|
||||||
|
$fetched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// git ls-remote (klappt wenn Credentials im git-Config hinterlegt sind)
|
||||||
|
if (!$fetched) {
|
||||||
|
$lsOut = [];
|
||||||
|
@exec('git -C ' . escapeshellarg($appPath) . ' ls-remote --tags origin \'v*\' 2>/dev/null', $lsOut);
|
||||||
|
$lsVersions = [];
|
||||||
|
foreach ($lsOut as $line) {
|
||||||
|
if (preg_match('~refs/tags/(v[\d.]+)$~', $line, $m)) {
|
||||||
|
$lsVersions[] = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($lsVersions)) {
|
||||||
|
usort($lsVersions, 'version_compare');
|
||||||
|
$latest = end($lsVersions);
|
||||||
|
@mkdir(dirname($remoteFile), 0755, true);
|
||||||
|
file_put_contents($remoteFile, $latest);
|
||||||
|
$fetched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direkter git fetch als letzter Fallback
|
||||||
|
if (!$fetched) {
|
||||||
|
@exec('git -C ' . escapeshellarg($appPath) . ' fetch --tags origin 2>/dev/null', $_, $rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// version_remote von Helfer geschrieben; als frisch wenn < 2h alt
|
||||||
|
$remoteRaw = '';
|
||||||
|
if (file_exists($remoteFile) && (time() - filemtime($remoteFile)) < 7200) {
|
||||||
|
$remoteRaw = trim((string) file_get_contents($remoteFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lokale Tags (nach fetch hoffentlich aktuell)
|
||||||
|
$out = [];
|
||||||
|
@exec("git -C " . escapeshellarg($appPath) . " tag -l 'v*' --sort=-v:refname 2>/dev/null", $out);
|
||||||
|
$latestTagRaw = $remoteRaw !== '' ? $remoteRaw : trim($out[0] ?? '');
|
||||||
|
|
||||||
$latestNorm = $this->normalizeVersion($latestTagRaw);
|
$latestNorm = $this->normalizeVersion($latestTagRaw);
|
||||||
|
|
||||||
// Nichts gefunden -> alles leeren
|
|
||||||
if (!$latestNorm) {
|
if (!$latestNorm) {
|
||||||
cache()->forget('updates:latest');
|
cache()->forget('updates:latest');
|
||||||
cache()->forget('updates:latest_raw');
|
cache()->forget('updates:latest_raw');
|
||||||
cache()->forget('mailwolt.update_available'); // legacy
|
cache()->forget('mailwolt.update_available');
|
||||||
$this->warn('Keine Release-Tags gefunden.');
|
$this->warn('Keine Release-Tags gefunden.');
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nur wenn wirklich neuer als installiert -> Keys setzen
|
|
||||||
if ($currentNorm && version_compare($latestNorm, $currentNorm, '>')) {
|
if ($currentNorm && version_compare($latestNorm, $currentNorm, '>')) {
|
||||||
cache()->forever('updates:latest', $latestNorm);
|
cache()->forever('updates:latest', $latestNorm);
|
||||||
cache()->forever('updates:latest_raw', $latestTagRaw ?: ('v'.$latestNorm));
|
cache()->forever('updates:latest_raw', $latestTagRaw ?: ('v'.$latestNorm));
|
||||||
cache()->forever('mailwolt.update_available', $latestNorm); // legacy-kompat
|
cache()->forever('mailwolt.update_available', $latestNorm);
|
||||||
$this->info("Update verfügbar: {$latestTagRaw} (installiert: ".($currentRaw ?? $currentNorm).")");
|
$this->info("Update verfügbar: {$latestTagRaw} (installiert: ".($currentRaw ?? $currentNorm).")");
|
||||||
} else {
|
} else {
|
||||||
// Kein Update -> Keys löschen
|
|
||||||
cache()->forget('updates:latest');
|
cache()->forget('updates:latest');
|
||||||
cache()->forget('updates:latest_raw');
|
cache()->forget('updates:latest_raw');
|
||||||
cache()->forget('mailwolt.update_available'); // legacy
|
cache()->forget('mailwolt.update_available');
|
||||||
$this->info("Aktuell (installiert: ".($currentRaw ?? $currentNorm ?? 'unbekannt').").");
|
$this->info("Aktuell (installiert: ".($currentRaw ?? $currentNorm ?? 'unbekannt').").");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,28 +136,35 @@ class CheckUpdates extends Command
|
||||||
|
|
||||||
private function readInstalledVersionNorm(): ?string
|
private function readInstalledVersionNorm(): ?string
|
||||||
{
|
{
|
||||||
// Lokal: git describe gibt immer den aktuellen Stand
|
// version_raw ist die zuverlässigste Quelle – wird vom Update-Script geschrieben
|
||||||
if (app()->isLocal()) {
|
$rawVer = $this->normalizeVersion($this->readInstalledVersionRaw() ?? '');
|
||||||
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
|
|
||||||
$v = $this->normalizeVersion($tag);
|
$fileVer = null;
|
||||||
if ($v) return $v;
|
foreach (['/var/lib/mailwolt/version', base_path('VERSION')] as $p) {
|
||||||
|
$raw = @trim(@file_get_contents($p) ?: '');
|
||||||
|
if ($raw !== '') { $fileVer = $this->normalizeVersion($raw); break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
$paths = [
|
// version_raw bevorzugen (echter installierter Stand)
|
||||||
'/var/lib/mailwolt/version',
|
// Nur wenn version_raw fehlt: version-Datei oder git-Tag als Fallback
|
||||||
base_path('VERSION'),
|
if ($rawVer) {
|
||||||
];
|
return $rawVer;
|
||||||
foreach ($paths as $p) {
|
|
||||||
$raw = @trim(@file_get_contents($p) ?: '');
|
|
||||||
if ($raw !== '') return $this->normalizeVersion($raw);
|
|
||||||
}
|
}
|
||||||
$raw = $this->readInstalledVersionRaw();
|
|
||||||
return $raw ? $this->normalizeVersion($raw) : null;
|
// git-Tag als letzter Fallback (funktioniert auf Dev-Servern)
|
||||||
|
$out = [];
|
||||||
|
@exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null', $out);
|
||||||
|
$gitVer = $this->normalizeVersion(trim($out[0] ?? ''));
|
||||||
|
|
||||||
|
if ($gitVer && (!$fileVer || version_compare($gitVer, $fileVer, '>'))) {
|
||||||
|
return $gitVer;
|
||||||
|
}
|
||||||
|
return $fileVer ?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function readInstalledVersionRaw(): ?string
|
private function readInstalledVersionRaw(): ?string
|
||||||
{
|
{
|
||||||
$p = '/var/lib/mailwolt/version_raw'; // vom Wrapper (z.B. "v1.0.25" oder "v1.0.25-3-gabcd")
|
$p = '/var/lib/mailwolt/version_raw';
|
||||||
$raw = @trim(@file_get_contents($p) ?: '');
|
$raw = @trim(@file_get_contents($p) ?: '');
|
||||||
return $raw !== '' ? $raw : null;
|
return $raw !== '' ? $raw : null;
|
||||||
}
|
}
|
||||||
|
|
@ -92,8 +174,8 @@ class CheckUpdates extends Command
|
||||||
if (!$v) return null;
|
if (!$v) return null;
|
||||||
$v = trim($v);
|
$v = trim($v);
|
||||||
if ($v === '') return null;
|
if ($v === '') return null;
|
||||||
$v = ltrim($v, "vV \t\n\r\0\x0B"); // führendes v entfernen
|
$v = ltrim($v, "vV \t\n\r\0\x0B");
|
||||||
$v = preg_replace('/-.*$/', '', $v); // Build-/dirty-Suffix abschneiden
|
$v = preg_replace('/-.*$/', '', $v);
|
||||||
return $v !== '' ? $v : null;
|
return $v !== '' ? $v : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,13 @@ use Illuminate\Console\Command;
|
||||||
|
|
||||||
class MailwoltRestart extends Command
|
class MailwoltRestart extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'mailwolt:restart-services';
|
protected $signature = 'clubird:restart-services';
|
||||||
|
protected $aliases = ['mailwolt:restart-services'];
|
||||||
protected $description = 'Restart or reload MailWolt-related system services';
|
protected $description = 'Restart or reload MailWolt-related system services';
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$units = config('mailwolt.units', []);
|
$units = config('clubird.units', []);
|
||||||
|
|
||||||
foreach ($units as $u) {
|
foreach ($units as $u) {
|
||||||
$base = (string)($u['name'] ?? '');
|
$base = (string)($u['name'] ?? '');
|
||||||
|
|
@ -45,12 +46,12 @@ class MailwoltRestart extends Command
|
||||||
|
|
||||||
//class MailwoltRestart extends Command
|
//class MailwoltRestart extends Command
|
||||||
//{
|
//{
|
||||||
// protected $signature = 'mailwolt:restart-services';
|
// protected $signature = 'clubird:restart-services';
|
||||||
// protected $description = 'Restart or reload MailWolt-related system services';
|
// protected $description = 'Restart or reload MailWolt-related system services';
|
||||||
//
|
//
|
||||||
// public function handle(): int
|
// public function handle(): int
|
||||||
// {
|
// {
|
||||||
// $units = config('mailwolt.units', []);
|
// $units = config('clubird.units', []);
|
||||||
// $allowed = ['reload','restart','try-reload-or-restart'];
|
// $allowed = ['reload','restart','try-reload-or-restart'];
|
||||||
//
|
//
|
||||||
// foreach ($units as $u) {
|
// foreach ($units as $u) {
|
||||||
|
|
@ -90,12 +91,12 @@ class MailwoltRestart extends Command
|
||||||
//
|
//
|
||||||
//class MailwoltRestart extends Command
|
//class MailwoltRestart extends Command
|
||||||
//{
|
//{
|
||||||
// protected $signature = 'mailwolt:restart-services';
|
// protected $signature = 'clubird:restart-services';
|
||||||
// protected $description = 'Restart or reload MailWolt-related system services';
|
// protected $description = 'Restart or reload MailWolt-related system services';
|
||||||
//
|
//
|
||||||
// public function handle(): int
|
// public function handle(): int
|
||||||
// {
|
// {
|
||||||
// $units = config('mailwolt.units', []);
|
// $units = config('clubird.units', []);
|
||||||
//
|
//
|
||||||
// foreach ($units as $u) {
|
// foreach ($units as $u) {
|
||||||
// $unit = rtrim($u['name'] ?? '', '.service') . '.service';
|
// $unit = rtrim($u['name'] ?? '', '.service') . '.service';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MigrateConfigNames extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'clubird:migrate-config-names {--dry-run : Nur anzeigen was gemacht würde}';
|
||||||
|
protected $description = 'Benennt server-seitige Config-Dateien von mailwolt-* auf generische Namen um';
|
||||||
|
|
||||||
|
private array $renames = [
|
||||||
|
'/etc/rspamd/local.d/mailwolt-actions.conf' => '/etc/rspamd/local.d/actions.conf',
|
||||||
|
'/etc/dovecot/conf.d/99-mailwolt-tls.conf' => '/etc/dovecot/conf.d/99-tls.conf',
|
||||||
|
'/etc/postfix/mailwolt-tls.cf' => '/etc/postfix/tls.cf',
|
||||||
|
'/etc/fail2ban/jail.d/mailwolt-whitelist.local' => '/etc/fail2ban/jail.d/whitelist.local',
|
||||||
|
'/etc/fail2ban/jail.d/00-mailwolt-defaults.local'=> '/etc/fail2ban/jail.d/00-defaults.local',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$dry = $this->option('dry-run');
|
||||||
|
|
||||||
|
foreach ($this->renames as $old => $new) {
|
||||||
|
if (!file_exists($old)) {
|
||||||
|
$this->line(" <fg=gray>übersprungen</> {$old} (nicht vorhanden)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($new) && !is_link($new)) {
|
||||||
|
$this->line(" <fg=yellow>bereits vorhanden</> {$new}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dry) {
|
||||||
|
$this->line(" <fg=cyan>[dry-run]</> mv {$old} → {$new}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Als root direkt umbenennen; als www-data via sudo (sudo mv muss in sudoers sein)
|
||||||
|
if (posix_getuid() === 0) {
|
||||||
|
$ok = @rename($old, $new);
|
||||||
|
$errMsg = error_get_last()['message'] ?? '';
|
||||||
|
} else {
|
||||||
|
@exec('sudo -n mv ' . escapeshellarg($old) . ' ' . escapeshellarg($new) . ' 2>&1', $out, $rc);
|
||||||
|
$ok = ($rc === 0);
|
||||||
|
$errMsg = implode(' ', $out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ok) {
|
||||||
|
$this->info(" umbenannt: {$old} → {$new}");
|
||||||
|
} else {
|
||||||
|
$this->error(" FEHLER bei {$old}: {$errMsg}");
|
||||||
|
$this->line(" → manuell: <fg=yellow>mv {$old} {$new}</>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$dry) {
|
||||||
|
$this->info('Fertig. Dienste ggf. neu starten: rspamd, dovecot, postfix, fail2ban');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MigrateDovecot24 extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'clubird:migrate-dovecot24 {--dry-run : Nur anzeigen was geändert würde}';
|
||||||
|
protected $description = 'Migriert Dovecot-Config von 2.3 auf 2.4 Syntax';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$dry = $this->option('dry-run');
|
||||||
|
|
||||||
|
// 1) dovecot.conf: dovecot_config_version als erste Zeile
|
||||||
|
$this->fixDovecotConf($dry);
|
||||||
|
|
||||||
|
// 2) 10-auth.conf: disable_plaintext_auth → auth_allow_cleartext
|
||||||
|
$this->fixAuthConf($dry);
|
||||||
|
|
||||||
|
// 3) 10-mail.conf: mail_location → mail_driver + mail_path
|
||||||
|
$this->fixMailConf($dry);
|
||||||
|
|
||||||
|
// 4) 10-master.conf: einzeilige Blöcke aufsplitten
|
||||||
|
$this->fixMasterConf($dry);
|
||||||
|
|
||||||
|
// 5) 10-ssl.conf: ssl_cert/ssl_key → ssl_server_cert_file/ssl_server_key_file
|
||||||
|
$this->fixSslConf($dry);
|
||||||
|
|
||||||
|
// 6) auth-sql.conf.ext: neue passdb/userdb-Syntax
|
||||||
|
$this->fixAuthSqlConf($dry);
|
||||||
|
|
||||||
|
if (!$dry) {
|
||||||
|
$this->info('Fertig. Starte dovecot neu…');
|
||||||
|
@exec('systemctl restart dovecot 2>&1', $out, $rc);
|
||||||
|
if ($rc === 0) {
|
||||||
|
$this->info('Dovecot erfolgreich gestartet.');
|
||||||
|
} else {
|
||||||
|
$this->error('Dovecot-Neustart fehlgeschlagen: ' . implode("\n", $out));
|
||||||
|
$this->line('Prüfen mit: doveconf -n 2>&1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fixDovecotConf(bool $dry): void
|
||||||
|
{
|
||||||
|
$file = '/etc/dovecot/dovecot.conf';
|
||||||
|
$content = @file_get_contents($file);
|
||||||
|
if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; }
|
||||||
|
|
||||||
|
if (str_starts_with(trim($content), 'dovecot_config_version')) {
|
||||||
|
// Falsche Version ersetzen
|
||||||
|
$new = preg_replace('/^dovecot_config_version\s*=\s*\S+/m', 'dovecot_config_version = 2.4.1', $content);
|
||||||
|
} else {
|
||||||
|
$new = "dovecot_config_version = 2.4.1\n" . $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; }
|
||||||
|
|
||||||
|
if ($dry) { $this->line(" [dry-run] würde dovecot_config_version ergänzen in {$file}"); return; }
|
||||||
|
file_put_contents($file, $new);
|
||||||
|
$this->info(" aktualisiert: {$file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fixMailConf(bool $dry): void
|
||||||
|
{
|
||||||
|
$file = '/etc/dovecot/conf.d/10-mail.conf';
|
||||||
|
$content = @file_get_contents($file);
|
||||||
|
if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; }
|
||||||
|
|
||||||
|
$new = preg_replace_callback(
|
||||||
|
'/^mail_location\s*=\s*(\w+):(.+)$/m',
|
||||||
|
function ($m) {
|
||||||
|
return "mail_driver = {$m[1]}\nmail_path = {$m[2]}";
|
||||||
|
},
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; }
|
||||||
|
|
||||||
|
if ($dry) { $this->line(" [dry-run] würde mail_location aufteilen in {$file}"); return; }
|
||||||
|
file_put_contents($file, $new);
|
||||||
|
$this->info(" aktualisiert: {$file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fixMasterConf(bool $dry): void
|
||||||
|
{
|
||||||
|
$file = '/etc/dovecot/conf.d/10-master.conf';
|
||||||
|
$content = @file_get_contents($file);
|
||||||
|
if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; }
|
||||||
|
|
||||||
|
// Einzeilige Blöcke "name { key = val }" → mehrzeilig
|
||||||
|
$new = preg_replace_callback(
|
||||||
|
'/^(\s*)(\S+)\s*\{\s*([^}]+)\s*\}(\s*)$/m',
|
||||||
|
function ($m) {
|
||||||
|
$indent = $m[1];
|
||||||
|
$name = $m[2];
|
||||||
|
$inner = trim($m[3]);
|
||||||
|
$pairs = preg_split('/\s+(?=\w+=)/', $inner);
|
||||||
|
$body = implode("\n{$indent} ", array_map('trim', $pairs));
|
||||||
|
return "{$indent}{$name} {\n{$indent} {$body}\n{$indent}}";
|
||||||
|
},
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; }
|
||||||
|
|
||||||
|
if ($dry) { $this->line(" [dry-run] würde einzeilige Blöcke aufsplitten in {$file}"); return; }
|
||||||
|
file_put_contents($file, $new);
|
||||||
|
$this->info(" aktualisiert: {$file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fixSslConf(bool $dry): void
|
||||||
|
{
|
||||||
|
$file = '/etc/dovecot/conf.d/10-ssl.conf';
|
||||||
|
$content = @file_get_contents($file);
|
||||||
|
if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; }
|
||||||
|
|
||||||
|
// ssl_cert = </path> → ssl_server_cert_file = /path
|
||||||
|
$new = preg_replace('/^ssl_cert\s*=\s*<(.+)$/m', 'ssl_server_cert_file = $1', $content);
|
||||||
|
$new = preg_replace('/^ssl_key\s*=\s*<(.+)$/m', 'ssl_server_key_file = $1', $new);
|
||||||
|
// Direkte Pfade ohne < (falls schon teilweise migriert)
|
||||||
|
$new = preg_replace('/^ssl_cert\s*=\s*(?!<)(.+)$/m', 'ssl_server_cert_file = $1', $new);
|
||||||
|
$new = preg_replace('/^ssl_key\s*=\s*(?!<)(.+)$/m', 'ssl_server_key_file = $1', $new);
|
||||||
|
|
||||||
|
// dovecot_storage_version in dovecot.conf (wenn noch nicht vorhanden)
|
||||||
|
$mainConf = '/etc/dovecot/dovecot.conf';
|
||||||
|
$mainContent = (string) @file_get_contents($mainConf);
|
||||||
|
if (!str_contains($mainContent, 'dovecot_storage_version')) {
|
||||||
|
if (!$dry) {
|
||||||
|
file_put_contents($mainConf, $mainContent . "\ndovecot_storage_version = 2.4.1\n");
|
||||||
|
$this->info(" dovecot_storage_version ergänzt in {$mainConf}");
|
||||||
|
} else {
|
||||||
|
$this->line(" [dry-run] würde dovecot_storage_version ergänzen in {$mainConf}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; }
|
||||||
|
|
||||||
|
if ($dry) { $this->line(" [dry-run] würde ssl_cert/ssl_key umbenennen in {$file}"); return; }
|
||||||
|
file_put_contents($file, $new);
|
||||||
|
$this->info(" aktualisiert: {$file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fixAuthConf(bool $dry): void
|
||||||
|
{
|
||||||
|
$file = '/etc/dovecot/conf.d/10-auth.conf';
|
||||||
|
$content = @file_get_contents($file);
|
||||||
|
if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; }
|
||||||
|
|
||||||
|
$new = str_replace('disable_plaintext_auth = yes', 'auth_allow_cleartext = no', $content);
|
||||||
|
$new = str_replace('disable_plaintext_auth = no', 'auth_allow_cleartext = yes', $new);
|
||||||
|
|
||||||
|
if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; }
|
||||||
|
|
||||||
|
if ($dry) { $this->line(" [dry-run] würde disable_plaintext_auth ersetzen in {$file}"); return; }
|
||||||
|
file_put_contents($file, $new);
|
||||||
|
$this->info(" aktualisiert: {$file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fixAuthSqlConf(bool $dry): void
|
||||||
|
{
|
||||||
|
$file = '/etc/dovecot/conf.d/auth-sql.conf.ext';
|
||||||
|
$sqlConf = '/etc/dovecot/dovecot-sql.conf.ext';
|
||||||
|
|
||||||
|
// Alte Werte aus dovecot-sql.conf.ext lesen
|
||||||
|
$sqlRaw = (string) @file_get_contents($sqlConf);
|
||||||
|
$host = $this->parseSqlValue($sqlRaw, 'host') ?: '127.0.0.1';
|
||||||
|
$dbname = $this->parseSqlValue($sqlRaw, 'dbname') ?: 'mailwolt';
|
||||||
|
$user = $this->parseSqlValue($sqlRaw, 'user') ?: 'mailwolt';
|
||||||
|
$pass = $this->parseSqlValue($sqlRaw, 'password') ?: '';
|
||||||
|
$scheme = $this->parseSqlValue($sqlRaw, 'default_pass_scheme') ?: 'BLF-CRYPT';
|
||||||
|
|
||||||
|
// password_query extrahieren (auch aus Kommentaren)
|
||||||
|
$query = $this->parsePasswordQuery($sqlRaw);
|
||||||
|
|
||||||
|
$new = "mysql {$host} {\n"
|
||||||
|
. " dbname = {$dbname}\n"
|
||||||
|
. " user = {$user}\n"
|
||||||
|
. " password = {$pass}\n"
|
||||||
|
. "}\n\n"
|
||||||
|
. "passdb sql {\n"
|
||||||
|
. " default_password_scheme = {$scheme}\n"
|
||||||
|
. " query = {$query}\n"
|
||||||
|
. "}\n\n"
|
||||||
|
. "userdb static {\n"
|
||||||
|
. " fields {\n"
|
||||||
|
. " uid = vmail\n"
|
||||||
|
. " gid = vmail\n"
|
||||||
|
. " home = /var/mail/vhosts/%{user|domain}/%{user|username}\n"
|
||||||
|
. " }\n"
|
||||||
|
. "}\n";
|
||||||
|
|
||||||
|
if ($dry) {
|
||||||
|
$this->line(" [dry-run] würde {$file} neu schreiben:");
|
||||||
|
$this->line($new);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($file, $new);
|
||||||
|
$this->info(" neu geschrieben: {$file}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseSqlValue(string $content, string $key): ?string
|
||||||
|
{
|
||||||
|
// Aus connect-Zeile: connect = host=x dbname=y user=z password=w
|
||||||
|
if (preg_match('/^connect\s*=\s*(.+)/m', $content, $m)) {
|
||||||
|
$connect = $m[1];
|
||||||
|
if (preg_match('/' . preg_quote($key, '/') . '\s*=\s*(\S+)/i', $connect, $m2)) {
|
||||||
|
return trim($m2[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Direkte Zeile: key = value
|
||||||
|
if (preg_match('/^' . preg_quote($key, '/') . '\s*=\s*(.+)/m', $content, $m)) {
|
||||||
|
return trim($m[1]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parsePasswordQuery(string $content): string
|
||||||
|
{
|
||||||
|
// Auskommentierte oder aktive password_query / query Zeile
|
||||||
|
if (preg_match('/^#?\s*password_query\s*=\s*(.+)/m', $content, $m)) {
|
||||||
|
$q = trim($m[1]);
|
||||||
|
// %u → %{user}
|
||||||
|
$q = str_replace("'%u'", "'%{user}'", $q);
|
||||||
|
return rtrim($q, ';');
|
||||||
|
}
|
||||||
|
return "SELECT email AS user, password_hash AS password FROM mail_users WHERE email = '%{user}' AND is_active = 1 LIMIT 1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class MigrateEnvReverb extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'clubird:migrate-env-reverb';
|
||||||
|
protected $aliases = ['mailwolt:migrate-env-reverb'];
|
||||||
|
protected $description = 'Migriert veraltete REVERB_* .env-Werte auf Domain-Basis';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$env = base_path('.env');
|
||||||
|
if (!file_exists($env)) {
|
||||||
|
$this->warn('.env nicht gefunden.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($env);
|
||||||
|
|
||||||
|
// APP_HOST ermitteln: .env → APP_URL → DB-Setting ui_domain
|
||||||
|
$appHost = $this->extractVar($content, 'APP_HOST');
|
||||||
|
if (!$appHost || preg_match('/^\d+\.\d+\.\d+\.\d+$/', $appHost)) {
|
||||||
|
$appUrl = $this->extractVar($content, 'APP_URL');
|
||||||
|
$appHost = parse_url($appUrl ?: '', PHP_URL_HOST) ?: '';
|
||||||
|
}
|
||||||
|
if (!$appHost || preg_match('/^\d+\.\d+\.\d+\.\d+$/', $appHost)) {
|
||||||
|
try {
|
||||||
|
$appHost = (string) \App\Models\Setting::get('ui_domain', '');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$appHost = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$appHost || preg_match('/^\d+\.\d+\.\d+\.\d+$/', $appHost)) {
|
||||||
|
$this->warn('Kein gültiger Hostname gefunden – Migration übersprungen.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scheme = $this->detectScheme($appHost, $content);
|
||||||
|
$correctPort = $scheme === 'https' ? '443' : '80';
|
||||||
|
$viteHost = $this->extractVar($content, 'VITE_REVERB_HOST');
|
||||||
|
$currentPort = $this->extractVar($content, 'REVERB_PORT');
|
||||||
|
|
||||||
|
$hostOk = ($viteHost === '${REVERB_HOST}' || $viteHost === $appHost);
|
||||||
|
$portOk = ($currentPort === $correctPort);
|
||||||
|
|
||||||
|
if ($hostOk && $portOk) {
|
||||||
|
$this->info('REVERB-Werte bereits korrekt.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$port = $correctPort;
|
||||||
|
|
||||||
|
$fixes = [
|
||||||
|
'REVERB_HOST' => '${APP_HOST}',
|
||||||
|
'REVERB_PORT' => $port,
|
||||||
|
'REVERB_SCHEME' => $scheme,
|
||||||
|
'REVERB_PATH' => '/ws',
|
||||||
|
'REVERB_SERVER_HOST' => '127.0.0.1',
|
||||||
|
'REVERB_SERVER_PORT' => '8080',
|
||||||
|
'REVERB_SERVER_SCHEME' => 'http',
|
||||||
|
'REVERB_SERVER_PATH' => '',
|
||||||
|
'VITE_REVERB_HOST' => '${REVERB_HOST}',
|
||||||
|
'VITE_REVERB_PORT' => '${REVERB_PORT}',
|
||||||
|
'VITE_REVERB_SCHEME' => '${REVERB_SCHEME}',
|
||||||
|
'VITE_REVERB_PATH' => '${REVERB_PATH}',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($fixes as $key => $val) {
|
||||||
|
if (preg_match("/^{$key}=/m", $content)) {
|
||||||
|
$content = preg_replace("/^{$key}=.*/m", "{$key}={$val}", $content);
|
||||||
|
} else {
|
||||||
|
$content .= "\n{$key}={$val}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// APP_HOST setzen falls fehlend
|
||||||
|
if (!$this->extractVar($content, 'APP_HOST')) {
|
||||||
|
$content .= "\nAPP_HOST={$appHost}";
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($env, $content);
|
||||||
|
$this->info("REVERB .env migriert für Host: {$appHost}");
|
||||||
|
|
||||||
|
// Assets neu bauen damit wsHost korrekt eingebacken wird
|
||||||
|
$buildLog = base_path('../mailwolt-frontend-build.log');
|
||||||
|
exec('cd ' . escapeshellarg(base_path()) . ' && npm run build --silent 2>/dev/null', $out, $rc);
|
||||||
|
if ($rc !== 0) {
|
||||||
|
$this->warn('npm run build fehlgeschlagen – bitte manuell ausführen.');
|
||||||
|
} else {
|
||||||
|
$this->info('Assets neu gebaut.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectScheme(string $host, string $envContent): string
|
||||||
|
{
|
||||||
|
// 1. APP_URL in .env bereits https?
|
||||||
|
$appUrl = $this->extractVar($envContent, 'APP_URL');
|
||||||
|
if (str_starts_with($appUrl, 'https://')) return 'https';
|
||||||
|
|
||||||
|
// 2. nginx-Konfiguration prüfen (world-readable)
|
||||||
|
foreach (glob('/etc/nginx/sites-enabled/*') ?: [] as $f) {
|
||||||
|
$c = @file_get_contents($f) ?: '';
|
||||||
|
if (str_contains($c, $host) && str_contains($c, 'ssl_certificate')) return 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. letsencrypt-Pfade (falls doch lesbar)
|
||||||
|
if (file_exists("/etc/letsencrypt/renewal/{$host}.conf")
|
||||||
|
|| is_dir("/etc/letsencrypt/live/{$host}")
|
||||||
|
|| file_exists("/etc/letsencrypt/live/{$host}/fullchain.pem")) {
|
||||||
|
return 'https';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'http';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractVar(string $content, string $key): string
|
||||||
|
{
|
||||||
|
preg_match("/^{$key}=(.*)$/m", $content, $m);
|
||||||
|
return trim($m[1] ?? '', " \t\"'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//
|
//
|
||||||
//class ProvisionCert extends Command
|
//class ProvisionCert extends Command
|
||||||
//{
|
//{
|
||||||
//// protected $signature = 'mailwolt:provision-cert
|
//// protected $signature = 'clubird:provision-cert
|
||||||
//// {domain : z.B. mail.example.com}
|
//// {domain : z.B. mail.example.com}
|
||||||
//// {--email= : E-Mail für Let\'s Encrypt}
|
//// {--email= : E-Mail für Let\'s Encrypt}
|
||||||
//// {--self-signed : Statt LE ein self-signed Zertifikat erzeugen}';
|
//// {--self-signed : Statt LE ein self-signed Zertifikat erzeugen}';
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ use Illuminate\Console\Command;
|
||||||
|
|
||||||
class WizardDomains extends Command
|
class WizardDomains extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'mailwolt:wizard-domains
|
protected $signature = 'clubird:wizard-domains
|
||||||
{--ui= : UI-Domain}
|
{--ui= : UI-Domain}
|
||||||
{--mail= : Mail-Domain}
|
{--mail= : Mail-Domain}
|
||||||
{--webmail= : Webmail-Domain}
|
{--webmail= : Webmail-Domain}
|
||||||
{--ssl=1 : SSL automatisch (1/0)}';
|
{--ssl=1 : SSL automatisch (1/0)}';
|
||||||
|
protected $aliases = ['mailwolt:wizard-domains'];
|
||||||
|
|
||||||
protected $description = 'Wizard: Domains einrichten mit Status-Dateien';
|
protected $description = 'Wizard: Domains einrichten mit Status-Dateien';
|
||||||
|
|
||||||
|
|
@ -26,7 +27,6 @@ class WizardDomains extends Command
|
||||||
|
|
||||||
@mkdir(self::STATE_DIR, 0755, true);
|
@mkdir(self::STATE_DIR, 0755, true);
|
||||||
|
|
||||||
// Start: alle auf pending
|
|
||||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'pending');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'pending');
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ class WizardDomains extends Command
|
||||||
$domains = ['ui' => $ui, 'mail' => $mail, 'webmail' => $webmail];
|
$domains = ['ui' => $ui, 'mail' => $mail, 'webmail' => $webmail];
|
||||||
$allOk = true;
|
$allOk = true;
|
||||||
|
|
||||||
|
// DNS prüfen
|
||||||
foreach ($domains as $key => $domain) {
|
foreach ($domains as $key => $domain) {
|
||||||
if (!$domain) {
|
if (!$domain) {
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'skip');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'skip');
|
||||||
|
|
@ -42,47 +43,82 @@ class WizardDomains extends Command
|
||||||
|
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'running');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'running');
|
||||||
|
|
||||||
// DNS prüfen
|
|
||||||
$hasDns = checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA');
|
$hasDns = checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA');
|
||||||
if (!$hasDns) {
|
if (!$hasDns) {
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'nodns');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'nodns');
|
||||||
$allOk = false;
|
$allOk = false;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SSL-Zertifikat anfordern
|
if (!$allOk) {
|
||||||
if ($ssl) {
|
// Domains die noch auf "running" stehen wurden nie verarbeitet → error
|
||||||
$out = shell_exec(sprintf(
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
'sudo -n certbot certonly --nginx --non-interactive --agree-tos -m root@%s -d %s 2>&1',
|
$status = trim((string) @file_get_contents(self::STATE_DIR . "/{$key}"));
|
||||||
escapeshellarg($domain),
|
if ($status === 'running') {
|
||||||
escapeshellarg($domain)
|
|
||||||
));
|
|
||||||
$certOk = str_contains((string) $out, 'Successfully') || str_contains((string) $out, 'Certificate not yet due for renewal');
|
|
||||||
if (!$certOk) {
|
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'error');
|
file_put_contents(self::STATE_DIR . "/{$key}", 'error');
|
||||||
$allOk = false;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
file_put_contents(self::STATE_DIR . '/done', '0');
|
||||||
file_put_contents(self::STATE_DIR . "/{$key}", 'done');
|
Setting::set('ssl_configured', '0');
|
||||||
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nginx neu konfigurieren (alle Domains auf einmal)
|
// Nginx-Vhosts + optionales SSL via mailwolt-apply-domains
|
||||||
if ($allOk) {
|
// Das Script erstellt erst die Vhosts (mit ACME-Location), dann certbot --webroot
|
||||||
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
||||||
shell_exec(sprintf(
|
$out = shell_exec(sprintf(
|
||||||
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d 2>&1',
|
'sudo -n %s --ui-host %s --webmail-host %s --mail-host %s --ssl-auto %d',
|
||||||
escapeshellarg($helper),
|
escapeshellarg($helper),
|
||||||
escapeshellarg($ui),
|
escapeshellarg($ui),
|
||||||
escapeshellarg($webmail),
|
escapeshellarg($webmail),
|
||||||
escapeshellarg($mail),
|
escapeshellarg($mail),
|
||||||
$ssl ? 1 : 0,
|
$ssl ? 1 : 0,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Shell-Script schreibt per-Domain-Status selbst in die State-Dateien.
|
||||||
|
// Fallback: Domains die noch auf running/pending stehen auf error setzen.
|
||||||
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
|
$status = trim((string) @file_get_contents(self::STATE_DIR . "/{$key}"));
|
||||||
|
if ($status === 'running' || $status === 'pending') {
|
||||||
|
file_put_contents(self::STATE_DIR . "/{$key}", 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents(self::STATE_DIR . '/done', $allOk ? '1' : '0');
|
// done-Datei: Shell-Script schreibt "1"/"0"; Fallback wenn Script abstürzte.
|
||||||
Setting::set('ssl_configured', $allOk ? '1' : '0');
|
$doneVal = trim((string) @file_get_contents(self::STATE_DIR . '/done'));
|
||||||
|
if ($doneVal === '') {
|
||||||
|
file_put_contents(self::STATE_DIR . '/done', '0');
|
||||||
|
$doneVal = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ssl_configured anhand tatsächlich ausgestellter LE-Zertifikate bestimmen
|
||||||
|
$hasAnyCert = false;
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
if ($domain && is_dir("/etc/letsencrypt/live/{$domain}")) {
|
||||||
|
$hasAnyCert = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Setting::set('ssl_configured', $hasAnyCert ? '1' : '0');
|
||||||
|
|
||||||
|
// SESSION_SECURE_COOKIE wird nicht automatisch gesetzt —
|
||||||
|
// nginx leitet HTTP→HTTPS weiter, Secure-Flag wird im Admin gesetzt
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function updateEnv(string $path, string $key, string $value): void
|
||||||
|
{
|
||||||
|
$content = @file_get_contents($path) ?: '';
|
||||||
|
$pattern = '/^' . preg_quote($key, '/') . '=[^\r\n]*/m';
|
||||||
|
$line = $key . '=' . $value;
|
||||||
|
|
||||||
|
if (preg_match($pattern, $content)) {
|
||||||
|
$content = preg_replace($pattern, $line, $content);
|
||||||
|
} else {
|
||||||
|
$content .= "\n{$line}";
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($path, $content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,20 @@ namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class LoginController extends Controller
|
class LoginController extends Controller
|
||||||
{
|
{
|
||||||
public function show()
|
public function show()
|
||||||
{
|
{
|
||||||
return view('auth.login'); // enthält @livewire('login-form')
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(Request $request)
|
||||||
|
{
|
||||||
|
Auth::logout();
|
||||||
|
$request->session()->invalidate();
|
||||||
|
$request->session()->regenerateToken();
|
||||||
|
return redirect()->route('login');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,10 @@ class ValidateHost
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$base = config('mailwolt.domain.base');
|
$base = config('clubird.domain.base');
|
||||||
$uiSub = config('mailwolt.domain.ui');
|
$uiSub = config('clubird.domain.ui');
|
||||||
$mtaSub = config('mailwolt.domain.mail');
|
$mtaSub = config('clubird.domain.mail');
|
||||||
$wmHost = config('mailwolt.domain.webmail_host');
|
$wmHost = config('clubird.domain.webmail_host');
|
||||||
|
|
||||||
$allowed = array_filter([
|
$allowed = array_filter([
|
||||||
$wmHost,
|
$wmHost,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class ClamavEnable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public int $timeout = 120;
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
exec('sudo -n /usr/local/sbin/mailwolt-clamav enable 2>&1', $out, $rc);
|
||||||
|
\Log::info('ClamavEnable job', ['rc' => $rc, 'out' => $out]);
|
||||||
|
if ($rc !== 0) {
|
||||||
|
throw new \RuntimeException('mailwolt-clamav enable failed (rc=' . $rc . '): ' . implode(' ', $out));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ class InstallDkimKey implements ShouldQueue
|
||||||
public int $dkimKeyId,
|
public int $dkimKeyId,
|
||||||
public string $privPath,
|
public string $privPath,
|
||||||
public string $dnsTxtContent,
|
public string $dnsTxtContent,
|
||||||
public string $selector = 'mwl1',
|
public string $selector = 'clb1',
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ class ProvisionCertJob implements ShouldQueue
|
||||||
if ($this->useLetsEncrypt) {
|
if ($this->useLetsEncrypt) {
|
||||||
$this->emit($task, 'running', 'Let’s Encrypt wird ausgeführt…', $mode);
|
$this->emit($task, 'running', 'Let’s Encrypt wird ausgeführt…', $mode);
|
||||||
|
|
||||||
$exit = Artisan::call('mailwolt:provision-cert', [
|
$exit = Artisan::call('clubird:provision-cert', [
|
||||||
'domain' => $this->domain,
|
'domain' => $this->domain,
|
||||||
'--email' => $this->email ?? '',
|
'--email' => $this->email ?? '',
|
||||||
]);
|
]);
|
||||||
|
|
@ -83,14 +83,14 @@ class ProvisionCertJob implements ShouldQueue
|
||||||
|
|
||||||
// Fallback → self-signed
|
// Fallback → self-signed
|
||||||
$mode = 'self-signed';
|
$mode = 'self-signed';
|
||||||
$exit = Artisan::call('mailwolt:provision-cert', [
|
$exit = Artisan::call('clubird:provision-cert', [
|
||||||
'domain' => $this->domain,
|
'domain' => $this->domain,
|
||||||
'--self-signed' => true,
|
'--self-signed' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->emit($task, 'running', 'Self-Signed Zertifikat wird erstellt…', $mode);
|
$this->emit($task, 'running', 'Self-Signed Zertifikat wird erstellt…', $mode);
|
||||||
$exit = Artisan::call('mailwolt:provision-cert', [
|
$exit = Artisan::call('clubird:provision-cert', [
|
||||||
'domain' => $this->domain,
|
'domain' => $this->domain,
|
||||||
'--self-signed' => true,
|
'--self-signed' => true,
|
||||||
]);
|
]);
|
||||||
|
|
@ -120,7 +120,7 @@ class ProvisionCertJob implements ShouldQueue
|
||||||
// $task->update(['message' => 'Let’s Encrypt wird ausgeführt…']);
|
// $task->update(['message' => 'Let’s Encrypt wird ausgeführt…']);
|
||||||
// $this->syncCache($task);
|
// $this->syncCache($task);
|
||||||
//
|
//
|
||||||
// $exit = Artisan::call('mailwolt:provision-cert', [
|
// $exit = Artisan::call('clubird:provision-cert', [
|
||||||
// 'domain' => $this->domain,
|
// 'domain' => $this->domain,
|
||||||
// '--email' => $this->email ?? '',
|
// '--email' => $this->email ?? '',
|
||||||
// ]);
|
// ]);
|
||||||
|
|
@ -131,7 +131,7 @@ class ProvisionCertJob implements ShouldQueue
|
||||||
// $this->syncCache($task);
|
// $this->syncCache($task);
|
||||||
//
|
//
|
||||||
// // Fallback: Self-Signed
|
// // Fallback: Self-Signed
|
||||||
// $exit = Artisan::call('mailwolt:provision-cert', [
|
// $exit = Artisan::call('clubird:provision-cert', [
|
||||||
// 'domain' => $this->domain,
|
// 'domain' => $this->domain,
|
||||||
// '--self-signed' => true,
|
// '--self-signed' => true,
|
||||||
// ]);
|
// ]);
|
||||||
|
|
@ -140,7 +140,7 @@ class ProvisionCertJob implements ShouldQueue
|
||||||
// $task->update(['message' => 'Self-Signed wird erstellt…']);
|
// $task->update(['message' => 'Self-Signed wird erstellt…']);
|
||||||
// $this->syncCache($task);
|
// $this->syncCache($task);
|
||||||
//
|
//
|
||||||
// $exit = Artisan::call('mailwolt:provision-cert', [
|
// $exit = Artisan::call('clubird:provision-cert', [
|
||||||
// 'domain' => $this->domain,
|
// 'domain' => $this->domain,
|
||||||
// '--self-signed' => true,
|
// '--self-signed' => true,
|
||||||
// ]);
|
// ]);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ class LoginForm extends Component
|
||||||
|
|
||||||
public string $name = '';
|
public string $name = '';
|
||||||
public string $password = '';
|
public string $password = '';
|
||||||
|
public bool $remember = false;
|
||||||
public ?string $error = null;
|
public ?string $error = null;
|
||||||
public bool $show = false;
|
public bool $show = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,18 @@ class Wizard extends Component
|
||||||
|
|
||||||
private const STATE_DIR = '/var/lib/mailwolt/wizard';
|
private const STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||||
|
|
||||||
public function mount(): void
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->instance_name = config('app.name', 'Mailwolt');
|
$this->instance_name = config('app.name', 'Mailwolt');
|
||||||
$this->timezone = Setting::get('timezone', 'Europe/Berlin');
|
try {
|
||||||
$this->locale = Setting::get('locale', 'de');
|
$this->timezone = Setting::get('timezone', 'Europe/Berlin');
|
||||||
$this->ui_domain = Setting::get('ui_domain', '');
|
$this->locale = Setting::get('locale', 'de');
|
||||||
$this->mail_domain = Setting::get('mail_domain', '');
|
$this->ui_domain = Setting::get('ui_domain', '');
|
||||||
$this->webmail_domain = Setting::get('webmail_domain', '');
|
$this->mail_domain = Setting::get('mail_domain', '');
|
||||||
|
$this->webmail_domain = Setting::get('webmail_domain', '');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// DB noch nicht migriert — Standardwerte bleiben
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedUiDomain(): void { $this->fillEmptyDomains($this->ui_domain); }
|
public function updatedUiDomain(): void { $this->fillEmptyDomains($this->ui_domain); }
|
||||||
|
|
@ -90,10 +94,10 @@ class Wizard extends Component
|
||||||
3 => $this->validate([
|
3 => $this->validate([
|
||||||
'admin_name' => 'required|string|min:2|max:64',
|
'admin_name' => 'required|string|min:2|max:64',
|
||||||
'admin_email' => 'required|email|max:190',
|
'admin_email' => 'required|email|max:190',
|
||||||
'admin_password' => 'required|string|min:10|same:admin_password_confirmation',
|
'admin_password' => 'required|string|min:6|same:admin_password_confirmation',
|
||||||
'admin_password_confirmation' => 'required',
|
'admin_password_confirmation' => 'required',
|
||||||
], [
|
], [
|
||||||
'admin_password.min' => 'Mindestens 10 Zeichen.',
|
'admin_password.min' => 'Mindestens 6 Zeichen.',
|
||||||
'admin_password.same' => 'Passwörter stimmen nicht überein.',
|
'admin_password.same' => 'Passwörter stimmen nicht überein.',
|
||||||
]),
|
]),
|
||||||
default => null,
|
default => null,
|
||||||
|
|
@ -112,7 +116,7 @@ class Wizard extends Component
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'admin_name' => 'required|string|min:2|max:64',
|
'admin_name' => 'required|string|min:2|max:64',
|
||||||
'admin_email' => 'required|email|max:190',
|
'admin_email' => 'required|email|max:190',
|
||||||
'admin_password' => 'required|string|min:10|same:admin_password_confirmation',
|
'admin_password' => 'required|string|min:6|same:admin_password_confirmation',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Settings + .env speichern
|
// Settings + .env speichern
|
||||||
|
|
@ -148,10 +152,10 @@ class Wizard extends Component
|
||||||
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
||||||
}
|
}
|
||||||
|
|
||||||
$ssl = (!$this->skipSsl && app()->isProduction()) ? 1 : 0;
|
$ssl = $this->skipSsl ? 0 : 1;
|
||||||
$artisan = base_path('artisan');
|
$artisan = base_path('artisan');
|
||||||
$cmd = sprintf(
|
$cmd = sprintf(
|
||||||
'nohup php %s mailwolt:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
|
'nohup php %s clubird:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
|
||||||
escapeshellarg($artisan),
|
escapeshellarg($artisan),
|
||||||
escapeshellarg($this->ui_domain),
|
escapeshellarg($this->ui_domain),
|
||||||
escapeshellarg($this->mail_domain),
|
escapeshellarg($this->mail_domain),
|
||||||
|
|
@ -165,6 +169,8 @@ class Wizard extends Component
|
||||||
|
|
||||||
public function pollSetup(): void
|
public function pollSetup(): void
|
||||||
{
|
{
|
||||||
|
if ($this->setupDone) return;
|
||||||
|
|
||||||
foreach (['ui', 'mail', 'webmail'] as $key) {
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
$file = self::STATE_DIR . "/{$key}";
|
$file = self::STATE_DIR . "/{$key}";
|
||||||
$this->domainStatus[$key] = is_readable($file)
|
$this->domainStatus[$key] = is_readable($file)
|
||||||
|
|
@ -178,9 +184,36 @@ class Wizard extends Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function retryDomains(): void
|
||||||
|
{
|
||||||
|
@unlink(self::STATE_DIR . '/done');
|
||||||
|
foreach (['ui', 'mail', 'webmail'] as $k) {
|
||||||
|
file_put_contents(self::STATE_DIR . "/{$k}", 'pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->domainStatus = ['ui' => 'pending', 'mail' => 'pending', 'webmail' => 'pending'];
|
||||||
|
$this->setupDone = false;
|
||||||
|
|
||||||
|
$ssl = $this->skipSsl ? 0 : 1;
|
||||||
|
$artisan = base_path('artisan');
|
||||||
|
$cmd = sprintf(
|
||||||
|
'nohup php %s clubird:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=%d > /dev/null 2>&1 &',
|
||||||
|
escapeshellarg($artisan),
|
||||||
|
escapeshellarg($this->ui_domain),
|
||||||
|
escapeshellarg($this->mail_domain),
|
||||||
|
escapeshellarg($this->webmail_domain),
|
||||||
|
$ssl,
|
||||||
|
);
|
||||||
|
@shell_exec($cmd);
|
||||||
|
}
|
||||||
|
|
||||||
public function goToLogin(): mixed
|
public function goToLogin(): mixed
|
||||||
{
|
{
|
||||||
return redirect()->route('login')->with('setup_done', true);
|
$sslOk = Setting::get('ssl_configured', '0') === '1' && $this->ui_domain;
|
||||||
|
$url = $sslOk
|
||||||
|
? 'https://' . $this->ui_domain . '/login'
|
||||||
|
: '/login';
|
||||||
|
return redirect()->to($url)->with('setup_done', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function writeEnv(array $values): void
|
private function writeEnv(array $values): void
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class DkimStatus extends Component
|
||||||
?: optional(
|
?: optional(
|
||||||
$domain->dkimKeys()->where('is_active', true)->latest()->first()
|
$domain->dkimKeys()->where('is_active', true)->latest()->first()
|
||||||
)->selector
|
)->selector
|
||||||
?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
?: (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,7 +46,7 @@ class DkimStatus extends Component
|
||||||
// public function regenerate(?string $selector = null): void
|
// public function regenerate(?string $selector = null): void
|
||||||
// {
|
// {
|
||||||
// $selector = $selector
|
// $selector = $selector
|
||||||
// ?: ($this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'));
|
// ?: ($this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'clb1'));
|
||||||
//
|
//
|
||||||
// Log::info('DKIM regenerate() CLICKED', [
|
// Log::info('DKIM regenerate() CLICKED', [
|
||||||
// 'domain' => $this->domain->domain,
|
// 'domain' => $this->domain->domain,
|
||||||
|
|
@ -79,7 +79,7 @@ class DkimStatus extends Component
|
||||||
public function regenerate(?string $selector = null): void
|
public function regenerate(?string $selector = null): void
|
||||||
{
|
{
|
||||||
$selector = $selector
|
$selector = $selector
|
||||||
?: ($this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1'));
|
?: ($this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'clb1'));
|
||||||
|
|
||||||
Log::info('DKIM regenerate() CLICKED', [
|
Log::info('DKIM regenerate() CLICKED', [
|
||||||
'domain' => $this->domain->domain,
|
'domain' => $this->domain->domain,
|
||||||
|
|
@ -126,7 +126,7 @@ class DkimStatus extends Component
|
||||||
|
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
$sel = $this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
$sel = $this->selector ?: (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
$dkimOk = $this->isDkimReady($this->domain->domain, $sel);
|
$dkimOk = $this->isDkimReady($this->domain->domain, $sel);
|
||||||
|
|
||||||
return view('livewire.ui.domain.dkim-status', compact('dkimOk'));
|
return view('livewire.ui.domain.dkim-status', compact('dkimOk'));
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ class DomainCreateModal extends ModalComponent
|
||||||
$this->max_quota_per_mailbox_mb = config('mailpool.defaults.max_quota_per_mailbox_mb', 3072);
|
$this->max_quota_per_mailbox_mb = config('mailpool.defaults.max_quota_per_mailbox_mb', 3072);
|
||||||
$this->total_quota_mb = (int)config('mailpool.defaults.total_quota_mb', 10240);
|
$this->total_quota_mb = (int)config('mailpool.defaults.total_quota_mb', 10240);
|
||||||
|
|
||||||
$this->dkim_selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
$this->dkim_selector = (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
$this->dkim_bits = (int) config('mailpool.defaults.dkim_bits', 2048);
|
$this->dkim_bits = (int) config('mailpool.defaults.dkim_bits', 2048);
|
||||||
|
|
||||||
// Speicherpool-Grenze
|
// Speicherpool-Grenze
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,13 @@ class DomainDnsModal extends ModalComponent
|
||||||
$ipv6 = $ips['ipv6'];
|
$ipv6 = $ips['ipv6'];
|
||||||
|
|
||||||
$this->zone = $this->extractZone($d->domain);
|
$this->zone = $this->extractZone($d->domain);
|
||||||
$mta_sub = env('MTA_SUB');
|
|
||||||
$base = env('BASE_DOMAIN');
|
// Setting::get('mail_domain') liefert den vollen MTA-FQDN (z.B. mail.example.com)
|
||||||
$mailServerFqdn = "{$mta_sub}.{$base}";
|
// Fallback auf config-Werte, die env() sicher kapseln
|
||||||
|
$mailDomainSetting = strtolower(trim((string) \App\Models\Setting::get('mail_domain', '')));
|
||||||
|
$mta_sub = config('mailpool.mta_sub', 'mail');
|
||||||
|
$base = config('mailpool.platform_zone', 'example.com');
|
||||||
|
$mailServerFqdn = $mailDomainSetting ?: "{$mta_sub}.{$base}";
|
||||||
|
|
||||||
// --- Infrastruktur (global) ---
|
// --- Infrastruktur (global) ---
|
||||||
$this->static = [
|
$this->static = [
|
||||||
|
|
@ -88,7 +92,7 @@ class DomainDnsModal extends ModalComponent
|
||||||
$dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
|
$dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
|
||||||
|
|
||||||
$dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
$dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
||||||
$selector = $dkim ? $dkim->selector : 'mwl1';
|
$selector = $dkim ? $dkim->selector : 'clb1';
|
||||||
$dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
$dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
||||||
$dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
$dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
||||||
? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt)
|
? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt)
|
||||||
|
|
@ -167,7 +171,7 @@ class DomainDnsModal extends ModalComponent
|
||||||
// --- Komfort / Web ---
|
// --- Komfort / Web ---
|
||||||
[
|
[
|
||||||
'type' => 'CNAME',
|
'type' => 'CNAME',
|
||||||
'name' => env('WEBMAIL_SUB') . ".{$this->domainName}",
|
'name' => (config('clubird.domain.webmail') ?: env('WEBMAIL_SUB', 'webmail')) . ".{$this->domainName}",
|
||||||
'value' => "{$mailServerFqdn}.",
|
'value' => "{$mailServerFqdn}.",
|
||||||
'helpLabel' => 'Webmail Alias',
|
'helpLabel' => 'Webmail Alias',
|
||||||
'helpUrl' => 'https://en.wikipedia.org/wiki/CNAME_record',
|
'helpUrl' => 'https://en.wikipedia.org/wiki/CNAME_record',
|
||||||
|
|
@ -670,7 +674,7 @@ class DomainDnsModal extends ModalComponent
|
||||||
// $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
|
// $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
|
||||||
//
|
//
|
||||||
// $dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
// $dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
||||||
// $selector = $dkim ? $dkim->selector : 'mwl1';
|
// $selector = $dkim ? $dkim->selector : 'clb1';
|
||||||
// $dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
// $dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
||||||
// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
||||||
// ? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt)
|
// ? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt)
|
||||||
|
|
@ -1055,7 +1059,7 @@ class DomainDnsModal extends ModalComponent
|
||||||
//
|
//
|
||||||
// $dkim = DB::table('dkim_keys')
|
// $dkim = DB::table('dkim_keys')
|
||||||
// ->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
// ->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first();
|
||||||
// $selector = $dkim ? $dkim->selector : 'mwl1';
|
// $selector = $dkim ? $dkim->selector : 'clb1';
|
||||||
// $dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
// $dkimHost = "{$selector}._domainkey.{$this->domainName}";
|
||||||
// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=')
|
||||||
// ? 'v=DKIM1; k=rsa; p=' . $dkim->public_key_txt
|
// ? 'v=DKIM1; k=rsa; p=' . $dkim->public_key_txt
|
||||||
|
|
@ -1172,10 +1176,10 @@ class DomainDnsModal extends ModalComponent
|
||||||
////
|
////
|
||||||
//// // Placeholder-Werte, sofern du sie anderswo speicherst gern ersetzen:
|
//// // Placeholder-Werte, sofern du sie anderswo speicherst gern ersetzen:
|
||||||
//// $serverIp = config('app.server_ip', 'DEINE.SERVER.IP');
|
//// $serverIp = config('app.server_ip', 'DEINE.SERVER.IP');
|
||||||
//// $mxHost = config('mailwolt.mx_fqdn', 'mx.' . $this->domain->domain);
|
//// $mxHost = config('clubird.mx_fqdn', 'mx.' . $this->domain->domain);
|
||||||
//// $selector = optional(
|
//// $selector = optional(
|
||||||
//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first()
|
//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first()
|
||||||
//// )->selector ?? 'mwl1';
|
//// )->selector ?? 'clb1';
|
||||||
////
|
////
|
||||||
//// $dkimTxt = optional(
|
//// $dkimTxt = optional(
|
||||||
//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first()
|
//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first()
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ use App\Models\BackupJob;
|
||||||
use App\Models\BackupPolicy;
|
use App\Models\BackupPolicy;
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Models\MailUser;
|
use App\Models\MailUser;
|
||||||
|
use App\Models\SandboxRoute;
|
||||||
use App\Models\Setting as SettingModel;
|
use App\Models\Setting as SettingModel;
|
||||||
use App\Support\CacheVer;
|
use App\Support\CacheVer;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -19,13 +21,7 @@ class Dashboard extends Component
|
||||||
{
|
{
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
$cached = Cache::get(CacheVer::k('health:services'), []);
|
$services = $this->loadServices();
|
||||||
$rows = $cached['rows'] ?? [];
|
|
||||||
$services = array_map(fn($r) => [
|
|
||||||
'name' => $r['label'] ?? ucfirst($r['name']),
|
|
||||||
'type' => $r['hint'] ?? '',
|
|
||||||
'status' => ($r['ok'] ?? false) ? 'online' : 'offline',
|
|
||||||
], $rows);
|
|
||||||
|
|
||||||
[$cpu, $cpuCores, $cpuMhz] = $this->cpu();
|
[$cpu, $cpuCores, $cpuMhz] = $this->cpu();
|
||||||
[$ramPercent, $ramUsed, $ramTotal] = $this->ram();
|
[$ramPercent, $ramUsed, $ramTotal] = $this->ram();
|
||||||
|
|
@ -35,7 +31,15 @@ class Dashboard extends Component
|
||||||
|
|
||||||
$servicesActive = count(array_filter($services, fn($s) => $s['status'] === 'online'));
|
$servicesActive = count(array_filter($services, fn($s) => $s['status'] === 'online'));
|
||||||
|
|
||||||
$sslConfigured = SettingModel::get('ssl_configured', '1') === '1';
|
// Echte Zertifikatsdateien prüfen — nicht nur DB-Wert (kann veraltet sein)
|
||||||
|
$uiDomain = (string) SettingModel::get('ui_domain', '');
|
||||||
|
$sslConfigured = !$uiDomain
|
||||||
|
|| file_exists("/etc/letsencrypt/renewal/{$uiDomain}.conf")
|
||||||
|
|| is_dir("/etc/letsencrypt/live/{$uiDomain}");
|
||||||
|
// DB-Wert nachziehen damit Banner nach einmaligem Check dauerhaft weg ist
|
||||||
|
if ($sslConfigured && SettingModel::get('ssl_configured', '0') !== '1') {
|
||||||
|
SettingModel::set('ssl_configured', '1');
|
||||||
|
}
|
||||||
|
|
||||||
return view('livewire.ui.nx.dashboard', [
|
return view('livewire.ui.nx.dashboard', [
|
||||||
'sslConfigured' => $sslConfigured,
|
'sslConfigured' => $sslConfigured,
|
||||||
|
|
@ -43,7 +47,8 @@ class Dashboard extends Component
|
||||||
'mailboxCount' => MailUser::where('is_system', false)->where('is_active', true)->count(),
|
'mailboxCount' => MailUser::where('is_system', false)->where('is_active', true)->count(),
|
||||||
'servicesActive' => $servicesActive,
|
'servicesActive' => $servicesActive,
|
||||||
'servicesTotal' => count($services),
|
'servicesTotal' => count($services),
|
||||||
'alertCount' => 0,
|
'alertCount' => SandboxRoute::where('is_active', true)->count(),
|
||||||
|
'sandboxAlerts' => SandboxRoute::activeRoutes(),
|
||||||
'backup' => $this->backupData(),
|
'backup' => $this->backupData(),
|
||||||
'mailHostname' => gethostname() ?: 'mailserver',
|
'mailHostname' => gethostname() ?: 'mailserver',
|
||||||
'services' => $services,
|
'services' => $services,
|
||||||
|
|
@ -62,12 +67,82 @@ class Dashboard extends Component
|
||||||
'diskUsedGb' => $diskUsedGb,
|
'diskUsedGb' => $diskUsedGb,
|
||||||
'diskFreeGb' => $diskFreeGb,
|
'diskFreeGb' => $diskFreeGb,
|
||||||
'diskTotalGb' => $diskTotalGb,
|
'diskTotalGb' => $diskTotalGb,
|
||||||
|
'updateLatest' => Cache::get('updates:latest_raw') ?: (Cache::get('updates:latest') ? 'v' . Cache::get('updates:latest') : null),
|
||||||
...$this->mailSecurity(),
|
...$this->mailSecurity(),
|
||||||
'ports' => $this->ports(),
|
'ports' => $this->ports(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function loadServices(): array
|
||||||
|
{
|
||||||
|
$allCards = config('woltguard.cards', []);
|
||||||
|
$dashKeys = config('woltguard.dashboard', array_keys($allCards));
|
||||||
|
|
||||||
|
// 1) Monit-Cache für nicht-optionale Dienste
|
||||||
|
$cached = Cache::get(CacheVer::k('health:services'), []);
|
||||||
|
$monitRows = $cached['rows'] ?? [];
|
||||||
|
$monitIndex = [];
|
||||||
|
foreach ($monitRows as $r) {
|
||||||
|
$monitIndex[strtolower($r['name'] ?? '')] = $r;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
foreach ($dashKeys as $key) {
|
||||||
|
$card = $allCards[$key] ?? null;
|
||||||
|
if (!$card) continue;
|
||||||
|
$optional = $card['optional'] ?? false;
|
||||||
|
|
||||||
|
// Optionale Dienste (z.B. ClamAV) immer live prüfen – Monit-Cache kann veraltet sein
|
||||||
|
if (!$optional && isset($monitIndex[strtolower($card['label'] ?? '')])) {
|
||||||
|
$rows[] = $monitIndex[strtolower($card['label'])];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isOk = false;
|
||||||
|
foreach ($card['sources'] as $src) {
|
||||||
|
if ($this->probeSource($src)) { $isOk = true; break; }
|
||||||
|
}
|
||||||
|
if (!$isOk && $optional) continue;
|
||||||
|
$rows[] = ['label' => $card['label'], 'hint' => $card['hint'], 'ok' => $isOk];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: wenn gar keine Daten, alle live proben
|
||||||
|
if (empty($rows) && !empty($monitRows)) {
|
||||||
|
$rows = $monitRows;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map(fn($r) => [
|
||||||
|
'name' => $r['label'] ?? ucfirst($r['name'] ?? ''),
|
||||||
|
'type' => $r['hint'] ?? '',
|
||||||
|
'status' => ($r['ok'] ?? false) ? 'online' : 'offline',
|
||||||
|
], $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function probeSource(string $src): bool
|
||||||
|
{
|
||||||
|
if (str_starts_with($src, 'systemd:')) {
|
||||||
|
$unit = substr($src, 8);
|
||||||
|
$exit = null;
|
||||||
|
@exec("systemctl is-active --quiet " . escapeshellarg($unit) . " 2>/dev/null", $_, $exit);
|
||||||
|
return $exit === 0;
|
||||||
|
}
|
||||||
|
if (str_starts_with($src, 'tcp:')) {
|
||||||
|
[, $host, $port] = explode(':', $src, 3);
|
||||||
|
$fp = @fsockopen($host, (int)$port, $e1, $e2, 1);
|
||||||
|
if (is_resource($fp)) { fclose($fp); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (str_starts_with($src, 'socket:')) {
|
||||||
|
return @file_exists(substr($src, 7));
|
||||||
|
}
|
||||||
|
if ($src === 'db') {
|
||||||
|
try { \Illuminate\Support\Facades\DB::connection()->getPdo(); return true; }
|
||||||
|
catch (\Throwable) { return false; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private function ports(): array
|
private function ports(): array
|
||||||
{
|
{
|
||||||
$check = [25, 465, 587, 110, 143, 993, 995, 80, 443];
|
$check = [25, 465, 587, 110, 143, 993, 995, 80, 443];
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ namespace App\Livewire\Ui\Nx\Domain;
|
||||||
|
|
||||||
use App\Models\Domain;
|
use App\Models\Domain;
|
||||||
use App\Services\DkimService;
|
use App\Services\DkimService;
|
||||||
use Illuminate\Support\Facades\Process;
|
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\On;
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
|
|
@ -39,27 +38,12 @@ class DnsDkim extends Component
|
||||||
{
|
{
|
||||||
$domain = Domain::findOrFail($id);
|
$domain = Domain::findOrFail($id);
|
||||||
$selector = optional($domain->dkimKeys()->where('is_active', true)->latest()->first())->selector
|
$selector = optional($domain->dkimKeys()->where('is_active', true)->latest()->first())->selector
|
||||||
?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
?: (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
/** @var DkimService $svc */
|
/** @var DkimService $svc */
|
||||||
$svc = app(DkimService::class);
|
$svc = app(DkimService::class);
|
||||||
$res = $svc->generateForDomain($domain, 2048, $selector);
|
$svc->generateForDomain($domain, 2048, $selector);
|
||||||
$priv = $res['priv_path'] ?? storage_path("app/private/dkim/{$domain->domain}/{$selector}.private");
|
|
||||||
$txt = storage_path("app/private/dkim/{$domain->domain}/{$selector}.txt");
|
|
||||||
|
|
||||||
if (!is_readable($txt) && !empty($res['dns_txt'])) {
|
|
||||||
file_put_contents($txt, $res['dns_txt']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$proc = Process::run(['sudo', '-n', '/usr/local/sbin/mailwolt-install-dkim',
|
|
||||||
$domain->domain, $selector, $priv, $txt]);
|
|
||||||
|
|
||||||
if (!$proc->successful()) {
|
|
||||||
throw new \RuntimeException($proc->errorOutput());
|
|
||||||
}
|
|
||||||
|
|
||||||
Process::run(['sudo', '-n', '/usr/bin/systemctl', 'reload', 'opendkim']);
|
|
||||||
|
|
||||||
$this->dispatch('toast', type: 'done', badge: 'DKIM',
|
$this->dispatch('toast', type: 'done', badge: 'DKIM',
|
||||||
title: 'DKIM erneuert', text: "Schlüssel für <b>{$domain->domain}</b> wurde neu generiert.", duration: 5000);
|
title: 'DKIM erneuert', text: "Schlüssel für <b>{$domain->domain}</b> wurde neu generiert.", duration: 5000);
|
||||||
|
|
@ -71,13 +55,13 @@ class DnsDkim extends Component
|
||||||
|
|
||||||
private function dkimReady(string $domain, string $selector): bool
|
private function dkimReady(string $domain, string $selector): bool
|
||||||
{
|
{
|
||||||
return Process::run(['sudo', '-n', '/usr/bin/test', '-s',
|
$path = storage_path("app/private/dkim/{$domain}/{$selector}.private");
|
||||||
"/etc/opendkim/keys/{$domain}/{$selector}.private"])->successful();
|
return is_file($path) && filesize($path) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
$defaultSelector = (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
$defaultSelector = (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
|
|
||||||
$mapped = Domain::where('is_server', false)
|
$mapped = Domain::where('is_server', false)
|
||||||
->with(['dkimKeys' => fn($q) => $q->where('is_active', true)->latest()])
|
->with(['dkimKeys' => fn($q) => $q->where('is_active', true)->latest()])
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,48 @@ class QuarantineList extends Component
|
||||||
#[On('quarantine:updated')]
|
#[On('quarantine:updated')]
|
||||||
public function refresh(): void {}
|
public function refresh(): void {}
|
||||||
|
|
||||||
|
// ── Löschen (lokal via Cache, RSpamd hat keine per-entry API) ────────────
|
||||||
|
|
||||||
|
private function hiddenKey(): string
|
||||||
|
{
|
||||||
|
return 'quarantine.hidden.' . session()->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHidden(): array
|
||||||
|
{
|
||||||
|
return \Cache::get($this->hiddenKey(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveHidden(array $ids): void
|
||||||
|
{
|
||||||
|
\Cache::put($this->hiddenKey(), array_values(array_unique($ids)), now()->addDays(7));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteEntry(string $id): void
|
||||||
|
{
|
||||||
|
$hidden = $this->getHidden();
|
||||||
|
$hidden[] = $id;
|
||||||
|
$this->saveHidden($hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteCurrentTab(): void
|
||||||
|
{
|
||||||
|
$all = $this->fetchHistory();
|
||||||
|
$hidden = $this->getHidden();
|
||||||
|
|
||||||
|
$toHide = match($this->filter) {
|
||||||
|
'suspicious' => array_filter($all, fn($m) => $m['action'] !== 'no action'),
|
||||||
|
'all' => $all,
|
||||||
|
default => array_filter($all, fn($m) => $m['action'] === $this->filter),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($toHide as $m) {
|
||||||
|
$hidden[] = $m['id'];
|
||||||
|
}
|
||||||
|
$this->saveHidden($hidden);
|
||||||
|
$this->resetPage();
|
||||||
|
}
|
||||||
|
|
||||||
public function updatedFilter(): void { $this->resetPage(); }
|
public function updatedFilter(): void { $this->resetPage(); }
|
||||||
public function updatedSearch(): void { $this->resetPage(); }
|
public function updatedSearch(): void { $this->resetPage(); }
|
||||||
public function updatedPerPage(): void { $this->resetPage(); }
|
public function updatedPerPage(): void { $this->resetPage(); }
|
||||||
|
|
@ -43,7 +85,11 @@ class QuarantineList extends Component
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
$all = $this->fetchHistory();
|
$hidden = $this->getHidden();
|
||||||
|
$all = array_values(array_filter(
|
||||||
|
$this->fetchHistory(),
|
||||||
|
fn($m) => !in_array($m['id'], $hidden, true)
|
||||||
|
));
|
||||||
|
|
||||||
$suspicious = array_values(array_filter($all, fn($m) => $m['action'] !== 'no action'));
|
$suspicious = array_values(array_filter($all, fn($m) => $m['action'] !== 'no action'));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -126,21 +126,20 @@ class SearchPaletteModal extends ModalComponent
|
||||||
|
|
||||||
public function go(string $type, int $id): void
|
public function go(string $type, int $id): void
|
||||||
{
|
{
|
||||||
// Schließe die Palette …
|
|
||||||
$this->dispatch('closeModal');
|
|
||||||
|
|
||||||
// … und navigiere / öffne Kontext:
|
|
||||||
// - Domain → scrolle/markiere Domainkarte
|
|
||||||
// - Mailbox → öffne Bearbeiten-Modal
|
|
||||||
// Passe an, was du bevorzugst:
|
|
||||||
if ($type === 'domain') {
|
if ($type === 'domain') {
|
||||||
$this->dispatch('focus:domain', id: $id);
|
$component = 'ui.domain.modal.domain-edit-modal';
|
||||||
|
$arguments = ['domainId' => $id];
|
||||||
} elseif ($type === 'mailbox') {
|
} elseif ($type === 'mailbox') {
|
||||||
// direkt Edit-Modal auf
|
$component = 'ui.mail.modal.mailbox-edit-modal';
|
||||||
$this->dispatch('openModal', component:'ui.mail.modal.mailbox-edit-modal', arguments: [$id]);
|
$arguments = ['mailboxId' => $id];
|
||||||
} elseif ($type === 'user') {
|
} else {
|
||||||
$this->dispatch('focus:user', id: $id);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search palette schließen, dann nach dem State-Reset (300 ms) das Ziel-Modal öffnen
|
||||||
|
$this->forceClose()->closeModal();
|
||||||
|
$payload = json_encode(['component' => $component, 'arguments' => $arguments]);
|
||||||
|
$this->js("setTimeout(()=>Livewire.dispatch('openModal',{$payload}),350)");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function modalMaxWidth(): string
|
public static function modalMaxWidth(): string
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\Security;
|
||||||
|
|
||||||
|
use App\Support\CacheVer;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Attributes\Title;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('layouts.dvx')]
|
||||||
|
#[Title('Virenschutz · Mailwolt')]
|
||||||
|
class ClamavManager extends Component
|
||||||
|
{
|
||||||
|
public bool $running = false;
|
||||||
|
public bool $enabled = false;
|
||||||
|
public string $dbDate = '—';
|
||||||
|
public string $dbVersion = '—';
|
||||||
|
public ?int $ramMb = null;
|
||||||
|
public string $lastError = '';
|
||||||
|
public string $lastSuccess = '';
|
||||||
|
public bool $starting = false;
|
||||||
|
public int $startSecs = 0;
|
||||||
|
|
||||||
|
private const STARTING_FLAG = '/tmp/mw-clamav-starting';
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$this->refresh();
|
||||||
|
$this->restoreStartingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function restoreStartingState(): void
|
||||||
|
{
|
||||||
|
if (!file_exists(self::STARTING_FLAG)) return;
|
||||||
|
if ($this->running) {
|
||||||
|
@unlink(self::STARTING_FLAG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$elapsed = time() - (int) @file_get_contents(self::STARTING_FLAG);
|
||||||
|
if ($elapsed > 180) {
|
||||||
|
@unlink(self::STARTING_FLAG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->starting = true;
|
||||||
|
$this->startSecs = max(0, $elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refresh(): void
|
||||||
|
{
|
||||||
|
$this->lastError = '';
|
||||||
|
$this->lastSuccess = '';
|
||||||
|
$this->running = $this->serviceActive();
|
||||||
|
$this->enabled = $this->serviceEnabled();
|
||||||
|
$this->ramMb = $this->running ? $this->readRamMb() : null;
|
||||||
|
[$this->dbDate, $this->dbVersion] = $this->readDbInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enable(): void
|
||||||
|
{
|
||||||
|
$this->lastError = '';
|
||||||
|
$this->lastSuccess = '';
|
||||||
|
[$ok, $msg] = $this->runCmd('enable');
|
||||||
|
if (!$ok) {
|
||||||
|
$this->lastError = $msg;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file_put_contents(self::STARTING_FLAG, time());
|
||||||
|
$this->enabled = true;
|
||||||
|
$this->starting = true;
|
||||||
|
$this->startSecs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pollStatus(): void
|
||||||
|
{
|
||||||
|
if (!$this->starting) return;
|
||||||
|
$this->startSecs += 3;
|
||||||
|
$this->running = $this->serviceActive();
|
||||||
|
if ($this->running) {
|
||||||
|
@unlink(self::STARTING_FLAG);
|
||||||
|
$this->starting = false;
|
||||||
|
$this->startSecs = 0;
|
||||||
|
$this->enabled = $this->serviceEnabled();
|
||||||
|
$this->ramMb = $this->readRamMb();
|
||||||
|
$this->lastSuccess = 'ClamAV ist aktiv und läuft.';
|
||||||
|
Cache::forget(CacheVer::k('health:services'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disable(): void
|
||||||
|
{
|
||||||
|
[$ok, $msg] = $this->runCmd('disable');
|
||||||
|
if ($ok) $this->lastSuccess = 'ClamAV wurde gestoppt und deaktiviert.';
|
||||||
|
else $this->lastError = $msg;
|
||||||
|
$this->running = $this->serviceActive();
|
||||||
|
$this->enabled = $this->serviceEnabled();
|
||||||
|
Cache::forget(CacheVer::k('health:services'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateDb(): void
|
||||||
|
{
|
||||||
|
[$ok, $msg] = $this->runCmd('freshclam');
|
||||||
|
if ($ok) $this->lastSuccess = 'Virensignaturen wurden aktualisiert.';
|
||||||
|
else $this->lastError = $msg;
|
||||||
|
[$this->dbDate, $this->dbVersion] = $this->readDbInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function runCmd(string $action): array
|
||||||
|
{
|
||||||
|
$out = []; $rc = null;
|
||||||
|
exec('sudo -n /usr/local/sbin/mailwolt-clamav ' . escapeshellarg($action) . ' 2>&1', $out, $rc);
|
||||||
|
\Log::info('ClamAV ' . $action, ['rc' => $rc, 'out' => $out]);
|
||||||
|
$msg = implode(' ', $out) ?: 'Unbekannter Fehler (rc=' . $rc . ')';
|
||||||
|
return [$rc === 0, $msg];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isInstalled(): bool
|
||||||
|
{
|
||||||
|
return file_exists('/usr/sbin/clamd')
|
||||||
|
|| file_exists('/lib/systemd/system/clamav-daemon.service')
|
||||||
|
|| file_exists('/usr/lib/systemd/system/clamav-daemon.service');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serviceActive(): bool
|
||||||
|
{
|
||||||
|
$exit = null;
|
||||||
|
@exec('systemctl is-active --quiet clamav-daemon 2>/dev/null', $_, $exit);
|
||||||
|
return $exit === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serviceEnabled(): bool
|
||||||
|
{
|
||||||
|
$exit = null;
|
||||||
|
@exec('systemctl is-enabled --quiet clamav-daemon 2>/dev/null', $_, $exit);
|
||||||
|
return $exit === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readRamMb(): ?int
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
@exec("ps -C clamd -o rss= 2>/dev/null", $out);
|
||||||
|
$kb = array_sum(array_map('intval', array_filter($out)));
|
||||||
|
return $kb > 0 ? (int) round($kb / 1024) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readDbInfo(): array
|
||||||
|
{
|
||||||
|
$paths = [
|
||||||
|
'/var/lib/clamav/main.cvd',
|
||||||
|
'/var/lib/clamav/main.cld',
|
||||||
|
'/var/lib/clamav/daily.cvd',
|
||||||
|
'/var/lib/clamav/daily.cld',
|
||||||
|
];
|
||||||
|
$latest = 0;
|
||||||
|
foreach ($paths as $p) {
|
||||||
|
if (@file_exists($p)) {
|
||||||
|
$mtime = @filemtime($p);
|
||||||
|
if ($mtime > $latest) $latest = $mtime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($latest === 0) return ['—', '—'];
|
||||||
|
$date = date('d.m.Y H:i', $latest);
|
||||||
|
|
||||||
|
$ver = '—';
|
||||||
|
$out = [];
|
||||||
|
@exec('sigtool --info /var/lib/clamav/daily.cld 2>/dev/null | grep "^Version:" | head -1', $out);
|
||||||
|
if (empty($out)) {
|
||||||
|
@exec('sigtool --info /var/lib/clamav/daily.cvd 2>/dev/null | grep "^Version:" | head -1', $out);
|
||||||
|
}
|
||||||
|
if (!empty($out[0])) {
|
||||||
|
$ver = trim(str_replace('Version:', '', $out[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$date, $ver];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.security.clamav-manager');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,22 +15,9 @@ class Fail2banBanlist extends Component
|
||||||
public ?string $jail = null;
|
public ?string $jail = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Struktur für Blade (reine Ausgabe, keine Logik im Blade):
|
|
||||||
* [
|
|
||||||
* [
|
|
||||||
* 'ip' => '1.2.3.4',
|
|
||||||
* 'jail' => 'recidive',
|
|
||||||
* 'permanent' => false,
|
|
||||||
* 'label' => 'Temporär', // oder 'Permanent'
|
|
||||||
* 'box' => 'border-amber-400/20 bg-white/3', // Kartenstil
|
|
||||||
* 'badge' => 'border-amber-400/30 bg-amber-500/10 text-amber-200',
|
|
||||||
* 'btn' => 'border-rose-400/30 bg-rose-500/10 text-rose-200 hover:border-rose-400/50',
|
|
||||||
* ],
|
|
||||||
* ...
|
|
||||||
* ]
|
|
||||||
*
|
|
||||||
* @var array<int,array{
|
* @var array<int,array{
|
||||||
* ip:string,jail:string,permanent:bool,label:string,box:string,badge:string,btn:string
|
* ip:string,jail:string,service:string,permanent:bool,label:string,
|
||||||
|
* remaining:string,box:string,badge:string,dot:string,btn:string
|
||||||
* }>
|
* }>
|
||||||
*/
|
*/
|
||||||
public array $rows = [];
|
public array $rows = [];
|
||||||
|
|
@ -75,7 +62,9 @@ class Fail2banBanlist extends Component
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$permanent = $this->isPermanent($j, $ip);
|
$banInfo = $this->getBanInfo($j, $ip);
|
||||||
|
$permanent = $banInfo['permanent'];
|
||||||
|
$remaining = $banInfo['remaining'];
|
||||||
|
|
||||||
if ($permanent) {
|
if ($permanent) {
|
||||||
$box = 'border-rose-400/30 bg-rose-500/5';
|
$box = 'border-rose-400/30 bg-rose-500/5';
|
||||||
|
|
@ -94,9 +83,11 @@ class Fail2banBanlist extends Component
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'ip' => $ip,
|
'ip' => $ip,
|
||||||
'jail' => $j,
|
'jail' => $j,
|
||||||
|
'service' => $this->serviceLabel($j),
|
||||||
'permanent' => $permanent,
|
'permanent' => $permanent,
|
||||||
'style' => $style,
|
'style' => $style,
|
||||||
'label' => $label,
|
'label' => $label,
|
||||||
|
'remaining' => $remaining,
|
||||||
'box' => $box,
|
'box' => $box,
|
||||||
'badge' => $badge,
|
'badge' => $badge,
|
||||||
'dot' => $dot,
|
'dot' => $dot,
|
||||||
|
|
@ -140,35 +131,63 @@ class Fail2banBanlist extends Component
|
||||||
|
|
||||||
/* ================= helpers ================= */
|
/* ================= helpers ================= */
|
||||||
|
|
||||||
/** Prüft via SQLite, ob der **letzte** Ban für (jail, ip) permanent ist (bantime < 0). */
|
/** Gibt permanent-Flag und verbleibende Zeit für (jail, ip) zurück. */
|
||||||
private function isPermanent(string $jail, string $ip): bool
|
private function getBanInfo(string $jail, string $ip): array
|
||||||
{
|
{
|
||||||
$db = $this->getDbFile();
|
$fallbackPermanent = ($jail === 'mailwolt-blacklist');
|
||||||
if ($db === '' || !is_readable($db)) {
|
|
||||||
// Fallback: Blacklist-Jail ist per Design permanent
|
$cmd = sprintf('sudo -n /usr/local/sbin/clubird-f2b-baninfo %s %s 2>&1',
|
||||||
return $jail === 'mailwolt-blacklist';
|
escapeshellarg($jail), escapeshellarg($ip));
|
||||||
|
$out = trim((string)@shell_exec($cmd));
|
||||||
|
|
||||||
|
if ($out !== '') {
|
||||||
|
[$timeofban, $bantime] = array_pad(explode('|', $out), 2, '0');
|
||||||
|
$timeofban = (int)$timeofban;
|
||||||
|
$bantime = (int)$bantime;
|
||||||
|
|
||||||
|
if ($bantime < 0) {
|
||||||
|
return ['permanent' => true, 'remaining' => ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
$remaining = ($timeofban + $bantime) - time();
|
||||||
|
if ($remaining > 0) {
|
||||||
|
return ['permanent' => false, 'remaining' => $this->formatRemaining($remaining)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$q = <<<SQL
|
// Fallback: DB-Eintrag ist abgelaufen (fail2ban-Neustart setzt Timer intern neu,
|
||||||
WITH last AS (
|
// ohne die DB zu aktualisieren) → konfigurierte Jail-Banzeit als Näherung
|
||||||
SELECT MAX(timeofban) AS t
|
$cfgBantime = (int)trim($this->f2b('get ' . escapeshellarg($jail) . ' bantime'));
|
||||||
FROM bans
|
if ($cfgBantime < 0) {
|
||||||
WHERE jail = '$jail' AND ip = '$ip'
|
return ['permanent' => true, 'remaining' => ''];
|
||||||
)
|
}
|
||||||
SELECT bantime
|
if ($cfgBantime > 0) {
|
||||||
FROM bans, last
|
return ['permanent' => false, 'remaining' => '≤ ' . $this->formatRemaining($cfgBantime)];
|
||||||
WHERE jail = '$jail' AND ip = '$ip' AND timeofban = last.t
|
}
|
||||||
LIMIT 1;
|
|
||||||
SQL;
|
|
||||||
|
|
||||||
$cmd = sprintf(
|
return ['permanent' => $fallbackPermanent, 'remaining' => ''];
|
||||||
'sudo -n /usr/bin/sqlite3 -readonly %s %s 2>&1',
|
}
|
||||||
escapeshellarg($db),
|
|
||||||
escapeshellarg($q)
|
private function formatRemaining(int $seconds): string
|
||||||
);
|
{
|
||||||
$out = trim((string)@shell_exec($cmd));
|
if ($seconds <= 0) return 'läuft ab';
|
||||||
if ($out === '') return ($jail === 'mailwolt-blacklist'); // Fallback
|
if ($seconds < 60) return "noch {$seconds} Sek.";
|
||||||
return ((int)$out) < 0;
|
if ($seconds < 3600) return 'noch ' . (int)ceil($seconds / 60) . ' Min.';
|
||||||
|
if ($seconds < 86400) return 'noch ' . round($seconds / 3600, 1) . ' Std.';
|
||||||
|
return 'noch ' . (int)ceil($seconds / 86400) . ' Tage';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serviceLabel(string $jail): string
|
||||||
|
{
|
||||||
|
return match(true) {
|
||||||
|
$jail === 'sshd' => 'SSH',
|
||||||
|
$jail === 'dovecot' => 'IMAP/POP3',
|
||||||
|
str_starts_with($jail, 'postfix') => 'SMTP',
|
||||||
|
str_starts_with($jail, 'nginx') => 'Web',
|
||||||
|
$jail === 'recidive' => 'Recidive',
|
||||||
|
str_contains($jail, 'blacklist') => 'Blacklist',
|
||||||
|
default => $jail,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Liste aller Jails */
|
/** Liste aller Jails */
|
||||||
|
|
@ -188,13 +207,4 @@ SQL;
|
||||||
return (string) @shell_exec('sudo -n /usr/bin/fail2ban-client '.$args.' 2>&1');
|
return (string) @shell_exec('sudo -n /usr/bin/fail2ban-client '.$args.' 2>&1');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pfad zur Fail2Ban-SQLite-DB holen */
|
|
||||||
private function getDbFile(): string
|
|
||||||
{
|
|
||||||
$out = $this->f2b('get dbfile');
|
|
||||||
$lines = array_values(array_filter(array_map('trim', preg_split('/\r?\n/', $out))));
|
|
||||||
$path = end($lines) ?: '';
|
|
||||||
$path = preg_replace('/^`?-?\s*/', '', $path);
|
|
||||||
return $path ?: '/var/lib/fail2ban/fail2ban.sqlite3';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ bantime.factor = {$s->bantime_factor}
|
||||||
bantime.maxtime = {$s->max_bantime}
|
bantime.maxtime = {$s->max_bantime}
|
||||||
CONF;
|
CONF;
|
||||||
|
|
||||||
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/00-mailwolt-defaults.local', $content);
|
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/00-defaults.local', $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function writeWhitelistConfig(): void
|
protected function writeWhitelistConfig(): void
|
||||||
|
|
@ -146,7 +146,7 @@ CONF;
|
||||||
|
|
||||||
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||||
|
|
||||||
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/whitelist.local', $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- Helper ---------------- */
|
/* ---------------- Helper ---------------- */
|
||||||
|
|
@ -320,7 +320,7 @@ CONF;
|
||||||
//bantime.maxtime = {$s->max_bantime}
|
//bantime.maxtime = {$s->max_bantime}
|
||||||
//CONF;
|
//CONF;
|
||||||
//
|
//
|
||||||
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/00-mailwolt-defaults.local', $content);
|
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/00-defaults.local', $content);
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// protected function writeWhitelistConfig(): void
|
// protected function writeWhitelistConfig(): void
|
||||||
|
|
@ -330,7 +330,7 @@ CONF;
|
||||||
//
|
//
|
||||||
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||||
//
|
//
|
||||||
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/whitelist.local', $content);
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// /**
|
// /**
|
||||||
|
|
@ -493,7 +493,7 @@ CONF;
|
||||||
//bantime.factor = {$s->bantime_factor}
|
//bantime.factor = {$s->bantime_factor}
|
||||||
//bantime.maxtime = {$s->max_bantime}
|
//bantime.maxtime = {$s->max_bantime}
|
||||||
//CONF;
|
//CONF;
|
||||||
// file_put_contents('/etc/fail2ban/jail.d/00-mailwolt-defaults.local', $content);
|
// file_put_contents('/etc/fail2ban/jail.d/00-defaults.local', $content);
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// protected function writeWhitelistConfig(): void
|
// protected function writeWhitelistConfig(): void
|
||||||
|
|
@ -501,7 +501,7 @@ CONF;
|
||||||
// $ips = Fail2banIpList::where('type','whitelist')->pluck('ip')->toArray();
|
// $ips = Fail2banIpList::where('type','whitelist')->pluck('ip')->toArray();
|
||||||
// $ignore = implode(' ', array_unique(array_filter($ips)));
|
// $ignore = implode(' ', array_unique(array_filter($ips)));
|
||||||
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||||
// file_put_contents('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
// file_put_contents('/etc/fail2ban/jail.d/whitelist.local', $content);
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// private function writeRootFileViaTee(string $target, string $content): void
|
// private function writeRootFileViaTee(string $target, string $content): void
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ class Fail2banIpModal extends ModalComponent
|
||||||
$ignore = implode(' ', array_unique(array_filter($ips)));
|
$ignore = implode(' ', array_unique(array_filter($ips)));
|
||||||
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
$content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||||
|
|
||||||
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
$this->writeRootFileViaTee('/etc/fail2ban/jail.d/whitelist.local', $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function writeRootFileViaTee(string $target, string $content): void
|
private function writeRootFileViaTee(string $target, string $content): void
|
||||||
|
|
@ -293,7 +293,7 @@ class Fail2banIpModal extends ModalComponent
|
||||||
// Fail2banIpList::create(['ip' => $ip, 'type' => $this->type]);
|
// Fail2banIpList::create(['ip' => $ip, 'type' => $this->type]);
|
||||||
//
|
//
|
||||||
// if ($this->type === 'whitelist') {
|
// if ($this->type === 'whitelist') {
|
||||||
// $this->writeWhitelistConfig(); // schreibt /etc/fail2ban/jail.d/mailwolt-whitelist.local
|
// $this->writeWhitelistConfig(); // schreibt /etc/fail2ban/jail.d/whitelist.local
|
||||||
// $this->reloadFail2ban(); // f2b neu laden
|
// $this->reloadFail2ban(); // f2b neu laden
|
||||||
// } else {
|
// } else {
|
||||||
// // Blacklist = sofort bannen im dedizierten Jail
|
// // Blacklist = sofort bannen im dedizierten Jail
|
||||||
|
|
@ -356,7 +356,7 @@ class Fail2banIpModal extends ModalComponent
|
||||||
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||||
//
|
//
|
||||||
// // sicher in Root-Pfad schreiben (sudo tee)
|
// // sicher in Root-Pfad schreiben (sudo tee)
|
||||||
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/mailwolt-whitelist.local', $content);
|
// $this->writeRootFileViaTee('/etc/fail2ban/jail.d/whitelist.local', $content);
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// private function writeRootFileViaTee(string $target, string $content): void
|
// private function writeRootFileViaTee(string $target, string $content): void
|
||||||
|
|
@ -540,7 +540,7 @@ class Fail2banIpModal extends ModalComponent
|
||||||
// $ignore = implode(' ', array_unique(array_filter($ips)));
|
// $ignore = implode(' ', array_unique(array_filter($ips)));
|
||||||
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
// $content = "[DEFAULT]\nignoreip = {$ignore}\n";
|
||||||
//
|
//
|
||||||
// $file = '/etc/fail2ban/jail.d/mailwolt-whitelist.local';
|
// $file = '/etc/fail2ban/jail.d/whitelist.local';
|
||||||
// $tmp = $file.'.tmp';
|
// $tmp = $file.'.tmp';
|
||||||
// @file_put_contents($tmp, $content, LOCK_EX);
|
// @file_put_contents($tmp, $content, LOCK_EX);
|
||||||
// @chmod($tmp, 0644);
|
// @chmod($tmp, 0644);
|
||||||
|
|
|
||||||
|
|
@ -2,90 +2,54 @@
|
||||||
|
|
||||||
namespace App\Livewire\Ui\Security\Modal;
|
namespace App\Livewire\Ui\Security\Modal;
|
||||||
|
|
||||||
|
use App\Services\TotpService;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Livewire\Attributes\On;
|
use Livewire\Attributes\On;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
use Vectorface\GoogleAuthenticator;
|
|
||||||
|
|
||||||
class TotpSetupModal extends ModalComponent
|
class TotpSetupModal extends ModalComponent
|
||||||
{
|
{
|
||||||
public string $secret;
|
public string $step = 'scan';
|
||||||
public string $otp = '';
|
public string $code = '';
|
||||||
public string $qrPng; // PNG Data-URI
|
public string $secret = '';
|
||||||
public bool $alreadyActive = false;
|
public array $recoveryCodes = [];
|
||||||
|
public string $qrSvg = '';
|
||||||
|
|
||||||
// << Wichtig: je Modal eigene Breite >>
|
public static function modalMaxWidth(): string { return 'md'; }
|
||||||
public static function modalMaxWidth(): string
|
|
||||||
{
|
|
||||||
// mögliche Werte: 'sm','md','lg','xl','2xl','3xl','4xl','5xl','6xl','7xl'
|
|
||||||
return 'xl'; // kompakt für TOTP
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$totp = app(TotpService::class);
|
||||||
|
$this->secret = $totp->generateSecret();
|
||||||
$ga = new GoogleAuthenticator();
|
$this->qrSvg = $totp->qrCodeSvg(Auth::user(), $this->secret);
|
||||||
|
|
||||||
// Falls User schon Secret hat: wiederverwenden, sonst neues anlegen
|
|
||||||
$this->secret = $user->totp_secret ?: $ga->createSecret();
|
|
||||||
|
|
||||||
$issuer = config('app.name', 'MailWolt');
|
|
||||||
// getQRCodeUrl(accountName, secret, issuer) => PNG Data-URI
|
|
||||||
$this->qrPng = $ga->getQRCodeUrl($user->email, $this->secret, $issuer);
|
|
||||||
|
|
||||||
$this->alreadyActive = (bool) ($user->two_factor_enabled ?? false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[On('security:totp:enable')]
|
public function verify(): void
|
||||||
public function verifyAndEnable(string $code): void
|
|
||||||
{
|
{
|
||||||
$code = preg_replace('/\D/', '', $code ?? '');
|
$this->validate(['code' => 'required|digits:6']);
|
||||||
if (strlen($code) !== 6) {
|
|
||||||
$this->dispatch('toast', body: 'Bitte 6-stelligen Code eingeben.');
|
$totp = app(TotpService::class);
|
||||||
|
|
||||||
|
if (!$totp->verify($this->secret, $this->code)) {
|
||||||
|
$this->addError('code', 'Ungültiger Code. Bitte erneut versuchen.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$ga = new GoogleAuthenticator();
|
$this->recoveryCodes = $totp->enable(Auth::user(), $this->secret);
|
||||||
$ok = $ga->verifyCode($this->secret, $code, 2); // 2 × 30 s Toleranz
|
$this->step = 'codes';
|
||||||
|
|
||||||
if (!$ok) {
|
|
||||||
$this->dispatch('toast', body: 'Code ungültig. Versuche es erneut.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = Auth::user();
|
|
||||||
$user->totp_secret = $this->secret;
|
|
||||||
$user->two_factor_enabled = true;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$this->dispatch('totp-enabled');
|
|
||||||
$this->dispatch('toast', body: 'TOTP aktiviert.');
|
|
||||||
$this->dispatch('closeModal');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function disable(): void
|
public function done(): void
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$this->dispatch('toast', type: 'done', badge: '2FA',
|
||||||
$user->totp_secret = null;
|
title: 'TOTP aktiviert',
|
||||||
$user->two_factor_enabled = false;
|
text: 'Zwei-Faktor-Authentifizierung ist jetzt aktiv.', duration: 5000);
|
||||||
$user->save();
|
$this->dispatch('2fa-status-changed');
|
||||||
|
$this->closeModal();
|
||||||
$this->dispatch('totp-disabled');
|
|
||||||
$this->dispatch('toast', body: 'TOTP deaktiviert.');
|
|
||||||
$this->dispatch('closeModal');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function saveAccount() { /* $this->validate(..); user->update([...]) */ }
|
|
||||||
public function changePassword() { /* validate & set */ }
|
|
||||||
public function changeEmail() { /* validate, send verify link, etc. */ }
|
|
||||||
|
|
||||||
public function openRecovery() { /* optional modal or page */ }
|
|
||||||
public function logoutOthers() { /* … */ }
|
|
||||||
public function logoutSession(string $id) { /* … */ }
|
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.ui.security.modal.totp-setup-modal');
|
return view('livewire.ui.system.modal.totp-setup-modal');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class RspamdForm extends Component
|
||||||
|
|
||||||
private function writeRspamdConfig(): void
|
private function writeRspamdConfig(): void
|
||||||
{
|
{
|
||||||
$target = '/etc/rspamd/local.d/mailwolt-actions.conf';
|
$target = '/etc/rspamd/local.d/actions.conf';
|
||||||
$content = <<<CONF
|
$content = <<<CONF
|
||||||
actions {
|
actions {
|
||||||
reject = {$this->reject_score};
|
reject = {$this->reject_score};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Livewire\Ui\Security;
|
namespace App\Livewire\Ui\Security;
|
||||||
|
|
||||||
|
use App\Models\Setting;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -12,9 +13,26 @@ class SslCertificatesTable extends Component
|
||||||
{
|
{
|
||||||
public array $certs = [];
|
public array $certs = [];
|
||||||
|
|
||||||
|
// Domains (aus Einstellungen)
|
||||||
|
public string $uiDomain = '';
|
||||||
|
public string $webmailDomain = '';
|
||||||
|
public string $mailDomain = '';
|
||||||
|
|
||||||
|
// Provisioning
|
||||||
|
public bool $sslProvisioning = false;
|
||||||
|
public bool $sslDone = false;
|
||||||
|
public array $sslProgress = ['ui' => 'pending', 'webmail' => 'pending', 'mail' => 'pending'];
|
||||||
|
|
||||||
|
private const SSL_STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
|
$this->uiDomain = (string) Setting::get('ui_domain', '');
|
||||||
|
$this->webmailDomain = (string) Setting::get('webmail_domain', '');
|
||||||
|
$this->mailDomain = (string) Setting::get('mail_domain', '');
|
||||||
|
|
||||||
$this->certs = $this->loadCertificates();
|
$this->certs = $this->loadCertificates();
|
||||||
|
$this->restoreSslProvisioningState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refresh(): void
|
public function refresh(): void
|
||||||
|
|
@ -24,6 +42,55 @@ class SslCertificatesTable extends Component
|
||||||
text: 'Zertifikatsliste wurde neu geladen.', duration: 3000);
|
text: 'Zertifikatsliste wurde neu geladen.', duration: 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function startSslProvisioning(): void
|
||||||
|
{
|
||||||
|
if (! ($this->uiDomain && $this->webmailDomain && $this->mailDomain)) {
|
||||||
|
$this->dispatch('toast', type: 'warn', badge: 'SSL',
|
||||||
|
title: 'Domains fehlen',
|
||||||
|
text: 'Bitte erst alle Domains unter Einstellungen speichern.', duration: 6000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mkdir(self::SSL_STATE_DIR, 0755, true);
|
||||||
|
@unlink(self::SSL_STATE_DIR . '/done');
|
||||||
|
foreach (['ui', 'mail', 'webmail'] as $k) {
|
||||||
|
file_put_contents(self::SSL_STATE_DIR . "/{$k}", 'pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sslProgress = ['ui' => 'pending', 'webmail' => 'pending', 'mail' => 'pending'];
|
||||||
|
$this->sslDone = false;
|
||||||
|
$this->sslProvisioning = true;
|
||||||
|
|
||||||
|
$artisan = base_path('artisan');
|
||||||
|
$cmd = sprintf(
|
||||||
|
'nohup php %s clubird:wizard-domains --ui=%s --mail=%s --webmail=%s --ssl=1 > /dev/null 2>&1 &',
|
||||||
|
escapeshellarg($artisan),
|
||||||
|
escapeshellarg($this->uiDomain),
|
||||||
|
escapeshellarg($this->mailDomain),
|
||||||
|
escapeshellarg($this->webmailDomain),
|
||||||
|
);
|
||||||
|
@shell_exec($cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pollSsl(): void
|
||||||
|
{
|
||||||
|
if (! $this->sslProvisioning || $this->sslDone) return;
|
||||||
|
|
||||||
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
|
$file = self::SSL_STATE_DIR . "/{$key}";
|
||||||
|
$this->sslProgress[$key] = is_readable($file)
|
||||||
|
? trim((string) @file_get_contents($file))
|
||||||
|
: 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
$done = @file_get_contents(self::SSL_STATE_DIR . '/done');
|
||||||
|
if ($done !== false) {
|
||||||
|
$this->sslDone = true;
|
||||||
|
$this->sslProvisioning = false;
|
||||||
|
$this->certs = $this->loadCertificates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function renew(string $name): void
|
public function renew(string $name): void
|
||||||
{
|
{
|
||||||
$safe = preg_replace('/[^a-z0-9._-]/i', '', $name);
|
$safe = preg_replace('/[^a-z0-9._-]/i', '', $name);
|
||||||
|
|
@ -44,6 +111,21 @@ class SslCertificatesTable extends Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function restoreSslProvisioningState(): void
|
||||||
|
{
|
||||||
|
$doneFile = self::SSL_STATE_DIR . '/done';
|
||||||
|
if (! file_exists($doneFile)) return;
|
||||||
|
|
||||||
|
$this->sslDone = true;
|
||||||
|
$this->sslProvisioning = false;
|
||||||
|
foreach (['ui', 'mail', 'webmail'] as $key) {
|
||||||
|
$file = self::SSL_STATE_DIR . "/{$key}";
|
||||||
|
if (is_readable($file)) {
|
||||||
|
$this->sslProgress[$key] = trim((string) @file_get_contents($file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function loadCertificates(): array
|
private function loadCertificates(): array
|
||||||
{
|
{
|
||||||
$out = (string) @shell_exec('sudo -n /usr/bin/certbot certificates 2>&1');
|
$out = (string) @shell_exec('sudo -n /usr/bin/certbot certificates 2>&1');
|
||||||
|
|
@ -51,7 +133,7 @@ class SslCertificatesTable extends Component
|
||||||
if (str_contains($out, 'No certificates found')) return [];
|
if (str_contains($out, 'No certificates found')) return [];
|
||||||
|
|
||||||
$certs = [];
|
$certs = [];
|
||||||
$blocks = preg_split('/\n(?=Certificate Name:)/m', $out);
|
$blocks = preg_split('/\n(?=\s*Certificate Name:)/m', $out);
|
||||||
|
|
||||||
foreach ($blocks as $block) {
|
foreach ($blocks as $block) {
|
||||||
if (!preg_match('/Certificate Name:\s*(.+)/i', $block, $nameM)) continue;
|
if (!preg_match('/Certificate Name:\s*(.+)/i', $block, $nameM)) continue;
|
||||||
|
|
@ -63,6 +145,13 @@ class SslCertificatesTable extends Component
|
||||||
$expiryRaw = trim($expiryM[1] ?? '');
|
$expiryRaw = trim($expiryM[1] ?? '');
|
||||||
$daysLeft = null;
|
$daysLeft = null;
|
||||||
$expired = false;
|
$expired = false;
|
||||||
|
$expiryDate = null;
|
||||||
|
|
||||||
|
// Datum extrahieren (Format: "2026-07-24 10:30:00+00:00 (VALID: 88 days)")
|
||||||
|
if (preg_match('/(\d{4}-\d{2}-\d{2})/', $expiryRaw, $dateM)) {
|
||||||
|
$ts = strtotime($dateM[1]);
|
||||||
|
$expiryDate = $ts ? date('d.m.Y', $ts) : null;
|
||||||
|
}
|
||||||
|
|
||||||
if (preg_match('/VALID: (\d+) days/i', $expiryRaw, $dM)) {
|
if (preg_match('/VALID: (\d+) days/i', $expiryRaw, $dM)) {
|
||||||
$daysLeft = (int) $dM[1];
|
$daysLeft = (int) $dM[1];
|
||||||
|
|
@ -75,12 +164,12 @@ class SslCertificatesTable extends Component
|
||||||
$domains = $domainsRaw !== '' ? array_values(array_filter(explode(' ', $domainsRaw))) : [];
|
$domains = $domainsRaw !== '' ? array_values(array_filter(explode(' ', $domainsRaw))) : [];
|
||||||
|
|
||||||
$certs[] = [
|
$certs[] = [
|
||||||
'name' => trim($nameM[1]),
|
'name' => trim($nameM[1]),
|
||||||
'domains' => $domains,
|
'domains' => $domains,
|
||||||
'expiry' => $expiryRaw,
|
'expiry_date' => $expiryDate,
|
||||||
'days_left' => $daysLeft,
|
'days_left' => $daysLeft,
|
||||||
'expired' => $expired,
|
'expired' => $expired,
|
||||||
'cert_path' => trim($certM[1] ?? ''),
|
'cert_path' => trim($certM[1] ?? ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ class TlsCiphersForm extends Component
|
||||||
|
|
||||||
private function writePostfixConfig(): void
|
private function writePostfixConfig(): void
|
||||||
{
|
{
|
||||||
$target = '/etc/postfix/mailwolt-tls.cf';
|
$target = '/etc/postfix/tls.cf';
|
||||||
$content = "smtpd_tls_protocols = {$this->postfix_protocols}\n"
|
$content = "smtpd_tls_protocols = {$this->postfix_protocols}\n"
|
||||||
. "smtp_tls_protocols = {$this->postfix_protocols}\n"
|
. "smtp_tls_protocols = {$this->postfix_protocols}\n"
|
||||||
. "smtpd_tls_ciphers = {$this->postfix_ciphers}\n"
|
. "smtpd_tls_ciphers = {$this->postfix_ciphers}\n"
|
||||||
|
|
@ -106,7 +106,7 @@ class TlsCiphersForm extends Component
|
||||||
|
|
||||||
private function writeDovecotConfig(): void
|
private function writeDovecotConfig(): void
|
||||||
{
|
{
|
||||||
$target = '/etc/dovecot/conf.d/99-mailwolt-tls.conf';
|
$target = '/etc/dovecot/conf.d/99-tls.conf';
|
||||||
$content = "ssl_min_protocol = {$this->dovecot_min_proto}\n"
|
$content = "ssl_min_protocol = {$this->dovecot_min_proto}\n"
|
||||||
. "ssl_cipher_list = {$this->dovecot_ciphers}\n";
|
. "ssl_cipher_list = {$this->dovecot_ciphers}\n";
|
||||||
$this->tee($target, $content);
|
$this->tee($target, $content);
|
||||||
|
|
|
||||||
|
|
@ -129,10 +129,10 @@ class DomainsSslForm extends Component
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->base_domain = (string) config('mailwolt.domain.base', '');
|
$this->base_domain = (string) config('clubird.domain.base', '');
|
||||||
$this->ui_sub = (string) config('mailwolt.domain.ui', '');
|
$this->ui_sub = (string) config('clubird.domain.ui', '');
|
||||||
$this->webmail_sub = (string) config('mailwolt.domain.webmail', '');
|
$this->webmail_sub = (string) config('clubird.domain.webmail', '');
|
||||||
$this->mta_sub = (string) config('mailwolt.domain.mail', '');
|
$this->mta_sub = (string) config('clubird.domain.mail', '');
|
||||||
|
|
||||||
$this->loadMtaStsFromFileIfPossible();
|
$this->loadMtaStsFromFileIfPossible();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class DomainsSslForm extends Component
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$this->mail_domain_readonly = (string) config('mailwolt.domain.mail', 'mx');
|
$this->mail_domain_readonly = (string) config('clubird.domain.mail', 'mx');
|
||||||
$this->ui_domain = Setting::get('ui_domain', $this->ui_domain);
|
$this->ui_domain = Setting::get('ui_domain', $this->ui_domain);
|
||||||
$this->webmail_domain = Setting::get('webmail_domain', $this->webmail_domain);
|
$this->webmail_domain = Setting::get('webmail_domain', $this->webmail_domain);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ class InstallerPage extends Component
|
||||||
|
|
||||||
if ($this->rc === 0 && !$this->postActionsDone) {
|
if ($this->rc === 0 && !$this->postActionsDone) {
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
||||||
|
@shell_exec('nohup php /var/www/mailwolt/artisan db:seed --class="Database\\\\Seeders\\\\SystemDomainSeeder" --force >/dev/null 2>&1 &');
|
||||||
$this->postActionsDone = true;
|
$this->postActionsDone = true;
|
||||||
|
|
||||||
$this->dispatch('toast', type: 'done', badge: 'Installer',
|
$this->dispatch('toast', type: 'done', badge: 'Installer',
|
||||||
|
|
|
||||||
|
|
@ -20,21 +20,22 @@ class ApiKeyCreateModal extends ModalComponent
|
||||||
'domains:write' => 'Domains schreiben',
|
'domains:write' => 'Domains schreiben',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static function closeModalOnClickAway(): bool { return false; }
|
||||||
|
public static function closeModalOnEscape(): bool { return false; }
|
||||||
|
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
||||||
|
|
||||||
public function create(): void
|
public function create(): void
|
||||||
{
|
{
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'name' => 'required|string|max:80',
|
'name' => 'required|string|max:80',
|
||||||
'selected' => 'required|array|min:1',
|
'selected' => 'required|array|min:1',
|
||||||
'selected.*' => 'in:' . implode(',', array_keys(self::$availableScopes)),
|
'selected.*' => 'in:' . implode(',', array_keys(self::$availableScopes)),
|
||||||
], [
|
], [
|
||||||
'selected.required' => 'Bitte mindestens einen Scope auswählen.',
|
'selected.required' => 'Bitte mindestens einen Scope auswählen.',
|
||||||
'selected.min' => 'Bitte mindestens einen Scope auswählen.',
|
'selected.min' => 'Bitte mindestens einen Scope auswählen.',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$token = Auth::user()->createToken(
|
$token = Auth::user()->createToken($this->name, $this->selected);
|
||||||
$this->name,
|
|
||||||
$this->selected,
|
|
||||||
);
|
|
||||||
|
|
||||||
$pat = $token->accessToken;
|
$pat = $token->accessToken;
|
||||||
if ($this->sandbox) {
|
if ($this->sandbox) {
|
||||||
|
|
@ -43,7 +44,10 @@ class ApiKeyCreateModal extends ModalComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->dispatch('token-created', plainText: $token->plainTextToken);
|
$this->dispatch('token-created', plainText: $token->plainTextToken);
|
||||||
$this->closeModal();
|
$this->dispatch('openModal',
|
||||||
|
component: 'ui.system.modal.api-key-show-modal',
|
||||||
|
arguments: ['plainText' => $token->plainTextToken],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleAll(): void
|
public function toggleAll(): void
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\System\Modal;
|
||||||
|
|
||||||
|
use App\Models\PersonalAccessToken;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
|
class ApiKeyDeleteModal extends ModalComponent
|
||||||
|
{
|
||||||
|
public int $tokenId;
|
||||||
|
public string $tokenName = '';
|
||||||
|
|
||||||
|
public function mount(int $tokenId): void
|
||||||
|
{
|
||||||
|
$token = PersonalAccessToken::where('tokenable_id', Auth::id())
|
||||||
|
->where('tokenable_type', Auth::user()::class)
|
||||||
|
->findOrFail($tokenId);
|
||||||
|
|
||||||
|
$this->tokenId = $tokenId;
|
||||||
|
$this->tokenName = $token->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(): void
|
||||||
|
{
|
||||||
|
PersonalAccessToken::where('tokenable_id', Auth::id())
|
||||||
|
->where('tokenable_type', Auth::user()::class)
|
||||||
|
->findOrFail($this->tokenId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$this->dispatch('toast', type: 'done', badge: 'API Key',
|
||||||
|
title: 'Gelöscht', text: "Key <b>{$this->tokenName}</b> wurde entfernt.", duration: 4000);
|
||||||
|
|
||||||
|
$this->dispatch('token-deleted');
|
||||||
|
$this->closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.system.modal.api-key-delete-modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\System\Modal;
|
||||||
|
|
||||||
|
use App\Models\PersonalAccessToken;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
|
class ApiKeyScopesModal extends ModalComponent
|
||||||
|
{
|
||||||
|
public string $tokenName = '';
|
||||||
|
public array $scopes = [];
|
||||||
|
|
||||||
|
public function mount(int $tokenId): void
|
||||||
|
{
|
||||||
|
$token = PersonalAccessToken::where('tokenable_id', Auth::id())
|
||||||
|
->where('tokenable_type', Auth::user()::class)
|
||||||
|
->findOrFail($tokenId);
|
||||||
|
|
||||||
|
$this->tokenName = $token->name;
|
||||||
|
$this->scopes = $token->abilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.system.modal.api-key-scopes-modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,9 +13,18 @@ class ApiKeyShowModal extends ModalComponent
|
||||||
$this->plainText = $plainText;
|
$this->plainText = $plainText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function closeModalOnClickAway(): bool { return false; }
|
||||||
|
public static function closeModalOnEscape(): bool { return false; }
|
||||||
|
public static function closeModalOnEscapeIsForceful(): bool { return false; }
|
||||||
|
|
||||||
|
public function dismiss(): void
|
||||||
|
{
|
||||||
|
$this->forceClose()->closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
public static function modalMaxWidth(): string
|
public static function modalMaxWidth(): string
|
||||||
{
|
{
|
||||||
return 'md';
|
return '2xl';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,24 @@ class UpdateModal extends ModalComponent
|
||||||
$st = @trim(@file_get_contents(self::STATE_DIR.'/state') ?: '');
|
$st = @trim(@file_get_contents(self::STATE_DIR.'/state') ?: '');
|
||||||
$rcRaw = @trim(@file_get_contents(self::STATE_DIR.'/rc') ?: '');
|
$rcRaw = @trim(@file_get_contents(self::STATE_DIR.'/rc') ?: '');
|
||||||
|
|
||||||
|
// Log einlesen
|
||||||
|
$lines = @file(self::LOG, FILE_IGNORE_NEW_LINES) ?: [];
|
||||||
|
|
||||||
|
// Nur als "done" gelten wenn [DONE]-Marker im Log steht —
|
||||||
|
// verhindert dass das Modal fertig zeigt während das Script noch läuft
|
||||||
|
$logDone = in_array('[DONE]', array_map('trim', $lines), true);
|
||||||
|
|
||||||
|
if ($st === 'done' && !$logDone) {
|
||||||
|
$st = 'running'; // Noch warten bis Log vollständig
|
||||||
|
}
|
||||||
|
|
||||||
$this->state = $st ?: 'unknown';
|
$this->state = $st ?: 'unknown';
|
||||||
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||||||
|
|
||||||
// Log einlesen
|
$this->tail = array_slice(
|
||||||
$lines = @file(self::LOG, FILE_IGNORE_NEW_LINES) ?: [];
|
array_filter($lines, fn($l) => trim($l) !== '[DONE]'),
|
||||||
$this->tail = array_slice($lines, -30);
|
-30
|
||||||
|
);
|
||||||
|
|
||||||
$last = trim($this->tail ? end($this->tail) : '');
|
$last = trim($this->tail ? end($this->tail) : '');
|
||||||
$last = preg_replace('/^\[\w\]\s*/', '', $last);
|
$last = preg_replace('/^\[\w\]\s*/', '', $last);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\System;
|
||||||
|
|
||||||
|
use App\Services\TotpService;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
use Livewire\Attributes\Title;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('layouts.dvx')]
|
||||||
|
#[Title('Profil · Mailwolt')]
|
||||||
|
class ProfilePage extends Component
|
||||||
|
{
|
||||||
|
public string $name = '';
|
||||||
|
public string $email = '';
|
||||||
|
|
||||||
|
public string $current_password = '';
|
||||||
|
public string $new_password = '';
|
||||||
|
public string $new_password_confirmation = '';
|
||||||
|
|
||||||
|
public bool $totpEnabled = false;
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
$u = Auth::user();
|
||||||
|
$this->name = $u->name ?? '';
|
||||||
|
$this->email = $u->email ?? '';
|
||||||
|
$this->totpEnabled = app(TotpService::class)->isEnabled($u);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('2fa-status-changed')]
|
||||||
|
public function refreshTotpStatus(): void
|
||||||
|
{
|
||||||
|
$this->totpEnabled = app(TotpService::class)->isEnabled(Auth::user());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveProfile(): void
|
||||||
|
{
|
||||||
|
$this->validate([
|
||||||
|
'name' => 'required|string|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$u = Auth::user();
|
||||||
|
$u->name = $this->name;
|
||||||
|
$u->save();
|
||||||
|
|
||||||
|
$this->dispatch('toast', type: 'done', badge: 'Profil',
|
||||||
|
title: 'Gespeichert',
|
||||||
|
text: 'Profilname wurde aktualisiert.', duration: 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changePassword(): void
|
||||||
|
{
|
||||||
|
if ($this->new_password === '') {
|
||||||
|
$this->addError('new_password', 'Kein neues Passwort eingegeben.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validate([
|
||||||
|
'current_password' => 'required|string',
|
||||||
|
'new_password' => 'required|string|min:8',
|
||||||
|
'new_password_confirmation' => 'required|same:new_password',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$u = Auth::user();
|
||||||
|
if (!Hash::check($this->current_password, $u->password)) {
|
||||||
|
$this->addError('current_password', 'Aktuelles Passwort ist falsch.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$u->password = Hash::make($this->new_password);
|
||||||
|
$u->save();
|
||||||
|
|
||||||
|
$this->reset(['current_password', 'new_password', 'new_password_confirmation']);
|
||||||
|
|
||||||
|
$this->dispatch('toast', type: 'done', badge: 'Profil',
|
||||||
|
title: 'Passwort geändert',
|
||||||
|
text: 'Dein Passwort wurde aktualisiert.', duration: 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disableTotp(): void
|
||||||
|
{
|
||||||
|
app(TotpService::class)->disable(Auth::user());
|
||||||
|
session()->forget('2fa_verified');
|
||||||
|
$this->totpEnabled = false;
|
||||||
|
|
||||||
|
$this->dispatch('toast', type: 'done', badge: '2FA',
|
||||||
|
title: 'TOTP deaktiviert',
|
||||||
|
text: 'Zwei-Faktor-Authentifizierung wurde deaktiviert.', duration: 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.system.profile-page');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Ui\System;
|
||||||
|
|
||||||
|
use App\Models\SandboxRoute;
|
||||||
|
use App\Services\SandboxService;
|
||||||
|
use Livewire\Attributes\Computed;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class SandboxRoutes extends Component
|
||||||
|
{
|
||||||
|
public string $newType = 'domain';
|
||||||
|
public string $newTarget = '';
|
||||||
|
public ?string $syncMessage = null;
|
||||||
|
public bool $syncOk = false;
|
||||||
|
|
||||||
|
public function boot(SandboxService $sandboxService): void
|
||||||
|
{
|
||||||
|
$this->sandboxService = $sandboxService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addRoute(): void
|
||||||
|
{
|
||||||
|
if ($this->newType !== 'global') {
|
||||||
|
$this->validate(['newTarget' => 'required|string|max:255']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $this->newType === 'global' ? null : trim($this->newTarget);
|
||||||
|
$this->sandboxService->enable($this->newType, $target);
|
||||||
|
$this->sandboxService->syncTransportFile();
|
||||||
|
$this->dispatch('sandbox-route-changed');
|
||||||
|
$this->newTarget = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeRoute(int $id): void
|
||||||
|
{
|
||||||
|
$this->sandboxService->delete($id);
|
||||||
|
$this->sandboxService->syncTransportFile();
|
||||||
|
$this->dispatch('sandbox-route-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleRoute(int $id): void
|
||||||
|
{
|
||||||
|
$route = SandboxRoute::findOrFail($id);
|
||||||
|
$route->is_active = !$route->is_active;
|
||||||
|
$route->save();
|
||||||
|
$this->sandboxService->syncTransportFile();
|
||||||
|
$this->dispatch('sandbox-route-changed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncPostfix(): void
|
||||||
|
{
|
||||||
|
$result = $this->sandboxService->syncTransportFile();
|
||||||
|
$this->syncOk = $result['ok'];
|
||||||
|
$this->syncMessage = $result['message'];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function routes()
|
||||||
|
{
|
||||||
|
return SandboxRoute::orderBy('type')->orderBy('target')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.ui.system.sandbox-routes');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,12 +7,18 @@ use App\Models\Setting;
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
|
use Livewire\Attributes\Url;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
#[Layout('layouts.dvx')]
|
#[Layout('layouts.dvx')]
|
||||||
#[Title('Einstellungen · Mailwolt')]
|
#[Title('Einstellungen · Mailwolt')]
|
||||||
class SettingsForm extends Component
|
class SettingsForm extends Component
|
||||||
{
|
{
|
||||||
|
#[Url]
|
||||||
|
public string $tab = 'general';
|
||||||
|
|
||||||
|
private const VALID_TABS = ['general', 'domains', 'backup', 'notifications'];
|
||||||
|
|
||||||
// Allgemein
|
// Allgemein
|
||||||
public string $instance_name = '';
|
public string $instance_name = '';
|
||||||
public string $locale = 'de';
|
public string $locale = 'de';
|
||||||
|
|
@ -24,6 +30,15 @@ class SettingsForm extends Component
|
||||||
public string $mail_domain = '';
|
public string $mail_domain = '';
|
||||||
public string $webmail_domain = '';
|
public string $webmail_domain = '';
|
||||||
|
|
||||||
|
// SSL-Status (read-only, für Anzeige in Einstellungen)
|
||||||
|
public array $sslCerts = [];
|
||||||
|
|
||||||
|
private const SSL_STATE_DIR = '/var/lib/mailwolt/wizard';
|
||||||
|
|
||||||
|
// Benachrichtigungen
|
||||||
|
public string $notify_sender_name = '';
|
||||||
|
public string $notify_admin_email = '';
|
||||||
|
|
||||||
// Sicherheit
|
// Sicherheit
|
||||||
public int $rate_limit = 5;
|
public int $rate_limit = 5;
|
||||||
public int $password_min = 10;
|
public int $password_min = 10;
|
||||||
|
|
@ -47,8 +62,10 @@ class SettingsForm extends Component
|
||||||
'locale' => 'required|string|max:10',
|
'locale' => 'required|string|max:10',
|
||||||
'timezone' => 'required|string|max:64',
|
'timezone' => 'required|string|max:64',
|
||||||
'session_timeout' => 'required|integer|min:5|max:1440',
|
'session_timeout' => 'required|integer|min:5|max:1440',
|
||||||
'rate_limit' => 'required|integer|min:1|max:100',
|
'notify_sender_name' => 'nullable|string|max:80',
|
||||||
'password_min' => 'required|integer|min:6|max:128',
|
'notify_admin_email' => 'nullable|email|max:190',
|
||||||
|
'rate_limit' => 'required|integer|min:1|max:100',
|
||||||
|
'password_min' => 'required|integer|min:6|max:128',
|
||||||
'backup_enabled' => 'boolean',
|
'backup_enabled' => 'boolean',
|
||||||
'backup_preset' => 'required|in:hourly,daily,weekly,monthly,custom',
|
'backup_preset' => 'required|in:hourly,daily,weekly,monthly,custom',
|
||||||
'backup_time' => 'required_unless:backup_preset,hourly,custom|regex:/^\d{1,2}:\d{2}$/',
|
'backup_time' => 'required_unless:backup_preset,hourly,custom|regex:/^\d{1,2}:\d{2}$/',
|
||||||
|
|
@ -123,8 +140,13 @@ class SettingsForm extends Component
|
||||||
Setting::setMany($persist);
|
Setting::setMany($persist);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit);
|
$this->loadSslStatus();
|
||||||
$this->password_min = (int) Setting::get('password_min', $this->password_min);
|
$defaultSenderName = ($this->instance_name ?: 'Mailwolt') . ' Benachrichtigung';
|
||||||
|
$defaultAdminEmail = auth()->user()?->system_notify_email ?? '';
|
||||||
|
$this->notify_sender_name = (string) Setting::get('notify_sender_name', $defaultSenderName);
|
||||||
|
$this->notify_admin_email = (string) Setting::get('notify_admin_email', $defaultAdminEmail);
|
||||||
|
$this->rate_limit = (int) Setting::get('rate_limit', $this->rate_limit);
|
||||||
|
$this->password_min = (int) Setting::get('password_min', $this->password_min);
|
||||||
|
|
||||||
// Backup aus BackupPolicy laden
|
// Backup aus BackupPolicy laden
|
||||||
$policy = BackupPolicy::first();
|
$policy = BackupPolicy::first();
|
||||||
|
|
@ -145,13 +167,20 @@ class SettingsForm extends Component
|
||||||
$this->validate();
|
$this->validate();
|
||||||
|
|
||||||
Setting::setMany([
|
Setting::setMany([
|
||||||
'locale' => $this->locale,
|
'locale' => $this->locale,
|
||||||
'timezone' => $this->timezone,
|
'timezone' => $this->timezone,
|
||||||
'session_timeout' => $this->session_timeout,
|
'session_timeout' => $this->session_timeout,
|
||||||
'rate_limit' => $this->rate_limit,
|
'notify_sender_name' => $this->notify_sender_name,
|
||||||
'password_min' => $this->password_min,
|
'notify_admin_email' => $this->notify_admin_email,
|
||||||
|
'rate_limit' => $this->rate_limit,
|
||||||
|
'password_min' => $this->password_min,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// MAIL_FROM_NAME in .env synchronisieren
|
||||||
|
if ($this->notify_sender_name) {
|
||||||
|
$this->writeEnv(['MAIL_FROM_NAME' => $this->notify_sender_name]);
|
||||||
|
}
|
||||||
|
|
||||||
// Backup-Policy speichern
|
// Backup-Policy speichern
|
||||||
$cron = $this->buildCron();
|
$cron = $this->buildCron();
|
||||||
BackupPolicy::updateOrCreate([], [
|
BackupPolicy::updateOrCreate([], [
|
||||||
|
|
@ -174,7 +203,6 @@ class SettingsForm extends Component
|
||||||
{
|
{
|
||||||
$this->validate($this->domainRules(), $this->domainMessages());
|
$this->validate($this->domainRules(), $this->domainMessages());
|
||||||
|
|
||||||
// Normalize: strip schema + trailing slashes
|
|
||||||
$this->ui_domain = $this->cleanDomain($this->ui_domain);
|
$this->ui_domain = $this->cleanDomain($this->ui_domain);
|
||||||
$this->mail_domain = $this->cleanDomain($this->mail_domain);
|
$this->mail_domain = $this->cleanDomain($this->mail_domain);
|
||||||
$this->webmail_domain = $this->cleanDomain($this->webmail_domain);
|
$this->webmail_domain = $this->cleanDomain($this->webmail_domain);
|
||||||
|
|
@ -185,18 +213,88 @@ class SettingsForm extends Component
|
||||||
'webmail_domain' => $this->webmail_domain,
|
'webmail_domain' => $this->webmail_domain,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Immer .env aktualisieren — unabhängig von DNS oder Nginx
|
|
||||||
$this->syncDomainsToEnv();
|
$this->syncDomainsToEnv();
|
||||||
|
|
||||||
// Nginx-Konfiguration nur anwenden wenn alle Domains gesetzt sind
|
$warnings = [];
|
||||||
|
|
||||||
if ($this->ui_domain && $this->mail_domain && $this->webmail_domain) {
|
if ($this->ui_domain && $this->mail_domain && $this->webmail_domain) {
|
||||||
$sslAuto = app()->isProduction();
|
$nginxOk = $this->applyDomains(false);
|
||||||
$this->applyDomains($sslAuto);
|
if ($nginxOk === false) {
|
||||||
|
$warnings[] = 'Nginx konnte nicht neu geladen werden — DNS noch nicht aktiv oder Helper fehlt.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->dispatch('toast', type: 'done', badge: 'Domains',
|
$sysmailErr = $this->syncSysmailDomain();
|
||||||
title: 'Domains gespeichert',
|
if ($sysmailErr) {
|
||||||
text: 'Konfiguration wurde übernommen.', duration: 4000);
|
$warnings[] = 'System-Domain: ' . $sysmailErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->mail_domain) {
|
||||||
|
$this->applyPostfixHostname($this->mail_domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->loadSslStatus();
|
||||||
|
|
||||||
|
if ($warnings) {
|
||||||
|
$this->dispatch('toast', type: 'warn', badge: 'Domains',
|
||||||
|
title: 'Domains gespeichert — mit Hinweisen',
|
||||||
|
text: implode(' · ', $warnings),
|
||||||
|
duration: 0);
|
||||||
|
} else {
|
||||||
|
$this->dispatch('toast', type: 'done', badge: 'Domains',
|
||||||
|
title: 'Domains gespeichert',
|
||||||
|
text: 'Konfiguration wurde übernommen.',
|
||||||
|
duration: 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function syncSysmailDomain(): ?string
|
||||||
|
{
|
||||||
|
// BASE_DOMAIN aus config, env oder mail_domain ableiten
|
||||||
|
$platformBase = strtolower((string) config('mailpool.platform_zone', ''));
|
||||||
|
if (!$platformBase || $platformBase === 'example.com') {
|
||||||
|
$platformBase = strtolower((string) env('BASE_DOMAIN', ''));
|
||||||
|
}
|
||||||
|
if (!$platformBase || $platformBase === 'example.com') {
|
||||||
|
// Fallback: aus mail_domain den Hostnamen ohne ersten Subdomain-Teil
|
||||||
|
$mailDomain = strtolower(trim((string) Setting::get('mail_domain', '')));
|
||||||
|
if ($mailDomain) {
|
||||||
|
$parts = explode('.', $mailDomain);
|
||||||
|
$platformBase = count($parts) > 2
|
||||||
|
? implode('.', array_slice($parts, 1))
|
||||||
|
: $mailDomain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$platformBase || $platformBase === 'example.com') return null;
|
||||||
|
|
||||||
|
$systemSub = strtolower(config('mailpool.platform_system_zone', 'sysmail'));
|
||||||
|
$expectedFqdn = "{$systemSub}.{$platformBase}";
|
||||||
|
|
||||||
|
$existing = \App\Models\Domain::where('is_system', true)
|
||||||
|
->where(fn($q) => $q->whereNull('is_server')->orWhere('is_server', false))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing && $existing->domain === $expectedFqdn) return null;
|
||||||
|
|
||||||
|
if ($existing && $existing->domain !== $expectedFqdn) {
|
||||||
|
\App\Models\MailUser::where('domain_id', $existing->id)->forceDelete();
|
||||||
|
$existing->forceDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Artisan::call('db:seed', [
|
||||||
|
'--class' => 'Database\\Seeders\\SystemDomainSeeder',
|
||||||
|
'--force' => true,
|
||||||
|
]);
|
||||||
|
\Illuminate\Support\Facades\Log::info('SystemDomainSeeder ok', ['fqdn' => $expectedFqdn]);
|
||||||
|
return null;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('SystemDomainSeeder failed', [
|
||||||
|
'fqdn' => $expectedFqdn,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
return $e->getMessage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cleanDomain(string $value): string
|
private function cleanDomain(string $value): string
|
||||||
|
|
@ -206,21 +304,56 @@ class SettingsForm extends Component
|
||||||
return rtrim($value, '/');
|
return rtrim($value, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function openSslModal(): void
|
public function loadSslStatus(): void
|
||||||
{
|
{
|
||||||
if (! ($this->ui_domain && $this->mail_domain && $this->webmail_domain)) {
|
clearstatcache(true);
|
||||||
$this->dispatch('toast', type: 'warn', badge: 'SSL',
|
|
||||||
title: 'Domains fehlen',
|
|
||||||
text: 'Bitte erst alle drei Domains speichern.', duration: 5000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->dispatch('openModal', component: 'ui.system.modal.ssl-provision-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
#[\Livewire\Attributes\On('ssl:provision')]
|
$domains = [
|
||||||
public function provisionSsl(): void
|
'ui' => $this->ui_domain,
|
||||||
{
|
'webmail' => $this->webmail_domain,
|
||||||
$this->applyDomains(true);
|
'mail' => $this->mail_domain,
|
||||||
|
];
|
||||||
|
|
||||||
|
$certs = [];
|
||||||
|
foreach ($domains as $key => $domain) {
|
||||||
|
if (! $domain) {
|
||||||
|
$certs[$key] = ['domain' => '', 'has_cert' => false, 'expiry' => null, 'status' => 'nodomain'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$certFile = "/etc/letsencrypt/live/{$domain}/fullchain.pem";
|
||||||
|
$certDir = "/etc/letsencrypt/live/{$domain}";
|
||||||
|
// renewal/ ist immer 755 und world-readable — zuverlässigster Existenzcheck.
|
||||||
|
// is_dir() als Fallback falls renewal-Datei fehlt (manuell ausgestellte Certs).
|
||||||
|
$renewalConf = "/etc/letsencrypt/renewal/{$domain}.conf";
|
||||||
|
$certExists = file_exists($renewalConf) || is_dir($certDir);
|
||||||
|
|
||||||
|
if (! $certExists) {
|
||||||
|
$certs[$key] = ['domain' => $domain, 'has_cert' => false, 'expiry' => null, 'status' => 'missing'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sudo nötig: archive/ ist 700 (Symlink-Ziel), voller Pfad für sudoers-Matching
|
||||||
|
$expiry = trim((string) @shell_exec(
|
||||||
|
"sudo -n /usr/bin/openssl x509 -enddate -noout -in " . escapeshellarg($certFile) . " 2>/dev/null | sed 's/notAfter=//'"
|
||||||
|
));
|
||||||
|
$expiryTs = $expiry ? strtotime($expiry) : null;
|
||||||
|
$daysLeft = $expiryTs ? (int) round(($expiryTs - time()) / 86400) : null;
|
||||||
|
$certStatus = match (true) {
|
||||||
|
$daysLeft === null => 'ok',
|
||||||
|
$daysLeft <= 0 => 'expired',
|
||||||
|
$daysLeft <= 14 => 'expiring',
|
||||||
|
default => 'ok',
|
||||||
|
};
|
||||||
|
$certs[$key] = [
|
||||||
|
'domain' => $domain,
|
||||||
|
'has_cert' => true,
|
||||||
|
'expiry' => $expiryTs ? date('d.m.Y', $expiryTs) : null,
|
||||||
|
'days' => $daysLeft,
|
||||||
|
'status' => $certStatus,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$this->sslCerts = $certs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedUiDomain(): void { $this->validateOnly('ui_domain', $this->domainRules(), $this->domainMessages()); }
|
public function updatedUiDomain(): void { $this->validateOnly('ui_domain', $this->domainRules(), $this->domainMessages()); }
|
||||||
|
|
@ -271,7 +404,7 @@ class SettingsForm extends Component
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function applyDomains(bool $sslAuto = false): void
|
private function applyDomains(bool $sslAuto = false): bool
|
||||||
{
|
{
|
||||||
// DNS prüfen — Warnung anzeigen, aber Nginx trotzdem versuchen
|
// DNS prüfen — Warnung anzeigen, aber Nginx trotzdem versuchen
|
||||||
$unreachable = [];
|
$unreachable = [];
|
||||||
|
|
@ -282,12 +415,8 @@ class SettingsForm extends Component
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($unreachable) {
|
if ($unreachable) {
|
||||||
$this->dispatch('toast', type: 'warn', badge: 'DNS',
|
Log::info('mailwolt-apply-domains skipped (DNS not ready)', ['unreachable' => $unreachable]);
|
||||||
title: 'DNS noch nicht erreichbar',
|
return false;
|
||||||
text: 'Kein DNS-Eintrag für: ' . implode(', ', $unreachable) . '. Domains wurden trotzdem gespeichert — Nginx-Konfiguration bitte nach DNS-Setup erneut speichern.',
|
|
||||||
duration: 9000,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
$helper = '/usr/local/sbin/mailwolt-apply-domains';
|
||||||
|
|
@ -307,40 +436,47 @@ class SettingsForm extends Component
|
||||||
|
|
||||||
if ($ok) {
|
if ($ok) {
|
||||||
Setting::set('ssl_configured', '1');
|
Setting::set('ssl_configured', '1');
|
||||||
$this->dispatch('toast', type: 'done', badge: 'Nginx',
|
return true;
|
||||||
title: 'Nginx aktualisiert',
|
|
||||||
text: 'Nginx-Konfiguration wurde neu geladen.',
|
|
||||||
duration: 5000,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$this->dispatch('toast', type: 'warn', badge: 'Nginx',
|
|
||||||
title: 'Nginx-Konfiguration fehlgeschlagen',
|
|
||||||
text: 'Nginx konnte nicht neu geladen werden. Domains wurden trotzdem gespeichert. Siehe Laravel-Log.',
|
|
||||||
duration: 7000,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log::warning('mailwolt-apply-domains failed', ['output' => $output]);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function syncDomainsToEnv(): void
|
private function syncDomainsToEnv(): void
|
||||||
{
|
{
|
||||||
$base = rtrim((string) config('mailwolt.domain.base', ''), '.');
|
// BASE_DOMAIN aus mail_domain ableiten (mx.pxo.at → pxo.at)
|
||||||
|
$derivedBase = '';
|
||||||
|
if ($this->mail_domain) {
|
||||||
|
$parts = explode('.', strtolower(trim($this->mail_domain)));
|
||||||
|
$derivedBase = count($parts) > 2
|
||||||
|
? implode('.', array_slice($parts, 1))
|
||||||
|
: implode('.', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = $derivedBase ?: rtrim((string) config('clubird.domain.base', ''), '.');
|
||||||
|
|
||||||
$sub = function (string $full) use ($base): string {
|
$sub = function (string $full) use ($base): string {
|
||||||
$full = strtolower(trim($full));
|
$full = strtolower(trim($full));
|
||||||
if ($base && str_ends_with($full, '.' . $base)) {
|
if ($base && str_ends_with($full, '.' . $base)) {
|
||||||
return substr($full, 0, -(strlen($base) + 1));
|
return substr($full, 0, -(strlen($base) + 1));
|
||||||
}
|
}
|
||||||
// Kein passender Base — ersten Label nehmen
|
|
||||||
$parts = explode('.', $full);
|
$parts = explode('.', $full);
|
||||||
return count($parts) > 1 ? $parts[0] : '';
|
return count($parts) > 1 ? $parts[0] : '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$this->writeEnv([
|
$env = [
|
||||||
'WEBMAIL_SUB' => $this->webmail_domain ? $sub($this->webmail_domain) : '',
|
'WEBMAIL_SUB' => $this->webmail_domain ? $sub($this->webmail_domain) : '',
|
||||||
'WEBMAIL_DOMAIN' => $this->webmail_domain ?: '',
|
'WEBMAIL_DOMAIN' => $this->webmail_domain ?: '',
|
||||||
'UI_SUB' => $this->ui_domain ? $sub($this->ui_domain) : '',
|
'UI_SUB' => $this->ui_domain ? $sub($this->ui_domain) : '',
|
||||||
'MTA_SUB' => $this->mail_domain ? $sub($this->mail_domain) : '',
|
'MTA_SUB' => $this->mail_domain ? $sub($this->mail_domain) : '',
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
if ($derivedBase && $derivedBase !== 'example.com') {
|
||||||
|
$env['BASE_DOMAIN'] = $derivedBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writeEnv($env);
|
||||||
|
|
||||||
Artisan::call('config:clear');
|
Artisan::call('config:clear');
|
||||||
Artisan::call('route:clear');
|
Artisan::call('route:clear');
|
||||||
|
|
@ -366,9 +502,23 @@ class SettingsForm extends Component
|
||||||
file_put_contents($path, $content);
|
file_put_contents($path, $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function applyPostfixHostname(string $mailHost): void
|
||||||
|
{
|
||||||
|
$safeHost = preg_replace('/[^a-zA-Z0-9.\-]/', '', $mailHost);
|
||||||
|
if (!$safeHost) return;
|
||||||
|
|
||||||
|
$helper = '/usr/local/sbin/mailwolt-apply-hostname';
|
||||||
|
@shell_exec(sprintf('sudo -n %s %s 2>/dev/null', escapeshellarg($helper), escapeshellarg($safeHost)));
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
|
if (!in_array($this->tab, self::VALID_TABS, true)) {
|
||||||
return view('livewire.ui.system.settings-form', compact('timezones'));
|
$this->tab = 'allgemein';
|
||||||
|
}
|
||||||
|
|
||||||
|
$timezones = \DateTimeZone::listIdentifiers(\DateTimeZone::ALL);
|
||||||
|
$backupMeta = BackupPolicy::first();
|
||||||
|
return view('livewire.ui.system.settings-form', compact('timezones', 'backupMeta'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,8 @@ class UpdateCard extends Component
|
||||||
$this->recompute();
|
$this->recompute();
|
||||||
$this->progressLine = '';
|
$this->progressLine = '';
|
||||||
|
|
||||||
if ($this->running) {
|
if ($this->running || $this->lowState === 'done') {
|
||||||
|
// Update läuft oder abgeschlossen aber Post-Actions noch nicht ausgeführt
|
||||||
$this->state = 'running';
|
$this->state = 'running';
|
||||||
$this->dispatch('openModal', component: 'ui.system.modal.update-modal');
|
$this->dispatch('openModal', component: 'ui.system.modal.update-modal');
|
||||||
}
|
}
|
||||||
|
|
@ -66,24 +67,45 @@ class UpdateCard extends Component
|
||||||
|
|
||||||
public function runUpdate(): void
|
public function runUpdate(): void
|
||||||
{
|
{
|
||||||
// evtl. alte Einträge aufräumen
|
// State-Dateien VOR dem Start zurücksetzen — verhindert dass pollUpdate()
|
||||||
|
// das "done" vom letzten Update liest und sofort als "fertig" wertet.
|
||||||
|
@mkdir('/var/lib/mailwolt/update', 0755, true);
|
||||||
|
@file_put_contents('/var/lib/mailwolt/update/state', 'starting');
|
||||||
|
@unlink('/var/lib/mailwolt/update/rc');
|
||||||
|
|
||||||
Cache::forget('mailwolt.update_available');
|
Cache::forget('mailwolt.update_available');
|
||||||
Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
|
Cache::put($this->cacheStartedAtKey, time(), now()->addHour());
|
||||||
$this->dispatch('openModal', component: 'ui.system.modal.update-modal');
|
$this->dispatch('openModal', component: 'ui.system.modal.update-modal');
|
||||||
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-update >/dev/null 2>&1 &');
|
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-update >/dev/null 2>&1 &');
|
||||||
|
|
||||||
// Sofort ins Running gehen
|
// Sofort ins Running gehen
|
||||||
$this->latest = null;
|
$this->latest = null;
|
||||||
$this->displayLatest = null;
|
$this->displayLatest = null;
|
||||||
$this->hasUpdate = false;
|
$this->hasUpdate = false;
|
||||||
$this->state = 'running';
|
$this->state = 'running';
|
||||||
$this->running = true;
|
$this->running = true;
|
||||||
$this->errorLine = null;
|
$this->rc = null;
|
||||||
|
$this->lowState = 'starting';
|
||||||
|
$this->errorLine = null;
|
||||||
$this->progressLine = 'Update gestartet …';
|
$this->progressLine = 'Update gestartet …';
|
||||||
|
$this->postActionsDone = false;
|
||||||
|
|
||||||
$this->recomputeUi();
|
$this->recomputeUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function openLogs(): void
|
||||||
|
{
|
||||||
|
$this->dispatch('openModal', component: 'ui.system.modal.update-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refreshVersions(): void
|
||||||
|
{
|
||||||
|
if ($this->state === 'running') return;
|
||||||
|
$this->reloadVersionsAndStatus();
|
||||||
|
$this->recompute();
|
||||||
|
$this->recomputeUi();
|
||||||
|
}
|
||||||
|
|
||||||
public function pollUpdate(): void
|
public function pollUpdate(): void
|
||||||
{
|
{
|
||||||
$this->refreshLowLevelState();
|
$this->refreshLowLevelState();
|
||||||
|
|
@ -112,22 +134,30 @@ class UpdateCard extends Component
|
||||||
|
|
||||||
if ($this->rc === 0 && !$this->postActionsDone) {
|
if ($this->rc === 0 && !$this->postActionsDone) {
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &');
|
||||||
|
@shell_exec('nohup php /var/www/mailwolt/artisan optimize >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan rbl:probe --force >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan rbl:probe --force >/dev/null 2>&1 &');
|
||||||
$this->postActionsDone = true;
|
$this->postActionsDone = true;
|
||||||
|
@file_put_contents('/var/lib/mailwolt/update/state', 'complete');
|
||||||
|
|
||||||
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
$ver = $this->displayCurrent ?? 'aktuelle Version';
|
||||||
$this->progressLine = 'Update abgeschlossen: ' . $ver;
|
$this->progressLine = 'Update abgeschlossen: ' . $ver;
|
||||||
// Optional: NICHT sofort reloaden – Nutzer entscheidet
|
|
||||||
// $this->dispatch('reload-page', delay: 6000);
|
$this->dispatch('toast', type: 'done', badge: 'System',
|
||||||
|
title: 'Update abgeschlossen',
|
||||||
|
text: "Mailwolt wurde auf {$ver} aktualisiert.",
|
||||||
|
duration: 6000);
|
||||||
|
$this->dispatch('closeModal');
|
||||||
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
} elseif ($this->rc !== null && $this->rc !== 0 && !$this->postActionsDone) {
|
||||||
$this->postActionsDone = true;
|
$this->postActionsDone = true;
|
||||||
|
@file_put_contents('/var/lib/mailwolt/update/state', 'complete');
|
||||||
$this->errorLine = "Update fehlgeschlagen (rc={$this->rc}).";
|
$this->errorLine = "Update fehlgeschlagen (rc={$this->rc}).";
|
||||||
$this->dispatch('toast', type:'error', title:'Update fehlgeschlagen',
|
$this->dispatch('toast', type: 'error', title: 'Update fehlgeschlagen',
|
||||||
text:$this->progressLine ?: 'Bitte Logs prüfen: /var/log/mailwolt-update.log',
|
text: $this->progressLine ?: 'Bitte Logs prüfen: /var/log/mailwolt-update.log',
|
||||||
badge:'System', duration:0);
|
badge: 'System', duration: 0);
|
||||||
|
$this->dispatch('closeModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->state = 'idle';
|
$this->state = 'idle';
|
||||||
|
|
@ -200,7 +230,7 @@ class UpdateCard extends Component
|
||||||
// Update-Checker schreibt:
|
// Update-Checker schreibt:
|
||||||
// - updates:latest (normiert)
|
// - updates:latest (normiert)
|
||||||
// - updates:latest_raw (original)
|
// - updates:latest_raw (original)
|
||||||
$latNorm = Cache::get('updates:latest');
|
$latNorm = $this->normalizeVersion(Cache::get('updates:latest') ?? '');
|
||||||
$latRaw = Cache::get('updates:latest_raw');
|
$latRaw = Cache::get('updates:latest_raw');
|
||||||
|
|
||||||
// Legacy-Fallback
|
// Legacy-Fallback
|
||||||
|
|
@ -226,7 +256,9 @@ class UpdateCard extends Component
|
||||||
|
|
||||||
$this->displayCurrent = $curNorm ? 'v' . $curNorm : null;
|
$this->displayCurrent = $curNorm ? 'v' . $curNorm : null;
|
||||||
|
|
||||||
if (!$this->displayLatest && $latNorm) {
|
if (!$this->hasUpdate) {
|
||||||
|
$this->displayLatest = null;
|
||||||
|
} elseif (!$this->displayLatest && $latNorm) {
|
||||||
$this->displayLatest = 'v' . $latNorm;
|
$this->displayLatest = 'v' . $latNorm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -306,11 +338,10 @@ class UpdateCard extends Component
|
||||||
|
|
||||||
$this->lowState = $state !== '' ? $state : null;
|
$this->lowState = $state !== '' ? $state : null;
|
||||||
|
|
||||||
// running: solange NICHT 'done'
|
$this->running = ($this->lowState === 'running');
|
||||||
$this->running = ($this->lowState !== 'done');
|
|
||||||
|
|
||||||
// rc erst freigeben, wenn wirklich done
|
// rc freigeben wenn 'done' (Post-Actions ausstehend) oder 'complete' (bereits gelaufen)
|
||||||
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int)$rcRaw : null;
|
$this->rc = (in_array($this->lowState, ['done', 'complete']) && is_numeric($rcRaw)) ? (int)$rcRaw : null;
|
||||||
|
|
||||||
$this->progressLine = $this->tailUpdateLog();
|
$this->progressLine = $this->tailUpdateLog();
|
||||||
}
|
}
|
||||||
|
|
@ -318,27 +349,24 @@ class UpdateCard extends Component
|
||||||
protected function readCurrentVersion(): ?string
|
protected function readCurrentVersion(): ?string
|
||||||
{
|
{
|
||||||
// 1) normierte Version vom Wrapper
|
// 1) normierte Version vom Wrapper
|
||||||
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
$fileVer = $this->normalizeVersion(@trim(@file_get_contents(self::VERSION_FILE) ?: ''));
|
||||||
if ($v !== '') return $v;
|
|
||||||
|
|
||||||
// 2) raw -> normieren
|
// 2) git-Tag (bevorzugt wenn neuer als Datei – z.B. auf Dev-Server)
|
||||||
|
$out = [];
|
||||||
|
@exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null', $out);
|
||||||
|
$gitVer = $this->normalizeVersion(trim($out[0] ?? ''));
|
||||||
|
|
||||||
|
if ($gitVer && (!$fileVer || version_compare($gitVer, $fileVer, '>'))) {
|
||||||
|
return $gitVer;
|
||||||
|
}
|
||||||
|
if ($fileVer) return $fileVer;
|
||||||
|
|
||||||
|
// 3) raw -> normieren
|
||||||
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
||||||
if ($raw !== '') return $this->normalizeVersion($raw);
|
if ($raw !== '') return $this->normalizeVersion($raw);
|
||||||
|
|
||||||
// 3) build.info
|
|
||||||
$build = @file_get_contents(self::BUILD_INFO);
|
|
||||||
if ($build) {
|
|
||||||
foreach (preg_split('/\R+/', $build) as $line) {
|
|
||||||
if (str_starts_with($line, 'version=')) {
|
|
||||||
$v = $this->normalizeVersion(trim(substr($line, 8)));
|
|
||||||
if ($v) return $v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Fallback auf config(app.version)
|
// 4) Fallback auf config(app.version)
|
||||||
$v = $this->normalizeVersion(config('app.version') ?: '');
|
return $this->normalizeVersion(config('app.version') ?: '') ?: null;
|
||||||
return $v ?: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function normalizeVersion(?string $v): ?string
|
protected function normalizeVersion(?string $v): ?string
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ class UpdatePage extends Component
|
||||||
|
|
||||||
public function checkForUpdates(): void
|
public function checkForUpdates(): void
|
||||||
{
|
{
|
||||||
@shell_exec('php ' . base_path('artisan') . ' mailwolt:check-updates 2>&1');
|
@shell_exec('php ' . base_path('artisan') . ' clubird:check-updates 2>&1');
|
||||||
$this->reloadVersionsAndStatus();
|
$this->reloadVersionsAndStatus();
|
||||||
$this->recompute();
|
$this->recompute();
|
||||||
|
|
||||||
|
|
@ -133,6 +133,7 @@ class UpdatePage extends Component
|
||||||
|
|
||||||
if ($this->rc === 0 && !$this->postActionsDone) {
|
if ($this->rc === 0 && !$this->postActionsDone) {
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan optimize:clear >/dev/null 2>&1 &');
|
||||||
|
@shell_exec('nohup php /var/www/mailwolt/artisan config:cache >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &');
|
||||||
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
@shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &');
|
||||||
$this->postActionsDone = true;
|
$this->postActionsDone = true;
|
||||||
|
|
@ -169,7 +170,7 @@ class UpdatePage extends Component
|
||||||
{
|
{
|
||||||
$this->current = $this->readCurrentVersion();
|
$this->current = $this->readCurrentVersion();
|
||||||
|
|
||||||
$latNorm = Cache::get('updates:latest');
|
$latNorm = $this->normalizeVersion(Cache::get('updates:latest') ?? '');
|
||||||
$latRaw = Cache::get('updates:latest_raw');
|
$latRaw = Cache::get('updates:latest_raw');
|
||||||
|
|
||||||
if (!$latNorm && ($legacy = Cache::get('mailwolt.update_available'))) {
|
if (!$latNorm && ($legacy = Cache::get('mailwolt.update_available'))) {
|
||||||
|
|
@ -194,7 +195,9 @@ class UpdatePage extends Component
|
||||||
|
|
||||||
$this->displayCurrent = $curNorm ? 'v' . $curNorm : null;
|
$this->displayCurrent = $curNorm ? 'v' . $curNorm : null;
|
||||||
|
|
||||||
if (!$this->displayLatest && $latNorm) {
|
if (!$this->hasUpdate) {
|
||||||
|
$this->displayLatest = null;
|
||||||
|
} elseif (!$this->displayLatest && $latNorm) {
|
||||||
$this->displayLatest = 'v' . $latNorm;
|
$this->displayLatest = 'v' . $latNorm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +208,7 @@ class UpdatePage extends Component
|
||||||
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
|
$rcRaw = @trim(@file_get_contents(self::STATE_DIR . '/rc') ?: '');
|
||||||
|
|
||||||
$this->lowState = $state !== '' ? $state : null;
|
$this->lowState = $state !== '' ? $state : null;
|
||||||
$this->running = ($this->lowState !== 'done');
|
$this->running = ($this->lowState === 'running');
|
||||||
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
|
$this->rc = ($this->lowState === 'done' && is_numeric($rcRaw)) ? (int) $rcRaw : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,16 +258,14 @@ class UpdatePage extends Component
|
||||||
|
|
||||||
protected function readCurrentVersion(): ?string
|
protected function readCurrentVersion(): ?string
|
||||||
{
|
{
|
||||||
// Lokal: direkt aus git describe lesen damit Entwicklungsumgebung immer aktuell ist
|
|
||||||
if (app()->isLocal()) {
|
|
||||||
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
|
|
||||||
$v = $this->normalizeVersion($tag);
|
|
||||||
if ($v) return $v;
|
|
||||||
}
|
|
||||||
|
|
||||||
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
$v = @trim(@file_get_contents(self::VERSION_FILE) ?: '');
|
||||||
if ($v !== '') return $v;
|
if ($v !== '') return $v;
|
||||||
|
|
||||||
|
// Fallback: git tag (lokal immer, production wenn Datei fehlt)
|
||||||
|
$tag = @trim((string) shell_exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null'));
|
||||||
|
$v = $this->normalizeVersion($tag);
|
||||||
|
if ($v) return $v;
|
||||||
|
|
||||||
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
$raw = @trim(@file_get_contents(self::VERSION_FILE_RAW) ?: '');
|
||||||
if ($raw !== '') return $this->normalizeVersion($raw);
|
if ($raw !== '') return $this->normalizeVersion($raw);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
class SandboxRoute extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['type', 'target', 'is_active'];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All active routes ordered by type and target.
|
||||||
|
*/
|
||||||
|
public static function activeRoutes(): Collection
|
||||||
|
{
|
||||||
|
return static::where('is_active', true)
|
||||||
|
->orderBy('type')
|
||||||
|
->orderBy('target')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether sandbox is active for a given domain.
|
||||||
|
* True if global sandbox is active OR domain-specific rule is active.
|
||||||
|
*/
|
||||||
|
public static function isActiveForDomain(string $domain): bool
|
||||||
|
{
|
||||||
|
return static::where('is_active', true)
|
||||||
|
->where(function ($q) use ($domain) {
|
||||||
|
$q->where('type', 'global')
|
||||||
|
->orWhere(function ($q2) use ($domain) {
|
||||||
|
$q2->where('type', 'domain')->where('target', $domain);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether sandbox is active for a given address.
|
||||||
|
* True if global is active OR domain matches OR address-specific rule exists.
|
||||||
|
*/
|
||||||
|
public static function isActiveForAddress(string $address): bool
|
||||||
|
{
|
||||||
|
$domain = substr(strrchr($address, '@'), 1) ?: '';
|
||||||
|
|
||||||
|
return static::where('is_active', true)
|
||||||
|
->where(function ($q) use ($address, $domain) {
|
||||||
|
$q->where('type', 'global')
|
||||||
|
->orWhere(function ($q2) use ($domain) {
|
||||||
|
$q2->where('type', 'domain')->where('target', $domain);
|
||||||
|
})
|
||||||
|
->orWhere(function ($q3) use ($address) {
|
||||||
|
$q3->where('type', 'address')->where('target', $address);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,11 +17,17 @@ class DomainObserver
|
||||||
{
|
{
|
||||||
if ($domain->is_server) return;
|
if ($domain->is_server) return;
|
||||||
|
|
||||||
$selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
$selector = (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
$bits = (int) config('mailpool.defaults.dkim_bits', 2048);
|
$bits = (int) config('mailpool.defaults.dkim_bits', 2048);
|
||||||
|
|
||||||
// Service erledigt: Key generieren, DB (upsert) pflegen, Helper ausführen, OpenDKIM reloaden
|
try {
|
||||||
app(\App\Services\DkimService::class)->generateForDomain($domain, $bits, $selector);
|
app(\App\Services\DkimService::class)->generateForDomain($domain, $bits, $selector);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('DKIM install skipped (sudo/helper not ready)', [
|
||||||
|
'domain' => $domain->domain,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// DNS-Records: aktiven Key aus DB lesen und provisionieren
|
// DNS-Records: aktiven Key aus DB lesen und provisionieren
|
||||||
$active = $domain->dkimKeys()->where('is_active', true)->latest()->first();
|
$active = $domain->dkimKeys()->where('is_active', true)->latest()->first();
|
||||||
|
|
@ -50,7 +56,7 @@ class DomainObserver
|
||||||
|
|
||||||
// Selector VOR dem Delete einsammeln (Relation oder direkte Query)
|
// Selector VOR dem Delete einsammeln (Relation oder direkte Query)
|
||||||
$selectors = DkimKey::where('domain_id', $domain->id)->pluck('selector')->all();
|
$selectors = DkimKey::where('domain_id', $domain->id)->pluck('selector')->all();
|
||||||
$selectors = $selectors ?: ['mwl1'];
|
$selectors = $selectors ?: ['clb1'];
|
||||||
|
|
||||||
foreach ($selectors as $sel) {
|
foreach ($selectors as $sel) {
|
||||||
$cmd = ['sudo','-n','/usr/local/sbin/mailwolt-remove-dkim', $domain->domain, $sel];
|
$cmd = ['sudo','-n','/usr/local/sbin/mailwolt-remove-dkim', $domain->domain, $sel];
|
||||||
|
|
@ -130,7 +136,7 @@ class DomainObserver
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// $selector = (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
// $selector = (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
// $bits = (int) config('mailpool.defaults.dkim_bits', 2048);
|
// $bits = (int) config('mailpool.defaults.dkim_bits', 2048);
|
||||||
//
|
//
|
||||||
// $res = app(\App\Services\DkimService::class)
|
// $res = app(\App\Services\DkimService::class)
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,16 @@ class AppServiceProvider extends ServiceProvider
|
||||||
} catch (\Throwable) {}
|
} catch (\Throwable) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
config(['app.version' => trim(@file_get_contents('/var/lib/mailwolt/version')) ?: 'dev']);
|
$fileVer = trim(@file_get_contents('/var/lib/mailwolt/version') ?: '');
|
||||||
|
$out = []; @exec('git -C ' . escapeshellarg(base_path()) . ' describe --tags --abbrev=0 2>/dev/null', $out);
|
||||||
|
$gitVer = ltrim(trim($out[0] ?? ''), 'vV');
|
||||||
|
$appVer = ($gitVer && (!$fileVer || version_compare($gitVer, $fileVer, '>'))) ? $gitVer : ($fileVer ?: 'dev');
|
||||||
|
// Versions-Datei selbst aktualisieren wenn veraltet
|
||||||
|
if ($appVer !== 'dev' && $appVer !== $fileVer) {
|
||||||
|
@mkdir('/var/lib/mailwolt', 0755, true);
|
||||||
|
@file_put_contents('/var/lib/mailwolt/version', $appVer);
|
||||||
|
}
|
||||||
|
config(['app.version' => $appVer]);
|
||||||
if (file_exists(base_path('.git/HEAD'))) {
|
if (file_exists(base_path('.git/HEAD'))) {
|
||||||
$ref = trim(file_get_contents(base_path('.git/HEAD')));
|
$ref = trim(file_get_contents(base_path('.git/HEAD')));
|
||||||
if (str_starts_with($ref, 'ref:')) {
|
if (str_starts_with($ref, 'ref:')) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class DkimService
|
||||||
/** Erzeugt Keypair & gibt den TXT-Record (ohne Host) zurück. */
|
/** Erzeugt Keypair & gibt den TXT-Record (ohne Host) zurück. */
|
||||||
public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array
|
public function generateForDomain(Domain $domain, int $bits = 2048, string $selector = null): array
|
||||||
{
|
{
|
||||||
$selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
$selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
|
|
||||||
// $dirKey = $this->safeKey($domain->domain);
|
// $dirKey = $this->safeKey($domain->domain);
|
||||||
// $selKey = $this->safeKey($selector, 32);
|
// $selKey = $this->safeKey($selector, 32);
|
||||||
|
|
@ -205,7 +205,7 @@ class DkimService
|
||||||
? $domain->dkimKeys()->pluck('selector')->all()
|
? $domain->dkimKeys()->pluck('selector')->all()
|
||||||
: \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name))
|
: \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name))
|
||||||
->pluck('selector')->all();
|
->pluck('selector')->all();
|
||||||
$keys = $keys ?: ['mwl1'];
|
$keys = $keys ?: ['clb1'];
|
||||||
} else {
|
} else {
|
||||||
$keys = [$selector];
|
$keys = [$selector];
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +241,7 @@ class DkimService
|
||||||
// : \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name))
|
// : \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name))
|
||||||
// ->pluck('selector')->all();
|
// ->pluck('selector')->all();
|
||||||
//
|
//
|
||||||
// $keys = $keys ?: ['mwl1']; // notfalls versuchen wir Standard
|
// $keys = $keys ?: ['clb1']; // notfalls versuchen wir Standard
|
||||||
// } else {
|
// } else {
|
||||||
// $keys = [$selector];
|
// $keys = [$selector];
|
||||||
// }
|
// }
|
||||||
|
|
@ -265,7 +265,7 @@ class DkimService
|
||||||
// public function removeForDomain(Domain|string $domain, ?string $selector = null): void
|
// public function removeForDomain(Domain|string $domain, ?string $selector = null): void
|
||||||
// {
|
// {
|
||||||
// $name = $domain instanceof \App\Models\Domain ? $domain->domain : $domain;
|
// $name = $domain instanceof \App\Models\Domain ? $domain->domain : $domain;
|
||||||
// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
|
// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'clb1');
|
||||||
//
|
//
|
||||||
// // Root-Helper ausführen
|
// // Root-Helper ausführen
|
||||||
// $p = Process::run([
|
// $p = Process::run([
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\SandboxRoute;
|
||||||
|
|
||||||
|
class SandboxService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Enable (or create) a sandbox route.
|
||||||
|
*/
|
||||||
|
public function enable(string $type, ?string $target): SandboxRoute
|
||||||
|
{
|
||||||
|
return SandboxRoute::updateOrCreate(
|
||||||
|
['type' => $type, 'target' => $target],
|
||||||
|
['is_active' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable a sandbox route by id.
|
||||||
|
*/
|
||||||
|
public function disable(int $id): void
|
||||||
|
{
|
||||||
|
SandboxRoute::findOrFail($id)->update(['is_active' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a sandbox route by id.
|
||||||
|
*/
|
||||||
|
public function delete(int $id): void
|
||||||
|
{
|
||||||
|
SandboxRoute::findOrFail($id)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write /etc/postfix/transport.sandbox and run postmap.
|
||||||
|
* Returns ['ok' => bool, 'message' => string].
|
||||||
|
*/
|
||||||
|
public function syncTransportFile(): array
|
||||||
|
{
|
||||||
|
$file = config('sandbox.transport_file', '/etc/postfix/transport.sandbox');
|
||||||
|
|
||||||
|
$routes = SandboxRoute::where('is_active', true)
|
||||||
|
->orderBy('type')
|
||||||
|
->orderBy('target')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
$hasGlobal = false;
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
if ($route->type === 'global') {
|
||||||
|
$hasGlobal = true;
|
||||||
|
} elseif ($route->type === 'domain') {
|
||||||
|
$lines[] = $route->target . ' sandbox:';
|
||||||
|
} elseif ($route->type === 'address') {
|
||||||
|
$lines[] = $route->target . ' sandbox:';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global catch-all goes at the end
|
||||||
|
if ($hasGlobal) {
|
||||||
|
$lines[] = '* sandbox:';
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = implode("\n", $lines);
|
||||||
|
if ($lines) {
|
||||||
|
$content .= "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'mw_sandbox_');
|
||||||
|
try {
|
||||||
|
file_put_contents($tmp, $content);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
@unlink($tmp);
|
||||||
|
return ['ok' => false, 'message' => 'Fehler beim Schreiben: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
$code = 0;
|
||||||
|
exec('sudo -n /usr/local/sbin/mailwolt-sandbox-sync ' . escapeshellarg($tmp) . ' 2>&1', $out, $code);
|
||||||
|
@unlink($tmp);
|
||||||
|
|
||||||
|
if ($code !== 0) {
|
||||||
|
return ['ok' => false, 'message' => 'Sync-Fehler: ' . implode(' ', $out)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['ok' => true, 'message' => 'Transport-Datei synchronisiert (' . count($lines) . ' Einträge).'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
using: function () {
|
using: function () {
|
||||||
// Webmail must be registered BEFORE web.php so its domain constraint
|
// Webmail must be registered BEFORE web.php so its domain constraint
|
||||||
// takes priority over the catch-all Route::get('/') in web.php.
|
// takes priority over the catch-all Route::get('/') in web.php.
|
||||||
$wmHost = config('mailwolt.domain.webmail_host');
|
$wmHost = config('clubird.domain.webmail_host');
|
||||||
|
|
||||||
if ($wmHost) {
|
if ($wmHost) {
|
||||||
Route::middleware('web')
|
Route::middleware('web')
|
||||||
|
|
@ -22,18 +22,28 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
->name('ui.webmail.')
|
->name('ui.webmail.')
|
||||||
->group(base_path('routes/webmail.php'));
|
->group(base_path('routes/webmail.php'));
|
||||||
|
|
||||||
// Path-based fallback (no names — avoids duplicate-name conflict)
|
// Path-based fallback mit eigenem Namen-Prefix — kein Konflikt mit web.php 'login'
|
||||||
Route::middleware('web')
|
Route::middleware('web')
|
||||||
->prefix('webmail')
|
->prefix('webmail')
|
||||||
|
->name('webmail.')
|
||||||
->group(base_path('routes/webmail.php'));
|
->group(base_path('routes/webmail.php'));
|
||||||
|
|
||||||
|
// UI-Routes nur auf UI-Domain binden, damit webmail.* sie nicht beantwortet
|
||||||
|
$uiHost = config('clubird.domain.ui') && config('clubird.domain.base')
|
||||||
|
? config('clubird.domain.ui') . '.' . config('clubird.domain.base')
|
||||||
|
: parse_url(config('app.url'), PHP_URL_HOST);
|
||||||
|
|
||||||
|
Route::middleware('web')
|
||||||
|
->domain($uiHost)
|
||||||
|
->group(base_path('routes/web.php'));
|
||||||
} else {
|
} else {
|
||||||
Route::middleware('web')
|
Route::middleware('web')
|
||||||
->prefix('webmail')
|
->prefix('webmail')
|
||||||
->name('ui.webmail.')
|
->name('ui.webmail.')
|
||||||
->group(base_path('routes/webmail.php'));
|
->group(base_path('routes/webmail.php'));
|
||||||
}
|
|
||||||
|
|
||||||
Route::middleware('web')->group(base_path('routes/web.php'));
|
Route::middleware('web')->group(base_path('routes/web.php'));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
return [
|
return [
|
||||||
'platform_zone' => env('BASE_DOMAIN', 'example.com'),
|
'platform_zone' => env('BASE_DOMAIN', 'example.com'),
|
||||||
'platform_system_zone' => env('SYSMAIL_SUB', 'sysmail'),
|
'platform_system_zone' => env('SYSMAIL_SUB', 'sysmail'),
|
||||||
|
'mta_sub' => env('MTA_SUB', 'mail'),
|
||||||
|
|
||||||
'fixed_reserve_mb' => env('MAILPOOL_FIXED_RESERVE_MB', 2048), // 2 GB
|
'fixed_reserve_mb' => env('MAILPOOL_FIXED_RESERVE_MB', 2048), // 2 GB
|
||||||
'percent_reserve' => env('MAILPOOL_PERCENT_RESERVE', 10), // 10 %
|
'percent_reserve' => env('MAILPOOL_PERCENT_RESERVE', 10), // 10 %
|
||||||
|
|
@ -18,7 +19,7 @@ return [
|
||||||
'fallback_du' => (bool) env('MAILPOOL_FALLBACK_DU', true),
|
'fallback_du' => (bool) env('MAILPOOL_FALLBACK_DU', true),
|
||||||
|
|
||||||
'defaults' => [
|
'defaults' => [
|
||||||
'dkim_selector' => 'mwl1',
|
'dkim_selector' => 'clb1',
|
||||||
'dkim_bits' => 2048,
|
'dkim_bits' => 2048,
|
||||||
|
|
||||||
'max_aliases' => (int) env('MAILPOOL_DEFAULT_MAX_ALIASES', 400),
|
'max_aliases' => (int) env('MAILPOOL_DEFAULT_MAX_ALIASES', 400),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ return [
|
||||||
'label' => 'Sicherheit', 'icon' => 'ph-shield', 'items' => [
|
'label' => 'Sicherheit', 'icon' => 'ph-shield', 'items' => [
|
||||||
['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
|
['label' => 'TLS & Ciphers', 'route' => 'ui.security.tls'],
|
||||||
['label' => 'Ratelimits', 'route' => 'ui.security.abuse'],
|
['label' => 'Ratelimits', 'route' => 'ui.security.abuse'],
|
||||||
|
['label' => 'Virenschutz', 'route' => 'ui.security.clamav'],
|
||||||
['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
|
['label' => 'Audit-Logs', 'route' => 'ui.security.audit'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
// Nur diese Keys erscheinen im Dashboard-Widget
|
||||||
|
'dashboard' => [
|
||||||
|
'postfix', 'dovecot', 'rspamd', 'clamav', 'db', 'redis', 'nginx', 'mw-ws', 'fail2ban',
|
||||||
|
],
|
||||||
|
|
||||||
'cards' => [
|
'cards' => [
|
||||||
// Mail
|
// Mail
|
||||||
'postfix' => [
|
'postfix' => [
|
||||||
|
|
@ -18,9 +23,10 @@ return [
|
||||||
],
|
],
|
||||||
'clamav' => [
|
'clamav' => [
|
||||||
'label' => 'ClamAV', 'hint' => 'Virenscanner',
|
'label' => 'ClamAV', 'hint' => 'Virenscanner',
|
||||||
|
'optional' => true,
|
||||||
'sources' => [
|
'sources' => [
|
||||||
'systemd:clamav-daemon', 'systemd:clamav-daemon@scan', 'systemd:clamd',
|
// Nur systemd – Socket-Datei bleibt nach dem Stoppen erhalten (false positive)
|
||||||
'socket:/run/clamav/clamd.ctl', 'pid:/run/clamav/clamd.pid', 'tcp:127.0.0.1:3310',
|
'systemd:clamav-daemon',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -64,6 +70,10 @@ return [
|
||||||
],
|
],
|
||||||
|
|
||||||
// Sonstiges
|
// Sonstiges
|
||||||
|
'monit' => [
|
||||||
|
'label' => 'Monit', 'hint' => 'Prozess-Monitoring',
|
||||||
|
'sources' => ['systemd:monit', 'tcp:127.0.0.1:2812'],
|
||||||
|
],
|
||||||
'fail2ban' => [
|
'fail2ban' => [
|
||||||
'label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection',
|
'label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection',
|
||||||
'sources' => ['systemd:fail2ban'],
|
'sources' => ['systemd:fail2ban'],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('sandbox_routes', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->enum('type', ['global', 'domain', 'address'])->default('domain');
|
||||||
|
$table->string('target')->nullable(); // domain or email, null = global
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->unique(['type', 'target']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('sandbox_routes');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('two_factor_methods', function (Blueprint $table) {
|
||||||
|
$table->text('secret')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('two_factor_methods', function (Blueprint $table) {
|
||||||
|
$table->string('secret', 255)->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -140,7 +140,7 @@ class SystemDomainSeeder extends Seeder
|
||||||
$this->command->line("System-Domain angelegt: {$systemDomain->domain}");
|
$this->command->line("System-Domain angelegt: {$systemDomain->domain}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$noReply = MailUser::firstOrCreate(
|
$noReply = MailUser::updateOrCreate(
|
||||||
['domain_id' => $systemDomain->id, 'localpart' => 'no-reply'],
|
['domain_id' => $systemDomain->id, 'localpart' => 'no-reply'],
|
||||||
[
|
[
|
||||||
'email' => 'no-reply@' . $systemDomain->domain,
|
'email' => 'no-reply@' . $systemDomain->domain,
|
||||||
|
|
@ -305,7 +305,7 @@ class SystemDomainSeeder extends Seeder
|
||||||
// // DKIM – Key erzeugen, falls keiner aktiv existiert
|
// // DKIM – Key erzeugen, falls keiner aktiv existiert
|
||||||
// if (!$systemDomain->dkimKeys()->where('is_active', true)->exists()) {
|
// if (!$systemDomain->dkimKeys()->where('is_active', true)->exists()) {
|
||||||
// [$privPem, $pubTxt] = $this->generateDkimKeyPair();
|
// [$privPem, $pubTxt] = $this->generateDkimKeyPair();
|
||||||
// $selector = 'mwl1'; // frei wählbar, später rotieren
|
// $selector = 'clb1'; // frei wählbar, später rotieren
|
||||||
//
|
//
|
||||||
// DkimKey::create([
|
// DkimKey::create([
|
||||||
// 'domain_id' => $systemDomain->id,
|
// 'domain_id' => $systemDomain->id,
|
||||||
|
|
@ -452,7 +452,7 @@ class SystemDomainSeeder extends Seeder
|
||||||
// // DKIM – Key erzeugen, falls keiner aktiv existiert
|
// // DKIM – Key erzeugen, falls keiner aktiv existiert
|
||||||
// if (!$systemDomain->dkimKeys()->where('is_active', true)->exists()) {
|
// if (!$systemDomain->dkimKeys()->where('is_active', true)->exists()) {
|
||||||
// [$privPem, $pubTxt] = $this->generateDkimKeyPair();
|
// [$privPem, $pubTxt] = $this->generateDkimKeyPair();
|
||||||
// $selector = 'mwl1'; // frei wählbar, später rotieren
|
// $selector = 'clb1'; // frei wählbar, später rotieren
|
||||||
//
|
//
|
||||||
// DkimKey::create([
|
// DkimKey::create([
|
||||||
// 'domain_id' => $systemDomain->id,
|
// 'domain_id' => $systemDomain->id,
|
||||||
|
|
@ -569,7 +569,7 @@ class SystemDomainSeeder extends Seeder
|
||||||
// // DKIM – Key erzeugen, falls keiner aktiv existiert
|
// // DKIM – Key erzeugen, falls keiner aktiv existiert
|
||||||
// if (! $domain->dkimKeys()->where('is_active', true)->exists()) {
|
// if (! $domain->dkimKeys()->where('is_active', true)->exists()) {
|
||||||
// [$privPem, $pubTxt] = $this->generateDkimKeyPair();
|
// [$privPem, $pubTxt] = $this->generateDkimKeyPair();
|
||||||
// $selector = 'mwl1'; // frei wählbar, z. B. rotierend später
|
// $selector = 'clb1'; // frei wählbar, z. B. rotierend später
|
||||||
//
|
//
|
||||||
// DkimKey::create([
|
// DkimKey::create([
|
||||||
// 'domain_id' => $domain->id,
|
// 'domain_id' => $domain->id,
|
||||||
|
|
|
||||||
532
installer.sh
532
installer.sh
|
|
@ -44,6 +44,10 @@ NODE_SETUP="${NODE_SETUP:-deb}"
|
||||||
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
|
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
|
||||||
BAR="──────────────────────────────────────────────────────────────────────────────"
|
BAR="──────────────────────────────────────────────────────────────────────────────"
|
||||||
|
|
||||||
|
# ===== Install-Log =====
|
||||||
|
LOG_FILE="/var/log/mailwolt-install.log"
|
||||||
|
> "$LOG_FILE"
|
||||||
|
|
||||||
header() {
|
header() {
|
||||||
echo -e "${CYAN}${BAR}${NC}"
|
echo -e "${CYAN}${BAR}${NC}"
|
||||||
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
|
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
|
||||||
|
|
@ -63,27 +67,72 @@ footer_ok() {
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
local app_name="${2:-$APP_NAME}"
|
local app_name="${2:-$APP_NAME}"
|
||||||
local app_dir="${3:-$APP_DIR}"
|
local app_dir="${3:-$APP_DIR}"
|
||||||
local nginx_site="${4:-$NGINX_SITE}"
|
local cert_dir="${4:-$CERT_DIR}"
|
||||||
local cert_dir="${5:-$CERT_DIR}"
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
echo -e "${GREEN} ✔ ${app_name} Bootstrap erfolgreich abgeschlossen${NC}"
|
echo -e "${GREEN} ✔ ${app_name} Installation erfolgreich abgeschlossen${NC}"
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
echo -e " Aufruf: ${CYAN}http://${ip}${NC} ${GREY}| https://${ip}${NC}"
|
echo -e ""
|
||||||
echo -e " Laravel Root: ${GREY}${app_dir}${NC}"
|
echo -e " ${CYAN}➜ Setup-Wizard jetzt öffnen:${NC}"
|
||||||
echo -e " Nginx Site: ${GREY}${nginx_site}${NC}"
|
echo -e " ${CYAN}http://${ip}/setup${NC}"
|
||||||
echo -e " Self-signed Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC}"
|
echo -e ""
|
||||||
echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
echo -e " Laravel Root: ${GREY}${app_dir}${NC}"
|
||||||
echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}"
|
echo -e " Mail-TLS Cert: ${GREY}${cert_dir}/{cert.pem,key.pem}${NC} (Postfix/Dovecot)"
|
||||||
echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}"
|
echo -e " Postfix/Dovecot: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
echo -e "${GREEN}${BAR}${NC}"
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
_STEP_T=0
|
||||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
_SPIN_PID=
|
||||||
err() { echo -e "${RED}[x]${NC} $*"; }
|
|
||||||
|
_spin_bg() {
|
||||||
|
local chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
||||||
|
local n=${#chars} i=0
|
||||||
|
while true; do
|
||||||
|
printf "\r \033[0;90m${chars:$i:1}\033[0m"
|
||||||
|
i=$(( (i + 1) % n ))
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
start_spin() { _spin_bg & _SPIN_PID=$!; }
|
||||||
|
stop_spin() {
|
||||||
|
[[ -n "${_SPIN_PID:-}" ]] || return 0
|
||||||
|
kill "$_SPIN_PID" 2>/dev/null || true
|
||||||
|
wait "$_SPIN_PID" 2>/dev/null || true
|
||||||
|
_SPIN_PID=
|
||||||
|
printf "\r\033[K"
|
||||||
|
}
|
||||||
|
trap 'stop_spin' EXIT INT TERM
|
||||||
|
|
||||||
|
step() {
|
||||||
|
local msg="$1" dur="${2:-}"
|
||||||
|
[ -n "$dur" ] && dur=" ${GREY}(~${dur})${NC}" || dur=""
|
||||||
|
printf "\n${CYAN} ▸${NC} %-42s%b\n" "$msg" "$dur"
|
||||||
|
_STEP_T=$SECONDS
|
||||||
|
}
|
||||||
|
ok() {
|
||||||
|
stop_spin
|
||||||
|
local t=$(( SECONDS - _STEP_T ))
|
||||||
|
[ $t -gt 1 ] && printf " ${GREEN}✔${NC} ${GREY}%ds${NC}\n" $t || printf " ${GREEN}✔${NC}\n"
|
||||||
|
}
|
||||||
|
warn() { stop_spin; printf " ${YELLOW}⚠${NC} %s\n" "$*"; }
|
||||||
|
err() { stop_spin; printf " ${RED}✗${NC} %s\n" "$*"; }
|
||||||
|
|
||||||
|
quietly() {
|
||||||
|
start_spin
|
||||||
|
if ! "$@" >> "$LOG_FILE" 2>&1; then
|
||||||
|
stop_spin
|
||||||
|
printf " ${RED}✗${NC} Fehlgeschlagen. Letzte Log-Zeilen:\n\n"
|
||||||
|
tail -20 "$LOG_FILE" | sed 's/^/ /'
|
||||||
|
printf "\n ${GREY}Vollständiges Log: %s${NC}\n\n" "$LOG_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
stop_spin
|
||||||
|
}
|
||||||
|
try_quiet() { "$@" >> "$LOG_FILE" 2>&1 || true; }
|
||||||
|
log() { :; }
|
||||||
require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
|
require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
|
||||||
|
|
||||||
# ===== IP ermitteln =====
|
# ===== IP ermitteln =====
|
||||||
|
|
@ -100,30 +149,37 @@ gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" ||
|
||||||
pw() { gen 28; }
|
pw() { gen 28; }
|
||||||
short() { gen 16; }
|
short() { gen 16; }
|
||||||
|
|
||||||
|
# ===== Argument-Parsing =====
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-dev) APP_ENV="local"; APP_DEBUG="true" ;;
|
||||||
|
-stag|-staging) APP_ENV="staging"; APP_DEBUG="false" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
# ===== Start =====
|
# ===== Start =====
|
||||||
require_root
|
require_root
|
||||||
header
|
header
|
||||||
|
|
||||||
SERVER_IP="$(detect_ip)"
|
SERVER_IP="$(detect_ip)"
|
||||||
|
APP_PW="${APP_PW:-$(pw)}"
|
||||||
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN
|
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}" # Wizard setzt später FQDN
|
||||||
TZ="${TZ:-""}" # leer; Wizard setzt final
|
TZ="${TZ:-""}" # leer; Wizard setzt final
|
||||||
|
|
||||||
echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}"
|
echo -e "\n ${GREY}Server-IP: ${SERVER_IP} Log: ${LOG_FILE}${NC}"
|
||||||
[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
|
[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
|
||||||
|
|
||||||
log "Paketquellen aktualisieren…"
|
step "Paketquellen aktualisieren" "10 Sek"
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update -y
|
quietly apt-get update -y
|
||||||
|
ok
|
||||||
|
|
||||||
# ---- MariaDB-Workaround (fix für mariadb-common prompt) ----
|
|
||||||
log "MariaDB-Workaround vorbereiten…"
|
|
||||||
mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
|
mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
|
||||||
[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
||||||
|
|
||||||
# ---- Basis-Pakete installieren ----
|
step "Pakete installieren" "2–5 Min"
|
||||||
log "Pakete installieren… (dies kann einige Minuten dauern)"
|
quietly apt-get -y -o Dpkg::Options::="--force-confdef" \
|
||||||
#apt-get install -y \
|
|
||||||
apt-get -y -o Dpkg::Options::="--force-confdef" \
|
|
||||||
-o Dpkg::Options::="--force-confold" install \
|
-o Dpkg::Options::="--force-confold" install \
|
||||||
postfix postfix-mysql \
|
postfix postfix-mysql \
|
||||||
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
||||||
|
|
@ -132,20 +188,20 @@ apt-get -y -o Dpkg::Options::="--force-confdef" \
|
||||||
rspamd \
|
rspamd \
|
||||||
opendkim opendkim-tools \
|
opendkim opendkim-tools \
|
||||||
nginx \
|
nginx \
|
||||||
php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd unzip curl \
|
php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd php-sqlite3 unzip curl acl \
|
||||||
composer \
|
composer \
|
||||||
certbot python3-certbot-nginx \
|
certbot python3-certbot-nginx \
|
||||||
fail2ban \
|
fail2ban \
|
||||||
ca-certificates rsyslog sudo openssl netcat-openbsd monit
|
ca-certificates rsyslog sudo openssl netcat-openbsd monit git
|
||||||
|
ok
|
||||||
|
|
||||||
# ===== Verzeichnisse / User =====
|
step "Benutzer & Verzeichnisse anlegen" "5 Sek"
|
||||||
log "Verzeichnisse und Benutzer anlegen…"
|
|
||||||
mkdir -p ${CERT_DIR} /etc/postfix /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
|
mkdir -p ${CERT_DIR} /etc/postfix /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
|
||||||
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
quietly bash -c "id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail"
|
||||||
chown -R vmail:vmail /var/mail
|
chown -R vmail:vmail /var/mail
|
||||||
|
quietly bash -c "id '$APP_USER' >/dev/null 2>&1 || adduser --disabled-password --gecos '' '$APP_USER'"
|
||||||
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
quietly usermod -a -G www-data "$APP_USER"
|
||||||
usermod -a -G www-data "$APP_USER"
|
ok
|
||||||
|
|
||||||
# ===== Self-signed TLS (SAN = IP) =====
|
# ===== Self-signed TLS (SAN = IP) =====
|
||||||
CERT="${CERT_DIR}/cert.pem"
|
CERT="${CERT_DIR}/cert.pem"
|
||||||
|
|
@ -153,7 +209,7 @@ KEY="${CERT_DIR}/key.pem"
|
||||||
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
||||||
|
|
||||||
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
|
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
|
||||||
log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
|
step "Self-Signed Zertifikat erstellen" "5 Sek"
|
||||||
cat > "$OSSL_CFG" <<CFG
|
cat > "$OSSL_CFG" <<CFG
|
||||||
[req]
|
[req]
|
||||||
default_bits = 2048
|
default_bits = 2048
|
||||||
|
|
@ -173,26 +229,26 @@ subjectAltName = @alt_names
|
||||||
[alt_names]
|
[alt_names]
|
||||||
IP.1 = ${SERVER_IP}
|
IP.1 = ${SERVER_IP}
|
||||||
CFG
|
CFG
|
||||||
openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
|
quietly openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
|
||||||
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
||||||
chmod 600 "$KEY"; chmod 644 "$CERT"
|
chmod 600 "$KEY"; chmod 644 "$CERT"
|
||||||
|
ok
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ===== MariaDB vorbereiten =====
|
# ===== MariaDB vorbereiten =====
|
||||||
log "MariaDB vorbereiten…"
|
step "Datenbank einrichten" "10 Sek"
|
||||||
systemctl enable --now mariadb
|
quietly systemctl enable --now mariadb
|
||||||
DB_NAME="${DB_USER}"
|
|
||||||
DB_USER="${DB_USER}"
|
|
||||||
DB_PASS="$(pw)"
|
DB_PASS="$(pw)"
|
||||||
mysql -uroot <<SQL
|
quietly mysql -uroot <<SQL
|
||||||
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
||||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
SQL
|
SQL
|
||||||
|
ok
|
||||||
|
|
||||||
# ===== Postfix konfigurieren (25/465/587) =====
|
# ===== Postfix konfigurieren (25/465/587) =====
|
||||||
log "Postfix konfigurieren…"
|
step "Mailserver konfigurieren (Postfix / Dovecot / Rspamd)" "15 Sek"
|
||||||
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
||||||
postconf -e "myorigin = \$myhostname"
|
postconf -e "myorigin = \$myhostname"
|
||||||
postconf -e "mydestination = "
|
postconf -e "mydestination = "
|
||||||
|
|
@ -271,10 +327,9 @@ CONF
|
||||||
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
||||||
|
|
||||||
systemctl enable --now postfix
|
try_quiet systemctl enable --now postfix
|
||||||
|
|
||||||
# ===== Dovecot konfigurieren (IMAP/POP3 + SSL) =====
|
# ===== Dovecot konfigurieren (IMAP/POP3 + SSL) =====
|
||||||
log "Dovecot konfigurieren…"
|
|
||||||
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
||||||
!include_try /etc/dovecot/conf.d/*.conf
|
!include_try /etc/dovecot/conf.d/*.conf
|
||||||
CONF
|
CONF
|
||||||
|
|
@ -350,15 +405,14 @@ ssl_cert = <${CERT}
|
||||||
ssl_key = <${KEY}
|
ssl_key = <${KEY}
|
||||||
CONF
|
CONF
|
||||||
|
|
||||||
systemctl enable --now dovecot
|
try_quiet systemctl enable --now dovecot
|
||||||
|
|
||||||
# ===== Rspamd & OpenDKIM =====
|
# ===== Rspamd & OpenDKIM =====
|
||||||
log "Rspamd + OpenDKIM aktivieren…"
|
|
||||||
cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF'
|
cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF'
|
||||||
password = "admin";
|
password = "admin";
|
||||||
bind_socket = "127.0.0.1:11334";
|
bind_socket = "127.0.0.1:11334";
|
||||||
CONF
|
CONF
|
||||||
systemctl enable --now rspamd || true
|
try_quiet systemctl enable --now rspamd
|
||||||
|
|
||||||
cat > /etc/opendkim.conf <<'CONF'
|
cat > /etc/opendkim.conf <<'CONF'
|
||||||
Syslog yes
|
Syslog yes
|
||||||
|
|
@ -374,17 +428,17 @@ LogWhy yes
|
||||||
OversignHeaders From
|
OversignHeaders From
|
||||||
# KeyTable / SigningTable später im Wizard
|
# KeyTable / SigningTable später im Wizard
|
||||||
CONF
|
CONF
|
||||||
systemctl enable --now opendkim || true
|
try_quiet systemctl enable --now opendkim
|
||||||
|
try_quiet systemctl enable --now redis-server
|
||||||
# ===== Redis =====
|
ok
|
||||||
systemctl enable --now redis-server
|
|
||||||
|
|
||||||
# ===== Nginx: Laravel vHost (80/443) =====
|
# ===== Nginx: Laravel vHost (80/443) =====
|
||||||
log "Nginx konfigurieren…"
|
step "Webserver konfigurieren (Nginx)" "5 Sek"
|
||||||
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
||||||
|
|
||||||
PHP_FPM_SOCK="/run/php/php-fpm.sock"
|
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
||||||
[ -S "/run/php/php8.2-fpm.sock" ] && PHP_FPM_SOCK="/run/php/php8.2-fpm.sock"
|
PHP_FPM_SOCK="/run/php/php${PHPV}-fpm.sock"
|
||||||
|
[ -S "$PHP_FPM_SOCK" ] || PHP_FPM_SOCK="/run/php/php-fpm.sock"
|
||||||
|
|
||||||
cat > ${NGINX_SITE} <<CONF
|
cat > ${NGINX_SITE} <<CONF
|
||||||
server {
|
server {
|
||||||
|
|
@ -398,6 +452,10 @@ server {
|
||||||
access_log /var/log/nginx/${APP_USER}_access.log;
|
access_log /var/log/nginx/${APP_USER}_access.log;
|
||||||
error_log /var/log/nginx/${APP_USER}_error.log;
|
error_log /var/log/nginx/${APP_USER}_error.log;
|
||||||
|
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/letsencrypt;
|
||||||
|
try_files \$uri =404;
|
||||||
|
}
|
||||||
location / {
|
location / {
|
||||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
try_files \$uri \$uri/ /index.php?\$query_string;
|
||||||
}
|
}
|
||||||
|
|
@ -406,38 +464,7 @@ server {
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
fastcgi_pass unix:${PHP_FPM_SOCK};
|
||||||
}
|
}
|
||||||
location ^~ /livewire/ {
|
location ^~ /livewire/ {
|
||||||
try_files $uri /index.php?$query_string;
|
try_files \$uri /index.php?\$query_string;
|
||||||
}
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
|
|
||||||
expires 30d;
|
|
||||||
access_log off;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
listen [::]:443 ssl http2;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
ssl_certificate ${CERT};
|
|
||||||
ssl_certificate_key ${KEY};
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/${APP_USER}_ssl_access.log;
|
|
||||||
error_log /var/log/nginx/${APP_USER}_ssl_error.log;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files \$uri \$uri/ /index.php?\$query_string;
|
|
||||||
}
|
|
||||||
location ~ \.php\$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_pass unix:${PHP_FPM_SOCK};
|
|
||||||
}
|
|
||||||
location ^~ /livewire/ {
|
|
||||||
try_files $uri /index.php?$query_string;
|
|
||||||
}
|
}
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
|
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
|
|
@ -446,176 +473,182 @@ server {
|
||||||
}
|
}
|
||||||
CONF
|
CONF
|
||||||
ln -sf ${NGINX_SITE} ${NGINX_SITE_LINK}
|
ln -sf ${NGINX_SITE} ${NGINX_SITE_LINK}
|
||||||
nginx -t && systemctl enable --now nginx
|
try_quiet nginx -t
|
||||||
|
try_quiet systemctl enable --now nginx
|
||||||
|
ok
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
step "Projekt herunterladen (Git)" "15 Sek"
|
||||||
case "$1" in
|
|
||||||
-dev)
|
|
||||||
APP_ENV="local"
|
|
||||||
APP_DEBUG="true"
|
|
||||||
;;
|
|
||||||
-stag|-staging)
|
|
||||||
APP_ENV="staging"
|
|
||||||
APP_DEBUG="false"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
# ===== Laravel installieren (als eigener User) =====
|
|
||||||
log "Laravel installieren…"
|
|
||||||
mkdir -p "$(dirname "$APP_DIR")"
|
mkdir -p "$(dirname "$APP_DIR")"
|
||||||
chown -R "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")"
|
chown "$APP_USER":$APP_GROUP "$(dirname "$APP_DIR")"
|
||||||
|
|
||||||
if [ ! -d "${APP_DIR}" ] || [ -z "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]; then
|
if [ ! -d "${APP_DIR}/.git" ]; then
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction"
|
rm -rf "${APP_DIR}"
|
||||||
|
quietly sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
||||||
|
else
|
||||||
|
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git checkout ${GIT_BRANCH} && git pull --ff-only"
|
||||||
fi
|
fi
|
||||||
|
ok
|
||||||
|
|
||||||
|
# ===== .env erstellen und befüllen =====
|
||||||
APP_URL="http://${SERVER_IP}"
|
APP_URL="http://${SERVER_IP}"
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force"
|
|
||||||
|
|
||||||
# .env befüllen (MariaDB & Redis Sessions)
|
|
||||||
sed -i "s|^APP_NAME=.*|APP_NAME=${APP_NAME}|g" "${APP_DIR}/.env"
|
sed -i "s|^APP_NAME=.*|APP_NAME=${APP_NAME}|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^APP_URL=.*|APP_URL=${APP_URL}|g" "${APP_DIR}/.env"
|
sed -i "s|^APP_URL=.*|APP_URL=${APP_URL}|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^APP_ENV=.*|APP_ENV=${APP_ENV}|g" "${APP_DIR}/.env"
|
sed -i "s|^APP_ENV=.*|APP_ENV=${APP_ENV}|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^APP_DEBUG=.*|APP_DEBUG=${APP_DEBUG}|g" "${APP_DIR}/.env"
|
sed -i "s|^APP_DEBUG=.*|APP_DEBUG=${APP_DEBUG}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
sed -i "s|^DB_CONNECTION=.*|DB_CONNECTION=mysql|g" "${APP_DIR}/.env"
|
sed -i "s|^DB_CONNECTION=.*|DB_CONNECTION=mysql|g" "${APP_DIR}/.env"
|
||||||
sed -i -E "s|^[#[:space:]]*DB_HOST=.*|DB_HOST=127.0.0.1|g" "${APP_DIR}/.env"
|
sed -i -E "s|^[#[:space:]]*DB_HOST=.*|DB_HOST=127.0.0.1|g" "${APP_DIR}/.env"
|
||||||
sed -i -E "s|^[#[:space:]]*DB_PORT=.*|DB_PORT=3306|g" "${APP_DIR}/.env"
|
sed -i -E "s|^[#[:space:]]*DB_PORT=.*|DB_PORT=3306|g" "${APP_DIR}/.env"
|
||||||
sed -i -E "s|^[#[:space:]]*DB_DATABASE=.*|DB_DATABASE=${DB_NAME}|g" "${APP_DIR}/.env"
|
sed -i -E "s|^[#[:space:]]*DB_DATABASE=.*|DB_DATABASE=${DB_NAME}|g" "${APP_DIR}/.env"
|
||||||
sed -i -E "s|^[#[:space:]]*DB_USERNAME=.*|DB_USERNAME=${DB_USER}|g" "${APP_DIR}/.env"
|
sed -i -E "s|^[#[:space:]]*DB_USERNAME=.*|DB_USERNAME=${DB_USER}|g" "${APP_DIR}/.env"
|
||||||
sed -i -E "s|^[#[:space:]]*DB_PASSWORD=.*|DB_PASSWORD=${DB_PASS}|g" "${APP_DIR}/.env"
|
sed -i -E "s|^[#[:space:]]*DB_PASSWORD=.*|DB_PASSWORD=${DB_PASS}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
sed -i "s|^CACHE_DRIVER=.*|CACHE_DRIVER=redis|g" "${APP_DIR}/.env"
|
sed -i "s|^CACHE_DRIVER=.*|CACHE_DRIVER=redis|g" "${APP_DIR}/.env"
|
||||||
sed -i -E "s|^[#[:space:]]*CACHE_PREFIX=.*|CACHE_PREFIX=${APP_USER}|g" "${APP_DIR}/.env"
|
sed -i -E "s|^[#[:space:]]*CACHE_PREFIX=.*|CACHE_PREFIX=${APP_USER}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
sed -i "s|^SESSION_DRIVER=.*|SESSION_DRIVER=redis|g" "${APP_DIR}/.env"
|
sed -i "s|^SESSION_DRIVER=.*|SESSION_DRIVER=redis|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^REDIS_HOST=.*|REDIS_HOST=127.0.0.1|g" "${APP_DIR}/.env"
|
sed -i "s|^REDIS_HOST=.*|REDIS_HOST=127.0.0.1|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=null|g" "${APP_DIR}/.env"
|
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=null|g" "${APP_DIR}/.env"
|
||||||
sed -i "s|^REDIS_PORT=.*|REDIS_PORT=6379|g" "${APP_DIR}/.env"
|
sed -i "s|^REDIS_PORT=.*|REDIS_PORT=6379|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
|
REVERB_APP_ID="$(short)"
|
||||||
|
REVERB_APP_KEY="$(short)"
|
||||||
|
REVERB_APP_SECRET="$(short)"
|
||||||
|
grep -q '^REVERB_APP_ID=' "${APP_DIR}/.env" \
|
||||||
|
|| printf '\nBROADCAST_CONNECTION=reverb\nREVERB_APP_ID=%s\nREVERB_APP_KEY=%s\nREVERB_APP_SECRET=%s\nREVERB_HOST=127.0.0.1\nREVERB_PORT=8080\nREVERB_SCHEME=http\nVITE_REVERB_APP_KEY=%s\nVITE_REVERB_HOST=%s\nVITE_REVERB_PORT=8080\nVITE_REVERB_SCHEME=http\n' \
|
||||||
|
"$REVERB_APP_ID" "$REVERB_APP_KEY" "$REVERB_APP_SECRET" "$REVERB_APP_KEY" "$SERVER_IP" >> "${APP_DIR}/.env"
|
||||||
|
|
||||||
# === Bootstrap-Admin für den ersten Login (nur .env, kein DB-User) ===
|
# Bootstrap-Admin für den ersten Login
|
||||||
BOOTSTRAP_USER="${APP_USER}"
|
BOOTSTRAP_USER="${APP_USER}"
|
||||||
BOOTSTRAP_EMAIL="${APP_USER}@localhost"
|
BOOTSTRAP_EMAIL="${APP_USER}@localhost"
|
||||||
BOOTSTRAP_PASS="$(openssl rand -base64 18 | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 12)"
|
BOOTSTRAP_PASS="$(openssl rand -base64 18 | LC_ALL=C tr -dc 'A-Za-z0-9' | head -c 12)"
|
||||||
BOOTSTRAP_HASH="$(php -r 'echo password_hash($argv[1], PASSWORD_BCRYPT);' "$BOOTSTRAP_PASS")"
|
BOOTSTRAP_HASH="$(php -r 'echo password_hash($argv[1], PASSWORD_BCRYPT);' "$BOOTSTRAP_PASS")"
|
||||||
|
|
||||||
grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" || echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env"
|
grep -q '^SETUP_PHASE=' "${APP_DIR}/.env" \
|
||||||
|
|| echo "SETUP_PHASE=bootstrap" >> "${APP_DIR}/.env"
|
||||||
sed -i "s|^SETUP_PHASE=.*|SETUP_PHASE=bootstrap|g" "${APP_DIR}/.env"
|
sed -i "s|^SETUP_PHASE=.*|SETUP_PHASE=bootstrap|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
grep -q '^BOOTSTRAP_ADMIN_USER=' "${APP_DIR}/.env" || echo "BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}" >> "${APP_DIR}/.env"
|
grep -q '^BOOTSTRAP_ADMIN_USER=' "${APP_DIR}/.env" \
|
||||||
|
|| echo "BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}" >> "${APP_DIR}/.env"
|
||||||
sed -i "s|^BOOTSTRAP_ADMIN_USER=.*|BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}|g" "${APP_DIR}/.env"
|
sed -i "s|^BOOTSTRAP_ADMIN_USER=.*|BOOTSTRAP_ADMIN_USER=${BOOTSTRAP_USER}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" || echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "$>
|
grep -q '^BOOTSTRAP_ADMIN_EMAIL=' "${APP_DIR}/.env" \
|
||||||
|
|| echo "BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}" >> "${APP_DIR}/.env"
|
||||||
sed -i "s|^BOOTSTRAP_ADMIN_EMAIL=.*|BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}|g" "${APP_DIR}/.env"
|
sed -i "s|^BOOTSTRAP_ADMIN_EMAIL=.*|BOOTSTRAP_ADMIN_EMAIL=${BOOTSTRAP_EMAIL}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" || echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env"
|
grep -q '^BOOTSTRAP_ADMIN_PASSWORD_HASH=' "${APP_DIR}/.env" \
|
||||||
|
|| echo "BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}" >> "${APP_DIR}/.env"
|
||||||
sed -i "s|^BOOTSTRAP_ADMIN_PASSWORD_HASH=.*|BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}|g" "${APP_DIR}/.env"
|
sed -i "s|^BOOTSTRAP_ADMIN_PASSWORD_HASH=.*|BOOTSTRAP_ADMIN_PASSWORD_HASH=${BOOTSTRAP_HASH}|g" "${APP_DIR}/.env"
|
||||||
|
|
||||||
# ===== Node/NPM installieren (für Vite/Tailwind Build) =====
|
step "PHP-Abhängigkeiten installieren (Composer)" "1–2 Min"
|
||||||
if [ "$NODE_SETUP" = "nodesource" ]; then
|
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-dev --optimize-autoloader --no-interaction"
|
||||||
# LTS via NodeSource (empfohlen für aktuelle LTS)
|
ok
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
||||||
apt-get install -y nodejs
|
|
||||||
else
|
|
||||||
# Debian-Repo (ok für Basics, aber u.U. älter)
|
|
||||||
apt-get install -y nodejs npm
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== Projekt aus Git holen (PLATZHALTER) =====
|
step "Datenbank migrieren" "15 Sek"
|
||||||
# Falls dein Repo später bereitsteht, überschreibt dieser Block das leere/Standard-Laravel.
|
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force"
|
||||||
if [ "${GIT_REPO}" != "https://example.com/your-repo-placeholder.git" ]; then
|
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force"
|
||||||
if [ ! -d "${APP_DIR}/.git" ]; then
|
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan storage:link --force"
|
||||||
sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan config:cache"
|
||||||
|
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan route:cache"
|
||||||
|
try_quiet sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan view:cache"
|
||||||
|
ok
|
||||||
|
|
||||||
|
if [ -f "${APP_DIR}/package.json" ]; then
|
||||||
|
step "Frontend bauen (npm)" "1–3 Min"
|
||||||
|
# Node/npm falls noch nicht installiert
|
||||||
|
if ! command -v node >/dev/null 2>&1; then
|
||||||
|
if [ "$NODE_SETUP" = "nodesource" ]; then
|
||||||
|
quietly bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh && bash /tmp/nodesource_setup.sh && rm -f /tmp/nodesource_setup.sh"
|
||||||
|
quietly apt-get install -y nodejs
|
||||||
|
else
|
||||||
|
quietly apt-get install -y nodejs npm
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
quietly sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
||||||
|
if ! sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build" >> "$LOG_FILE" 2>&1; then
|
||||||
|
warn "npm run build fehlgeschlagen — manuell nachholen: cd ${APP_DIR} && npm run build"
|
||||||
else
|
else
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git checkout ${GIT_BRANCH} && git pull --ff-only"
|
ok
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ===== Frontend Build (nur wenn package.json existiert) =====
|
mkdir -p /var/lib/mailwolt/wizard
|
||||||
if [ -f "${APP_DIR}/package.json" ]; then
|
chown www-data:www-data /var/lib/mailwolt/wizard
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
chmod 775 /var/lib/mailwolt/wizard
|
||||||
# Prod-Build (Vite/Tailwind)
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || npm run build:prod || true"
|
step "Hilfsskripte & Konfiguration installieren" "5 Sek"
|
||||||
|
install -m 755 "${APP_DIR}/scripts/mailwolt-apply-domains" /usr/local/sbin/mailwolt-apply-domains
|
||||||
|
install -m 755 "${APP_DIR}/scripts/mailwolt-fetch-tags" /usr/local/sbin/mailwolt-fetch-tags
|
||||||
|
|
||||||
|
# ===== mailwolt-update installieren =====
|
||||||
|
install -m 755 "${APP_DIR}/scripts/update.sh" /usr/local/sbin/mailwolt-update
|
||||||
|
|
||||||
|
# ===== Sudoers für www-data (helper + update) =====
|
||||||
|
cat > /etc/sudoers.d/mailwolt-certbot <<'SUDOERS'
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-apply-domains
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-fetch-tags
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/bin/certbot
|
||||||
|
www-data ALL=(root) NOPASSWD: /usr/bin/openssl x509 -enddate -noout -in /etc/letsencrypt/live/*/fullchain.pem
|
||||||
|
SUDOERS
|
||||||
|
chmod 440 /etc/sudoers.d/mailwolt-certbot
|
||||||
|
|
||||||
|
# git safe.directory damit spätere pulls als root möglich sind
|
||||||
|
git config --global --add safe.directory "${APP_DIR}" || true
|
||||||
|
|
||||||
|
# ===== Version-Datei schreiben =====
|
||||||
|
mkdir -p /var/lib/mailwolt
|
||||||
|
GIT_TAG="$(sudo -u "$APP_USER" -H bash -lc "git -C ${APP_DIR} ls-remote --tags --sort=-v:refname origin 'v*' 2>/dev/null | grep -v '\^{}' | head -1 | sed 's|.*refs/tags/||'")"
|
||||||
|
if [ -n "$GIT_TAG" ]; then
|
||||||
|
echo "${GIT_TAG#v}" > /var/lib/mailwolt/version
|
||||||
|
echo "$GIT_TAG" > /var/lib/mailwolt/version_raw
|
||||||
|
else
|
||||||
|
warn "Kein Git-Tag gefunden — Version-Datei wird nicht geschrieben"
|
||||||
fi
|
fi
|
||||||
|
ok
|
||||||
|
|
||||||
|
step "Berechtigungen setzen & Dienste starten" "10 Sek"
|
||||||
# ===== App-User/Gruppen & Rechte (am ENDE ausführen) =====
|
|
||||||
APP_USER="${APP_USER:-${APP_NAME}app}"
|
|
||||||
APP_GROUP="${APP_GROUP}"
|
|
||||||
APP_PW="${APP_PW:-changeme123}"
|
|
||||||
APP_DIR="${APP_DIR}"
|
|
||||||
|
|
||||||
# User anlegen (nur falls noch nicht vorhanden) + Passwort setzen + Gruppe
|
|
||||||
if ! id -u "$APP_USER" >/dev/null 2>&1; then
|
if ! id -u "$APP_USER" >/dev/null 2>&1; then
|
||||||
adduser --disabled-password --gecos "" "$APP_USER"
|
quietly adduser --disabled-password --gecos "" "$APP_USER"
|
||||||
echo "${APP_USER}:${APP_PW}" | chpasswd
|
|
||||||
fi
|
fi
|
||||||
usermod -a -G "$APP_GROUP" "$APP_USER"
|
echo "${APP_USER}:${APP_PW}" | quietly chpasswd
|
||||||
|
quietly usermod -a -G "$APP_GROUP" "$APP_USER"
|
||||||
|
|
||||||
# Besitz & Rechte
|
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
||||||
find "$APP_DIR" -type d -exec chmod 775 {} \;
|
find "$APP_DIR" -type d -exec chmod 775 {} \; 2>>"$LOG_FILE"
|
||||||
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
find "$APP_DIR" -type f -exec chmod 664 {} \; 2>>"$LOG_FILE"
|
||||||
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
|
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
|
||||||
if command -v setfacl >/dev/null 2>&1; then
|
if command -v setfacl >/dev/null 2>&1; then
|
||||||
setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
try_quiet setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
|
||||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
try_quiet setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
|
||||||
setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
|
||||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${YELLOW}[i] App-User: ${APP_USER} Passwort: ${APP_PW}${NC}"
|
|
||||||
|
|
||||||
|
|
||||||
# Optional: ACLs, falls verfügbar (robuster bei gemischten Schreibzugriffen)
|
|
||||||
if command -v setfacl >/dev/null 2>&1; then
|
|
||||||
setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
|
||||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
|
||||||
setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX \
|
|
||||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
|
grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
|
||||||
grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
|
grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
|
||||||
|
try_quiet sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002"
|
||||||
|
|
||||||
# 7) npm respektiert umask – zur Sicherheit direkt setzen (für APP_USER)
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# 8) PHP-FPM-Worker laufen als www-data (Standard). Stelle sicher, dass der FPM-Socket group-writable ist:
|
|
||||||
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
|
||||||
FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
|
FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
|
||||||
if [ -f "$FPM_POOL" ]; then
|
if [ -f "$FPM_POOL" ]; then
|
||||||
sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
|
sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
|
||||||
sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
|
sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
|
||||||
sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
|
sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
|
||||||
systemctl restart php${PHPV}-fpm || true
|
try_quiet systemctl restart php${PHPV}-fpm
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 9) Optional: deinem Shell-/IDE-User ebenfalls Schreibrechte geben
|
|
||||||
IDE_USER="${SUDO_USER:-}"
|
IDE_USER="${SUDO_USER:-}"
|
||||||
if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
|
if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
|
||||||
usermod -a -G "$APP_GROUP" "$IDE_USER" || true
|
try_quiet usermod -a -G "$APP_GROUP" "$IDE_USER"
|
||||||
setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
|
try_quiet setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||||
setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
|
try_quiet setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
|
||||||
echo -e "${YELLOW}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Webstack neu laden
|
try_quiet systemctl reload nginx
|
||||||
systemctl reload nginx || true
|
try_quiet systemctl restart php*-fpm
|
||||||
systemctl restart php*-fpm || true
|
ok
|
||||||
|
|
||||||
# Hinweis zur neuen Gruppenzugehörigkeit
|
# ===== Monit =====
|
||||||
|
|
||||||
echo -e "${YELLOW}[i]${NC} SHELL: Du kannst dich nun als Benutzer '${APP_USER}' mit dem Passwort '${APP_PW}' anmelden."
|
|
||||||
echo -e "${YELLOW}[i]${NC} Hinweis: Nach dem ersten Login solltest du das Passwort mit 'passwd ${APP_USER}' ändern."
|
|
||||||
echo -e "${YELLOW}[i]${NC} Damit die Gruppenrechte (${APP_GROUP}) aktiv werden, bitte einmal ab- und wieder anmelden."
|
|
||||||
|
|
||||||
# ===== Monit (Watchdog) – installiert, aber NICHT aktiviert =====
|
|
||||||
log "Monit (Watchdog) installieren (deaktiviert)"
|
|
||||||
cat > /etc/monit/monitrc <<'EOF'
|
cat > /etc/monit/monitrc <<'EOF'
|
||||||
set daemon 60
|
set daemon 60
|
||||||
set logfile syslog facility log_daemon
|
set logfile syslog facility log_daemon
|
||||||
|
|
@ -624,62 +657,127 @@ set logfile syslog facility log_daemon
|
||||||
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
||||||
start program = "/bin/systemctl start postfix"
|
start program = "/bin/systemctl start postfix"
|
||||||
stop program = "/bin/systemctl stop postfix"
|
stop program = "/bin/systemctl stop postfix"
|
||||||
if failed port 25 protocol smtp then restart
|
if failed host 127.0.0.1 port 25 protocol smtp for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process dovecot with pidfile /var/run/dovecot/master.pid
|
check process dovecot with pidfile /run/dovecot/master.pid
|
||||||
start program = "/bin/systemctl start dovecot"
|
start program = "/bin/systemctl start dovecot"
|
||||||
stop program = "/bin/systemctl stop dovecot"
|
stop program = "/bin/systemctl stop dovecot"
|
||||||
if failed port 143 type tcp then restart
|
if failed host 127.0.0.1 port 143 type tcp for 3 cycles then restart
|
||||||
if failed port 993 type tcp ssl then restart
|
if failed host 127.0.0.1 port 993 type tcpssl for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process mariadb with pidfile /var/run/mysqld/mysqld.pid
|
check process mariadb matching "mysqld"
|
||||||
start program = "/bin/systemctl start mariadb"
|
start program = "/bin/systemctl start mariadb"
|
||||||
stop program = "/bin/systemctl stop mariadb"
|
stop program = "/bin/systemctl stop mariadb"
|
||||||
if failed port 3306 type tcp then restart
|
if failed host 127.0.0.1 port 3306 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process redis with pidfile /run/redis/redis-server.pid
|
check process redis with pidfile /run/redis/redis-server.pid
|
||||||
start program = "/bin/systemctl start redis-server"
|
start program = "/bin/systemctl start redis-server"
|
||||||
stop program = "/bin/systemctl stop redis-server"
|
stop program = "/bin/systemctl stop redis-server"
|
||||||
if failed port 6379 type tcp then restart
|
if failed host 127.0.0.1 port 6379 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process rspamd with pidfile /run/rspamd/rspamd.pid
|
check process rspamd matching "rspamd: main process"
|
||||||
start program = "/bin/systemctl start rspamd"
|
start program = "/bin/systemctl start rspamd" with timeout 60 seconds
|
||||||
stop program = "/bin/systemctl stop rspamd"
|
stop program = "/bin/systemctl stop rspamd"
|
||||||
if failed port 11332 type tcp then restart
|
if failed host 127.0.0.1 port 11332 type tcp for 3 cycles then restart
|
||||||
|
if failed host 127.0.0.1 port 11334 type tcp for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
||||||
start program = "/bin/systemctl start opendkim"
|
start program = "/bin/systemctl start opendkim"
|
||||||
stop program = "/bin/systemctl stop opendkim"
|
stop program = "/bin/systemctl stop opendkim"
|
||||||
if failed port 8891 type tcp then restart
|
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
|
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
||||||
|
start program = "/bin/systemctl start opendmarc"
|
||||||
|
stop program = "/bin/systemctl stop opendmarc"
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
check process nginx with pidfile /run/nginx.pid
|
check process nginx with pidfile /run/nginx.pid
|
||||||
start program = "/bin/systemctl start nginx"
|
start program = "/bin/systemctl start nginx"
|
||||||
stop program = "/bin/systemctl stop nginx"
|
stop program = "/bin/systemctl stop nginx"
|
||||||
if failed port 80 type tcp then restart
|
if failed host 127.0.0.1 port 80 type tcp for 2 cycles then restart
|
||||||
if failed port 443 type tcp ssl then restart
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
|
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
||||||
|
start program = "/bin/systemctl start fail2ban"
|
||||||
|
stop program = "/bin/systemctl stop fail2ban"
|
||||||
|
if 5 restarts within 10 cycles then alert
|
||||||
|
|
||||||
|
check process clamav matching "clamd"
|
||||||
|
start program = "/bin/systemctl start clamav-daemon"
|
||||||
|
stop program = "/bin/systemctl stop clamav-daemon"
|
||||||
|
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
||||||
|
if 5 restarts within 10 cycles then unmonitor
|
||||||
EOF
|
EOF
|
||||||
chmod 600 /etc/monit/monitrc
|
chmod 600 /etc/monit/monitrc
|
||||||
systemctl disable --now monit || true
|
monit -t || { warn "Monit-Config ungültig — prüfe /etc/monit/monitrc"; }
|
||||||
apt-mark hold monit >/dev/null 2>&1 || true
|
try_quiet systemctl enable --now monit
|
||||||
|
|
||||||
# ===== Smoke-Test (alle Ports, mit Timeouts) =====
|
# ===== Smoke-Test =====
|
||||||
log "Smoke-Test (Ports & Banner):"
|
step "Dienste prüfen (Port-Check)"
|
||||||
set +e
|
_sok=0; _sfail=0
|
||||||
printf "[25] " && timeout 6s bash -lc 'printf "EHLO localhost\r\nQUIT\r\n" | nc -v -w 4 127.0.0.1 25 2>&1' || true
|
|
||||||
printf "[465] " && timeout 6s openssl s_client -connect 127.0.0.1:465 -brief -quiet </dev/null || echo "[465] Verbindung fehlgeschlagen"
|
smoke_smtp() {
|
||||||
printf "[587] " && timeout 6s openssl s_client -starttls smtp -connect 127.0.0.1:587 -brief -quiet </dev/null || echo "[587] Verbindung fehlgeschlagen"
|
local port="$1" label="$2"
|
||||||
printf "[110] " && timeout 6s bash -lc 'printf "QUIT\r\n" | nc -v -w 4 127.0.0.1 110 2>&1' || true
|
local out
|
||||||
printf "[995] " && timeout 6s openssl s_client -connect 127.0.0.1:995 -brief -quiet </dev/null || echo "[995] Verbindung fehlgeschlagen"
|
out=$(printf "EHLO localhost\r\nQUIT\r\n" | timeout 5s nc -w3 127.0.0.1 "$port" 2>/dev/null || true)
|
||||||
printf "[143] " && timeout 6s bash -lc 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -v -w 4 127.0.0.1 143 2>&1' || true
|
if echo "$out" | grep -q '^220'; then
|
||||||
printf "[993] " && timeout 6s openssl s_client -connect 127.0.0.1:993 -brief -quiet </dev/null || echo "[993] Verbindung fehlgeschlagen"
|
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
||||||
set -e
|
else
|
||||||
|
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
smoke_tls() {
|
||||||
|
local port="$1" label="$2" extra="${3:-}"
|
||||||
|
local out
|
||||||
|
out=$(timeout 5s openssl s_client $extra -connect 127.0.0.1:"$port" -brief -quiet </dev/null 2>&1 || true)
|
||||||
|
if echo "$out" | grep -qiE '(CONNECTED|depth|Verify|^220|\+OK|OK)'; then
|
||||||
|
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
||||||
|
else
|
||||||
|
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
smoke_imap() {
|
||||||
|
local port="$1" label="$2"
|
||||||
|
local out
|
||||||
|
out=$(printf ". CAPABILITY\r\n. LOGOUT\r\n" | timeout 5s nc -w3 127.0.0.1 "$port" 2>/dev/null || true)
|
||||||
|
if echo "$out" | grep -qi 'CAPABILITY'; then
|
||||||
|
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
||||||
|
else
|
||||||
|
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
smoke_pop3() {
|
||||||
|
local port="$1" label="$2"
|
||||||
|
local out
|
||||||
|
out=$(printf "QUIT\r\n" | timeout 5s nc -w3 127.0.0.1 "$port" 2>/dev/null || true)
|
||||||
|
if echo "$out" | grep -qi '^\+OK'; then
|
||||||
|
printf " ${GREEN}✔${NC} %-5s %s\n" "$port" "$label"; (( _sok++ )) || true
|
||||||
|
else
|
||||||
|
printf " ${YELLOW}⚠${NC} %-5s %s — nicht erreichbar\n" "$port" "$label"; (( _sfail++ )) || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
smoke_smtp 25 "SMTP"
|
||||||
|
smoke_tls 465 "SMTPS" ""
|
||||||
|
smoke_tls 587 "Submission" "-starttls smtp"
|
||||||
|
smoke_imap 143 "IMAP"
|
||||||
|
smoke_tls 993 "IMAPS" ""
|
||||||
|
smoke_pop3 110 "POP3"
|
||||||
|
smoke_tls 995 "POP3S" ""
|
||||||
|
|
||||||
|
printf "\n ${GREY}%d/%d Dienste erreichbar${NC}\n" "$_sok" "$(( _sok + _sfail ))"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=============================================================="
|
echo -e " ${GREY}Bootstrap-Login (nur für ERSTEN Login & Wizard):${NC}"
|
||||||
echo " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
|
echo -e " ${CYAN}User: ${NC}${BOOTSTRAP_USER}"
|
||||||
echo " User: ${BOOTSTRAP_USER}"
|
echo -e " ${CYAN}Passwort: ${NC}${BOOTSTRAP_PASS}"
|
||||||
echo " Passwort: ${BOOTSTRAP_PASS}"
|
echo -e " ${GREY}Log: ${LOG_FILE}${NC}"
|
||||||
echo "=============================================================="
|
|
||||||
echo
|
echo
|
||||||
|
|
||||||
footer_ok "$SERVER_IP"
|
footer_ok "$SERVER_IP"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,418 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>CluBird — Logo</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=DM+Sans:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #07080e;
|
||||||
|
--surface: #0b0d16;
|
||||||
|
--border: #181c2e;
|
||||||
|
--border2: #252840;
|
||||||
|
--acc: #6366f1;
|
||||||
|
--acc-h: #4f46e5;
|
||||||
|
--acc-l: #818cf8;
|
||||||
|
--acc-bg: #1a1a3a;
|
||||||
|
--acc-bd: #2d2f5a;
|
||||||
|
--t1: #e8eaf6;
|
||||||
|
--t2: #9a9ec8;
|
||||||
|
--t3: #4a5070;
|
||||||
|
--t4: #252840;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--t1);
|
||||||
|
font-family: 'DM Sans', system-ui, sans-serif;
|
||||||
|
padding: 40px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-family: 'DM Sans', sans-serif;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--t3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section { margin-bottom: 48px; }
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 28px 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-light {
|
||||||
|
background: #f0f2ff;
|
||||||
|
border: 1px solid #d4d8ff;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 28px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--t3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Logo Lockup ── */
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordmark {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wordmark .clu { color: var(--t1); }
|
||||||
|
.wordmark .bird { color: var(--acc); }
|
||||||
|
.wordmark-light .clu { color: #0f1120; }
|
||||||
|
.wordmark-light .bird { color: var(--acc); }
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--t3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.8px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Divider ── */
|
||||||
|
.divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--border);
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Family ── */
|
||||||
|
.family {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 20px 28px;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--border2);
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-name {
|
||||||
|
font-family: 'Syne', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.family-sub {
|
||||||
|
font-size: 8.5px;
|
||||||
|
color: var(--t3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ── 1. HAUPTLOGO ── -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Hauptlogo — Dark Mode</h2>
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<!-- Groß -->
|
||||||
|
<div>
|
||||||
|
<div class="card-label">Groß</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon" style="width:48px;height:48px;background:linear-gradient(135deg,#6366f1 0%,#4338ca 100%);box-shadow:0 0 20px rgba(99,102,241,.35);">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||||
|
<!-- Körper -->
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<!-- Flügel links oben -->
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
<!-- Flügel rechts -->
|
||||||
|
<path d="M24 15 C24 15 27 13 28 10 C26 11 24 12 23 13.5" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
<!-- Auge -->
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<!-- Schwanz / Brust -->
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="wordmark" style="font-size:28px;">
|
||||||
|
<span class="clu">Clu</span><span class="bird">Bird</span>
|
||||||
|
</div>
|
||||||
|
<div class="tagline">Mail Server Management</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Standard -->
|
||||||
|
<div>
|
||||||
|
<div class="card-label">Standard</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon" style="width:36px;height:36px;background:linear-gradient(135deg,#6366f1 0%,#4338ca 100%);box-shadow:0 0 14px rgba(99,102,241,.3);">
|
||||||
|
<svg width="21" height="21" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M24 15 C24 15 27 13 28 10 C26 11 24 12 23 13.5" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="wordmark" style="font-size:21px;">
|
||||||
|
<span class="clu">Clu</span><span class="bird">Bird</span>
|
||||||
|
</div>
|
||||||
|
<div class="tagline">by Aziros</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Klein / Sidebar -->
|
||||||
|
<div>
|
||||||
|
<div class="card-label">Klein / Sidebar</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon" style="width:28px;height:28px;background:linear-gradient(135deg,#6366f1 0%,#4338ca 100%);border-radius:7px;">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="2" fill="white" opacity=".95"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="wordmark" style="font-size:16px;">
|
||||||
|
<span class="clu">Clu</span><span class="bird">Bird</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 2. LIGHT MODE ── -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Light Mode</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<div class="card-label">Standard Light</div>
|
||||||
|
<div class="card-light">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon" style="width:36px;height:36px;background:linear-gradient(135deg,#6366f1 0%,#4338ca 100%);border-radius:10px;box-shadow:0 4px 12px rgba(99,102,241,.25);">
|
||||||
|
<svg width="21" height="21" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M24 15 C24 15 27 13 28 10 C26 11 24 12 23 13.5" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="wordmark wordmark-light" style="font-size:21px;">
|
||||||
|
<span class="clu">Clu</span><span class="bird">Bird</span>
|
||||||
|
</div>
|
||||||
|
<div class="tagline" style="color:#8892aa;">Mail Server Management</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 3. ICON VARIANTEN ── -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Icon-Only — alle Größen</h2>
|
||||||
|
<div class="card" style="display:flex;gap:20px;align-items:flex-end;width:fit-content;">
|
||||||
|
|
||||||
|
<!-- 64px -->
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;">
|
||||||
|
<div style="width:64px;height:64px;border-radius:16px;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;box-shadow:0 0 24px rgba(99,102,241,.4);">
|
||||||
|
<svg width="38" height="38" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M24 15 C24 15 27 13 28 10 C26 11 24 12 23 13.5" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:9px;color:var(--t3);">64px</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 48px -->
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;">
|
||||||
|
<div style="width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;box-shadow:0 0 16px rgba(99,102,241,.35);">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M24 15 C24 15 27 13 28 10 C26 11 24 12 23 13.5" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:9px;color:var(--t3);">48px</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 36px -->
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;">
|
||||||
|
<div style="width:36px;height:36px;border-radius:9px;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="21" height="21" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:9px;color:var(--t3);">36px</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 24px -->
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;">
|
||||||
|
<div style="width:24px;height:24px;border-radius:6px;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="2" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:9px;color:var(--t3);">24px</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;">
|
||||||
|
<div style="width:16px;height:16px;border-radius:4px;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="3" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="3" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:9px;color:var(--t3);">Favicon</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rund -->
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:8px;">
|
||||||
|
<div style="width:48px;height:48px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;box-shadow:0 0 16px rgba(99,102,241,.35);">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M24 15 C24 15 27 13 28 10 C26 11 24 12 23 13.5" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:9px;color:var(--t3);">Rund</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 4. WORDMARK VARIANTEN ── -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Wordmark</h2>
|
||||||
|
<div class="card" style="display:flex;flex-direction:column;gap:16px;width:fit-content;">
|
||||||
|
<div class="wordmark" style="font-size:32px;"><span class="clu">Clu</span><span class="bird">Bird</span></div>
|
||||||
|
<div class="wordmark" style="font-size:22px;"><span class="clu">Clu</span><span class="bird">Bird</span></div>
|
||||||
|
<div style="font-family:'Syne',sans-serif;font-size:18px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--t1);">CLU<span style="color:var(--acc);">BIRD</span></div>
|
||||||
|
<div style="font-family:'Syne',sans-serif;font-size:16px;font-weight:800;color:var(--t1);">clu<span style="color:var(--acc);">bird</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── 5. PRODUKTFAMILIE ── -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Aziros Produktfamilie</h2>
|
||||||
|
<div class="family">
|
||||||
|
|
||||||
|
<!-- CluPilot -->
|
||||||
|
<div class="family-item">
|
||||||
|
<div style="width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;box-shadow:0 0 10px rgba(99,102,241,.3);">
|
||||||
|
<span style="font-size:11px;font-weight:800;color:#fff;font-family:'Syne',sans-serif;">CP</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="family-name"><span style="color:var(--t1);">Clu</span><span style="color:var(--acc);">Pilot</span></div>
|
||||||
|
<div class="family-sub">Infrastruktur</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="plus">+</div>
|
||||||
|
|
||||||
|
<!-- CluBird -->
|
||||||
|
<div class="family-item">
|
||||||
|
<div style="width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,#6366f1,#4338ca);display:flex;align-items:center;justify-content:center;box-shadow:0 0 10px rgba(99,102,241,.3);">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 28 28" fill="none">
|
||||||
|
<path d="M5 19 C5 19 8 10 15 9 C19 8 23 11 24 15" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M15 9 C15 9 12 5 8 5 C9.5 7 10 9 11 10" stroke="white" stroke-width="1.8" stroke-linecap="round" fill="none"/>
|
||||||
|
<circle cx="19" cy="8" r="1.5" fill="white" opacity=".95"/>
|
||||||
|
<path d="M5 19 C6.5 22 9 23.5 12 23.5 C15 23.5 18 22 19.5 19" stroke="white" stroke-width="2.2" stroke-linecap="round" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="family-name"><span style="color:var(--t1);">Clu</span><span style="color:var(--acc);">Bird</span></div>
|
||||||
|
<div class="family-sub">Mail Management</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="plus">=</div>
|
||||||
|
|
||||||
|
<!-- Aziros Suite -->
|
||||||
|
<div>
|
||||||
|
<div style="font-family:'Syne',sans-serif;font-size:15px;font-weight:800;color:var(--t1);">Aziros <span style="color:var(--acc);">Suite</span></div>
|
||||||
|
<div class="family-sub">Infrastructure + Mail</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AgentMigrationStateService">
|
|
||||||
<option name="migrationStatus" value="COMPLETED" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AskMigrationStateService">
|
|
||||||
<option name="migrationStatus" value="COMPLETED" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Ask2AgentMigrationStateService">
|
|
||||||
<option name="migrationStatus" value="COMPLETED" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="EditMigrationStateService">
|
|
||||||
<option name="migrationStatus" value="COMPLETED" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="WEB_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="MarkdownSettingsMigration">
|
|
||||||
<option name="stateVersion" value="1" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/mailwolt-installer.iml" filepath="$PROJECT_DIR$/.idea/mailwolt-installer.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="MessDetectorOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PHPCSFixerOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PHPCodeSnifferOptionsConfiguration">
|
|
||||||
<option name="highlightLevel" value="WARNING" />
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PhpStanOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PsalmOptionsConfiguration">
|
|
||||||
<option name="transferred" value="true" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# ===================== HTTP (Port 80) =====================
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
# ACME HTTP-01
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/letsencrypt;
|
|
||||||
allow all;
|
|
||||||
}
|
|
||||||
|
|
||||||
__HTTP_BODY__
|
|
||||||
}
|
|
||||||
|
|
||||||
# ===================== HTTPS (Port 443) ====================
|
|
||||||
__SSL_SERVER_BLOCK__
|
|
||||||
|
|
@ -1,909 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
(
|
|
||||||
export HISTFILE=
|
|
||||||
set +o history
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
##############################################
|
|
||||||
# MailWolt #
|
|
||||||
# Bootstrap Installer v1.0 #
|
|
||||||
##############################################
|
|
||||||
|
|
||||||
# ===== CLI-Flags (-dev / -stag) =====
|
|
||||||
APP_ENV="${APP_ENV:-production}"
|
|
||||||
APP_DEBUG="${APP_DEBUG:-false}"
|
|
||||||
|
|
||||||
DEV_MODE=0
|
|
||||||
STAG_MODE=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
-dev)
|
|
||||||
DEV_MODE=1
|
|
||||||
APP_ENV="local"
|
|
||||||
APP_DEBUG="true"
|
|
||||||
;;
|
|
||||||
-stag|-staging)
|
|
||||||
STAG_MODE=1
|
|
||||||
APP_ENV="staging"
|
|
||||||
APP_DEBUG="false"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
# ===== Branding & Pfade =====
|
|
||||||
APP_NAME="${APP_NAME:-MailWolt}"
|
|
||||||
|
|
||||||
APP_USER="${APP_USER:-mailwolt}"
|
|
||||||
APP_GROUP="${APP_GROUP:-www-data}"
|
|
||||||
|
|
||||||
APP_DIR="/var/www/${APP_USER}"
|
|
||||||
|
|
||||||
ADMIN_USER="${APP_USER}"
|
|
||||||
ADMIN_EMAIL="admin@localhost"
|
|
||||||
ADMIN_PASS="ChangeMe"
|
|
||||||
|
|
||||||
CONF_BASE="/etc/${APP_USER}"
|
|
||||||
CERT_DIR="${CONF_BASE}/ssl"
|
|
||||||
CERT="${CERT_DIR}/cert.pem"
|
|
||||||
KEY="${CERT_DIR}/key.pem"
|
|
||||||
|
|
||||||
NGINX_SITE="/etc/nginx/sites-available/${APP_USER}.conf"
|
|
||||||
NGINX_SITE_LINK="/etc/nginx/sites-enabled/${APP_USER}.conf"
|
|
||||||
|
|
||||||
DB_NAME="${DB_NAME:-${APP_USER}}"
|
|
||||||
DB_USER="${DB_USER:-${APP_USER}}"
|
|
||||||
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
|
|
||||||
GIT_REPO="${GIT_REPO:-http://10.10.20.81:3000/boban/mailwolt.git}"
|
|
||||||
GIT_BRANCH="${GIT_BRANCH:-main}"
|
|
||||||
|
|
||||||
NODE_SETUP="${NODE_SETUP:-deb}"
|
|
||||||
|
|
||||||
# ===== Styling =====
|
|
||||||
GREEN="\033[1;32m"; YELLOW="\033[1;33m"; RED="\033[1;31m"; CYAN="\033[1;36m"; GREY="\033[0;90m"; NC="\033[0m"
|
|
||||||
BAR="──────────────────────────────────────────────────────────────────────────────"
|
|
||||||
|
|
||||||
header() {
|
|
||||||
echo -e "${CYAN}${BAR}${NC}"
|
|
||||||
echo -e "${CYAN} 888b d888 d8b 888 888 888 888 888 ${NC}"
|
|
||||||
echo -e "${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888 ${NC}"
|
|
||||||
echo -e "${CYAN} 88888b.d88888 888 888 d8b 888 888 888 ${NC}"
|
|
||||||
echo -e "${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888 ${NC}"
|
|
||||||
echo -e "${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888 ${NC}"
|
|
||||||
echo -e "${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888 ${NC}"
|
|
||||||
echo -e "${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b. ${NC}"
|
|
||||||
echo -e "${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888 ${NC}"
|
|
||||||
echo -e "${CYAN}${BAR}${NC}"
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
print_bootstrap_summary() {
|
|
||||||
local ip="$1"
|
|
||||||
local admin_user="$2"
|
|
||||||
local admin_pass="$3"
|
|
||||||
local GREEN="\033[1;32m"
|
|
||||||
local CYAN="\033[1;36m"
|
|
||||||
local GREY="\033[0;90m"
|
|
||||||
local YELLOW="\033[1;33m"
|
|
||||||
local RED="\033[1;31m"
|
|
||||||
local NC="\033[0m"
|
|
||||||
local BAR="${BAR:-──────────────────────────────────────────────────────────────────────────────}"
|
|
||||||
local scheme="http"
|
|
||||||
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then scheme="https"; fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
|
||||||
echo -e "${GREEN} ✔ ${APP_NAME} Bootstrap erfolgreich abgeschlossen${NC}"
|
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
|
||||||
echo -e " Bootstrap-Login (nur für ERSTEN Login & Wizard):"
|
|
||||||
echo -e " User: ${YELLOW}${admin_user}${NC}"
|
|
||||||
echo -e " Passwort: ${RED}${admin_pass}${NC}"
|
|
||||||
echo
|
|
||||||
echo -e " Aufruf: ${CYAN}${scheme}://${ip}${NC}"
|
|
||||||
echo -e " Laravel Root: ${GREY}${APP_DIR}${NC}"
|
|
||||||
echo -e " Nginx Site: ${GREY}${NGINX_SITE}${NC}"
|
|
||||||
echo -e " Self-signed Cert: ${GREY}${CERT_DIR}/{cert.pem,key.pem}${NC}"
|
|
||||||
echo -e " Postfix/Dovecot Ports aktiv: ${GREY}25, 465, 587, 110, 995, 143, 993${NC}"
|
|
||||||
echo -e " Rspamd/OpenDKIM: ${GREY}aktiv (DKIM-Keys später im Wizard)${NC}"
|
|
||||||
echo -e " Monit (Watchdog): ${GREY}installiert, NICHT aktiviert${NC}"
|
|
||||||
echo -e "${GREEN}${BAR}${NC}"
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
log() { echo -e "${GREEN}[+]${NC} $*"; }
|
|
||||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
|
||||||
err() { echo -e "${RED}[x]${NC} $*"; }
|
|
||||||
require_root() { [ "$(id -u)" -eq 0 ] || { err "Bitte als root ausführen."; exit 1; }; }
|
|
||||||
|
|
||||||
detect_ip() {
|
|
||||||
local ip
|
|
||||||
ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')" || true
|
|
||||||
[[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
|
||||||
[[ -n "${ip:-}" ]] || { err "Konnte Server-IP nicht ermitteln."; exit 1; }
|
|
||||||
echo "$ip"
|
|
||||||
}
|
|
||||||
|
|
||||||
gen() { head -c 512 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c "${1:-28}" || true; }
|
|
||||||
pw() { gen 28; }
|
|
||||||
short() { gen 16; }
|
|
||||||
|
|
||||||
# ===== Start =====
|
|
||||||
require_root
|
|
||||||
header
|
|
||||||
|
|
||||||
SERVER_IP="$(detect_ip)"
|
|
||||||
MAIL_HOSTNAME="${MAIL_HOSTNAME:-"bootstrap.local"}"
|
|
||||||
TZ="${TZ:-""}"
|
|
||||||
|
|
||||||
echo -e "${GREY}Server-IP erkannt: ${SERVER_IP}${NC}"
|
|
||||||
[ -n "$TZ" ] && { ln -fs "/usr/share/zoneinfo/${TZ}" /etc/localtime || true; }
|
|
||||||
|
|
||||||
log "Paketquellen aktualisieren…"
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
apt-get update -y
|
|
||||||
|
|
||||||
# ---- MariaDB-Workaround ----
|
|
||||||
log "MariaDB-Workaround vorbereiten…"
|
|
||||||
mkdir -p /etc/mysql /etc/mysql/mariadb.conf.d
|
|
||||||
[ -f /etc/mysql/mariadb.cnf ] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
|
||||||
|
|
||||||
# ---- Basis-Pakete installieren ----
|
|
||||||
log "Pakete installieren… (dies kann einige Minuten dauern)"
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
apt-get -y -o Dpkg::Options::="--force-confdef" \
|
|
||||||
-o Dpkg::Options::="--force-confold" install \
|
|
||||||
postfix postfix-mysql \
|
|
||||||
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
|
||||||
mariadb-server mariadb-client \
|
|
||||||
redis-server \
|
|
||||||
rspamd \
|
|
||||||
opendkim opendkim-tools \
|
|
||||||
nginx \
|
|
||||||
php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql php-redis php-gd unzip curl \
|
|
||||||
composer git \
|
|
||||||
certbot python3-certbot-nginx \
|
|
||||||
fail2ban \
|
|
||||||
ca-certificates rsyslog sudo openssl netcat-openbsd monit acl
|
|
||||||
|
|
||||||
NGINX_HTTP2_SUPPORTED=0
|
|
||||||
if nginx -V 2>&1 | grep -q http_v2; then
|
|
||||||
NGINX_HTTP2_SUPPORTED=1
|
|
||||||
log "Nginx: HTTP/2-Unterstützung vorhanden ✅"
|
|
||||||
else
|
|
||||||
warn "Nginx: HTTP/2-Modul nicht gefunden – wechsle auf 'nginx-full'…"
|
|
||||||
apt-get install -y nginx-full || true
|
|
||||||
systemctl restart nginx || true
|
|
||||||
if nginx -V 2>&1 | grep -q http_v2; then
|
|
||||||
NGINX_HTTP2_SUPPORTED=1
|
|
||||||
log "Nginx: HTTP/2 jetzt verfügbar ✅"
|
|
||||||
else
|
|
||||||
warn "HTTP/2 weiterhin nicht verfügbar (verwende SSL ohne http2)."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$NGINX_HTTP2_SUPPORTED" = "1" ]; then
|
|
||||||
NGINX_HTTP2_SUFFIX=" http2"
|
|
||||||
else
|
|
||||||
NGINX_HTTP2_SUFFIX=""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== Verzeichnisse / User =====
|
|
||||||
log "Verzeichnisse und Benutzer anlegen…"
|
|
||||||
mkdir -p "${CERT_DIR}" /etc/postfix/sql /etc/dovecot/conf.d /etc/rspamd/local.d /var/mail/vhosts
|
|
||||||
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
|
||||||
chown -R vmail:vmail /var/mail
|
|
||||||
|
|
||||||
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
|
||||||
usermod -a -G "$APP_GROUP" "$APP_USER"
|
|
||||||
|
|
||||||
# ===== Self-signed TLS (SAN = IP) =====
|
|
||||||
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
|
||||||
if [ ! -s "$CERT" ] || [ ! -s "$KEY" ]; then
|
|
||||||
log "Erzeuge Self-Signed TLS Zertifikat (SAN=IP:${SERVER_IP})…"
|
|
||||||
install -d -m 0750 -o root -g "${APP_USER}" "${CERT_DIR}"
|
|
||||||
cat > "$OSSL_CFG" <<CFG
|
|
||||||
[req]
|
|
||||||
default_bits = 2048
|
|
||||||
prompt = no
|
|
||||||
default_md = sha256
|
|
||||||
req_extensions = req_ext
|
|
||||||
distinguished_name = dn
|
|
||||||
|
|
||||||
[dn]
|
|
||||||
CN = ${SERVER_IP}
|
|
||||||
O = ${APP_NAME}
|
|
||||||
C = DE
|
|
||||||
|
|
||||||
[req_ext]
|
|
||||||
subjectAltName = @alt_names
|
|
||||||
|
|
||||||
[alt_names]
|
|
||||||
IP.1 = ${SERVER_IP}
|
|
||||||
CFG
|
|
||||||
openssl req -x509 -newkey rsa:2048 -days 825 -nodes \
|
|
||||||
-keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
|
||||||
chown root:"${APP_USER}" "$KEY" "$CERT"
|
|
||||||
chmod 640 "$KEY" "$CERT"
|
|
||||||
chmod 750 "${CERT_DIR}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEV_USER="${SUDO_USER:-$USER}"
|
|
||||||
if command -v setfacl >/dev/null 2>&1; then
|
|
||||||
setfacl -m u:${DEV_USER}:x "${CONF_BASE}" "${CERT_DIR}" || true
|
|
||||||
setfacl -m u:${DEV_USER}:r "$CERT" "$KEY" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== MariaDB =====
|
|
||||||
log "MariaDB vorbereiten…"
|
|
||||||
systemctl enable --now mariadb
|
|
||||||
mysql -uroot <<SQL
|
|
||||||
CREATE DATABASE IF NOT EXISTS ${DB_NAME}
|
|
||||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost';
|
|
||||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1';
|
|
||||||
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
|
||||||
ALTER USER '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
|
|
||||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
|
||||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
SQL
|
|
||||||
|
|
||||||
# ===== Postfix =====
|
|
||||||
postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
|
||||||
postconf -e "myorigin = \$myhostname"
|
|
||||||
postconf -e "mydestination = "
|
|
||||||
postconf -e "inet_interfaces = all"
|
|
||||||
postconf -e "inet_protocols = ipv4"
|
|
||||||
postconf -e "smtpd_banner = \$myhostname ESMTP"
|
|
||||||
postconf -e "smtpd_tls_cert_file = ${CERT}"
|
|
||||||
postconf -e "smtpd_tls_key_file = ${KEY}"
|
|
||||||
postconf -e "smtpd_tls_security_level = may"
|
|
||||||
postconf -e "smtp_tls_security_level = may"
|
|
||||||
postconf -e "smtpd_tls_received_header = yes"
|
|
||||||
postconf -e "disable_vrfy_command = yes"
|
|
||||||
postconf -e "smtpd_helo_required = yes"
|
|
||||||
postconf -e "milter_default_action = accept"
|
|
||||||
postconf -e "milter_protocol = 6"
|
|
||||||
postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
|
||||||
postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
|
||||||
postconf -e "smtpd_sasl_type = dovecot"
|
|
||||||
postconf -e "smtpd_sasl_path = private/auth"
|
|
||||||
postconf -e "smtpd_sasl_auth_enable = yes"
|
|
||||||
postconf -e "smtpd_sasl_security_options = noanonymous"
|
|
||||||
postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
|
|
||||||
postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
|
|
||||||
postconf -M "smtp/inet=smtp inet n - n - - smtpd -o smtpd_peername_lookup=no -o smtpd_timeout=30s"
|
|
||||||
postconf -M "submission/inet=submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_peername_lookup=no -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
|
||||||
postconf -M "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
|
||||||
postconf -M "pickup/unix=pickup unix n - y 60 1 pickup"
|
|
||||||
postconf -M "cleanup/unix=cleanup unix n - y - 0 cleanup"
|
|
||||||
postconf -M "qmgr/unix=qmgr unix n - n 300 1 qmgr"
|
|
||||||
|
|
||||||
install -d -o root -g postfix -m 750 /etc/postfix/sql
|
|
||||||
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
|
||||||
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
|
|
||||||
hosts = 127.0.0.1
|
|
||||||
user = ${DB_USER}
|
|
||||||
password = ${DB_PASS}
|
|
||||||
dbname = ${DB_NAME}
|
|
||||||
# query = SELECT 1 FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%s' AND u.is_active = 1 AND d.is_active = 1 LIMIT 1;
|
|
||||||
CONF
|
|
||||||
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
|
||||||
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
|
||||||
|
|
||||||
install -o root -g postfix -m 640 /dev/null /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
|
||||||
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
|
|
||||||
hosts = 127.0.0.1
|
|
||||||
user = ${DB_USER}
|
|
||||||
password = ${DB_PASS}
|
|
||||||
dbname = ${DB_NAME}
|
|
||||||
# query = SELECT destination FROM mail_aliases a JOIN domains d ON d.id = a.domain_id WHERE a.source = '%s' AND a.is_active = 1 AND d.is_active = 1 LIMIT 1;
|
|
||||||
CONF
|
|
||||||
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
|
||||||
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
|
||||||
|
|
||||||
systemctl restart postfix
|
|
||||||
systemctl enable --now postfix
|
|
||||||
|
|
||||||
# ===== Dovecot =====
|
|
||||||
log "Dovecot konfigurieren…"
|
|
||||||
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
|
||||||
!include_try /etc/dovecot/conf.d/*.conf
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/10-mail.conf <<'CONF'
|
|
||||||
protocols = imap pop3 lmtp
|
|
||||||
mail_location = maildir:/var/mail/vhosts/%d/%n
|
|
||||||
namespace inbox { inbox = yes }
|
|
||||||
mail_privileged_group = mail
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
|
|
||||||
disable_plaintext_auth = yes
|
|
||||||
auth_mechanisms = plain login
|
|
||||||
!include_try auth-sql.conf.ext
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat > /etc/dovecot/dovecot-sql.conf.ext <<CONF
|
|
||||||
driver = mysql
|
|
||||||
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
|
|
||||||
default_pass_scheme = BLF-CRYPT
|
|
||||||
# password_query = SELECT email AS user, password_hash AS password FROM mail_users WHERE email = '%u' AND is_active = 1 LIMIT 1;
|
|
||||||
CONF
|
|
||||||
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
|
|
||||||
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF'
|
|
||||||
passdb { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext }
|
|
||||||
userdb { driver = static args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n }
|
|
||||||
CONF
|
|
||||||
sudo chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
|
|
||||||
sudo chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/10-master.conf <<'CONF'
|
|
||||||
service lmtp {
|
|
||||||
unix_listener /var/spool/postfix/private/dovecot-lmtp { mode = 0600 user = postfix group = postfix }
|
|
||||||
}
|
|
||||||
service auth {
|
|
||||||
unix_listener /var/spool/postfix/private/auth { mode = 0660 user = postfix group = postfix }
|
|
||||||
}
|
|
||||||
service imap-login {
|
|
||||||
inet_listener imap { port = 143 }
|
|
||||||
inet_listener imaps { port = 993 ssl = yes }
|
|
||||||
}
|
|
||||||
service pop3-login {
|
|
||||||
inet_listener pop3 { port = 110 }
|
|
||||||
inet_listener pop3s { port = 995 ssl = yes }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/10-ssl.conf <<CONF
|
|
||||||
ssl = required
|
|
||||||
ssl_cert = <${CERT}
|
|
||||||
ssl_key = <${KEY}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
sudo mkdir -p /var/spool/postfix/private
|
|
||||||
sudo chown postfix:postfix /var/spool/postfix /var/spool/postfix/private
|
|
||||||
sudo chmod 0755 /var/spool/postfix /var/spool/postfix/private
|
|
||||||
|
|
||||||
sudo systemctl restart dovecot
|
|
||||||
sudo systemctl restart postfix
|
|
||||||
systemctl enable --now dovecot
|
|
||||||
|
|
||||||
# ===== Rspamd & OpenDKIM =====
|
|
||||||
log "Rspamd + OpenDKIM aktivieren…"
|
|
||||||
cat > /etc/rspamd/local.d/worker-controller.inc <<'CONF'
|
|
||||||
password = "admin";
|
|
||||||
bind_socket = "127.0.0.1:11334";
|
|
||||||
CONF
|
|
||||||
systemctl enable --now rspamd || true
|
|
||||||
|
|
||||||
cat > /etc/opendkim.conf <<'CONF'
|
|
||||||
Syslog yes
|
|
||||||
UMask 002
|
|
||||||
Mode sv
|
|
||||||
Socket inet:8891@127.0.0.1
|
|
||||||
Canonicalization relaxed/simple
|
|
||||||
On-BadSignature accept
|
|
||||||
On-Default accept
|
|
||||||
On-KeyNotFound accept
|
|
||||||
On-NoSignature accept
|
|
||||||
LogWhy yes
|
|
||||||
OversignHeaders From
|
|
||||||
# KeyTable / SigningTable werden nach dem Wizard gesetzt
|
|
||||||
CONF
|
|
||||||
systemctl enable --now opendkim || true
|
|
||||||
|
|
||||||
# ===== Redis =====
|
|
||||||
log "Redis absichern (Passwort setzen & nur localhost)…"
|
|
||||||
REDIS_CONF="/etc/redis/redis.conf"
|
|
||||||
REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
sed -i 's/^\s*#\?\s*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
|
|
||||||
sed -i 's/^\s*#\?\s*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
|
|
||||||
if grep -qE '^\s*#?\s*requirepass ' "$REDIS_CONF"; then
|
|
||||||
sed -i "s/^\s*#\?\s*requirepass .*/requirepass ${REDIS_PASS}/" "$REDIS_CONF"
|
|
||||||
else
|
|
||||||
printf "\nrequirepass %s\n" "${REDIS_PASS}" >> "$REDIS_CONF"
|
|
||||||
fi
|
|
||||||
systemctl enable --now redis-server
|
|
||||||
systemctl restart redis-server
|
|
||||||
|
|
||||||
# ===== Nginx =====
|
|
||||||
log "Nginx konfigurieren…"
|
|
||||||
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
|
||||||
|
|
||||||
detect_php_fpm_sock() {
|
|
||||||
for v in 8.3 8.2 8.1 8.0 7.4; do s="/run/php/php${v}-fpm.sock"; [ -S "$s" ] && { echo "$s"; return; }; done
|
|
||||||
[ -S "/run/php/php-fpm.sock" ] && { echo "/run/php/php-fpm.sock"; return; }
|
|
||||||
echo "127.0.0.1:9000"
|
|
||||||
}
|
|
||||||
PHP_FPM_SOCK="$(detect_php_fpm_sock)"
|
|
||||||
install -d -m 0755 /var/www/letsencrypt
|
|
||||||
|
|
||||||
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
|
|
||||||
cat > "${NGINX_SITE}" <<CONF
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; allow all; }
|
|
||||||
return 301 https://\$host\$request_uri;
|
|
||||||
}
|
|
||||||
server {
|
|
||||||
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
|
||||||
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
|
||||||
server_name _;
|
|
||||||
ssl_certificate ${CERT};
|
|
||||||
ssl_certificate_key ${KEY};
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
access_log /var/log/nginx/${APP_USER}_ssl_access.log;
|
|
||||||
error_log /var/log/nginx/${APP_USER}_ssl_error.log;
|
|
||||||
client_max_body_size 25m;
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
location ~ \.php\$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_FPM_SOCK}; }
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
location /ws {
|
|
||||||
proxy_pass http://127.0.0.1:8080;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection "Upgrade";
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
else
|
|
||||||
cat > "${NGINX_SITE}" <<CONF
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name _;
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
access_log /var/log/nginx/${APP_USER}_access.log;
|
|
||||||
error_log /var/log/nginx/${APP_USER}_error.log;
|
|
||||||
client_max_body_size 25m;
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
location ~ \.php\$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:${PHP_FPM_SOCK}; }
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
|
|
||||||
ln -sf "${NGINX_SITE}" "${NGINX_SITE_LINK}"
|
|
||||||
if nginx -t; then
|
|
||||||
systemctl enable --now nginx
|
|
||||||
systemctl reload nginx || true
|
|
||||||
else
|
|
||||||
echo "[x] Nginx-Konfiguration fehlerhaft – bitte /var/log/nginx/* prüfen."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== Laravel Projekt =====
|
|
||||||
log "Laravel bereitstellen…"
|
|
||||||
mkdir -p "$(dirname "$APP_DIR")"
|
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")"
|
|
||||||
|
|
||||||
log "Git Repo vorbereiten…"
|
|
||||||
if [ "${GIT_REPO}" = "https://example.com/your-repo-placeholder.git" ]; then
|
|
||||||
if [ ! -d "${APP_DIR}" ] || [ -z "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]; then
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd /var/www && COMPOSER_ALLOW_SUPERUSER=0 composer create-project laravel/laravel ${APP_USER} --no-interaction"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ ! -d "${APP_DIR}/.git" ]; then
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
|
||||||
else
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "
|
|
||||||
set -e
|
|
||||||
cd ${APP_DIR}
|
|
||||||
git checkout ${GIT_BRANCH} 2>/dev/null || git checkout -B ${GIT_BRANCH}
|
|
||||||
git fetch --depth=1 origin ${GIT_BRANCH}
|
|
||||||
if git merge-base --is-ancestor HEAD origin/${GIT_BRANCH}; then
|
|
||||||
git pull --ff-only
|
|
||||||
else
|
|
||||||
echo '[i] Non-fast-forward erkannt – setze hart auf origin/${GIT_BRANCH}.' >&2
|
|
||||||
git reset --hard origin/${GIT_BRANCH}
|
|
||||||
git clean -fd
|
|
||||||
fi
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
if [ -f "${APP_DIR}/composer.json" ]; then
|
|
||||||
if [ "${DEV_MODE}" = "1" ]; then
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
|
|
||||||
else
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist --no-dev"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== Node / Frontend =====
|
|
||||||
if [ -f "${APP_DIR}/package.json" ]; then
|
|
||||||
log "Node/NPM installieren…"
|
|
||||||
if command -v node >/dev/null 2>&1; then
|
|
||||||
NODE_MAJ=$(node -v | sed 's/^v//' | cut -d. -f1)
|
|
||||||
NODE_MIN=$(node -v | sed 's/^v//' | cut -d. -f2)
|
|
||||||
if [ "$NODE_MAJ" -lt 20 ] || { [ "$NODE_MAJ" -eq 20 ] && [ "$NODE_MIN" -lt 19 ]; }; then
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
||||||
apt-get install -y nodejs
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
||||||
apt-get install -y nodejs
|
|
||||||
fi
|
|
||||||
|
|
||||||
# .env anlegen & APP_KEY
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
|
||||||
if ! grep -q '^APP_KEY=' "${APP_DIR}/.env"; then echo "APP_KEY=" >> "${APP_DIR}/.env"; fi
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== .env füllen =====
|
|
||||||
ENV_FILE="${APP_DIR}/.env"
|
|
||||||
upsert_env () {
|
|
||||||
local key="$1" val="$2"
|
|
||||||
local esc_key esc_val
|
|
||||||
esc_key="$(printf '%s' "$key" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
|
|
||||||
esc_val="$(printf '%s' "$val" | sed -e 's/[&/]/\\&/g')"
|
|
||||||
if grep -qE "^[#[:space:]]*${esc_key}=" "$ENV_FILE"; then
|
|
||||||
sed -Ei "s|^[#[:space:]]*${esc_key}=.*|${key}=${esc_val}|g" "$ENV_FILE"
|
|
||||||
else
|
|
||||||
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then
|
|
||||||
upsert_env APP_URL "\"https://\${APP_HOST}\""
|
|
||||||
else
|
|
||||||
upsert_env APP_URL "\"http://\${APP_HOST}\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
upsert_env APP_HOST "${SERVER_IP}"
|
|
||||||
upsert_env APP_ADMIN_USER "${ADMIN_USER}"
|
|
||||||
upsert_env APP_ADMIN_EMAIL "${ADMIN_EMAIL}"
|
|
||||||
upsert_env APP_ADMIN_PASS "${ADMIN_PASS}"
|
|
||||||
upsert_env APP_NAME "${APP_NAME}"
|
|
||||||
upsert_env APP_ENV "${APP_ENV}"
|
|
||||||
upsert_env APP_DEBUG "${APP_DEBUG}"
|
|
||||||
|
|
||||||
upsert_env DB_CONNECTION "mysql"
|
|
||||||
upsert_env DB_HOST "127.0.0.1"
|
|
||||||
upsert_env DB_PORT "3306"
|
|
||||||
upsert_env DB_DATABASE "${DB_NAME}"
|
|
||||||
upsert_env DB_USERNAME "${DB_USER}"
|
|
||||||
upsert_env DB_PASSWORD "${DB_PASS}"
|
|
||||||
|
|
||||||
# -------- WICHTIG: Cache-Store auf REDIS setzen (verhindert DB-Tabellenfehler) --------
|
|
||||||
upsert_env CACHE_SETTINGS_STORE "redis"
|
|
||||||
upsert_env CACHE_STORE "redis" # <- HIER geändert (vorher: database)
|
|
||||||
upsert_env CACHE_DRIVER "redis"
|
|
||||||
upsert_env CACHE_PREFIX "${APP_USER}_cache"
|
|
||||||
|
|
||||||
upsert_env SESSION_DRIVER "redis"
|
|
||||||
upsert_env REDIS_CLIENT "phpredis"
|
|
||||||
upsert_env REDIS_HOST "127.0.0.1"
|
|
||||||
upsert_env REDIS_PORT "6379"
|
|
||||||
upsert_env REDIS_PASSWORD "${REDIS_PASS}"
|
|
||||||
upsert_env REDIS_DB "0"
|
|
||||||
upsert_env REDIS_CACHE_DB "1"
|
|
||||||
upsert_env REDIS_CACHE_CONNECTION "cache"
|
|
||||||
upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
|
|
||||||
|
|
||||||
# Reverb / Vite
|
|
||||||
upsert_env BROADCAST_DRIVER "reverb"
|
|
||||||
upsert_env QUEUE_CONNECTION "redis"
|
|
||||||
upsert_env REVERB_APP_ID "${APP_USER}"
|
|
||||||
upsert_env REVERB_APP_KEY "${APP_USER}-yhp47tbt1aebhr1fgvgj"
|
|
||||||
upsert_env REVERB_APP_SECRET "${APP_USER}-ulrdt9agwzkqwqsunbnb"
|
|
||||||
upsert_env REVERB_HOST "127.0.0.1"
|
|
||||||
upsert_env REVERB_PORT "443"
|
|
||||||
upsert_env REVERB_SCHEME "https"
|
|
||||||
upsert_env REVERB_PATH "/ws"
|
|
||||||
upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
|
|
||||||
upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
|
|
||||||
upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
|
|
||||||
upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
|
|
||||||
|
|
||||||
if [ "${DEV_MODE}" = "1" ]; then
|
|
||||||
sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
|
|
||||||
cat >> "${ENV_FILE}" <<CONF
|
|
||||||
# --- MailWolt DEV ---
|
|
||||||
VITE_DEV_HOST=127.0.0.1
|
|
||||||
VITE_DEV_PORT=5173
|
|
||||||
VITE_HMR_PROTOCOL=wss
|
|
||||||
VITE_HMR_CLIENT_PORT=443
|
|
||||||
VITE_HMR_HOST=${APP_HOST}
|
|
||||||
VITE_DEV_ORIGIN=${APP_URL}
|
|
||||||
# --- /MailWolt DEV ---
|
|
||||||
CONF
|
|
||||||
cat > "${APP_DIR}/vite.config.js" <<'JS'
|
|
||||||
import { defineConfig, loadEnv } from 'vite'
|
|
||||||
import laravel from 'laravel-vite-plugin'
|
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
|
||||||
export default ({ mode }) => {
|
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
|
||||||
const host = env.VITE_DEV_HOST || '127.0.0.1'
|
|
||||||
const port = Number(env.VITE_DEV_PORT || 5173)
|
|
||||||
const origin = env.VITE_DEV_ORIGIN || env.APP_URL || 'https://localhost'
|
|
||||||
const hmrHost = env.VITE_HMR_HOST || (new URL(origin)).hostname
|
|
||||||
return defineConfig({
|
|
||||||
plugins: [laravel({ input: ['resources/css/app.css','resources/js/app.js'], refresh: true }), tailwindcss()],
|
|
||||||
server: { host, port, https:false, strictPort:true,
|
|
||||||
hmr:{ protocol: env.VITE_HMR_PROTOCOL || 'wss', host: hmrHost, clientPort:Number(env.VITE_HMR_CLIENT_PORT||443) },
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
JS
|
|
||||||
chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/vite.config.js"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== Frontend Build =====
|
|
||||||
if [ -f "${APP_DIR}/package.json" ]; then
|
|
||||||
log "Frontend Build…"
|
|
||||||
if [ -f "${APP_DIR}/package-lock.json" ] || [ -f "${APP_DIR}/npm-shrinkwrap.json" ]; then
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm ci --no-audit --no-fund || npm install"
|
|
||||||
else
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm install"
|
|
||||||
fi
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build || true"
|
|
||||||
rm -f ${APP_DIR}/bootstrap/cache/*.php
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== Rechte / PHP-FPM =====
|
|
||||||
APP_PW="${APP_PW:-changeme}"
|
|
||||||
if ! id -u "$APP_USER" >/dev/null 2>&1; then
|
|
||||||
adduser --disabled-password --gecos "" "$APP_USER"
|
|
||||||
echo "${APP_USER}:${APP_PW}" | chpasswd
|
|
||||||
fi
|
|
||||||
usermod -a -G "$APP_GROUP" "$APP_USER"
|
|
||||||
|
|
||||||
# Sichert, dass alle nötigen Ordner existieren (idempotent)
|
|
||||||
install -d -m 0775 "${APP_DIR}/storage" \
|
|
||||||
"${APP_DIR}/storage/framework" \
|
|
||||||
"${APP_DIR}/storage/framework/cache" \
|
|
||||||
"${APP_DIR}/storage/framework/cache/data" \
|
|
||||||
"${APP_DIR}/storage/framework/sessions" \
|
|
||||||
"${APP_DIR}/storage/framework/views" \
|
|
||||||
"${APP_DIR}/bootstrap/cache"
|
|
||||||
|
|
||||||
# Besitz & Rechte
|
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
|
||||||
find "$APP_DIR" -type d -exec chmod 775 {} \;
|
|
||||||
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
|
||||||
|
|
||||||
[ -f "$APP_DIR/artisan" ] && chmod 755 "$APP_DIR/artisan"
|
|
||||||
[ -d "$APP_DIR/vendor/bin" ] && chmod -R 755 "$APP_DIR/vendor/bin"
|
|
||||||
[ -d "$APP_DIR/node_modules/.bin" ] && chmod -R 755 "$APP_DIR/node_modules/.bin"
|
|
||||||
[ -f "$APP_DIR/node_modules/vite/bin/vite.js" ] && chmod 755 "$APP_DIR/node_modules/vite/bin/vite.js"
|
|
||||||
find "$APP_DIR" -type f -name "*.sh" -exec chmod 755 {} \;
|
|
||||||
|
|
||||||
chmod -R 775 "$APP_DIR"/storage "$APP_DIR"/bootstrap/cache
|
|
||||||
|
|
||||||
if command -v setfacl >/dev/null 2>&1; then
|
|
||||||
setfacl -R -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
|
||||||
setfacl -dR -m u:${APP_USER}:rwX,g:${APP_GROUP}:rwX "${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
grep -q 'umask 002' /home/${APP_USER}/.profile 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.profile
|
|
||||||
grep -q 'umask 002' /home/${APP_USER}/.bashrc 2>/dev/null || echo 'umask 002' >> /home/${APP_USER}/.bashrc
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "npm config set umask 0002" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;')
|
|
||||||
FPM_POOL="/etc/php/${PHPV}/fpm/pool.d/www.conf"
|
|
||||||
if [ -f "$FPM_POOL" ]; then
|
|
||||||
sed -i 's/^;*listen\.owner.*/listen.owner = www-data/' "$FPM_POOL"
|
|
||||||
sed -i 's/^;*listen\.group.*/listen.group = www-data/' "$FPM_POOL"
|
|
||||||
sed -i 's/^;*listen\.mode.*/listen.mode = 0660/' "$FPM_POOL"
|
|
||||||
systemctl restart php${PHPV}-fpm || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
IDE_USER="${SUDO_USER:-}"
|
|
||||||
if [ -n "$IDE_USER" ] && id "$IDE_USER" >/dev/null 2>&1 && command -v setfacl >/dev/null 2>&1; then
|
|
||||||
usermod -a -G "$APP_GROUP" "$IDE_USER" || true
|
|
||||||
setfacl -R -m u:${IDE_USER}:rwX "$APP_DIR"
|
|
||||||
setfacl -dR -m u:${IDE_USER}:rwX "$APP_DIR"
|
|
||||||
echo -e "${GREEN}[i]${NC} Benutzer '${IDE_USER}' wurde für Schreibzugriff freigeschaltet (ACL + Gruppe ${APP_GROUP})."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Webstack neu laden
|
|
||||||
systemctl reload nginx || true
|
|
||||||
systemctl restart php*-fpm || true
|
|
||||||
|
|
||||||
# ===== Reverb systemd =====
|
|
||||||
cat > /etc/systemd/system/mailwolt-ws.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=MailWolt WebSocket Backend
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
Environment=NODE_ENV=production WS_PORT=8080
|
|
||||||
User=${APP_USER}
|
|
||||||
Group=${APP_GROUP}
|
|
||||||
WorkingDirectory=${APP_DIR}
|
|
||||||
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
|
|
||||||
Restart=always
|
|
||||||
RestartSec=2
|
|
||||||
StandardOutput=append:/var/log/mailwolt-ws.log
|
|
||||||
StandardError=append:/var/log/mailwolt-ws.log
|
|
||||||
KillSignal=SIGINT
|
|
||||||
TimeoutStopSec=15
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
chown root:root /etc/systemd/system/mailwolt-ws.service
|
|
||||||
chmod 644 /etc/systemd/system/mailwolt-ws.service
|
|
||||||
|
|
||||||
touch /var/log/mailwolt-ws.log
|
|
||||||
chown ${APP_USER}:${APP_GROUP} /var/log/mailwolt-ws.log
|
|
||||||
chmod 664 /var/log/mailwolt-ws.log
|
|
||||||
|
|
||||||
install -d -m 775 -o "${APP_USER}" -g "${APP_GROUP}" \
|
|
||||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache"
|
|
||||||
|
|
||||||
# ---- Laravel-Caches als APP_USER aufräumen (funktioniert jetzt ohne DB-Cache-Tabelle) ----
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache"
|
|
||||||
|
|
||||||
# HINWEIS: Wenn du unbedingt DATABASE als Cache-Store willst, dann vor obiger Zeile:
|
|
||||||
# sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan cache:table && php artisan migrate --force"
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi 2>/dev/null | grep -qE '(^| )reverb:start( |$)'"; then
|
|
||||||
systemctl enable --now mailwolt-ws
|
|
||||||
else
|
|
||||||
systemctl disable --now mailwolt-ws >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===== Monit =====
|
|
||||||
log "Monit konfigurieren & starten…"
|
|
||||||
cat > /etc/monit/monitrc <<'EOF'
|
|
||||||
set daemon 60
|
|
||||||
set logfile syslog facility log_daemon
|
|
||||||
|
|
||||||
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
|
||||||
start program = "/bin/systemctl start postfix"
|
|
||||||
stop program = "/bin/systemctl stop postfix"
|
|
||||||
if failed port 25 protocol smtp then restart
|
|
||||||
if failed port 465 type tcp ssl then restart
|
|
||||||
if failed port 587 type tcp then restart
|
|
||||||
|
|
||||||
check process dovecot with pidfile /var/run/dovecot/master.pid
|
|
||||||
start program = "/bin/systemctl start dovecot"
|
|
||||||
stop program = "/bin/systemctl stop dovecot"
|
|
||||||
if failed port 143 type tcp then restart
|
|
||||||
if failed port 993 type tcp ssl then restart
|
|
||||||
if failed port 110 type tcp then restart
|
|
||||||
if failed port 995 type tcp ssl then restart
|
|
||||||
|
|
||||||
check process mariadb with pidfile /var/run/mysqld/mysqld.pid
|
|
||||||
start program = "/bin/systemctl start mariadb"
|
|
||||||
stop program = "/bin/systemctl stop mariadb"
|
|
||||||
if failed port 3306 type tcp then restart
|
|
||||||
|
|
||||||
check process redis-server with pidfile /run/redis/redis-server.pid
|
|
||||||
start program = "/bin/systemctl start redis-server"
|
|
||||||
stop program = "/bin/systemctl stop redis-server"
|
|
||||||
if failed port 6379 type tcp then restart
|
|
||||||
|
|
||||||
check process rspamd with pidfile /run/rspamd/rspamd.pid
|
|
||||||
start program = "/bin/systemctl start rspamd"
|
|
||||||
stop program = "/bin/systemctl stop rspamd"
|
|
||||||
if failed port 11332 type tcp then restart
|
|
||||||
|
|
||||||
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
|
||||||
start program = "/bin/systemctl start opendkim"
|
|
||||||
stop program = "/bin/systemctl stop opendkim"
|
|
||||||
if failed port 8891 type tcp then restart
|
|
||||||
|
|
||||||
check process nginx with pidfile /run/nginx.pid
|
|
||||||
start program = "/bin/systemctl start nginx"
|
|
||||||
stop program = "/bin/systemctl stop nginx"
|
|
||||||
if failed port 80 type tcp then restart
|
|
||||||
if failed port 443 type tcp ssl then restart
|
|
||||||
|
|
||||||
check process mailwolt-ws matching "reverb:start"
|
|
||||||
start program = "/bin/systemctl start mailwolt-ws"
|
|
||||||
stop program = "/bin/systemctl stop mailwolt-ws"
|
|
||||||
if failed host 127.0.0.1 port 8080 type tcp for 2 cycles then restart
|
|
||||||
if 5 restarts within 5 cycles then timeout
|
|
||||||
EOF
|
|
||||||
chmod 600 /etc/monit/monitrc
|
|
||||||
monit -t && systemctl enable --now monit
|
|
||||||
monit reload
|
|
||||||
monit summary || true
|
|
||||||
|
|
||||||
# ===== Healthchecks =====
|
|
||||||
GREEN="\033[1;32m"; RED="\033[1;31m"; GREY="\033[0;90m"; NC="\033[0m"
|
|
||||||
ok(){ echo -e " [${GREEN}OK${NC}]"; }
|
|
||||||
fail(){ echo -e " [${RED}FAIL${NC}]"; }
|
|
||||||
|
|
||||||
echo "[+] Quick-Healthchecks…"
|
|
||||||
printf " • MariaDB … " ; mysqladmin ping --silent >/dev/null 2>&1 && ok || fail
|
|
||||||
printf " • Redis … " ; if command -v redis-cli >/dev/null 2>&1; then
|
|
||||||
if [ -n "${REDIS_PASS:-}" ] && [ "${REDIS_PASS}" != "null" ]; then
|
|
||||||
redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG && ok || fail
|
|
||||||
else
|
|
||||||
redis-cli ping 2>/dev/null | grep -q PONG && ok || fail
|
|
||||||
fi
|
|
||||||
else fail; fi
|
|
||||||
printf " • PHP-FPM … " ; if [[ "$PHP_FPM_SOCK" == 127.0.0.1:9000 ]]; then ss -ltn | grep -q ":9000 " && ok || fail; else [ -S "$PHP_FPM_SOCK" ] && ok || fail; fi
|
|
||||||
printf " • App … " ; if command -v curl >/dev/null 2>&1; then
|
|
||||||
if [ -s "${CERT}" ] && [ -s "${KEY}" ]; then curl -skI "https://127.0.0.1" >/dev/null 2>&1 && ok || fail; else curl -sI "http://127.0.0.1" >/dev/null 2>&1 && ok || fail; fi
|
|
||||||
else echo -e " ${GREY}(curl fehlt)${NC}"; fi
|
|
||||||
check_port(){ local label="$1" cmd="$2"; printf " • %-5s … " "$label"; timeout 8s bash -lc "$cmd" >/dev/null 2>&1 && ok || fail; }
|
|
||||||
check_port "25" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 25'
|
|
||||||
check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof'
|
|
||||||
check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof'
|
|
||||||
check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110'
|
|
||||||
check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof'
|
|
||||||
check_port "143" 'printf ". LOGOUT\r\n" | nc -w 3 127.0.0.1 143'
|
|
||||||
check_port "993" 'printf ". LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof'
|
|
||||||
|
|
||||||
print_bootstrap_summary "$SERVER_IP" "$ADMIN_USER" "$ADMIN_PASS"
|
|
||||||
|
|
||||||
# ===== MOTD =====
|
|
||||||
install -d /usr/local/bin
|
|
||||||
cat >/usr/local/bin/mw-motd <<'SH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
NC="\033[0m"; CY="\033[1;36m"; GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"; GY="\033[0;90m"
|
|
||||||
printf "\033[1;36m"
|
|
||||||
cat <<'ASCII'
|
|
||||||
:::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
|
|
||||||
+:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
|
|
||||||
+:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
|
|
||||||
+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
|
|
||||||
+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
|
|
||||||
#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
|
|
||||||
### ### ### ### ########### ########## ### ### ######## ########## ###
|
|
||||||
ASCII
|
|
||||||
printf "\033[0m\n"
|
|
||||||
now="$(date '+%Y-%m-%d %H:%M:%S %Z')"
|
|
||||||
fqdn="$(hostname -f 2>/dev/null || hostname)"
|
|
||||||
ip_int="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
|
||||||
ip_ext=""; command -v curl >/dev/null 2>&1 && ip_ext="$(curl -s --max-time 1 https://ifconfig.me || true)"
|
|
||||||
upt="$(uptime -p 2>/dev/null || true)"
|
|
||||||
cores="$(nproc 2>/dev/null || echo -n '?')"
|
|
||||||
mhz="$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/MHz/{gsub(/ /,"",$2); printf("%.0f MHz",$2); exit}')"
|
|
||||||
[ -z "$mhz" ] && mhz="$(awk -F: '/cpu MHz/{printf("%.0f MHz",$2); exit}' /proc/cpuinfo 2>/dev/null)"
|
|
||||||
load="$(awk '{print $1" / "$2" / "$3}' /proc/loadavg 2>/dev/null)"
|
|
||||||
mem_total="$(awk '/MemTotal/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
|
|
||||||
mem_free="$(awk '/MemAvailable/{printf "%.2f GB",$2/1024/1024}' /proc/meminfo)"
|
|
||||||
svc_status(){ systemctl is-active --quiet "$1" && echo -e "${GR}OK${NC}" || echo -e "${RD}FAIL${NC}"; }
|
|
||||||
printf "${CY}Information as of:${NC} ${YE}%s${NC}\n" "$now"
|
|
||||||
printf "${GY}FQDN :${NC} %s\n" "$fqdn"
|
|
||||||
if [ -n "$ip_ext" ]; then printf "${GY}IP :${NC} %s ${GY}(external:${NC} %s${GY})${NC}\n" "${ip_int:-?}" "$ip_ext"; else printf "${GY}IP :${NC} %s\n" "${ip_int:-?}"; fi
|
|
||||||
printf "${GY}Uptime :${NC} %s\n" "${upt:-?}"
|
|
||||||
printf "${GY}Core(s) :${NC} %s core(s) at ${CY}%s${NC}\n" "$cores" "${mhz:-?}"
|
|
||||||
printf "${GY}Load :${NC} %s (1 / 5 / 15)\n" "${load:-?}"
|
|
||||||
printf "${GY}Memory :${NC} ${RD}%s${NC} ${GY}(free)${NC} / ${CY}%s${NC} ${GY}(total)${NC}\n" "${mem_free:-?}" "${mem_total:-?}"
|
|
||||||
echo
|
|
||||||
printf "${GY}Services :${NC} postfix: $(svc_status postfix) dovecot: $(svc_status dovecot) nginx: $(svc_status nginx) mariadb: $(svc_status mariadb) redis: $(svc_status redis)\n"
|
|
||||||
SH
|
|
||||||
chmod +x /usr/local/bin/mw-motd
|
|
||||||
|
|
||||||
if [ -d /etc/update-motd.d ]; then
|
|
||||||
cat >/etc/update-motd.d/10-mailwolt <<'SH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
/usr/local/bin/mw-motd
|
|
||||||
SH
|
|
||||||
chmod +x /etc/update-motd.d/10-mailwolt
|
|
||||||
[ -f /etc/update-motd.d/50-motd-news ] && chmod -x /etc/update-motd.d/50-motd-news || true
|
|
||||||
[ -f /etc/update-motd.d/80-livepatch ] && chmod -x /etc/update-motd.d/80-livepatch || true
|
|
||||||
else
|
|
||||||
cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH'
|
|
||||||
case "$-" in *i*) /usr/local/bin/mw-motd ;; esac
|
|
||||||
SH
|
|
||||||
fi
|
|
||||||
: > /etc/motd 2>/dev/null || true
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
if [ -r /etc/mailwolt/installer.env ]; then
|
|
||||||
. /etc/mailwolt/installer.env
|
|
||||||
fi
|
|
||||||
|
|
||||||
REDIS_PASS="${REDIS_PASS:-}"
|
|
||||||
|
|
||||||
SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
find "$SCRIPTS_DIR/.." -type f -name "*.sh" -exec sed -i 's/\r$//' {} \; || true
|
|
||||||
|
|
||||||
log "Pakete installieren …"
|
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
|
||||||
apt-get update -y
|
|
||||||
# Minimal aber vollständig
|
|
||||||
apt-get -y -o Dpkg::Options::="--force-confdef" \
|
|
||||||
-o Dpkg::Options::="--force-confold" install \
|
|
||||||
postfix postfix-mysql dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql \
|
|
||||||
mariadb-server mariadb-client redis-server rspamd opendkim opendkim-tools opendmarc clamav \
|
|
||||||
clamav-daemon nginx php php-fpm php-cli php-mbstring php-xml php-curl php-zip php-mysql \
|
|
||||||
php-redis php-gd unzip curl composer git certbot python3-certbot-nginx fail2ban ca-certificates \
|
|
||||||
rsyslog sudo openssl monit acl netcat-openbsd jq sqlite3
|
|
||||||
|
|
||||||
# <<< Apache konsequent entfernen >>>
|
|
||||||
systemctl disable --now apache2 >/dev/null 2>&1 || true
|
|
||||||
apt-get -y purge 'apache2*' >/dev/null 2>&1 || true
|
|
||||||
apt-get -y autoremove >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
log "Systemuser/Dirs …"
|
|
||||||
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
|
||||||
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
|
||||||
# Systemuser/Dirs …
|
|
||||||
id vmail >/dev/null 2>&1 || adduser --system --group --home /var/mail vmail
|
|
||||||
id "$APP_USER" >/dev/null 2>&1 || adduser --disabled-password --gecos "" "$APP_USER"
|
|
||||||
|
|
||||||
# --- FIX: Gruppen und Berechtigungen für Maildir und Dovecot-Zugriff ---
|
|
||||||
# vmail soll primär der Gruppe "mail" angehören, zusätzlich dovecot
|
|
||||||
usermod -g mail -a -G dovecot vmail || true
|
|
||||||
|
|
||||||
# App-User in relevante Gruppen
|
|
||||||
usermod -a -G "$APP_GROUP" "$APP_USER" || true
|
|
||||||
usermod -a -G mail,dovecot "$APP_USER" || true
|
|
||||||
|
|
||||||
# Maildir-Baum für Gruppe mail lesbar
|
|
||||||
chgrp -R mail /var/mail/vhosts || true
|
|
||||||
chmod -R g+rx /var/mail/vhosts || true
|
|
||||||
|
|
||||||
# ACLs setzen, damit neue Verzeichnisse automatisch passende Rechte bekommen
|
|
||||||
setfacl -R -m g:mail:rx /var/mail/vhosts || true
|
|
||||||
setfacl -dR -m g:mail:rx /var/mail/vhosts || true
|
|
||||||
usermod -a -G "$APP_GROUP" "$APP_USER" || true
|
|
||||||
install -d -m 0755 -o root -g root /var/www
|
|
||||||
install -d -m 0775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR"
|
|
||||||
|
|
||||||
SUDOERS_DKIM="/etc/sudoers.d/mailwolt-dkim"
|
|
||||||
cat > "${SUDOERS_DKIM}" <<'EOF'
|
|
||||||
Defaults!/usr/local/sbin/mailwolt-install-dkim !requiretty
|
|
||||||
Defaults!/usr/local/sbin/mailwolt-remove-dkim !requiretty
|
|
||||||
Defaults!/usr/bin/systemctl !requiretty
|
|
||||||
Defaults!/usr/bin/test !requiretty
|
|
||||||
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/bin/test *
|
|
||||||
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install-dkim *
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-remove-dkim *
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/bin/systemctl reload opendkim
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/bin/test *
|
|
||||||
EOF
|
|
||||||
chown root:root "${SUDOERS_DKIM}"
|
|
||||||
chmod 440 "${SUDOERS_DKIM}"
|
|
||||||
|
|
||||||
if ! visudo -c -f "${SUDOERS_DKIM}" >/dev/null 2>&1; then
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_DKIM} – entferne Datei."
|
|
||||||
rm -f "${SUDOERS_DKIM}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SUDOERS_DOVEADM="/etc/sudoers.d/mailwolt-doveadm"
|
|
||||||
cat > "${SUDOERS_DOVEADM}" <<'EOF'
|
|
||||||
Cmnd_Alias MW_DOVEADM_STATUS = /usr/bin/doveadm -f tab mailbox status -u * messages INBOX, \
|
|
||||||
/usr/bin/doveadm mailbox status -u * messages INBOX
|
|
||||||
www-data ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
|
|
||||||
mailwolt ALL=(vmail) NOPASSWD: MW_DOVEADM_STATUS
|
|
||||||
EOF
|
|
||||||
chown root:root "${SUDOERS_DOVEADM}"
|
|
||||||
chmod 440 "${SUDOERS_DOVEADM}"
|
|
||||||
visudo -c -f "${SUDOERS_DOVEADM}" || rm -f "${SUDOERS_DOVEADM}"
|
|
||||||
|
|
||||||
log "MariaDB include-fix …"
|
|
||||||
mkdir -p /etc/mysql/mariadb.conf.d
|
|
||||||
[[ -f /etc/mysql/mariadb.cnf ]] || echo '!include /etc/mysql/mariadb.conf.d/*.cnf' > /etc/mysql/mariadb.cnf
|
|
||||||
|
|
||||||
log "Redis absichern …"
|
|
||||||
if [[ -z "${REDIS_PASS:-}" || "${REDIS_PASS}" == "changeme" ]]; then
|
|
||||||
REDIS_PASS="$(openssl rand -hex 16)"
|
|
||||||
export REDIS_PASS
|
|
||||||
log "Neues Redis-Passwort generiert."
|
|
||||||
fi
|
|
||||||
# Aktiven Redis-Config-Pfad aus systemd holen (Fallback: Standard)
|
|
||||||
REDIS_CONF="$(systemctl show -p ExecStart redis-server \
|
|
||||||
| sed -n 's/^ExecStart=.*redis-server[[:space:]]\+\([^[:space:]]\+\).*/\1/p')"
|
|
||||||
REDIS_CONF="${REDIS_CONF:-/etc/redis/redis.conf}"
|
|
||||||
|
|
||||||
# Bind + protected-mode hart setzen
|
|
||||||
sed -i 's/^[[:space:]]*#\?[[:space:]]*bind .*/bind 127.0.0.1/' "$REDIS_CONF"
|
|
||||||
sed -i 's/^[[:space:]]*#\?[[:space:]]*protected-mode .*/protected-mode yes/' "$REDIS_CONF"
|
|
||||||
|
|
||||||
# Vorherige requirepass-Zeilen entfernen (kommentiert/unkommentiert), dann neu schreiben
|
|
||||||
sed -i '/^[[:space:]]*#\?[[:space:]]*requirepass[[:space:]]\+/d' "$REDIS_CONF"
|
|
||||||
printf '\nrequirepass %s\n' "${REDIS_PASS}" >> "$REDIS_CONF"
|
|
||||||
|
|
||||||
# Dienst aktivieren & neu starten
|
|
||||||
systemctl enable --now redis-server
|
|
||||||
systemctl restart redis-server || true
|
|
||||||
|
|
||||||
# Sanity-Check (kein harter Exit, nur Log)
|
|
||||||
if redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q PONG; then
|
|
||||||
log "Redis mit Passwort OK."
|
|
||||||
else
|
|
||||||
warn "Redis PING mit Passwort fehlgeschlagen – bitte /etc/redis/redis.conf prüfen."
|
|
||||||
fi
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
CONF_BASE="/etc/${APP_USER}"
|
|
||||||
CERT_DIR="${CONF_BASE}/ssl"
|
|
||||||
UI_SSL_DIR="/etc/ssl/ui"; WEBMAIL_SSL_DIR="/etc/ssl/webmail"; MAIL_SSL_DIR="/etc/ssl/mail"
|
|
||||||
UI_CERT="${UI_SSL_DIR}/fullchain.pem"; UI_KEY="${UI_SSL_DIR}/privkey.pem"
|
|
||||||
WEBMAIL_CERT="${WEBMAIL_SSL_DIR}/fullchain.pem"; WEBMAIL_KEY="${WEBMAIL_SSL_DIR}/privkey.pem"
|
|
||||||
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"; MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
|
||||||
|
|
||||||
install -d -m 0750 "$CERT_DIR"
|
|
||||||
CERT="${CERT_DIR}/cert.pem"; KEY="${CERT_DIR}/key.pem"
|
|
||||||
|
|
||||||
if [[ ! -s "$CERT" || ! -s "$KEY" ]]; then
|
|
||||||
log "Self-signed Zertifikat erzeugen …"
|
|
||||||
OSSL_CFG="${CERT_DIR}/openssl.cnf"
|
|
||||||
cat > "$OSSL_CFG" <<CFG
|
|
||||||
[req]
|
|
||||||
default_bits=2048
|
|
||||||
prompt=no
|
|
||||||
default_md=sha256
|
|
||||||
req_extensions=req_ext
|
|
||||||
distinguished_name=dn
|
|
||||||
[dn]
|
|
||||||
CN=${SERVER_PUBLIC_IPV4}
|
|
||||||
O=${APP_NAME}
|
|
||||||
C=DE
|
|
||||||
[req_ext]
|
|
||||||
subjectAltName=@alt_names
|
|
||||||
[alt_names]
|
|
||||||
IP.1=${SERVER_PUBLIC_IPV4}
|
|
||||||
CFG
|
|
||||||
openssl req -x509 -newkey rsa:2048 -days 825 -nodes -keyout "$KEY" -out "$CERT" -config "$OSSL_CFG"
|
|
||||||
chgrp www-data "$CERT" "$KEY" || true
|
|
||||||
chmod 640 "$KEY" "$CERT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
install -d -m 0755 "$UI_SSL_DIR" "$WEBMAIL_SSL_DIR" "$MAIL_SSL_DIR"
|
|
||||||
ln -sf "$CERT" "$UI_CERT"; ln -sf "$KEY" "$UI_KEY"
|
|
||||||
ln -sf "$CERT" "$WEBMAIL_CERT";ln -sf "$KEY" "$WEBMAIL_KEY"
|
|
||||||
ln -sf "$CERT" "$MAIL_CERT"; ln -sf "$KEY" "$MAIL_KEY"
|
|
||||||
|
|
||||||
# --- Mail-Zertifikate: Rechte für Postfix & Dovecot -------------------------
|
|
||||||
# WICHTIG: Rechte am *Target* (KEY/CERT im $CERT_DIR) setzen, nicht an den Symlinks.
|
|
||||||
if [[ -f "$KEY" && -f "$CERT" ]]; then
|
|
||||||
echo "[+] Setze Berechtigungen für Mail-Zertifikate …"
|
|
||||||
# Key: nur root + Gruppe lesen. Gruppe → postfix
|
|
||||||
chgrp postfix "$KEY" || true
|
|
||||||
chmod 640 "$KEY" || true
|
|
||||||
# Dovecot zusätzlich Leserechte via ACL
|
|
||||||
setfacl -m u:dovecot:r "$KEY" || true
|
|
||||||
# Zertifikat darf weltweit lesbar sein
|
|
||||||
chmod 644 "$CERT" || true
|
|
||||||
else
|
|
||||||
echo "[!] Zertifikatsdateien fehlen: $KEY oder $CERT" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional: kurze Info, wohin verlinkt wurde
|
|
||||||
echo "[i] Mail TLS: $MAIL_CERT -> $CERT ; $MAIL_KEY -> $KEY"
|
|
||||||
|
|
@ -1,588 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
|
||||||
# 2) POSIX-kompatibler Deploy-Wrapper (von Certbot aufgerufen)
|
|
||||||
# -------------------------------------------------------------------
|
|
||||||
cat >/usr/local/sbin/mailwolt-deploy.sh <<'WRAP'
|
|
||||||
#!/bin/sh
|
|
||||||
# POSIX-safe Certbot deploy-hook (ohne bashisms)
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
# Installer-ENV laden (liefert UI_HOST/WEBMAIL_HOST/MAIL_HOSTNAME etc.)
|
|
||||||
if [ -r /etc/mailwolt/installer.env ]; then
|
|
||||||
. /etc/mailwolt/installer.env
|
|
||||||
fi
|
|
||||||
|
|
||||||
UI_HOST="${UI_HOST:-}"
|
|
||||||
WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
|
||||||
MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
|
||||||
ACME_BASE="/etc/letsencrypt/live"
|
|
||||||
|
|
||||||
copy_cert() {
|
|
||||||
le_base="$1" # z.B. /etc/letsencrypt/live/ui.example.com
|
|
||||||
target_dir="$2" # z.B. /etc/ssl/ui
|
|
||||||
|
|
||||||
cert="${le_base}/fullchain.pem"
|
|
||||||
key="${le_base}/privkey.pem"
|
|
||||||
|
|
||||||
[ -s "$cert" ] || { echo "[deploy] missing $cert"; return 1; }
|
|
||||||
[ -s "$key" ] || { echo "[deploy] missing $key"; return 1; }
|
|
||||||
|
|
||||||
mkdir -p "$target_dir"
|
|
||||||
|
|
||||||
# echte Dateien (keine Symlinks), feste Rechte
|
|
||||||
install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
|
||||||
install -m 0600 "$key" "${target_dir}/privkey.pem"
|
|
||||||
|
|
||||||
echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
|
||||||
}
|
|
||||||
|
|
||||||
reload_services() {
|
|
||||||
kind="$1" # ui | mail
|
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
|
||||||
if [ "$kind" = "mail" ]; then
|
|
||||||
systemctl reload postfix 2>/dev/null || true
|
|
||||||
systemctl reload dovecot 2>/dev/null || true
|
|
||||||
else
|
|
||||||
systemctl reload nginx 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Certbot-Kontext
|
|
||||||
LINEAGE="${RENEWED_LINEAGE:-}"
|
|
||||||
HOST=""
|
|
||||||
if [ -n "$LINEAGE" ]; then
|
|
||||||
HOST="$(basename "$LINEAGE")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
did_any=0
|
|
||||||
|
|
||||||
maybe_copy_for_host() {
|
|
||||||
host="$1"
|
|
||||||
dir="$2"
|
|
||||||
[ -n "$host" ] || return 0
|
|
||||||
|
|
||||||
# Fall A: Certbot liefert RENEWED_DOMAINS (Space-getrennt)
|
|
||||||
if [ -n "${RENEWED_DOMAINS:-}" ]; then
|
|
||||||
case " ${RENEWED_DOMAINS} " in
|
|
||||||
*" ${host} "*) copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1 ;;
|
|
||||||
esac
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fall B: Erst-issue / kein RENEWED_DOMAINS → über LINEAGE matchen
|
|
||||||
if [ -n "$HOST" ] && [ "$HOST" = "$host" ]; then
|
|
||||||
copy_cert "${ACME_BASE}/${host}" "${dir}" && did_any=1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gezieltes Kopieren
|
|
||||||
maybe_copy_for_host "$UI_HOST" "/etc/ssl/ui"
|
|
||||||
maybe_copy_for_host "$WEBMAIL_HOST" "/etc/ssl/webmail"
|
|
||||||
maybe_copy_for_host "$MAIL_HOSTNAME" "/etc/ssl/mail"
|
|
||||||
|
|
||||||
# Fallback (Erstlauf): kopiere vorhandene Lineages
|
|
||||||
if [ "$did_any" -eq 0 ]; then
|
|
||||||
[ -n "$UI_HOST" ] && [ -d "${ACME_BASE}/${UI_HOST}" ] && copy_cert "${ACME_BASE}/${UI_HOST}" "/etc/ssl/ui"
|
|
||||||
[ -n "$WEBMAIL_HOST" ] && [ -d "${ACME_BASE}/${WEBMAIL_HOST}" ] && copy_cert "${ACME_BASE}/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
|
||||||
[ -n "$MAIL_HOSTNAME" ] && [ -d "${ACME_BASE}/${MAIL_HOSTNAME}" ] && copy_cert "${ACME_BASE}/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# TLSA-Refresh (tolerant falls App noch nicht ready)
|
|
||||||
if command -v php >/dev/null 2>&1 && [ -f /var/www/mailwolt/artisan ]; then
|
|
||||||
(cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Services neu laden
|
|
||||||
if [ -n "$HOST" ]; then
|
|
||||||
if [ -n "$MAIL_HOSTNAME" ] && [ "$HOST" = "$MAIL_HOSTNAME" ]; then
|
|
||||||
reload_services mail
|
|
||||||
else
|
|
||||||
reload_services ui
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
reload_services ui
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
WRAP
|
|
||||||
chmod +x /usr/local/sbin/mailwolt-deploy.sh
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
|
||||||
# 3) Certbot deploy-hook, der den Wrapper aufruft
|
|
||||||
# -------------------------------------------------------------------
|
|
||||||
install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
|
||||||
cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
|
|
||||||
#!/bin/sh
|
|
||||||
exec /usr/local/sbin/mailwolt-deploy.sh
|
|
||||||
HOOK
|
|
||||||
chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
|
|
||||||
|
|
||||||
|
|
||||||
log "[✓] MailWolt Deploy-Hook eingerichtet"
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#source ./lib.sh
|
|
||||||
#
|
|
||||||
## Persistente Installer-Variablen (werden vom Wrapper gelesen)
|
|
||||||
#install -d -m 0755 /etc/mailwolt
|
|
||||||
#cat >/etc/mailwolt/installer.env <<EOF
|
|
||||||
#UI_HOST=${UI_HOST}
|
|
||||||
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
|
||||||
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
|
||||||
#BASE_DOMAIN=${BASE_DOMAIN}
|
|
||||||
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
|
||||||
#SYSMAIL_SUB="${SYSMAIL_SUB}"
|
|
||||||
#SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN}"
|
|
||||||
#DKIM_ENABLE="${DKIM_ENABLE}"
|
|
||||||
#DKIM_SELECTOR="${DKIM_SELECTOR}"
|
|
||||||
#DKIM_GENERATE="${DKIM_GENERATE}"
|
|
||||||
#APP_ENV=${APP_ENV:-production}
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
|
||||||
#
|
|
||||||
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
|
||||||
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
## Installer-Variablen laden
|
|
||||||
#set +u
|
|
||||||
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
#set -u
|
|
||||||
#
|
|
||||||
#UI_HOST="${UI_HOST:-}"
|
|
||||||
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
|
||||||
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
|
||||||
#
|
|
||||||
## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
|
|
||||||
#copy_cert() {
|
|
||||||
# local le_base="$1" target_dir="$2"
|
|
||||||
# local cert="${le_base}/fullchain.pem"
|
|
||||||
# local key="${le_base}/privkey.pem"
|
|
||||||
#
|
|
||||||
# [[ -s "$cert" && -s "$key" ]] || return 0
|
|
||||||
#
|
|
||||||
# install -d -m 0755 "$target_dir"
|
|
||||||
#
|
|
||||||
# # Vorhandene Symlinks entfernen, sonst kopierst du in die LE-Datei hinein
|
|
||||||
# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
|
|
||||||
# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
|
|
||||||
#
|
|
||||||
# # Echte Dateien ablegen
|
|
||||||
# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
|
||||||
# install -m 0600 "$key" "${target_dir}/privkey.pem"
|
|
||||||
#
|
|
||||||
# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
## Nur Domains bearbeiten, die in diesem Lauf betroffen sind.
|
|
||||||
## Bei manchen Distros ist RENEWED_DOMAINS auf Erst-issue leer -> Fallback nutzen.
|
|
||||||
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
|
||||||
#did_any=0
|
|
||||||
#
|
|
||||||
#maybe_copy_for() {
|
|
||||||
# local host="$1" dir="$2"
|
|
||||||
# [[ -z "$host" ]] && return 0
|
|
||||||
# if [[ "$RDOMS" == *" ${host} "* ]]; then
|
|
||||||
# copy_cert "/etc/letsencrypt/live/${host}" "${dir}"
|
|
||||||
# did_any=1
|
|
||||||
# fi
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
## 1) Normalfall: nur die vom Certbot gemeldeten Hosts kopieren
|
|
||||||
#maybe_copy_for "$UI_HOST" "/etc/ssl/ui"
|
|
||||||
#maybe_copy_for "$WEBMAIL_HOST" "/etc/ssl/webmail"
|
|
||||||
#maybe_copy_for "$MAIL_HOSTNAME" "/etc/ssl/mail"
|
|
||||||
#
|
|
||||||
## 2) Fallback: Beim Erstlauf/Edge-Cases alles kopieren, was bereits existiert
|
|
||||||
#if [[ "$did_any" -eq 0 ]]; then
|
|
||||||
# [[ -n "$UI_HOST" && -d "/etc/letsencrypt/live/${UI_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
|
||||||
# [[ -n "$WEBMAIL_HOST" && -d "/etc/letsencrypt/live/${WEBMAIL_HOST}" ]] && copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
|
||||||
# [[ -n "$MAIL_HOSTNAME" && -d "/etc/letsencrypt/live/${MAIL_HOSTNAME}"]] && copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}"/etc/ssl/mail
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## Optional: TLSA via Laravel (tolerant, falls App noch nicht gebaut)
|
|
||||||
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
|
|
||||||
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## Nginx nur neu laden, wenn aktiv
|
|
||||||
#if systemctl is-active --quiet nginx; then
|
|
||||||
# systemctl reload nginx || true
|
|
||||||
#fi
|
|
||||||
#WRAP
|
|
||||||
#chmod +x /usr/local/sbin/mw-deploy.sh
|
|
||||||
#
|
|
||||||
## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
|
|
||||||
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
|
||||||
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh <<'HOOK'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#exec /usr/local/sbin/mw-deploy.sh
|
|
||||||
#HOOK
|
|
||||||
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-certs.sh
|
|
||||||
#
|
|
||||||
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#source ./lib.sh
|
|
||||||
#
|
|
||||||
## Persistente Installer-Variablen (werden vom Wrapper gelesen)
|
|
||||||
#install -d -m 0755 /etc/mailwolt
|
|
||||||
#cat >/etc/mailwolt/installer.env <<EOF
|
|
||||||
#UI_HOST=${UI_HOST}
|
|
||||||
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
|
||||||
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
|
||||||
#BASE_DOMAIN=${BASE_DOMAIN}
|
|
||||||
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
|
||||||
#APP_ENV=${APP_ENV:-production}
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
|
||||||
#
|
|
||||||
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
|
||||||
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
## Installer-Variablen laden
|
|
||||||
#set +u
|
|
||||||
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
#set -u
|
|
||||||
#
|
|
||||||
#UI_HOST="${UI_HOST:-}"
|
|
||||||
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
|
||||||
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
|
||||||
#
|
|
||||||
## --- Kopieren statt Symlinks (damit Laravel lesen kann) ---------------------
|
|
||||||
#copy_cert() {
|
|
||||||
# local le_base="$1" target_dir="$2"
|
|
||||||
# local cert="${le_base}/fullchain.pem"
|
|
||||||
# local key="${le_base}/privkey.pem"
|
|
||||||
#
|
|
||||||
# [[ -s "$cert" && -s "$key" ]] || return 0
|
|
||||||
#
|
|
||||||
# # Zielordner sicherstellen
|
|
||||||
# install -d -m 0755 "$target_dir"
|
|
||||||
#
|
|
||||||
# # Falls vorher Symlinks existieren → entfernen, sonst würde "install" das Ziel des Links überschreiben
|
|
||||||
# [ -L "${target_dir}/fullchain.pem" ] && rm -f "${target_dir}/fullchain.pem"
|
|
||||||
# [ -L "${target_dir}/privkey.pem" ] && rm -f "${target_dir}/privkey.pem"
|
|
||||||
#
|
|
||||||
# # KOPIEREN mit sauberen Rechten (Chain world-readable, Key nur root)
|
|
||||||
# install -m 0644 "$cert" "${target_dir}/fullchain.pem"
|
|
||||||
# install -m 0600 "$key" "${target_dir}/privkey.pem"
|
|
||||||
#
|
|
||||||
# echo "[+] Copied ${target_dir}/fullchain.pem und privkey.pem ← ${le_base}"
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
## Nur für Domains arbeiten, die in diesem Lauf betroffen sind
|
|
||||||
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
|
||||||
#
|
|
||||||
## UI
|
|
||||||
#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
|
|
||||||
# copy_cert "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
|
||||||
#fi
|
|
||||||
## Webmail
|
|
||||||
#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
|
|
||||||
# copy_cert "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
|
||||||
#fi
|
|
||||||
## MX
|
|
||||||
#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
|
|
||||||
# copy_cert "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## Optional: TLSA via Laravel (still tolerant, falls App noch nicht gebaut)
|
|
||||||
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ] && [ -f /var/www/mailwolt/artisan ]; then
|
|
||||||
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## Nginx nur neu laden, wenn aktiv
|
|
||||||
#if systemctl is-active --quiet nginx; then
|
|
||||||
# systemctl reload nginx || true
|
|
||||||
#fi
|
|
||||||
#WRAP
|
|
||||||
#chmod +x /usr/local/sbin/mw-deploy.sh
|
|
||||||
#
|
|
||||||
## 2) Certbot-Deploy-Hook: ruft den Wrapper bei jeder erfolgreichen Ausstellung/Renew auf
|
|
||||||
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
|
||||||
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#exec /usr/local/sbin/mw-deploy.sh
|
|
||||||
#HOOK
|
|
||||||
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
|
||||||
#
|
|
||||||
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#source ./lib.sh
|
|
||||||
#
|
|
||||||
#install -d -m 0755 /etc/mailwolt
|
|
||||||
#cat >/etc/mailwolt/installer.env <<EOF
|
|
||||||
#UI_HOST=${UI_HOST}
|
|
||||||
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
|
||||||
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
|
||||||
#BASE_DOMAIN=${BASE_DOMAIN}
|
|
||||||
#LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
|
||||||
#APP_ENV=${APP_ENV:-production}
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
|
||||||
#
|
|
||||||
## 1) Wrapper, den Certbot bei Issue/Renew aufruft
|
|
||||||
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
## Installer-Variablen laden (UI_HOST, WEBMAIL_HOST, MAIL_HOSTNAME, optional LE_EMAIL etc.)
|
|
||||||
#set +u
|
|
||||||
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
#set -u
|
|
||||||
#
|
|
||||||
#UI_HOST="${UI_HOST:-}"
|
|
||||||
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
|
||||||
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
|
||||||
#
|
|
||||||
#link_if() {
|
|
||||||
# local le_base="$1" target_dir="$2"
|
|
||||||
# local cert="${le_base}/fullchain.pem"
|
|
||||||
# local key="${le_base}/privkey.pem"
|
|
||||||
# [[ -s "$cert" && -s "$key" ]] || return 0
|
|
||||||
# install -d -m 0755 "$target_dir"
|
|
||||||
# ln -sf "$cert" "${target_dir}/fullchain.pem"
|
|
||||||
# ln -sf "$key" "${target_dir}/privkey.pem"
|
|
||||||
# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
|
|
||||||
# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
|
|
||||||
# echo "[+] Linked ${target_dir} -> ${le_base}"
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
## Nur für Domains arbeiten, die im aktuellen Lauf erneuert/ausgestellt wurden
|
|
||||||
#RDOMS=" ${RENEWED_DOMAINS:-} "
|
|
||||||
#
|
|
||||||
## UI
|
|
||||||
#if [[ -n "$UI_HOST" && "$RDOMS" == *" ${UI_HOST} "* ]]; then
|
|
||||||
# link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
|
||||||
#fi
|
|
||||||
## Webmail
|
|
||||||
#if [[ -n "$WEBMAIL_HOST" && "$RDOMS" == *" ${WEBMAIL_HOST} "* ]]; then
|
|
||||||
# link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
|
||||||
#fi
|
|
||||||
## MX
|
|
||||||
#if [[ -n "$MAIL_HOSTNAME" && "$RDOMS" == *" ${MAIL_HOSTNAME} "* ]]; then
|
|
||||||
# link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## Optional: TLSA via Laravel, falls App schon vorhanden (sonst still überspringen)
|
|
||||||
#if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
|
|
||||||
# (cd /var/www/mailwolt && php artisan dns:tlsa:refresh) || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## Nginx nur neu laden, wenn aktiv
|
|
||||||
#if systemctl is-active --quiet nginx; then
|
|
||||||
# systemctl reload nginx || true
|
|
||||||
#fi
|
|
||||||
#WRAP
|
|
||||||
#chmod +x /usr/local/sbin/mw-deploy.sh
|
|
||||||
#
|
|
||||||
## 2) Certbot-Deploy-Hooks einrichten (ruft nur den Wrapper auf)
|
|
||||||
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
|
||||||
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#exec /usr/local/sbin/mw-deploy.sh
|
|
||||||
#HOOK
|
|
||||||
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
|
||||||
#
|
|
||||||
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#source ./lib.sh
|
|
||||||
#
|
|
||||||
#log "Let's Encrypt Deploy-Hooks und Wrapper anlegen …"
|
|
||||||
#
|
|
||||||
## 1) Wrapper-Skript, das Symlinks setzt und Nginx reloaded
|
|
||||||
#cat >/usr/local/sbin/mw-deploy.sh <<'WRAP'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
#link_if() {
|
|
||||||
# local le_base="$1" target_dir="$2"
|
|
||||||
# local cert="${le_base}/fullchain.pem"
|
|
||||||
# local key="${le_base}/privkey.pem"
|
|
||||||
# [[ -s "$cert" && -s "$key" ]] || return 0
|
|
||||||
# install -d -m 0755 "$target_dir"
|
|
||||||
# ln -sf "$cert" "${target_dir}/fullchain.pem"
|
|
||||||
# ln -sf "$key" "${target_dir}/privkey.pem"
|
|
||||||
# chmod 644 "${target_dir}/fullchain.pem" 2>/dev/null || true
|
|
||||||
# chmod 600 "${target_dir}/privkey.pem" 2>/dev/null || true
|
|
||||||
# echo "[+] Linked ${target_dir} -> ${le_base}"
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
#UI_HOST="${UI_HOST:-}"
|
|
||||||
#WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
|
||||||
#MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
|
||||||
#
|
|
||||||
#[[ -n "$UI_HOST" ]] && link_if "/etc/letsencrypt/live/${UI_HOST}" "/etc/ssl/ui"
|
|
||||||
#[[ -n "$WEBMAIL_HOST" ]] && link_if "/etc/letsencrypt/live/${WEBMAIL_HOST}" "/etc/ssl/webmail"
|
|
||||||
#[[ -n "$MAIL_HOSTNAME" ]] && link_if "/etc/letsencrypt/live/${MAIL_HOSTNAME}" "/etc/ssl/mail"
|
|
||||||
#
|
|
||||||
#if systemctl is-active --quiet nginx; then
|
|
||||||
# systemctl reload nginx || true
|
|
||||||
#fi
|
|
||||||
#WRAP
|
|
||||||
#
|
|
||||||
#chmod +x /usr/local/sbin/mw-deploy.sh
|
|
||||||
#
|
|
||||||
## 2) Certbot Deploy-Hook-Verzeichnis + Symlink für Renewals
|
|
||||||
#install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
|
||||||
#cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<'HOOK'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#exec /usr/local/sbin/mw-deploy.sh
|
|
||||||
#HOOK
|
|
||||||
#chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
|
||||||
#
|
|
||||||
#log "[✓] MailWolt Deploy-Hook eingerichtet"
|
|
||||||
#
|
|
||||||
###!/usr/bin/env bash
|
|
||||||
##set -euo pipefail
|
|
||||||
##source ./lib.sh
|
|
||||||
##
|
|
||||||
### ────────────────────────────────────────────────────────────────────────────
|
|
||||||
### 21-le-deploy-hook.sh
|
|
||||||
### • legt /etc/mailwolt/installer.env an (falls fehlt)
|
|
||||||
### • erzeugt Deploy-Hooks:
|
|
||||||
### - 50-mailwolt-symlinks.sh → verlinkt LE-Zerts nach /etc/ssl/{ui,webmail,mail}
|
|
||||||
### - 60-mailwolt-tlsa.sh → aktualisiert TLSA (3 1 1) für MX bei jedem Renew
|
|
||||||
### • KEIN Reload von Postfix/Dovecot (kommt später im Installer)
|
|
||||||
### ────────────────────────────────────────────────────────────────────────────
|
|
||||||
##
|
|
||||||
### 0) Hostnamen persistent speichern (für spätere Deploys)
|
|
||||||
##install -d -m 0755 /etc/mailwolt
|
|
||||||
##if [[ ! -f /etc/mailwolt/installer.env ]]; then
|
|
||||||
## cat >/etc/mailwolt/installer.env <<EOF
|
|
||||||
##UI_HOST=${UI_HOST}
|
|
||||||
##WEBMAIL_HOST=${WEBMAIL_HOST}
|
|
||||||
##MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
|
||||||
##EOF
|
|
||||||
## echo "[+] /etc/mailwolt/installer.env erstellt."
|
|
||||||
##fi
|
|
||||||
##
|
|
||||||
### 1) Deploy-Hooks-Verzeichnis anlegen
|
|
||||||
##install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
|
|
||||||
##
|
|
||||||
### ────────────────────────────────────────────────────────────────────────────
|
|
||||||
### 2) 50-mailwolt-symlinks.sh
|
|
||||||
### ────────────────────────────────────────────────────────────────────────────
|
|
||||||
##cat >/etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh <<HOOK
|
|
||||||
###!/usr/bin/env bash
|
|
||||||
##set -euo pipefail
|
|
||||||
##
|
|
||||||
##UI_LE="/etc/letsencrypt/live/${UI_HOST}"
|
|
||||||
##WEBMAIL_LE="/etc/letsencrypt/live/${WEBMAIL_HOST}"
|
|
||||||
##MX_LE="/etc/letsencrypt/live/${MAIL_HOSTNAME}"
|
|
||||||
##
|
|
||||||
##UI_SSL_DIR="/etc/ssl/ui"
|
|
||||||
##WEBMAIL_SSL_DIR="/etc/ssl/webmail"
|
|
||||||
##MAIL_SSL_DIR="/etc/ssl/mail"
|
|
||||||
##
|
|
||||||
### Zielverzeichnisse anlegen (einmalig)
|
|
||||||
##install -d -m 0755 "\$UI_SSL_DIR" "\$WEBMAIL_SSL_DIR" "\$MAIL_SSL_DIR"
|
|
||||||
##
|
|
||||||
##link_if() {
|
|
||||||
## local le_base="\$1" target_dir="\$2"
|
|
||||||
## local cert="\${le_base}/fullchain.pem"
|
|
||||||
## local key="\${le_base}/privkey.pem"
|
|
||||||
## [[ -s "\$cert" && -s "\$key" ]] || return 0
|
|
||||||
## ln -sf "\$cert" "\${target_dir}/fullchain.pem"
|
|
||||||
## ln -sf "\$key" "\${target_dir}/privkey.pem"
|
|
||||||
## chmod 644 "\${target_dir}/fullchain.pem" 2>/dev/null || true
|
|
||||||
## chmod 600 "\${target_dir}/privkey.pem" 2>/dev/null || true
|
|
||||||
## echo "[+] Linked \${target_dir} -> \${le_base}"
|
|
||||||
##}
|
|
||||||
##
|
|
||||||
### Verlinken (nur wenn Host konfiguriert)
|
|
||||||
##[[ -n "${UI_HOST}" ]] && link_if "\$UI_LE" "\$UI_SSL_DIR"
|
|
||||||
##[[ -n "${WEBMAIL_HOST}" ]] && link_if "\$WEBMAIL_LE" "\$WEBMAIL_SSL_DIR"
|
|
||||||
##[[ -n "${MAIL_HOSTNAME}" ]] && link_if "\$MX_LE" "\$MAIL_SSL_DIR"
|
|
||||||
##
|
|
||||||
### Nur reloaden, wenn Nginx aktiv ist (Installer startet ihn später erst)
|
|
||||||
##if systemctl is-active --quiet nginx; then
|
|
||||||
## systemctl reload nginx || true
|
|
||||||
##fi
|
|
||||||
##HOOK
|
|
||||||
##chmod +x /etc/letsencrypt/renewal-hooks/deploy/50-mailwolt-symlinks.sh
|
|
||||||
##
|
|
||||||
### ────────────────────────────────────────────────────────────────────────────
|
|
||||||
### 3) 60-mailwolt-tlsa.sh
|
|
||||||
### → nutzt Laravel, falls vorhanden; sonst Fallback mit OpenSSL.
|
|
||||||
### → schreibt nur, wenn sich der Hash geändert hat (idempotent)
|
|
||||||
### ────────────────────────────────────────────────────────────────────────────
|
|
||||||
##cat >/etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh <<'HOOK'
|
|
||||||
###!/usr/bin/env bash
|
|
||||||
##set -euo pipefail
|
|
||||||
##
|
|
||||||
### installer.env lesen
|
|
||||||
##set +u
|
|
||||||
##[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
##set -u
|
|
||||||
##
|
|
||||||
##APP_ENV_VAL="${APP_ENV:-production}"
|
|
||||||
##BASE_DOMAIN_VAL="${BASE_DOMAIN:-example.com}"
|
|
||||||
##
|
|
||||||
##case "$APP_ENV_VAL" in
|
|
||||||
## local|dev|development) exit 0 ;;
|
|
||||||
##esac
|
|
||||||
##[ "$BASE_DOMAIN_VAL" = "example.com" ] && exit 0
|
|
||||||
##
|
|
||||||
##MX_HOST="${MAIL_HOSTNAME:-}"
|
|
||||||
##SERVICE="_25._tcp"
|
|
||||||
##DNS_DIR="/etc/mailwolt/dns"
|
|
||||||
##OUT_FILE="${DNS_DIR}/${MX_HOST}.tlsa.txt"
|
|
||||||
##
|
|
||||||
### Nur reagieren, wenn MX-Zertifikat betroffen war
|
|
||||||
##case " ${RENEWED_DOMAINS:-} " in
|
|
||||||
## *" ${MX_HOST} "*) ;;
|
|
||||||
## *) exit 0 ;;
|
|
||||||
##esac
|
|
||||||
##
|
|
||||||
##CERT="${RENEWED_LINEAGE}/fullchain.pem"
|
|
||||||
##[ -s "$CERT" ] || exit 0
|
|
||||||
##
|
|
||||||
### Wenn Laravel vorhanden ist → interner Command (DB + Datei idempotent)
|
|
||||||
##if command -v php >/dev/null 2>&1 && [ -d /var/www/mailwolt ]; then
|
|
||||||
## cd /var/www/mailwolt || exit 0
|
|
||||||
## php artisan dns:tlsa:refresh || true
|
|
||||||
## exit 0
|
|
||||||
##fi
|
|
||||||
##
|
|
||||||
### Fallback: nur Datei aktualisieren, wenn Hash sich ändert
|
|
||||||
##HASH="$(openssl x509 -in "$CERT" -noout -pubkey \
|
|
||||||
## | openssl pkey -pubin -outform DER \
|
|
||||||
## | openssl dgst -sha256 | sed 's/^.*= //')"
|
|
||||||
##NEW_LINE="${SERVICE}.${MX_HOST}. IN TLSA 3 1 1 ${HASH}"
|
|
||||||
##
|
|
||||||
##mkdir -p "$DNS_DIR"
|
|
||||||
##
|
|
||||||
##if [ -r "$OUT_FILE" ] && grep -q "IN TLSA" "$OUT_FILE"; then
|
|
||||||
## if grep -q "$HASH" "$OUT_FILE"; then
|
|
||||||
## echo "[TLSA] Unverändert – kein Update nötig."
|
|
||||||
## exit 0
|
|
||||||
## fi
|
|
||||||
##fi
|
|
||||||
##
|
|
||||||
##echo "$NEW_LINE" > "$OUT_FILE"
|
|
||||||
##echo "[TLSA] Aktualisiert: $NEW_LINE"
|
|
||||||
##HOOK
|
|
||||||
##chmod +x /etc/letsencrypt/renewal-hooks/deploy/60-mailwolt-tlsa.sh
|
|
||||||
##
|
|
||||||
### ────────────────────────────────────────────────────────────────────────────
|
|
||||||
##echo "[✓] Deploy-Hooks installiert."
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Installiere DKIM-Helper …"
|
|
||||||
|
|
||||||
install -d -m 0755 /usr/local/sbin
|
|
||||||
|
|
||||||
cat >/usr/local/sbin/mailwolt-install-dkim <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DOMAIN="$1" # z.B. sysmail.toastra.com
|
|
||||||
SELECTOR="${2:-mwl1}"
|
|
||||||
|
|
||||||
[[ -n "$DOMAIN" ]] || { echo "Usage: $0 <domain> [selector]"; exit 2; }
|
|
||||||
|
|
||||||
KEYDIR="/etc/opendkim/keys/${DOMAIN}"
|
|
||||||
PRIV="${KEYDIR}/${SELECTOR}.private"
|
|
||||||
TXT="${KEYDIR}/${SELECTOR}.txt"
|
|
||||||
|
|
||||||
install -d -m 0750 -o opendkim -g opendkim "$KEYDIR"
|
|
||||||
|
|
||||||
if [[ ! -s "$PRIV" ]]; then
|
|
||||||
opendkim-genkey -b 2048 -s "$SELECTOR" -d "$DOMAIN" -D "$KEYDIR"
|
|
||||||
chown opendkim:opendkim "$PRIV"
|
|
||||||
chmod 600 "$PRIV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
grep -q "^${SELECTOR}\._domainkey\.${DOMAIN} " /etc/opendkim/KeyTable 2>/dev/null \
|
|
||||||
|| echo "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${PRIV}" >> /etc/opendkim/KeyTable
|
|
||||||
|
|
||||||
grep -q "^\*@${DOMAIN} " /etc/opendkim/SigningTable 2>/dev/null \
|
|
||||||
|| echo "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" >> /etc/opendkim/SigningTable
|
|
||||||
|
|
||||||
install -d -m 0755 /etc/mailwolt/dns
|
|
||||||
[[ -s "$TXT" ]] && cp -f "$TXT" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt" || true
|
|
||||||
|
|
||||||
systemctl restart opendkim
|
|
||||||
EOF
|
|
||||||
|
|
||||||
log "[✓] DKIM-Helper installiert: /usr/local/sbin/mailwolt-install-dkim"
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "MariaDB vorbereiten …"
|
|
||||||
systemctl enable --now mariadb
|
|
||||||
mysql -uroot <<SQL
|
|
||||||
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
|
|
||||||
CREATE USER IF NOT EXISTS '${DB_USER}'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
|
|
||||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
|
|
||||||
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'127.0.0.1';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
SQL
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
MAIL_SSL_DIR="/etc/ssl/mail"
|
|
||||||
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
|
|
||||||
MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
|
||||||
|
|
||||||
log "Postfix konfigurieren …"
|
|
||||||
|
|
||||||
# --- TLS-Dateirechte (falls du sie in /etc/mailwolt/ssl spiegelst) -----------
|
|
||||||
if [[ -e "${MAIL_KEY}" ]]; then
|
|
||||||
chgrp -R postfix /etc/mailwolt/ssl || true
|
|
||||||
chmod 750 /etc/mailwolt/ssl || true
|
|
||||||
chmod 640 /etc/mailwolt/ssl/key.pem /etc/mailwolt/ssl/cert.pem || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Basiskonfiguration -------------------------------------------------------
|
|
||||||
/usr/sbin/postconf -e "myhostname = ${MAIL_HOSTNAME}"
|
|
||||||
/usr/sbin/postconf -e "myorigin = \$myhostname"
|
|
||||||
/usr/sbin/postconf -e "mydestination = "
|
|
||||||
/usr/sbin/postconf -e "inet_interfaces = all"
|
|
||||||
/usr/sbin/postconf -e "inet_protocols = all"
|
|
||||||
/usr/sbin/postconf -e "smtpd_banner = \$myhostname ESMTP"
|
|
||||||
|
|
||||||
# --- TLS ----------------------------------------------------------------------
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_cert_file = ${MAIL_CERT}"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_key_file = ${MAIL_KEY}"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_security_level = may"
|
|
||||||
/usr/sbin/postconf -e "smtpd_use_tls = yes"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_received_header = yes"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_protocols = !SSLv2,!SSLv3"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_loglevel = 1"
|
|
||||||
/usr/sbin/postconf -e "smtp_tls_security_level = may"
|
|
||||||
/usr/sbin/postconf -e "smtp_tls_loglevel = 1"
|
|
||||||
|
|
||||||
DH_FILE="/etc/ssl/private/dhparam.pem"
|
|
||||||
if [[ ! -s "$DH_FILE" ]]; then
|
|
||||||
log "Generiere 2048-Bit DH-Parameter …"
|
|
||||||
openssl dhparam -out "$DH_FILE" 2048
|
|
||||||
chmod 600 "$DH_FILE"
|
|
||||||
chown root:root "$DH_FILE"
|
|
||||||
fi
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_dh1024_param_file = ${DH_FILE}"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_dh1024_param_file = ${DH_FILE}"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_eecdh_grade = strong"
|
|
||||||
/usr/sbin/postconf -e "tls_preempt_cipherlist = yes"
|
|
||||||
|
|
||||||
# Nur moderne TLS-Versionen (auch für ausgehendes SMTP)
|
|
||||||
# (überschreibt die älteren Zeilen oben)
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_mandatory_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
|
||||||
/usr/sbin/postconf -e "smtp_tls_protocols = !SSLv2,!SSLv3,!TLSv1,!TLSv1.1"
|
|
||||||
|
|
||||||
# Hohe Cipher, alte raus
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_ciphers = high"
|
|
||||||
/usr/sbin/postconf -e "smtp_tls_ciphers = high"
|
|
||||||
/usr/sbin/postconf -e "smtpd_tls_exclude_ciphers = aNULL,eNULL,MD5,RC4,DES,3DES"
|
|
||||||
/usr/sbin/postconf -e "smtp_tls_exclude_ciphers = aNULL,eNULL,MD5,RC4,DES,3DES"
|
|
||||||
|
|
||||||
# --- SMTP Sicherheit ----------------------------------------------------------
|
|
||||||
/usr/sbin/postconf -e "disable_vrfy_command = yes"
|
|
||||||
/usr/sbin/postconf -e "smtpd_helo_required = yes"
|
|
||||||
|
|
||||||
# --- Milter -------------------------------------------------------------------
|
|
||||||
/usr/sbin/postconf -e "milter_default_action = accept"
|
|
||||||
/usr/sbin/postconf -e "milter_protocol = 6"
|
|
||||||
/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
|
||||||
/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
|
||||||
|
|
||||||
# --- SASL Auth via Dovecot ----------------------------------------------------
|
|
||||||
/usr/sbin/postconf -e "smtpd_sasl_type = dovecot"
|
|
||||||
/usr/sbin/postconf -e "smtpd_sasl_path = private/auth"
|
|
||||||
/usr/sbin/postconf -e "smtpd_sasl_auth_enable = yes"
|
|
||||||
/usr/sbin/postconf -e "smtpd_sasl_security_options = noanonymous"
|
|
||||||
|
|
||||||
# --- Recipient & Relay Restriction --------------------------------------------
|
|
||||||
/usr/sbin/postconf -e "smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination"
|
|
||||||
/usr/sbin/postconf -e "smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination"
|
|
||||||
|
|
||||||
# --- Listener / Master.cf Definition ------------------------------------------
|
|
||||||
/usr/sbin/postconf -M "smtp/inet=smtp inet n - n - - smtpd -o smtpd_peername_lookup=no -o smtpd_timeout=30s"
|
|
||||||
/usr/sbin/postconf -M "submission/inet=submission inet n - n - - smtpd -o syslog_name=postfix/submission -o smtpd_peername_lookup=no -o smtpd_tls_security_level=encrypt -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
|
||||||
/usr/sbin/postconf -M "smtps/inet=smtps inet n - n - - smtpd -o syslog_name=postfix/smtps -o smtpd_peername_lookup=no -o smtpd_tls_wrappermode=yes -o smtpd_tls_auth_only=yes -o smtpd_sasl_auth_enable=yes -o smtpd_relay_restrictions=permit_sasl_authenticated,reject -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
|
|
||||||
|
|
||||||
# postscreen ggf. deaktivieren
|
|
||||||
sed -i 's/^[[:space:]]*smtp[[:space:]]\+inet[[:space:]]\+.*postscreen/# &/' /etc/postfix/master.cf || true
|
|
||||||
|
|
||||||
# --- SQL Maps (Verzeichnis zuerst!) -------------------------------------------
|
|
||||||
install -d -o root -g postfix -m 750 /etc/postfix/sql
|
|
||||||
|
|
||||||
# Domains
|
|
||||||
cat > /etc/postfix/sql/mysql-virtual-domains.cf <<CONF
|
|
||||||
hosts = 127.0.0.1
|
|
||||||
user = ${DB_USER}
|
|
||||||
password = ${DB_PASS}
|
|
||||||
dbname = ${DB_NAME}
|
|
||||||
query = SELECT 1 FROM domains WHERE domain = '%s' AND is_active = 1 LIMIT 1;
|
|
||||||
CONF
|
|
||||||
chown root:postfix /etc/postfix/sql/mysql-virtual-domains.cf
|
|
||||||
chmod 640 /etc/postfix/sql/mysql-virtual-domains.cf
|
|
||||||
|
|
||||||
# Mailboxen
|
|
||||||
cat > /etc/postfix/sql/mysql-virtual-mailbox-maps.cf <<CONF
|
|
||||||
hosts = 127.0.0.1
|
|
||||||
user = ${DB_USER}
|
|
||||||
password = ${DB_PASS}
|
|
||||||
dbname = ${DB_NAME}
|
|
||||||
query = SELECT 1 FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%s' AND u.is_active = 1 AND u.can_login = 1 AND u.password_hash IS NOT NULL AND d.is_active = 1 LIMIT 1;
|
|
||||||
CONF
|
|
||||||
chown root:postfix /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
|
||||||
chmod 640 /etc/postfix/sql/mysql-virtual-mailbox-maps.cf
|
|
||||||
|
|
||||||
# Aliase
|
|
||||||
cat > /etc/postfix/sql/mysql-virtual-alias-maps.cf <<CONF
|
|
||||||
hosts = 127.0.0.1
|
|
||||||
user = ${DB_USER}
|
|
||||||
password = ${DB_PASS}
|
|
||||||
dbname = ${DB_NAME}
|
|
||||||
query = SELECT COALESCE(mu.email, r.email) AS destination FROM mail_aliases a JOIN domains d ON d.id = a.domain_id JOIN mail_alias_recipients r ON r.alias_id = a.id LEFT JOIN mail_users mu ON mu.id = r.mail_user_id WHERE d.domain = SUBSTRING_INDEX('%s','@',-1) AND a.local = SUBSTRING_INDEX('%s','@', 1) AND a.is_active = 1 AND d.is_active = 1 AND (mu.email IS NOT NULL OR r.email IS NOT NULL) ORDER BY r.position ASC;
|
|
||||||
CONF
|
|
||||||
chown root:postfix /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
|
||||||
chmod 640 /etc/postfix/sql/mysql-virtual-alias-maps.cf
|
|
||||||
|
|
||||||
# Aktivieren
|
|
||||||
/usr/sbin/postconf -e "virtual_mailbox_domains = proxy:mysql:/etc/postfix/sql/mysql-virtual-domains.cf"
|
|
||||||
/usr/sbin/postconf -e "virtual_mailbox_maps = proxy:mysql:/etc/postfix/sql/mysql-virtual-mailbox-maps.cf"
|
|
||||||
/usr/sbin/postconf -e "virtual_alias_maps = proxy:mysql:/etc/postfix/sql/mysql-virtual-alias-maps.cf"
|
|
||||||
/usr/sbin/postconf -e "virtual_transport = lmtp:unix:private/dovecot-lmtp"
|
|
||||||
|
|
||||||
# --- Dienst aktivieren & neu laden --------------------------------------------
|
|
||||||
#systemctl enable postfix >/dev/null 2>&1 || true
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
MAIL_SSL_DIR="/etc/ssl/mail"
|
|
||||||
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
|
|
||||||
MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
|
||||||
|
|
||||||
log "Dovecot konfigurieren …"
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 1) vmail-Benutzer/Gruppe & Mailspool vorbereiten (DYNAMIC UID!)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Sicherstellen, dass die Gruppe 'mail' existiert (auf Debian/Ubuntu idR vorhanden)
|
|
||||||
getent group mail >/dev/null || groupadd -g 8 mail || true
|
|
||||||
|
|
||||||
# vmail anlegen, wenn er fehlt. Bevorzugt UID 109, falls frei – sonst automatisch.
|
|
||||||
if ! getent passwd vmail >/dev/null; then
|
|
||||||
if ! getent passwd 109 >/dev/null; then
|
|
||||||
useradd -u 109 -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
|
|
||||||
else
|
|
||||||
useradd -g mail -d /var/mail -M -s /usr/sbin/nologin vmail
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Tatsächliche vmail-UID ermitteln (wird unten in die Dovecot-Config geschrieben)
|
|
||||||
VMAIL_UID="$(id -u vmail)"
|
|
||||||
|
|
||||||
# Mailspool-Basis
|
|
||||||
install -d -m 0770 -o vmail -g mail /var/mail/vhosts
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 2) Dovecot Grundgerüst
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Hauptdatei
|
|
||||||
install -d -m 0755 /etc/dovecot/conf.d
|
|
||||||
cat > /etc/dovecot/dovecot.conf <<'CONF'
|
|
||||||
!include_try /etc/dovecot/conf.d/*.conf
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# Mail-Location & Namespace + UID-Grenzen
|
|
||||||
cat > /etc/dovecot/conf.d/10-mail.conf <<CONF
|
|
||||||
protocols = imap pop3 lmtp
|
|
||||||
mail_location = maildir:/var/mail/vhosts/%d/%n
|
|
||||||
|
|
||||||
namespace inbox {
|
|
||||||
inbox = yes
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_privileged_group = mail
|
|
||||||
mail_access_groups = mail
|
|
||||||
first_valid_uid = ${VMAIL_UID}
|
|
||||||
last_valid_uid = ${VMAIL_UID}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/15-mailboxes.conf <<'CONF'
|
|
||||||
namespace inbox {
|
|
||||||
inbox = yes
|
|
||||||
|
|
||||||
mailbox Drafts {
|
|
||||||
special_use = \Drafts
|
|
||||||
auto = subscribe
|
|
||||||
}
|
|
||||||
mailbox Junk {
|
|
||||||
special_use = \Junk
|
|
||||||
auto = subscribe
|
|
||||||
}
|
|
||||||
mailbox Trash {
|
|
||||||
special_use = \Trash
|
|
||||||
auto = subscribe
|
|
||||||
}
|
|
||||||
mailbox Sent {
|
|
||||||
special_use = \Sent
|
|
||||||
auto = subscribe
|
|
||||||
}
|
|
||||||
mailbox Archive {
|
|
||||||
special_use = \Archive
|
|
||||||
auto = create
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# Auth
|
|
||||||
cat > /etc/dovecot/conf.d/10-auth.conf <<'CONF'
|
|
||||||
disable_plaintext_auth = yes
|
|
||||||
auth_mechanisms = plain login
|
|
||||||
!include_try auth-sql.conf.ext
|
|
||||||
|
|
||||||
auth_cache_size = 10M
|
|
||||||
auth_cache_ttl = 1 hour
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# SQL-Anbindung (Passwörter aus App-DB)
|
|
||||||
cat > /etc/dovecot/dovecot-sql.conf.ext <<CONF
|
|
||||||
driver = mysql
|
|
||||||
connect = host=127.0.0.1 dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}
|
|
||||||
default_pass_scheme = BLF-CRYPT
|
|
||||||
password_query = SELECT u.email AS user, u.password_hash AS password FROM mail_users u JOIN domains d ON d.id = u.domain_id WHERE u.email = '%u' AND u.is_active = 1 AND u.can_login = 1 AND u.password_hash IS NOT NULL AND d.is_active = 1 LIMIT 1;
|
|
||||||
CONF
|
|
||||||
chown root:dovecot /etc/dovecot/dovecot-sql.conf.ext
|
|
||||||
chmod 640 /etc/dovecot/dovecot-sql.conf.ext
|
|
||||||
|
|
||||||
# Auth-SQL → userdb static auf vmail:mail (Home unter /var/mail/vhosts/%d/%n)
|
|
||||||
cat > /etc/dovecot/conf.d/auth-sql.conf.ext <<'CONF'
|
|
||||||
passdb {
|
|
||||||
driver = sql
|
|
||||||
args = /etc/dovecot/dovecot-sql.conf.ext
|
|
||||||
}
|
|
||||||
userdb {
|
|
||||||
driver = static
|
|
||||||
args = uid=vmail gid=mail home=/var/mail/vhosts/%d/%n
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
chown root:dovecot /etc/dovecot/conf.d/auth-sql.conf.ext
|
|
||||||
chmod 640 /etc/dovecot/conf.d/auth-sql.conf.ext
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 3) IMAP Optimierung (iOS/IDLE-freundlich)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/20-imap.conf <<'CONF'
|
|
||||||
# IMAP-spezifische Einstellungen
|
|
||||||
|
|
||||||
imap_idle_notify_interval = 2 mins
|
|
||||||
imap_hibernate_timeout = 0
|
|
||||||
|
|
||||||
protocol imap {
|
|
||||||
mail_max_userip_connections = 20
|
|
||||||
imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 4) Master Services (LMTP, AUTH, IMAP, POP3, STATS)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
cat > /etc/dovecot/conf.d/10-master.conf <<'CONF'
|
|
||||||
service lmtp {
|
|
||||||
unix_listener /var/spool/postfix/private/dovecot-lmtp {
|
|
||||||
mode = 0600
|
|
||||||
user = postfix
|
|
||||||
group = postfix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
service auth {
|
|
||||||
unix_listener /var/spool/postfix/private/auth {
|
|
||||||
mode = 0660
|
|
||||||
user = postfix
|
|
||||||
group = postfix
|
|
||||||
}
|
|
||||||
unix_listener auth-userdb {
|
|
||||||
mode = 0660
|
|
||||||
user = vmail
|
|
||||||
group = mail
|
|
||||||
}
|
|
||||||
process_limit = 1
|
|
||||||
}
|
|
||||||
service imap-login {
|
|
||||||
inet_listener imap {
|
|
||||||
port = 143
|
|
||||||
}
|
|
||||||
inet_listener imaps {
|
|
||||||
port = 993
|
|
||||||
ssl = yes
|
|
||||||
}
|
|
||||||
process_limit = 128
|
|
||||||
process_min_avail = 10
|
|
||||||
service_count = 0
|
|
||||||
vsz_limit = 512M
|
|
||||||
}
|
|
||||||
service pop3-login {
|
|
||||||
inet_listener pop3 {
|
|
||||||
port = 110
|
|
||||||
}
|
|
||||||
inet_listener pop3s {
|
|
||||||
port = 995
|
|
||||||
ssl = yes
|
|
||||||
}
|
|
||||||
process_limit = 50
|
|
||||||
service_count = 0
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# --- Dovecot: doveadm-server für App-Zugriff ---
|
|
||||||
cat >/etc/dovecot/conf.d/99-mailwolt-perms.conf <<'CONF'
|
|
||||||
service auth {
|
|
||||||
unix_listener auth-userdb {
|
|
||||||
mode = 0660
|
|
||||||
user = vmail
|
|
||||||
group = mail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
service stats {
|
|
||||||
unix_listener stats-reader {
|
|
||||||
mode = 0660
|
|
||||||
user = vmail
|
|
||||||
group = mail
|
|
||||||
}
|
|
||||||
unix_listener stats-writer {
|
|
||||||
mode = 0660
|
|
||||||
user = vmail
|
|
||||||
group = mail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 5) SSL-Konfiguration (ohne DH-Param-Erzeugung)
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
DOVECOT_SSL_CONF="/etc/dovecot/conf.d/10-ssl.conf"
|
|
||||||
touch "$DOVECOT_SSL_CONF"
|
|
||||||
grep -q '^ssl\s*=' "$DOVECOT_SSL_CONF" 2>/dev/null || echo "ssl = required" >> "$DOVECOT_SSL_CONF"
|
|
||||||
if grep -q '^\s*ssl_cert\s*=' "$DOVECOT_SSL_CONF"; then
|
|
||||||
sed -i "s|^\s*ssl_cert\s*=.*|ssl_cert = <${MAIL_CERT}|" "$DOVECOT_SSL_CONF"
|
|
||||||
else
|
|
||||||
echo "ssl_cert = <${MAIL_CERT}" >> "$DOVECOT_SSL_CONF"
|
|
||||||
fi
|
|
||||||
if grep -q '^\s*ssl_key\s*=' "$DOVECOT_SSL_CONF"; then
|
|
||||||
sed -i "s|^\s*ssl_key\s*=.*|ssl_key = <${MAIL_KEY}|" "$DOVECOT_SSL_CONF"
|
|
||||||
else
|
|
||||||
echo "ssl_key = <${MAIL_KEY}" >> "$DOVECOT_SSL_CONF"
|
|
||||||
fi
|
|
||||||
grep -q '^ssl_min_protocol' "$DOVECOT_SSL_CONF" || echo "ssl_min_protocol = TLSv1.2" >> "$DOVECOT_SSL_CONF"
|
|
||||||
grep -q '^ssl_prefer_server_ciphers' "$DOVECOT_SSL_CONF" || echo "ssl_prefer_server_ciphers = yes" >> "$DOVECOT_SSL_CONF"
|
|
||||||
|
|
||||||
grep -q '^ssl_dh' "$DOVECOT_SSL_CONF" || echo "ssl_dh = </etc/ssl/private/dhparam.pem" >> "$DOVECOT_SSL_CONF"
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 6) Verzeichnisse & Rechte prüfen
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
mkdir -p /var/spool/postfix/private
|
|
||||||
chown root:root /var/spool/postfix
|
|
||||||
chmod 0755 /var/spool/postfix
|
|
||||||
chown postfix:postfix /var/spool/postfix/private
|
|
||||||
chmod 0755 /var/spool/postfix/private
|
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# 7) Abschluss
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
log "Dovecot-Konfiguration abgeschlossen."
|
|
||||||
|
|
@ -1,319 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Rspamd + OpenDKIM einrichten …"
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# ENV laden
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
set +u
|
|
||||||
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
set -u
|
|
||||||
|
|
||||||
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
|
||||||
SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}" # z.B. sysmail.example.com
|
|
||||||
DKIM_ENABLE="${DKIM_ENABLE:-1}" # 1=OpenDKIM aktiv
|
|
||||||
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}" # z.B. mwl1
|
|
||||||
DKIM_GENERATE="${DKIM_GENERATE:-0}" # 1=Key generieren, falls fehlt
|
|
||||||
RSPAMD_CONTROLLER_PASSWORD="${RSPAMD_CONTROLLER_PASSWORD:-admin}"
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# Rspamd (Controller + Milter)
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
install -d -m 0750 /etc/rspamd/local.d
|
|
||||||
|
|
||||||
if command -v rspamadm >/dev/null 2>&1; then
|
|
||||||
RSPAMD_HASH="$(rspamadm pw -p "${RSPAMD_CONTROLLER_PASSWORD}")"
|
|
||||||
else
|
|
||||||
RSPAMD_HASH="${RSPAMD_CONTROLLER_PASSWORD}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat >/etc/rspamd/local.d/worker-controller.inc <<CONF
|
|
||||||
worker "controller" {
|
|
||||||
bind_socket = "127.0.0.1:11334";
|
|
||||||
password = "${RSPAMD_HASH}";
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat >/etc/rspamd/local.d/statistic.conf <<CONF
|
|
||||||
classifier "bayes" {
|
|
||||||
backend = "redis";
|
|
||||||
autolearn = true;
|
|
||||||
autolearn_threshold = 6.0;
|
|
||||||
ham_symbols = ["BAYES_HAM"];
|
|
||||||
spam_symbols = ["BAYES_SPAM"];
|
|
||||||
min_learns = 10;
|
|
||||||
store_tokens = true;
|
|
||||||
per_user = false;
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat >/etc/rspamd/local.d/worker-proxy.inc <<'CONF'
|
|
||||||
worker "proxy" {
|
|
||||||
bind_socket = "127.0.0.1:11332";
|
|
||||||
milter = yes;
|
|
||||||
timeout = 120s;
|
|
||||||
|
|
||||||
upstream "scan" {
|
|
||||||
default = yes;
|
|
||||||
self_scan = yes;
|
|
||||||
servers = "127.0.0.1:11333";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat >/etc/rspamd/local.d/worker-normal.inc <<'CONF'
|
|
||||||
worker "normal" {
|
|
||||||
bind_socket = "127.0.0.1:11333";
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat >/etc/rspamd/local.d/milter_headers.conf <<'CONF'
|
|
||||||
use = ["authentication-results"];
|
|
||||||
header = "Authentication-Results";
|
|
||||||
CONF
|
|
||||||
|
|
||||||
cat >/etc/rspamd/local.d/options.inc <<'CONF'
|
|
||||||
dns {
|
|
||||||
servers = ["9.9.9.9:53", "1.1.1.1:53"];
|
|
||||||
timeout = 5s;
|
|
||||||
retransmits = 2;
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# Rspamd Redis-Konfiguration
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
log "Rspamd Redis konfigurieren …"
|
|
||||||
|
|
||||||
: "${REDIS_PASS:=}"
|
|
||||||
|
|
||||||
cat >/etc/rspamd/local.d/redis.conf <<CONF
|
|
||||||
servers = "127.0.0.1:6379";
|
|
||||||
${REDIS_PASS:+password = "${REDIS_PASS}";}
|
|
||||||
db = 0;
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# Eigentümer und Rechte setzen
|
|
||||||
chown root:_rspamd /etc/rspamd/local.d /etc/rspamd/local.d/redis.conf
|
|
||||||
chmod 750 /etc/rspamd/local.d
|
|
||||||
chmod 640 /etc/rspamd/local.d/redis.conf
|
|
||||||
|
|
||||||
# Testweise prüfen, ob Redis erreichbar ist (nicht kritisch)
|
|
||||||
if command -v redis-cli >/dev/null 2>&1; then
|
|
||||||
if [[ -n "${REDIS_PASS}" ]]; then
|
|
||||||
if redis-cli -h 127.0.0.1 -p 6379 -a "${REDIS_PASS}" ping >/dev/null 2>&1; then
|
|
||||||
log "[✓] Redis erreichbar und Passwort akzeptiert."
|
|
||||||
else
|
|
||||||
log "[!] Warnung: Redis antwortet nicht oder Passwort falsch."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if redis-cli -h 127.0.0.1 -p 6379 ping >/dev/null 2>&1; then
|
|
||||||
log "[✓] Redis erreichbar (ohne Passwort)."
|
|
||||||
else
|
|
||||||
log "[!] Warnung: Redis antwortet nicht."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
systemctl enable --now rspamd || true
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# OpenDKIM – nur wenn DKIM_ENABLE=1
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
if [[ "${DKIM_ENABLE}" != "1" ]]; then
|
|
||||||
log "DKIM_ENABLE=0 → OpenDKIM wird übersprungen."
|
|
||||||
/usr/sbin/postconf -e "milter_default_action = accept"
|
|
||||||
/usr/sbin/postconf -e "milter_protocol = 6"
|
|
||||||
/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332"
|
|
||||||
/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
install -d -m 0755 /etc/opendkim
|
|
||||||
install -d -m 0750 /etc/opendkim/keys
|
|
||||||
chown -R opendkim:opendkim /etc/opendkim
|
|
||||||
chmod 750 /etc/opendkim/keys
|
|
||||||
|
|
||||||
# TrustedHosts
|
|
||||||
cat >/etc/opendkim/TrustedHosts <<'CONF'
|
|
||||||
127.0.0.1
|
|
||||||
::1
|
|
||||||
localhost
|
|
||||||
CONF
|
|
||||||
chown opendkim:opendkim /etc/opendkim/TrustedHosts
|
|
||||||
chmod 640 /etc/opendkim/TrustedHosts
|
|
||||||
|
|
||||||
# ── Key-Verzeichnis für SYSMAIL_DOMAIN vorbereiten ───────────────────────────
|
|
||||||
KEY_DIR="/etc/opendkim/keys/${SYSMAIL_DOMAIN}"
|
|
||||||
KEY_PRIV="${KEY_DIR}/${DKIM_SELECTOR}.private"
|
|
||||||
KEY_DNSTXT="${KEY_DIR}/${DKIM_SELECTOR}.txt"
|
|
||||||
install -d -m 0750 -o opendkim -g opendkim "${KEY_DIR}"
|
|
||||||
|
|
||||||
# ── Key optional generieren (nur wenn gewünscht) ─────────────────────────────
|
|
||||||
if [[ ! -s "${KEY_PRIV}" && "${DKIM_GENERATE}" = "1" ]]; then
|
|
||||||
if command -v opendkim-genkey >/dev/null 2>&1; then
|
|
||||||
opendkim-genkey -b 2048 -s "${DKIM_SELECTOR}" -d "${SYSMAIL_DOMAIN}" -D "${KEY_DIR}"
|
|
||||||
chown opendkim:opendkim "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
|
|
||||||
chmod 600 "${KEY_DIR}/${DKIM_SELECTOR}.private" || true
|
|
||||||
else
|
|
||||||
echo "[!] opendkim-genkey fehlt – kann DKIM-Key nicht generieren."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Key-/SigningTable nur anlegen, nicht leeren ───────────────────────────────
|
|
||||||
touch /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
|
||||||
chown opendkim:opendkim /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
|
||||||
chmod 640 /etc/opendkim/KeyTable /etc/opendkim/SigningTable
|
|
||||||
|
|
||||||
if [[ -s "${KEY_PRIV}" && "${BASE_DOMAIN}" != "example.com" ]]; then
|
|
||||||
LINE_KT="${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN} ${SYSMAIL_DOMAIN}:${DKIM_SELECTOR}:${KEY_PRIV}"
|
|
||||||
LINE_ST="*@${SYSMAIL_DOMAIN} ${DKIM_SELECTOR}._domainkey.${SYSMAIL_DOMAIN}"
|
|
||||||
grep -Fqx "$LINE_KT" /etc/opendkim/KeyTable || echo "$LINE_KT" >> /etc/opendkim/KeyTable
|
|
||||||
grep -Fqx "$LINE_ST" /etc/opendkim/SigningTable || echo "$LINE_ST" >> /etc/opendkim/SigningTable
|
|
||||||
else
|
|
||||||
echo "[i] Kein Private Key unter ${KEY_PRIV} – App-Helper trägt später ein."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Hauptkonfiguration ───────────────────────────────────────────────────────
|
|
||||||
cat >/etc/opendkim.conf <<'CONF'
|
|
||||||
Syslog yes
|
|
||||||
UMask 002
|
|
||||||
Mode sv
|
|
||||||
Socket inet:8891@127.0.0.1
|
|
||||||
PidFile /run/opendkim/opendkim.pid
|
|
||||||
Canonicalization relaxed/simple
|
|
||||||
|
|
||||||
On-BadSignature accept
|
|
||||||
On-Default accept
|
|
||||||
On-KeyNotFound accept
|
|
||||||
On-NoSignature accept
|
|
||||||
|
|
||||||
LogWhy yes
|
|
||||||
OversignHeaders From
|
|
||||||
|
|
||||||
KeyTable /etc/opendkim/KeyTable
|
|
||||||
SigningTable refile:/etc/opendkim/SigningTable
|
|
||||||
ExternalIgnoreList /etc/opendkim/TrustedHosts
|
|
||||||
InternalHosts /etc/opendkim/TrustedHosts
|
|
||||||
|
|
||||||
UserID opendkim:opendkim
|
|
||||||
AutoRestart yes
|
|
||||||
AutoRestartRate 10/1h
|
|
||||||
Background yes
|
|
||||||
DNSTimeout 5
|
|
||||||
SignatureAlgorithm rsa-sha256
|
|
||||||
SyslogSuccess yes
|
|
||||||
CONF
|
|
||||||
|
|
||||||
# ── systemd Drop-in: /run/opendkim sicherstellen ─────────────────────────────
|
|
||||||
install -d -m 0755 /etc/systemd/system/opendkim.service.d
|
|
||||||
cat >/etc/systemd/system/opendkim.service.d/override.conf <<'EOF'
|
|
||||||
[Service]
|
|
||||||
RuntimeDirectory=opendkim
|
|
||||||
RuntimeDirectoryMode=0755
|
|
||||||
EOF
|
|
||||||
|
|
||||||
install -d -o opendkim -g opendkim -m 0755 /run/opendkim
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# Root-Helper: DKIM installieren / entfernen + sudoers-Regel
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
install -d -m 0750 /usr/local/sbin
|
|
||||||
|
|
||||||
# --- mailwolt-install-dkim ------------------------------------
|
|
||||||
cat > /usr/local/sbin/mailwolt-install-dkim <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DOMAIN="$1"
|
|
||||||
SELECTOR="$2"
|
|
||||||
SRC_PRIV="$3"
|
|
||||||
SRC_TXT="${4:-}"
|
|
||||||
|
|
||||||
OKDIR="/etc/opendkim"
|
|
||||||
KEYDIR="${OKDIR}/keys/${DOMAIN}"
|
|
||||||
KEYPRI="${KEYDIR}/${SELECTOR}.private"
|
|
||||||
|
|
||||||
install -d -m 0750 -o opendkim -g opendkim "${KEYDIR}"
|
|
||||||
install -m 0600 -o opendkim -g opendkim "${SRC_PRIV}" "${KEYPRI}"
|
|
||||||
|
|
||||||
KT="${OKDIR}/KeyTable"
|
|
||||||
ST="${OKDIR}/SigningTable"
|
|
||||||
touch "$KT" "$ST"
|
|
||||||
chown opendkim:opendkim "$KT" "$ST"
|
|
||||||
chmod 0640 "$KT" "$ST"
|
|
||||||
|
|
||||||
LINE_KT="${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:${KEYPRI}"
|
|
||||||
LINE_ST="*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}"
|
|
||||||
|
|
||||||
grep -Fqx "$LINE_KT" "$KT" || echo "$LINE_KT" >> "$KT"
|
|
||||||
grep -Fqx "$LINE_ST" "$ST" || echo "$LINE_ST" >> "$ST"
|
|
||||||
|
|
||||||
if [[ -n "${SRC_TXT}" && -s "${SRC_TXT}" ]]; then
|
|
||||||
install -d -m 0755 /etc/mailwolt/dns
|
|
||||||
cp -f "${SRC_TXT}" "/etc/mailwolt/dns/dkim-${DOMAIN}.txt"
|
|
||||||
fi
|
|
||||||
|
|
||||||
systemctl is-active --quiet opendkim && systemctl reload opendkim || true
|
|
||||||
echo "OK"
|
|
||||||
EOSH
|
|
||||||
chmod 0750 /usr/local/sbin/mailwolt-install-dkim
|
|
||||||
chown root:root /usr/local/sbin/mailwolt-install-dkim
|
|
||||||
|
|
||||||
# --- 2) mailwolt-remove-dkim ----------------------------------
|
|
||||||
cat >/usr/local/sbin/mailwolt-remove-dkim <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
DOMAIN="$1" # z.B. kunden.tld oder sysmail.example.com
|
|
||||||
SELECTOR="$2" # z.B. mwl1
|
|
||||||
|
|
||||||
OKDIR="/etc/opendkim"
|
|
||||||
KEYDIR="${OKDIR}/keys/${DOMAIN}"
|
|
||||||
KEYPRI="${KEYDIR}/${SELECTOR}.private"
|
|
||||||
KT="${OKDIR}/KeyTable"
|
|
||||||
ST="${OKDIR}/SigningTable"
|
|
||||||
|
|
||||||
# Key-Datei löschen (falls vorhanden)
|
|
||||||
[[ -f "${KEYPRI}" ]] && rm -f "${KEYPRI}"
|
|
||||||
|
|
||||||
# Zeilen aus KeyTable und SigningTable entfernen
|
|
||||||
if [[ -f "$KT" ]]; then
|
|
||||||
tmp="$(mktemp)"; grep -v -F "${SELECTOR}._domainkey.${DOMAIN} ${DOMAIN}:${SELECTOR}:" "$KT" >"$tmp" && mv "$tmp" "$KT"
|
|
||||||
chown opendkim:opendkim "$KT"; chmod 0640 "$KT"
|
|
||||||
fi
|
|
||||||
if [[ -f "$ST" ]]; then
|
|
||||||
tmp="$(mktemp)"; grep -v -F "*@${DOMAIN} ${SELECTOR}._domainkey.${DOMAIN}" "$ST" >"$tmp" && mv "$tmp" "$ST"
|
|
||||||
chown opendkim:opendkim "$ST"; chmod 0640 "$ST"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verzeichnis ggf. aufräumen
|
|
||||||
rmdir "${KEYDIR}" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Dienst neu laden, falls aktiv
|
|
||||||
if systemctl is-active --quiet opendkim; then
|
|
||||||
systemctl reload opendkim || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "OK"
|
|
||||||
EOSH
|
|
||||||
chown root:root /usr/local/sbin/mailwolt-remove-dkim
|
|
||||||
chmod 0750 /usr/local/sbin/mailwolt-remove-dkim
|
|
||||||
|
|
||||||
# ── Dienst + Postfix-Milter aktivieren ─────────────────────────
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable opendkim || true
|
|
||||||
|
|
||||||
touch /run/mailwolt.need-apply-milters || true
|
|
||||||
|
|
||||||
chgrp _rspamd /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
|
|
||||||
chmod 0640 /etc/rspamd/local.d/*.inc /etc/rspamd/local.d/*.conf || true
|
|
||||||
|
|
||||||
#/usr/sbin/postconf -e "smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
|
||||||
#/usr/sbin/postconf -e "non_smtpd_milters = inet:127.0.0.1:11332, inet:127.0.0.1:8891"
|
|
||||||
|
|
||||||
log "[✓] Rspamd + OpenDKIM eingerichtet (läuft; signiert, sobald Keys vorhanden sind)."
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "OpenDMARC installieren/konfigurieren …"
|
|
||||||
|
|
||||||
# Flags laden
|
|
||||||
set +u
|
|
||||||
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
set -u
|
|
||||||
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
|
||||||
|
|
||||||
# Paket sicherstellen
|
|
||||||
if ! dpkg -s opendmarc >/dev/null 2>&1; then
|
|
||||||
apt-get update -qq
|
|
||||||
apt-get install -y opendmarc
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Config-Verzeichnisse
|
|
||||||
install -d -m 0755 /etc/opendmarc
|
|
||||||
install -d -m 0755 /run/opendmarc
|
|
||||||
|
|
||||||
# IgnoreHosts
|
|
||||||
cat >/etc/opendmarc/ignore.hosts <<'EOF'
|
|
||||||
127.0.0.1
|
|
||||||
::1
|
|
||||||
localhost
|
|
||||||
EOF
|
|
||||||
chmod 0644 /etc/opendmarc/ignore.hosts
|
|
||||||
|
|
||||||
# Hauptkonfiguration
|
|
||||||
cat >/etc/opendmarc.conf <<'EOF'
|
|
||||||
AuthservID mailwolt
|
|
||||||
TrustedAuthservIDs mailwolt
|
|
||||||
IgnoreHosts /etc/opendmarc/ignore.hosts
|
|
||||||
Syslog true
|
|
||||||
SoftwareHeader true
|
|
||||||
Socket local:/run/opendmarc/opendmarc.sock
|
|
||||||
RejectFailures false
|
|
||||||
EOF
|
|
||||||
chmod 0644 /etc/opendmarc.conf
|
|
||||||
|
|
||||||
# systemd Drop-in für RuntimeDirectory (robust nach Reboot)
|
|
||||||
install -d -m 0755 /etc/systemd/system/opendmarc.service.d
|
|
||||||
cat >/etc/systemd/system/opendmarc.service.d/override.conf <<'EOF'
|
|
||||||
[Service]
|
|
||||||
RuntimeDirectory=opendmarc
|
|
||||||
RuntimeDirectoryMode=0755
|
|
||||||
EOF
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
# Dienst nach Flag
|
|
||||||
if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
|
||||||
systemctl enable --now opendmarc
|
|
||||||
else
|
|
||||||
systemctl disable --now opendmarc || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Postfix-Milter-Kette konsistent setzen (Rspamd + OpenDKIM + optional OpenDMARC)
|
|
||||||
touch /run/mailwolt.need-apply-milters || true
|
|
||||||
|
|
||||||
log "[✓] OpenDMARC (ENABLE=${OPENDMARC_ENABLE}) bereit."
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "ClamAV (clamav-daemon) installieren/konfigurieren …"
|
|
||||||
|
|
||||||
# Flags laden
|
|
||||||
set +u
|
|
||||||
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
set -u
|
|
||||||
CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
|
||||||
|
|
||||||
# Pakete
|
|
||||||
if ! dpkg -s clamav-daemon >/dev/null 2>&1; then
|
|
||||||
apt-get update -qq
|
|
||||||
apt-get install -y clamav clamav-daemon
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Signaturen aktualisieren (erst Freshclam starten)
|
|
||||||
systemctl stop clamav-freshclam 2>/dev/null || true
|
|
||||||
freshclam || true
|
|
||||||
systemctl start clamav-freshclam || true
|
|
||||||
|
|
||||||
# clamd LocalSocket setzen
|
|
||||||
sed -i 's|^#\?LocalSocket .*|LocalSocket /run/clamav/clamd.ctl|' /etc/clamav/clamd.conf || true
|
|
||||||
install -d -m 0755 /run/clamav
|
|
||||||
chown clamav:clamav /run/clamav
|
|
||||||
|
|
||||||
# Dienst nach Flag
|
|
||||||
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
|
||||||
systemctl enable --now clamav-daemon
|
|
||||||
else
|
|
||||||
systemctl disable --now clamav-daemon || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rspamd-Integration (nur wenn aktiv)
|
|
||||||
AV_CONF="/etc/rspamd/local.d/antivirus.conf"
|
|
||||||
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
|
||||||
cat >"$AV_CONF" <<'EOF'
|
|
||||||
clamav {
|
|
||||||
symbol = "CLAM_VIRUS";
|
|
||||||
type = "clamav";
|
|
||||||
servers = "/run/clamav/clamd.ctl";
|
|
||||||
scan_mime_parts = true;
|
|
||||||
scan_text_mime = true;
|
|
||||||
max_size = 50mb;
|
|
||||||
log_clean = false;
|
|
||||||
action = "reject";
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
chown root:_rspamd "$AV_CONF" || true
|
|
||||||
chmod 0640 "$AV_CONF" || true
|
|
||||||
systemctl reload rspamd || systemctl restart rspamd
|
|
||||||
else
|
|
||||||
rm -f "$AV_CONF" || true
|
|
||||||
systemctl reload rspamd || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "[✓] ClamAV (ENABLE=${CLAMAV_ENABLE}) konfiguriert."
|
|
||||||
|
|
@ -1,230 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Fail2Ban installieren/konfigurieren …"
|
|
||||||
|
|
||||||
# Flags laden
|
|
||||||
set +u
|
|
||||||
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
set -u
|
|
||||||
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
|
|
||||||
# Paket
|
|
||||||
if ! dpkg -s fail2ban >/dev/null 2>&1; then
|
|
||||||
apt-get update -qq
|
|
||||||
apt-get install -y fail2ban sqlite3
|
|
||||||
fi
|
|
||||||
|
|
||||||
install -d -m 0755 /etc/fail2ban/jail.d
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Basis-Jails (praxisnah)
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
|
|
||||||
[sshd]
|
|
||||||
enabled = true
|
|
||||||
port = ssh
|
|
||||||
logpath = /var/log/auth.log
|
|
||||||
|
|
||||||
[postfix]
|
|
||||||
enabled = true
|
|
||||||
logpath = /var/log/mail.log
|
|
||||||
port = smtp,ssmtp,submission,465
|
|
||||||
|
|
||||||
[dovecot]
|
|
||||||
enabled = true
|
|
||||||
logpath = /var/log/mail.log
|
|
||||||
port = pop3,pop3s,imap,imaps,submission,465,587,993
|
|
||||||
|
|
||||||
[rspamd-controller]
|
|
||||||
enabled = true
|
|
||||||
port = 11334
|
|
||||||
filter = rspamd
|
|
||||||
logpath = /var/log/rspamd/rspamd.log
|
|
||||||
maxretry = 5
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# einfacher Filter für Rspamd-Controller
|
|
||||||
if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
|
|
||||||
cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
|
|
||||||
[Definition]
|
|
||||||
failregex = .*Authentication failed for user.* from <HOST>
|
|
||||||
ignoreregex =
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Fail2Ban-Backend auf SQLite umstellen
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
log "SQLite-Backend aktivieren …"
|
|
||||||
|
|
||||||
cat >/etc/fail2ban/fail2ban.local <<'EOF'
|
|
||||||
[Definition]
|
|
||||||
loglevel = INFO
|
|
||||||
logtarget = /var/log/fail2ban.log
|
|
||||||
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
|
|
||||||
dbpurgeage = 86400
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Datenbankverzeichnis sicherstellen
|
|
||||||
install -d -o fail2ban -g fail2ban -m 0750 /var/lib/fail2ban
|
|
||||||
|
|
||||||
# Falls DB nicht existiert, Dummy anlegen (wird vom Dienst erweitert)
|
|
||||||
if [ ! -f /var/lib/fail2ban/fail2ban.sqlite3 ]; then
|
|
||||||
sqlite3 /var/lib/fail2ban/fail2ban.sqlite3 "VACUUM;"
|
|
||||||
fi
|
|
||||||
chown fail2ban:fail2ban /var/lib/fail2ban/fail2ban.sqlite3
|
|
||||||
chmod 0640 /var/lib/fail2ban/fail2ban.sqlite3
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# sudoers für Web-UI
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Fail2Ban Blacklist-Jail
|
|
||||||
cat >/etc/fail2ban/jail.d/mailwolt-blacklist.local <<'EOF'
|
|
||||||
[mailwolt-blacklist]
|
|
||||||
enabled = true
|
|
||||||
filter = none
|
|
||||||
port = anyport
|
|
||||||
bantime = -1
|
|
||||||
findtime = 1
|
|
||||||
maxretry = 1
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >/etc/fail2ban/filter.d/none.conf <<'EOF'
|
|
||||||
[Definition]
|
|
||||||
failregex =
|
|
||||||
ignoreregex =
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 0640 /etc/fail2ban/filter.d/none.conf
|
|
||||||
|
|
||||||
|
|
||||||
SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
|
|
||||||
cat > "${SUDOERS_F2B}" <<'EOF'
|
|
||||||
Defaults:www-data !requiretty
|
|
||||||
www-data ALL=(root) NOPASSWD: \
|
|
||||||
/usr/bin/fail2ban-client, \
|
|
||||||
/usr/bin/fail2ban-client ping, \
|
|
||||||
/usr/bin/fail2ban-client status, \
|
|
||||||
/usr/bin/fail2ban-client status *, \
|
|
||||||
/usr/bin/fail2ban-client get *, \
|
|
||||||
/usr/bin/fail2ban-client set * banip *, \
|
|
||||||
/usr/bin/fail2ban-client set * unbanip *, \
|
|
||||||
/usr/bin/fail2ban-client reload, \
|
|
||||||
/usr/bin/journalctl, \
|
|
||||||
/bin/journalctl, \
|
|
||||||
/usr/bin/zgrep, \
|
|
||||||
/bin/zgrep, \
|
|
||||||
/usr/bin/grep, \
|
|
||||||
/bin/grep, \
|
|
||||||
/usr/bin/tail, \
|
|
||||||
/bin/tail, \
|
|
||||||
/usr/bin/sqlite3, \
|
|
||||||
/usr/bin/tee /etc/fail2ban/jail.d/*
|
|
||||||
EOF
|
|
||||||
chown root:root "${SUDOERS_F2B}"
|
|
||||||
chmod 440 "${SUDOERS_F2B}"
|
|
||||||
|
|
||||||
if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
|
|
||||||
rm -f "${SUDOERS_F2B}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Dienst aktivieren/deaktivieren
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
|
||||||
systemctl enable --now fail2ban
|
|
||||||
else
|
|
||||||
systemctl disable --now fail2ban || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
|
|
||||||
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#source ./lib.sh
|
|
||||||
#
|
|
||||||
#log "Fail2Ban installieren/konfigurieren …"
|
|
||||||
#
|
|
||||||
## Flags laden
|
|
||||||
#set +u
|
|
||||||
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
#set -u
|
|
||||||
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
#
|
|
||||||
## Paket
|
|
||||||
#if ! dpkg -s fail2ban >/dev/null 2>&1; then
|
|
||||||
# apt-get update -qq
|
|
||||||
# apt-get install -y fail2ban
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#install -d -m 0755 /etc/fail2ban/jail.d
|
|
||||||
#
|
|
||||||
## Basis-Jails (praxisnah)
|
|
||||||
#cat >/etc/fail2ban/jail.d/mailwolt.conf <<'EOF'
|
|
||||||
#[DEFAULT]
|
|
||||||
#bantime = 1h
|
|
||||||
#findtime = 10m
|
|
||||||
#maxretry = 5
|
|
||||||
#backend = auto
|
|
||||||
#
|
|
||||||
#[sshd]
|
|
||||||
#enabled = true
|
|
||||||
#port = ssh
|
|
||||||
#logpath = /var/log/auth.log
|
|
||||||
#
|
|
||||||
#[postfix]
|
|
||||||
#enabled = true
|
|
||||||
#logpath = /var/log/mail.log
|
|
||||||
#port = smtp,ssmtp,submission,465
|
|
||||||
#
|
|
||||||
#[dovecot]
|
|
||||||
#enabled = true
|
|
||||||
#logpath = /var/log/mail.log
|
|
||||||
#port = pop3,pop3s,imap,imaps,submission,465,587,993
|
|
||||||
#
|
|
||||||
#[rspamd-controller]
|
|
||||||
#enabled = true
|
|
||||||
#port = 11334
|
|
||||||
#filter = rspamd
|
|
||||||
#logpath = /var/log/rspamd/rspamd.log
|
|
||||||
#maxretry = 5
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## einfacher Filter für Rspamd-Controller
|
|
||||||
#if [ ! -f /etc/fail2ban/filter.d/rspamd.conf ]; then
|
|
||||||
# cat >/etc/fail2ban/filter.d/rspamd.conf <<'EOF'
|
|
||||||
#[Definition]
|
|
||||||
#failregex = .*Authentication failed for user.* from <HOST>
|
|
||||||
#ignoreregex =
|
|
||||||
#EOF
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#SUDOERS_F2B="/etc/sudoers.d/mailwolt-fail2ban"
|
|
||||||
#cat > "${SUDOERS_F2B}" <<'EOF'
|
|
||||||
#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
|
|
||||||
#EOF
|
|
||||||
#chown root:root "${SUDOERS_F2B}"
|
|
||||||
#chmod 440 "${SUDOERS_F2B}"
|
|
||||||
#
|
|
||||||
#if ! visudo -c -f "${SUDOERS_F2B}" >/dev/null 2>&1; then
|
|
||||||
# echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_F2B} – entferne Datei."
|
|
||||||
# rm -f "${SUDOERS_F2B}"
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#sudo tee /etc/sudoers.d/mailwolt-fail2ban >/dev/null <<'EOF'
|
|
||||||
#www-data ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status, /usr/bin/fail2ban-client status *
|
|
||||||
#EOF
|
|
||||||
#sudo visudo -cf /etc/sudoers.d/mailwolt-fail2ban
|
|
||||||
#
|
|
||||||
## Dienst nach Flag
|
|
||||||
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
|
||||||
# systemctl enable --now fail2ban
|
|
||||||
#else
|
|
||||||
# systemctl disable --now fail2ban || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#log "[✓] Fail2Ban (ENABLE=${FAIL2BAN_ENABLE}) bereit."
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
# nur ausführen, wenn vorherige Schritte das Flag gesetzt haben
|
|
||||||
if [[ -f /run/mailwolt.need-apply-milters ]]; then
|
|
||||||
if command -v /usr/local/sbin/mailwolt-apply-milters >/dev/null 2>&1; then
|
|
||||||
log "Setze Postfix-Milter-Kette (Rspamd/OpenDKIM[/OpenDMARC]) …"
|
|
||||||
/usr/local/sbin/mailwolt-apply-milters || true
|
|
||||||
else
|
|
||||||
# Fallback (ident wie im Tool)
|
|
||||||
/usr/sbin/postconf -e "milter_default_action = accept"
|
|
||||||
/usr/sbin/postconf -e "milter_protocol = 6"
|
|
||||||
CHAIN="inet:127.0.0.1:11333, inet:127.0.0.1:8891"
|
|
||||||
systemctl is-active --quiet opendmarc && CHAIN="$CHAIN, inet:127.0.0.1:8893" || true
|
|
||||||
/usr/sbin/postconf -e "smtpd_milters = $CHAIN"
|
|
||||||
/usr/sbin/postconf -e "non_smtpd_milters = $CHAIN"
|
|
||||||
systemctl reload postfix || true
|
|
||||||
fi
|
|
||||||
rm -f /run/mailwolt.need-apply-milters || true
|
|
||||||
log "[✓] Milter-Kette angewandt."
|
|
||||||
else
|
|
||||||
log "Milter-Kette: kein Bedarf (Flag nicht gesetzt) – überspringe."
|
|
||||||
fi
|
|
||||||
|
|
@ -1,521 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Nginx konfigurieren …"
|
|
||||||
|
|
||||||
# ── Flags/Umgebung (vom Bootstrap gesetzt; hier Fallbacks) ────────────────
|
|
||||||
DEV_MODE="${DEV_MODE:-0}" # 1 = DEV (Vite-Proxy aktiv), 0 = PROD
|
|
||||||
PROXY_MODE="${PROXY_MODE:-0}" # 1 = NPM/Proxy davor, Backend spricht nur HTTP:80
|
|
||||||
NPM_IP="${NPM_IP:-}" # z.B. 10.10.20.20
|
|
||||||
|
|
||||||
# Erwartet vom Bootstrap/Installer exportiert:
|
|
||||||
: "${UI_HOST:?UI_HOST fehlt}"
|
|
||||||
: "${WEBMAIL_HOST:?WEBMAIL_HOST fehlt}"
|
|
||||||
: "${APP_DIR:?APP_DIR fehlt}"
|
|
||||||
|
|
||||||
ACME_ROOT="/var/www/letsencrypt"
|
|
||||||
install -d -m 0755 "$ACME_ROOT"
|
|
||||||
|
|
||||||
# Default-Sites entfernen (verhindert doppelten default_server)
|
|
||||||
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default || true
|
|
||||||
|
|
||||||
# HTTP/2-Unterstützung erkennen
|
|
||||||
NGINX_HTTP2_SUFFIX=""
|
|
||||||
if nginx -V 2>&1 | grep -q http_v2; then
|
|
||||||
NGINX_HTTP2_SUFFIX=" http2"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PHP-FPM Socket/TCP finden → fastcgi_pass bauen
|
|
||||||
detect_php_fpm_sock(){
|
|
||||||
for v in 8.3 8.2 8.1 8.0 7.4; do
|
|
||||||
s="/run/php/php${v}-fpm.sock"
|
|
||||||
[[ -S "$s" ]] && { echo "unix:${s}"; return; }
|
|
||||||
done
|
|
||||||
[[ -S "/run/php/php-fpm.sock" ]] && { echo "unix:/run/php/php-fpm.sock"; return; }
|
|
||||||
echo "127.0.0.1:9000"
|
|
||||||
}
|
|
||||||
PHP_FPM_TARGET="$(detect_php_fpm_sock)"
|
|
||||||
if [[ "$PHP_FPM_TARGET" == unix:* ]]; then
|
|
||||||
FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
|
|
||||||
else
|
|
||||||
FASTCGI_PASS="fastcgi_pass ${PHP_FPM_TARGET};"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Builder 1: HTTP-only (Proxy-Mode: TLS endet im NPM) ───────────────────
|
|
||||||
## $1=host, $2=outfile
|
|
||||||
#build_site_http_only(){
|
|
||||||
# local host="$1" outfile="$2"
|
|
||||||
#
|
|
||||||
# local def=""
|
|
||||||
# [[ "${DEV_MODE}" = "1" ]] && def=" default_server"
|
|
||||||
# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
|
||||||
#
|
|
||||||
# cat > "$outfile" <<CONF
|
|
||||||
## --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy) ---
|
|
||||||
#server {
|
|
||||||
# listen 80;
|
|
||||||
# listen [::]:80;
|
|
||||||
# server_name ${host};
|
|
||||||
#
|
|
||||||
# # ACME HTTP-01 (optional; meist übernimmt das der Proxy)
|
|
||||||
# location ^~ /.well-known/acme-challenge/ {
|
|
||||||
# root ${ACME_ROOT};
|
|
||||||
# allow all;
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# root ${APP_DIR}/public;
|
|
||||||
# index index.php index.html;
|
|
||||||
#
|
|
||||||
# access_log /var/log/nginx/${host}_access.log;
|
|
||||||
# error_log /var/log/nginx/${host}_error.log;
|
|
||||||
#
|
|
||||||
# client_max_body_size 25m;
|
|
||||||
#
|
|
||||||
# location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
#
|
|
||||||
# location ~ \.php\$ {
|
|
||||||
# include snippets/fastcgi-php.conf;
|
|
||||||
# ${FASTCGI_PASS}
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
# location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
#
|
|
||||||
# # WebSocket: Laravel Reverb (Backend intern HTTP)
|
|
||||||
# location /ws/ {
|
|
||||||
# proxy_http_version 1.1;
|
|
||||||
# proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
# proxy_set_header Connection "Upgrade";
|
|
||||||
# proxy_set_header Host \$host;
|
|
||||||
# proxy_read_timeout 60s;
|
|
||||||
# proxy_send_timeout 60s;
|
|
||||||
# proxy_pass http://127.0.0.1:8080/;
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# # Reverb HTTP API
|
|
||||||
# location /apps/ {
|
|
||||||
# proxy_http_version 1.1;
|
|
||||||
# proxy_set_header Host \$host;
|
|
||||||
# proxy_read_timeout 60s;
|
|
||||||
# proxy_send_timeout 60s;
|
|
||||||
# proxy_pass http://127.0.0.1:8080/apps/;
|
|
||||||
# }
|
|
||||||
#CONF
|
|
||||||
#
|
|
||||||
# if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
# cat >> "$outfile" <<'CONF'
|
|
||||||
# # DEV: Vite-Proxy (HMR)
|
|
||||||
# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
|
||||||
# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
|
||||||
# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
|
||||||
#CONF
|
|
||||||
# fi
|
|
||||||
#
|
|
||||||
# echo "}" >> "$outfile"
|
|
||||||
#}
|
|
||||||
|
|
||||||
#build_site_http_only(){
|
|
||||||
# local host="$1" outfile="$2"
|
|
||||||
#
|
|
||||||
# # DEV: IP-Zugriff ohne Hostname → default_server + server_name _
|
|
||||||
# local def=""
|
|
||||||
# if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
# def=" default_server"
|
|
||||||
# host="_"
|
|
||||||
# fi
|
|
||||||
# [[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
|
||||||
#
|
|
||||||
# cat > "$outfile" <<CONF
|
|
||||||
## --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy/DEV) ---
|
|
||||||
#server {
|
|
||||||
# listen 80${def};
|
|
||||||
# listen [::]:80${def};
|
|
||||||
# server_name ${host};
|
|
||||||
#
|
|
||||||
# # ACME HTTP-01 (optional; meist übernimmt das der Proxy)
|
|
||||||
# location ^~ /.well-known/acme-challenge/ {
|
|
||||||
# root ${ACME_ROOT};
|
|
||||||
# allow all;
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# root ${APP_DIR}/public;
|
|
||||||
# index index.php index.html;
|
|
||||||
#
|
|
||||||
# access_log /var/log/nginx/${host/_/__}_access.log;
|
|
||||||
# error_log /var/log/nginx/${host/_/__}_error.log;
|
|
||||||
#
|
|
||||||
# client_max_body_size 25m;
|
|
||||||
#
|
|
||||||
# location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
#
|
|
||||||
# location ~ \.php\$ {
|
|
||||||
# include snippets/fastcgi-php.conf;
|
|
||||||
# ${FASTCGI_PASS}
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
# location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
#
|
|
||||||
# # WebSocket: Laravel Reverb
|
|
||||||
# location /ws/ {
|
|
||||||
# proxy_http_version 1.1;
|
|
||||||
# proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
# proxy_set_header Connection "Upgrade";
|
|
||||||
# proxy_set_header Host \$host;
|
|
||||||
# proxy_read_timeout 60s;
|
|
||||||
# proxy_send_timeout 60s;
|
|
||||||
# proxy_pass http://127.0.0.1:8080/;
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# # Reverb HTTP API
|
|
||||||
# location /apps/ {
|
|
||||||
# proxy_http_version 1.1;
|
|
||||||
# proxy_set_header Host \$host;
|
|
||||||
# proxy_read_timeout 60s;
|
|
||||||
# proxy_send_timeout 60s;
|
|
||||||
# proxy_pass http://127.0.0.1:8080/apps/;
|
|
||||||
# }
|
|
||||||
#CONF
|
|
||||||
#
|
|
||||||
# if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
# cat >> "$outfile" <<'CONF'
|
|
||||||
# # DEV: Vite-Proxy (HMR)
|
|
||||||
# location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
|
||||||
# location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
|
||||||
# location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
|
||||||
#CONF
|
|
||||||
# fi
|
|
||||||
#
|
|
||||||
# echo "}" >> "$outfile"
|
|
||||||
#}
|
|
||||||
|
|
||||||
# $1=host, $2=outfile, $3=default_flag (default|nodefault)
|
|
||||||
build_site_http_only(){
|
|
||||||
local host="$1" outfile="$2" def_flag="${3:-default}"
|
|
||||||
|
|
||||||
local def=""
|
|
||||||
if [[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]]; then
|
|
||||||
def=" default_server"
|
|
||||||
fi
|
|
||||||
[[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
|
||||||
|
|
||||||
cat > "$outfile" <<CONF
|
|
||||||
# --- ${host} : HTTP (kein Redirect, kein TLS; läuft hinter Reverse-Proxy/DEV) ---
|
|
||||||
server {
|
|
||||||
listen 80${def};
|
|
||||||
listen [::]:80${def};
|
|
||||||
server_name ${host};
|
|
||||||
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root ${ACME_ROOT};
|
|
||||||
allow all;
|
|
||||||
}
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/${host/_/__}_access.log;
|
|
||||||
error_log /var/log/nginx/${host/_/__}_error.log;
|
|
||||||
|
|
||||||
client_max_body_size 25m;
|
|
||||||
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
|
|
||||||
location ~ \.php\$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
${FASTCGI_PASS}
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
|
|
||||||
# WebSocket: Laravel Reverb
|
|
||||||
location /ws/ {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection "Upgrade";
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_pass http://127.0.0.1:8080/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reverb HTTP API
|
|
||||||
location /apps/ {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_pass http://127.0.0.1:8080/apps/;
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
cat >> "$outfile" <<'CONF'
|
|
||||||
# DEV: Vite-Proxy (HMR)
|
|
||||||
location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; }
|
|
||||||
location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; }
|
|
||||||
location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; }
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "}" >> "$outfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Builder 2: 80→443 Redirect + 443/TLS (Live-Server) ────────────────────
|
|
||||||
# $1=host, $2=cert_dir (/etc/ssl/ui | /etc/ssl/webmail), $3=outfile
|
|
||||||
build_site_tls(){
|
|
||||||
local host="$1" cert_dir="$2" outfile="$3"
|
|
||||||
local cert="${cert_dir}/fullchain.pem"
|
|
||||||
local key="${cert_dir}/privkey.pem"
|
|
||||||
|
|
||||||
cat > "$outfile" <<CONF
|
|
||||||
# --- ${host} : HTTP (ACME + Redirect) ---
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name ${host};
|
|
||||||
|
|
||||||
# ACME HTTP-01 auf Port 80
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root ${ACME_ROOT};
|
|
||||||
allow all;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 301 https://\$host\$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- ${host} : HTTPS ---
|
|
||||||
server {
|
|
||||||
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
|
||||||
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
|
||||||
server_name ${host};
|
|
||||||
|
|
||||||
ssl_certificate ${cert};
|
|
||||||
ssl_certificate_key ${key};
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
|
|
||||||
# WICHTIG: ACME auch auf 443, sonst 404 bei Redirects
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root ${ACME_ROOT};
|
|
||||||
allow all;
|
|
||||||
}
|
|
||||||
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php index.html;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/${host}_ssl_access.log;
|
|
||||||
error_log /var/log/nginx/${host}_ssl_error.log;
|
|
||||||
|
|
||||||
client_max_body_size 25m;
|
|
||||||
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
|
|
||||||
location ~ \.php\$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
${FASTCGI_PASS}
|
|
||||||
}
|
|
||||||
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|css|js|ico|svg)\$ { expires 30d; access_log off; }
|
|
||||||
|
|
||||||
# WebSocket: Laravel Reverb (Backend intern HTTP)
|
|
||||||
location /ws/ {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade \$http_upgrade;
|
|
||||||
proxy_set_header Connection "Upgrade";
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_pass http://127.0.0.1:8080/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reverb HTTP API
|
|
||||||
location /apps/ {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host \$host;
|
|
||||||
proxy_read_timeout 60s;
|
|
||||||
proxy_send_timeout 60s;
|
|
||||||
proxy_pass http://127.0.0.1:8080/apps/;
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
|
|
||||||
if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
cat >> "$outfile" <<'CONF'
|
|
||||||
# DEV: Vite-Proxy
|
|
||||||
location ^~ /@vite/ { proxy_pass http://127.0.0.1:5173/@vite/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
|
||||||
location ^~ /node_modules/ { proxy_pass http://127.0.0.1:5173/node_modules/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
|
||||||
location ^~ /resources/ { proxy_pass http://127.0.0.1:5173/resources/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; }
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "}" >> "$outfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
build_site_acme_only(){
|
|
||||||
local host="$1" outfile="$2"
|
|
||||||
|
|
||||||
cat > "$outfile" <<CONF
|
|
||||||
# --- ${host} : ACME-only (80 + 443), KEIN App-Root ---
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name ${host};
|
|
||||||
|
|
||||||
# HTTP-01 Challenge exakt ausliefern
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root ${ACME_ROOT};
|
|
||||||
default_type "text/plain";
|
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Alles andere → nach https
|
|
||||||
location / { return 301 https://\$host\$request_uri; }
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl${NGINX_HTTP2_SUFFIX};
|
|
||||||
listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
|
||||||
server_name ${host};
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/mail/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/ssl/mail/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
|
|
||||||
# Auch via https die Challenge bedienen (falls Redirects gefolgt werden)
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root ${ACME_ROOT};
|
|
||||||
default_type "text/plain";
|
|
||||||
try_files \$uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sonst nichts preisgeben
|
|
||||||
location / { return 444; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Builder Webmail: nur /webmail/* erlaubt, root → redirect ───────────────
|
|
||||||
build_webmail_http_only(){
|
|
||||||
local host="$1" outfile="$2" def_flag="${3:-nodefault}"
|
|
||||||
local def=""
|
|
||||||
[[ "${DEV_MODE}" = "1" && "${def_flag}" = "default" ]] && def=" default_server"
|
|
||||||
[[ -z "${host}" || "${host}" = "_" ]] && host="_"
|
|
||||||
cat > "$outfile" <<CONF
|
|
||||||
# --- ${host} : Webmail (domain-routing, volle Laravel-App) ---
|
|
||||||
server {
|
|
||||||
listen 80${def};
|
|
||||||
listen [::]:80${def};
|
|
||||||
server_name ${host};
|
|
||||||
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php;
|
|
||||||
access_log /var/log/nginx/${host/_/__}_webmail_access.log;
|
|
||||||
error_log /var/log/nginx/${host/_/__}_webmail_error.log;
|
|
||||||
client_max_body_size 25m;
|
|
||||||
location = / { return 301 http://\$host/login; }
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
location ~ \.php\$ { include snippets/fastcgi-php.conf; ${FASTCGI_PASS} }
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(css|js|ico|svg|woff2?|ttf|jpg|jpeg|png|gif)\$ { expires 30d; access_log off; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
}
|
|
||||||
|
|
||||||
build_webmail_tls(){
|
|
||||||
local host="$1" cert_dir="$2" outfile="$3"
|
|
||||||
cat > "$outfile" <<CONF
|
|
||||||
# --- ${host} : Webmail TLS (domain-routing, volle Laravel-App) ---
|
|
||||||
server {
|
|
||||||
listen 80; listen [::]:80; server_name ${host};
|
|
||||||
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
|
||||||
return 301 https://\$host\$request_uri;
|
|
||||||
}
|
|
||||||
server {
|
|
||||||
listen 443 ssl${NGINX_HTTP2_SUFFIX}; listen [::]:443 ssl${NGINX_HTTP2_SUFFIX};
|
|
||||||
server_name ${host};
|
|
||||||
ssl_certificate ${cert_dir}/fullchain.pem;
|
|
||||||
ssl_certificate_key ${cert_dir}/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
location ^~ /.well-known/acme-challenge/ { root ${ACME_ROOT}; allow all; }
|
|
||||||
root ${APP_DIR}/public;
|
|
||||||
index index.php;
|
|
||||||
access_log /var/log/nginx/${host}_webmail_ssl_access.log;
|
|
||||||
error_log /var/log/nginx/${host}_webmail_ssl_error.log;
|
|
||||||
client_max_body_size 25m;
|
|
||||||
location = / { return 301 https://\$host/login; }
|
|
||||||
location / { try_files \$uri \$uri/ /index.php?\$query_string; }
|
|
||||||
location ~ \.php\$ { include snippets/fastcgi-php.conf; ${FASTCGI_PASS} }
|
|
||||||
location ^~ /livewire/ { try_files \$uri /index.php?\$query_string; }
|
|
||||||
location ~* \.(css|js|ico|svg|woff2?|ttf|jpg|jpeg|png|gif)\$ { expires 30d; access_log off; }
|
|
||||||
}
|
|
||||||
CONF
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Sites erzeugen ─────────────────────────────────────────────────────────
|
|
||||||
MX_SITE="/etc/nginx/sites-available/mx-mailwolt.conf"
|
|
||||||
UI_SITE="/etc/nginx/sites-available/ui-mailwolt.conf"
|
|
||||||
WEBMAIL_SITE="/etc/nginx/sites-available/webmail-mailwolt.conf"
|
|
||||||
|
|
||||||
# UI & Webmail wie gehabt …
|
|
||||||
#if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
|
||||||
# build_site_http_only "$UI_HOST" "$UI_SITE"
|
|
||||||
# build_site_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
|
||||||
#else
|
|
||||||
# build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
|
||||||
# build_site_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
|
||||||
#fi
|
|
||||||
|
|
||||||
# UI & Webmail …
|
|
||||||
if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
build_site_http_only "_" "$UI_SITE" "default"
|
|
||||||
build_webmail_http_only "_" "$WEBMAIL_SITE" "nodefault"
|
|
||||||
else
|
|
||||||
if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
|
||||||
build_site_http_only "$UI_HOST" "$UI_SITE"
|
|
||||||
build_webmail_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
|
||||||
else
|
|
||||||
build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
|
||||||
build_webmail_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
#if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
# # DEV: per IP erreichbar → Catch-All („_“) und HTTP-only
|
|
||||||
# build_site_http_only "_" "$UI_SITE"
|
|
||||||
# build_site_http_only "_" "$WEBMAIL_SITE"
|
|
||||||
#else
|
|
||||||
# if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
|
||||||
# build_site_http_only "$UI_HOST" "$UI_SITE"
|
|
||||||
# build_site_http_only "$WEBMAIL_HOST" "$WEBMAIL_SITE"
|
|
||||||
# else
|
|
||||||
# build_site_tls "$UI_HOST" "/etc/ssl/ui" "$UI_SITE"
|
|
||||||
# build_site_tls "$WEBMAIL_HOST" "/etc/ssl/webmail" "$WEBMAIL_SITE"
|
|
||||||
# fi
|
|
||||||
#fi
|
|
||||||
|
|
||||||
# MX: **immer** ACME-only (kein Laravel dahinter)
|
|
||||||
build_site_acme_only "${MAIL_HOSTNAME}" "$MX_SITE"
|
|
||||||
|
|
||||||
ln -sf "$UI_SITE" /etc/nginx/sites-enabled/ui-mailwolt.conf
|
|
||||||
ln -sf "$WEBMAIL_SITE" /etc/nginx/sites-enabled/webmail-mailwolt.conf
|
|
||||||
ln -sf "$MX_SITE" /etc/nginx/sites-enabled/mx-mailwolt.conf
|
|
||||||
|
|
||||||
# ── Real-IP nur, wenn Proxy davor ──────────────────────────────────────────
|
|
||||||
if [[ "${PROXY_MODE}" -eq 1 && -n "${NPM_IP}" ]]; then
|
|
||||||
cat > /etc/nginx/conf.d/realip.conf <<NGX
|
|
||||||
real_ip_header X-Forwarded-For;
|
|
||||||
set_real_ip_from ${NPM_IP};
|
|
||||||
real_ip_recursive on;
|
|
||||||
NGX
|
|
||||||
else
|
|
||||||
rm -f /etc/nginx/conf.d/realip.conf || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Test & reload ──────────────────────────────────────────────────────────
|
|
||||||
if nginx -t; then
|
|
||||||
systemctl enable --now nginx >/dev/null 2>&1 || true
|
|
||||||
systemctl reload nginx || true
|
|
||||||
else
|
|
||||||
die "nginx -t fehlgeschlagen – siehe /var/log/nginx/*.log"
|
|
||||||
fi
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
ACME_WEBROOT="/var/www/letsencrypt"
|
|
||||||
install -d -m 0755 "${ACME_WEBROOT}/.well-known/acme-challenge"
|
|
||||||
|
|
||||||
# Staging optional (verbraucht kein Live-Limit)
|
|
||||||
CERTBOT_EXTRA=()
|
|
||||||
LE_STAGING="${LE_STAGING:-0}"
|
|
||||||
[[ "$LE_STAGING" = "1" ]] && CERTBOT_EXTRA+=(--test-cert)
|
|
||||||
|
|
||||||
# Einheitliche LE-Mail (Fallback)
|
|
||||||
LE_MAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
|
||||||
|
|
||||||
resolve_ok() {
|
|
||||||
local host="$1"
|
|
||||||
local pats=()
|
|
||||||
[[ -n "${SERVER_PUBLIC_IPV4:-}" ]] && pats+=("${SERVER_PUBLIC_IPV4//./\\.}")
|
|
||||||
[[ -n "${SERVER_PUBLIC_IPV6:-}" ]] && pats+=("${SERVER_PUBLIC_IPV6//:/\\:}")
|
|
||||||
[[ ${#pats[@]} -eq 0 ]] && return 0
|
|
||||||
getent ahosts "$host" | awk '{print $1}' | sort -u \
|
|
||||||
| grep -Eq "^($(IFS='|'; echo "${pats[*]}"))$"
|
|
||||||
}
|
|
||||||
|
|
||||||
probe_http() {
|
|
||||||
local host="$1"
|
|
||||||
echo test > "${ACME_WEBROOT}/.well-known/acme-challenge/_probe"
|
|
||||||
curl -fsS --max-time 5 -4 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null \
|
|
||||||
|| curl -fsS --max-time 5 -6 "http://${host}/.well-known/acme-challenge/_probe" >/dev/null
|
|
||||||
}
|
|
||||||
|
|
||||||
issue() {
|
|
||||||
local host="${1:-}"
|
|
||||||
[[ -z "$host" ]] && return 0
|
|
||||||
|
|
||||||
echo "[i] Versuche LE für ${host} …"
|
|
||||||
|
|
||||||
if ! resolve_ok "$host"; then
|
|
||||||
echo "[!] DNS zeigt (noch) nicht hierher – überspringe: ${host}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! probe_http "$host"; then
|
|
||||||
echo "[!] ACME-HTTP-Check für ${host} fehlgeschlagen (Port 80/IPv6/Firewall/Nginx prüfen)."
|
|
||||||
# wir versuchen trotzdem – Certbot meldet sich, falls es scheitert
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXTRA_ARGS=()
|
|
||||||
# Für MX den Key wiederverwenden → stabiler TLSA (3 1 1)
|
|
||||||
[[ "$host" == "${MAIL_HOSTNAME}" ]] && EXTRA_ARGS+=(--reuse-key)
|
|
||||||
|
|
||||||
# WICHTIG: Deploy-Wrapper anhängen, damit Symlinks/Nginx gesetzt werden
|
|
||||||
certbot certonly \
|
|
||||||
--agree-tos -m "${LE_MAIL}" --non-interactive \
|
|
||||||
--webroot -w "${ACME_WEBROOT}" -d "${host}" \
|
|
||||||
--deploy-hook /usr/local/sbin/mailwolt-deploy.sh \
|
|
||||||
"${EXTRA_ARGS[@]}" "${CERTBOT_EXTRA[@]}" || true
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "${BASE_DOMAIN}" != "example.com" ]]; then
|
|
||||||
issue "${UI_HOST:-}"
|
|
||||||
issue "${WEBMAIL_HOST:-}"
|
|
||||||
issue "${MAIL_HOSTNAME:-}"
|
|
||||||
|
|
||||||
# Nginx nur neu laden, wenn aktiv
|
|
||||||
if systemctl is-active --quiet nginx; then
|
|
||||||
systemctl reload nginx || true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[i] BASE_DOMAIN=example.com – LE wird übersprungen."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
# FIX: Validierung & Reparatur des Mail-Zertifikats
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
MAIL_SSL_DIR="/etc/ssl/mail"
|
|
||||||
install -d -m 0755 "$MAIL_SSL_DIR"
|
|
||||||
|
|
||||||
MAIL_CERT="${MAIL_SSL_DIR}/fullchain.pem"
|
|
||||||
MAIL_KEY="${MAIL_SSL_DIR}/privkey.pem"
|
|
||||||
|
|
||||||
HOST="${MAIL_HOSTNAME:-}"
|
|
||||||
LE_DIR=""
|
|
||||||
[[ -n "$HOST" ]] && LE_DIR="/etc/letsencrypt/live/${HOST}"
|
|
||||||
|
|
||||||
need_fix=0
|
|
||||||
|
|
||||||
# Ist der vorhandene Key gültig? (leer/nicht vorhanden/ungültig -> fix)
|
|
||||||
if [[ ! -s "$MAIL_KEY" ]] || ! openssl pkey -in "$MAIL_KEY" -noout >/dev/null 2>&1; then
|
|
||||||
need_fix=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Wenn Fix nötig: aus Let's Encrypt Live kopieren
|
|
||||||
if [[ $need_fix -eq 1 ]]; then
|
|
||||||
echo "[!] Ungültiger oder fehlender Mail-Private-Key – versuche Reparatur …"
|
|
||||||
if [[ -n "$LE_DIR" && -r "${LE_DIR}/privkey.pem" && -r "${LE_DIR}/fullchain.pem" ]]; then
|
|
||||||
cp -f "${LE_DIR}/privkey.pem" "$MAIL_KEY"
|
|
||||||
cp -f "${LE_DIR}/fullchain.pem" "$MAIL_CERT"
|
|
||||||
chown root:root "$MAIL_CERT" "$MAIL_KEY"
|
|
||||||
chmod 600 "$MAIL_KEY"
|
|
||||||
chmod 644 "$MAIL_CERT"
|
|
||||||
echo "[+] Zertifikate neu kopiert aus ${LE_DIR}."
|
|
||||||
# Reload NICHT sofort – flaggen für 90-services
|
|
||||||
touch /run/mailwolt.need-dovecot-reload
|
|
||||||
else
|
|
||||||
echo "[!] Konnte ${LE_DIR}/privkey.pem oder fullchain.pem nicht lesen – bitte prüfen."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "[✓] Mail-Zertifikat & -Key sind gültig."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optionaler Live-Check (nur wenn Host gesetzt)
|
|
||||||
if [[ -n "$HOST" ]]; then
|
|
||||||
if openssl s_client -connect "${HOST}:993" -servername "${HOST}" </dev/null 2>/dev/null \
|
|
||||||
| grep -q "Verify return code: 0"; then
|
|
||||||
echo "[✓] TLS-Handshake erfolgreich auf imaps://${HOST}:993."
|
|
||||||
else
|
|
||||||
echo "[!] TLS-Handshake auf imaps://${HOST}:993 fehlgeschlagen (Dovecot Reload folgt in 90-services, falls Flag gesetzt)."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
@ -1,343 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
# --- Helper: sicherer Frontend-Build als APP_USER ---------------------------
|
|
||||||
safe_frontend_build() {
|
|
||||||
echo "[i] Frontend build …"
|
|
||||||
|
|
||||||
# Verzeichnisse & Rechte vorbereiten (Gruppen-sticky & ACL)
|
|
||||||
install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" \
|
|
||||||
"${APP_DIR}/public/build" "${APP_DIR}/node_modules" "${APP_DIR}/.npm-cache"
|
|
||||||
|
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
|
|
||||||
find "${APP_DIR}" -type d -exec chmod 2775 {} \;
|
|
||||||
find "${APP_DIR}" -type f -exec chmod 664 {} \;
|
|
||||||
setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "${APP_DIR}" || true
|
|
||||||
|
|
||||||
# Vite-/Build-Reste bereinigen (falls mal root dort gebaut hat)
|
|
||||||
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
|
|
||||||
|
|
||||||
# npm auf projektlokales Cache konfigurieren
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cat > ~/.npmrc <<'RC'
|
|
||||||
fund=false
|
|
||||||
audit=false
|
|
||||||
prefer-offline=true
|
|
||||||
cache=${APP_DIR}/.npm-cache
|
|
||||||
RC"
|
|
||||||
|
|
||||||
# Node ggf. installieren
|
|
||||||
if ! command -v node >/dev/null 2>&1; then
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
|
||||||
apt-get install -y nodejs
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Dependencies + Build (als App-User)
|
|
||||||
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[!] Build fehlgeschlagen – Rechtefix + Clean + Retry …"
|
|
||||||
rm -rf "${APP_DIR}/node_modules/.vite" "${APP_DIR}/public/build/"* 2>/dev/null || true
|
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "${APP_DIR}"
|
|
||||||
find "${APP_DIR}" -type d -exec chmod 2775 {} \;
|
|
||||||
find "${APP_DIR}" -type f -exec chmod 664 {} \;
|
|
||||||
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && npm run build"
|
|
||||||
}
|
|
||||||
|
|
||||||
relink_and_reload() {
|
|
||||||
if [[ -d /etc/letsencrypt/renewal-hooks/deploy ]]; then
|
|
||||||
run-parts /etc/letsencrypt/renewal-hooks/deploy || true
|
|
||||||
fi
|
|
||||||
if systemctl is-active --quiet nginx; then
|
|
||||||
systemctl reload nginx || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
log "App bereitstellen …"
|
|
||||||
mkdir -p "$(dirname "$APP_DIR")"
|
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "$(dirname "$APP_DIR")"
|
|
||||||
|
|
||||||
# Repo holen oder Laravel anlegen – passe GIT_REPO/GIT_BRANCH bei Bedarf an
|
|
||||||
GIT_REPO="${GIT_REPO:-https://git.nexlab.at/boban/mailwolt.git}"
|
|
||||||
GIT_BRANCH="${GIT_BRANCH:-main}"
|
|
||||||
|
|
||||||
if [[ "${GIT_REPO}" == "https://example.com/your-repo-placeholder.git" ]]; then
|
|
||||||
[[ -d "$APP_DIR" && -n "$(ls -A "$APP_DIR" 2>/dev/null || true)" ]] || \
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd /var/www && composer create-project laravel/laravel ${APP_USER} --no-interaction"
|
|
||||||
else
|
|
||||||
if [[ ! -d "${APP_DIR}/.git" ]]; then
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "git clone --depth=1 -b ${GIT_BRANCH} ${GIT_REPO} ${APP_DIR}"
|
|
||||||
else
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && git fetch --depth=1 origin ${GIT_BRANCH} && git reset --hard origin/${GIT_BRANCH}"
|
|
||||||
fi
|
|
||||||
[[ -f "${APP_DIR}/composer.json" ]] && sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && composer install --no-interaction --prefer-dist"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ENV_FILE="${APP_DIR}/.env"
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && cp -n .env.example .env || true"
|
|
||||||
grep -q '^APP_KEY=' "$ENV_FILE" || echo "APP_KEY=" >> "$ENV_FILE"
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan key:generate --force || true"
|
|
||||||
|
|
||||||
# --- App-URL/Hosts ----------------------------------------------------------
|
|
||||||
#SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
|
|
||||||
#if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
|
|
||||||
# SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
|
||||||
# [[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
|
|
||||||
#fi
|
|
||||||
#[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
|
|
||||||
#
|
|
||||||
#UI_CERT="/etc/ssl/ui/fullchain.pem"
|
|
||||||
#UI_KEY="/etc/ssl/ui/privkey.pem"
|
|
||||||
#
|
|
||||||
#if [[ -n "${UI_HOST:-}" ]]; then
|
|
||||||
# APP_HOST_VAL="$UI_HOST"
|
|
||||||
# APP_URL_VAL="https://${UI_HOST}"
|
|
||||||
#else
|
|
||||||
# APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
|
||||||
# SCHEME="http"
|
|
||||||
# [[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
|
||||||
# APP_URL_VAL="${SCHEME}://${SERVER_PUBLIC_IPV4}"
|
|
||||||
#fi
|
|
||||||
|
|
||||||
SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-}"
|
|
||||||
if [[ -z "$SERVER_PUBLIC_IPV4" ]] && command -v curl >/dev/null 2>&1; then
|
|
||||||
SERVER_PUBLIC_IPV4="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
|
||||||
[[ "$SERVER_PUBLIC_IPV4" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || SERVER_PUBLIC_IPV4=""
|
|
||||||
fi
|
|
||||||
[[ -n "$SERVER_PUBLIC_IPV4" ]] || SERVER_PUBLIC_IPV4="$(detect_ip)"
|
|
||||||
|
|
||||||
UI_CERT="/etc/ssl/ui/fullchain.pem"
|
|
||||||
UI_KEY="/etc/ssl/ui/privkey.pem"
|
|
||||||
|
|
||||||
# DEV-Modus: immer IP als Host, http (kein example.com / keine Fake-Domain)
|
|
||||||
if [[ "${DEV_MODE:-0}" = "1" || "${APP_ENV:-production}" = "local" ]]; then
|
|
||||||
APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
|
||||||
APP_URL_VAL="http://${APP_HOST_VAL}"
|
|
||||||
else
|
|
||||||
# PROD/normal: wenn UI_HOST gesetzt → benutzen, sonst IP
|
|
||||||
if [[ -n "${UI_HOST:-}" ]]; then
|
|
||||||
APP_HOST_VAL="$UI_HOST"
|
|
||||||
APP_URL_VAL="https://${UI_HOST}"
|
|
||||||
else
|
|
||||||
APP_HOST_VAL="$SERVER_PUBLIC_IPV4"
|
|
||||||
SCHEME="http"
|
|
||||||
[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
|
||||||
APP_URL_VAL="${SCHEME}://${APP_HOST_VAL}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
SECURE=$([[ "${APP_ENV}" = "production" ]] && echo true || echo false)
|
|
||||||
|
|
||||||
# --- .env schreiben ---------------------------------------------------------
|
|
||||||
upsert_env APP_URL "${APP_URL_VAL}"
|
|
||||||
|
|
||||||
if [[ "${PROXY_MODE:-0}" -eq 1 ]]; then
|
|
||||||
TP_LIST="127.0.0.1,::1"
|
|
||||||
[[ -n "${NPM_IP:-}" ]] && TP_LIST="${TP_LIST},${NPM_IP}"
|
|
||||||
upsert_env TRUSTED_PROXIES "$TP_LIST"
|
|
||||||
upsert_env TRUSTED_HEADERS "x-forwarded-all"
|
|
||||||
else
|
|
||||||
upsert_env TRUSTED_PROXIES ""
|
|
||||||
upsert_env TRUSTED_HEADERS "x-forwarded-all"
|
|
||||||
fi
|
|
||||||
|
|
||||||
upsert_env APP_HOST "${APP_HOST_VAL}"
|
|
||||||
upsert_env APP_NAME "${APP_NAME}"
|
|
||||||
upsert_env APP_ENV "${APP_ENV:-production}"
|
|
||||||
upsert_env APP_DEBUG "${APP_DEBUG:-false}"
|
|
||||||
upsert_env APP_TIMEZONE "${APP_TZ:-UTC}"
|
|
||||||
|
|
||||||
upsert_env APP_LOCALE "${APP_LOCALE:-de}"
|
|
||||||
upsert_env APP_FALLBACK_LOCALE "en"
|
|
||||||
|
|
||||||
upsert_env SERVER_PUBLIC_IPV4 "${SERVER_PUBLIC_IPV4}"
|
|
||||||
upsert_env SERVER_PUBLIC_IPV6 "${SERVER_PUBLIC_IPV6:-}"
|
|
||||||
|
|
||||||
upsert_env SYSMAIL_SUB "${SYSMAIL_SUB}"
|
|
||||||
upsert_env SYSMAIL_DOMAIN "${SYSMAIL_DOMAIN}"
|
|
||||||
upsert_env DKIM_ENABLE "${DKIM_ENABLE}"
|
|
||||||
upsert_env DKIM_SELECTOR "${DKIM_SELECTOR}"
|
|
||||||
upsert_env DKIM_GENERATE "${DKIM_GENERATE}"
|
|
||||||
|
|
||||||
upsert_env BASE_DOMAIN "${BASE_DOMAIN}"
|
|
||||||
upsert_env UI_SUB "${UI_SUB}"
|
|
||||||
upsert_env WEBMAIL_SUB "${WEBMAIL_SUB}"
|
|
||||||
upsert_env MTA_SUB "${MTA_SUB}"
|
|
||||||
upsert_env LE_EMAIL "${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
|
||||||
|
|
||||||
upsert_env DB_CONNECTION "mysql"
|
|
||||||
upsert_env DB_HOST "127.0.0.1"
|
|
||||||
upsert_env DB_PORT "3306"
|
|
||||||
upsert_env DB_DATABASE "${DB_NAME}"
|
|
||||||
upsert_env DB_USERNAME "${DB_USER}"
|
|
||||||
upsert_env DB_PASSWORD "${DB_PASS}"
|
|
||||||
|
|
||||||
upsert_env CACHE_SETTINGS_STORE "redis"
|
|
||||||
upsert_env CACHE_STORE "redis"
|
|
||||||
upsert_env CACHE_DRIVER "redis"
|
|
||||||
upsert_env CACHE_PREFIX "${APP_USER_PREFIX}_cache:"
|
|
||||||
upsert_env SESSION_DRIVER "redis"
|
|
||||||
upsert_env SESSION_SECURE_COOKIE "${SECURE}" # DEV=false, PROD=true
|
|
||||||
upsert_env SESSION_SAMESITE "lax"
|
|
||||||
upsert_env REDIS_CLIENT "phpredis"
|
|
||||||
upsert_env REDIS_HOST "127.0.0.1"
|
|
||||||
upsert_env REDIS_PORT "6379"
|
|
||||||
upsert_env REDIS_PASSWORD "${REDIS_PASS}"
|
|
||||||
upsert_env REDIS_DB "0"
|
|
||||||
upsert_env REDIS_CACHE_DB "1"
|
|
||||||
upsert_env REDIS_CACHE_CONNECTION "cache"
|
|
||||||
upsert_env REDIS_CACHE_LOCK_CONNECTION "default"
|
|
||||||
|
|
||||||
upsert_env BROADCAST_DRIVER "reverb"
|
|
||||||
upsert_env QUEUE_CONNECTION "redis"
|
|
||||||
upsert_env LOG_CHANNEL "daily"
|
|
||||||
|
|
||||||
upsert_env REVERB_APP_ID "${APP_USER_PREFIX}"
|
|
||||||
grep -q '^REVERB_APP_KEY=' "$ENV_FILE" || upsert_env REVERB_APP_KEY "${APP_USER_PREFIX}_$(openssl rand -hex 16)"
|
|
||||||
grep -q '^REVERB_APP_SECRET=' "$ENV_FILE" || upsert_env REVERB_APP_SECRET "${APP_USER_PREFIX}_$(openssl rand -hex 32)"
|
|
||||||
upsert_env REVERB_HOST "\${APP_HOST}"
|
|
||||||
upsert_env REVERB_PORT "443"
|
|
||||||
upsert_env REVERB_SCHEME "https"
|
|
||||||
upsert_env REVERB_PATH "/ws"
|
|
||||||
upsert_env REVERB_SCALING_ENABLED "true"
|
|
||||||
upsert_env REVERB_SCALING_CHANNEL "reverb"
|
|
||||||
|
|
||||||
upsert_env VITE_REVERB_APP_KEY "\${REVERB_APP_KEY}"
|
|
||||||
upsert_env VITE_REVERB_HOST "\${REVERB_HOST}"
|
|
||||||
upsert_env VITE_REVERB_PORT "\${REVERB_PORT}"
|
|
||||||
upsert_env VITE_REVERB_SCHEME "\${REVERB_SCHEME}"
|
|
||||||
upsert_env VITE_REVERB_PATH "\${REVERB_PATH}"
|
|
||||||
|
|
||||||
upsert_env REVERB_SERVER_APP_KEY "\${REVERB_APP_KEY}"
|
|
||||||
upsert_env REVERB_SERVER_HOST "127.0.0.1"
|
|
||||||
upsert_env REVERB_SERVER_PORT "8080"
|
|
||||||
upsert_env REVERB_SERVER_PATH ""
|
|
||||||
upsert_env REVERB_SERVER_SCHEME "http"
|
|
||||||
|
|
||||||
# --- DEV Block (optional) ---------------------------------------------------
|
|
||||||
DEV_MODE="${DEV_MODE:-0}"
|
|
||||||
if [[ "$DEV_MODE" = "1" ]]; then
|
|
||||||
sed -i '/^# --- MailWolt DEV/,/^# --- \/MailWolt DEV/d' "${ENV_FILE}"
|
|
||||||
cat >> "${ENV_FILE}" <<CONF
|
|
||||||
# --- MailWolt DEV ---
|
|
||||||
VITE_DEV_HOST=127.0.0.1
|
|
||||||
VITE_DEV_PORT=5173
|
|
||||||
VITE_HMR_PROTOCOL=wss
|
|
||||||
VITE_HMR_CLIENT_PORT=443
|
|
||||||
VITE_HMR_HOST=${SERVER_PUBLIC_IPV4}
|
|
||||||
VITE_DEV_ORIGIN=$(grep '^APP_URL=' "${ENV_FILE}" | cut -d= -f2-)
|
|
||||||
# --- /MailWolt DEV ---
|
|
||||||
CONF
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Zertifikate in /etc/ssl/* bereitstellen, bevor Laravel irgendwas liest --
|
|
||||||
relink_and_reload
|
|
||||||
|
|
||||||
# --- RECHTE FIXEN: storage & bootstrap/cache (www-data + mailwolt) ----------
|
|
||||||
log "Setze korrekte Rechte für Laravel-Verzeichnisse …"
|
|
||||||
cd "${APP_DIR}"
|
|
||||||
chgrp -R www-data storage bootstrap/cache || true
|
|
||||||
find storage bootstrap/cache -type d -exec chmod 2775 {} \; || true
|
|
||||||
find storage bootstrap/cache -type f -exec chmod 0664 {} \; || true
|
|
||||||
setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx storage bootstrap/cache || true
|
|
||||||
setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx storage bootstrap/cache || true
|
|
||||||
log "[✓] Schreibrechte für Laravel korrigiert."
|
|
||||||
|
|
||||||
# --- DKIM: Verzeichnisse & Basisrechte --------------------------------------
|
|
||||||
install -d -m 2775 -o "$APP_USER" -g www-data "$APP_DIR/storage/app/private"
|
|
||||||
install -d -m 2775 -o "$APP_USER" -g www-data "$APP_DIR/storage/app/private/dkim"
|
|
||||||
setfacl -R -m u:${APP_USER}:rwx,u:www-data:rwx "$APP_DIR/storage/app/private" || true
|
|
||||||
setfacl -dR -m u:${APP_USER}:rwx,u:www-data:rwx "$APP_DIR/storage/app/private" || true
|
|
||||||
|
|
||||||
# --- OpenDKIM: keys & DNS-Verzeichnis --------------------------------------
|
|
||||||
install -d -m 0750 -o opendkim -g opendkim /etc/opendkim
|
|
||||||
install -d -m 0750 -o opendkim -g opendkim /etc/opendkim/keys
|
|
||||||
install -d -m 0755 -o root -g root /etc/mailwolt
|
|
||||||
install -d -m 0755 -o root -g root /etc/mailwolt/dns
|
|
||||||
|
|
||||||
# --- Caches leeren, Migrationen ausführen -----------------------------------
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear"
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan migrate --force"
|
|
||||||
|
|
||||||
# --- Seeder (legt Domains/DKIM etc. an) -------------------------------------
|
|
||||||
if [[ "${BASE_DOMAIN}" != "example.com" ]]; then
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan db:seed --class=SystemDomainSeeder --force"
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan db:seed --class=SystemBackupSeeder --force"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- DKIM für SYSMAIL_DOMAIN via App erzeugen & per Helper einhängen --------
|
|
||||||
DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
|
||||||
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
|
||||||
SYSMAIL_DOMAIN="${SYSMAIL_DOMAIN:-sysmail.${BASE_DOMAIN}}"
|
|
||||||
|
|
||||||
if [[ "${DKIM_ENABLE}" = "1" && -n "${SYSMAIL_DOMAIN}" ]]; then
|
|
||||||
log "Erzeuge/aktualisiere DKIM für ${SYSMAIL_DOMAIN} (Selector: ${DKIM_SELECTOR}) …"
|
|
||||||
|
|
||||||
# 1) In der App generieren (als mailwolt), und Pfad + TXT zurückgeben
|
|
||||||
OUT="$(sudo -u "${APP_USER}" -H bash -lc "
|
|
||||||
set -e
|
|
||||||
cd '${APP_DIR}'
|
|
||||||
php -r '
|
|
||||||
require \"vendor/autoload.php\";
|
|
||||||
\$app=require \"bootstrap/app.php\";
|
|
||||||
\$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
|
|
||||||
\$d = App\\Models\\Domain::firstOrCreate([\"domain\"=>\"${SYSMAIL_DOMAIN}\"],[\"is_active\"=>1,\"is_system\"=>1]);
|
|
||||||
\$r = app(App\\Services\\DkimService::class)->generateForDomain(\$d, 2048, \"${DKIM_SELECTOR}\");
|
|
||||||
echo \$r[\"priv_path\"], \"\\n\";
|
|
||||||
echo \$r[\"dns_txt\"], \"\\n\";
|
|
||||||
'
|
|
||||||
")"
|
|
||||||
|
|
||||||
PRIV_PATH="$(printf '%s\n' "$OUT" | sed -n '1p')"
|
|
||||||
DNS_TXT="$(printf '%s\n' "$OUT" | sed -n '2,$p')"
|
|
||||||
|
|
||||||
if [[ -z "$PRIV_PATH" || ! -s "$PRIV_PATH" ]]; then
|
|
||||||
echo "[!] DKIM priv_path fehlt oder Datei leer: $PRIV_PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TMP_TXT="$(mktemp /tmp/dkim_txt_XXXXXX.txt)"
|
|
||||||
printf '%s' "$DNS_TXT" >"$TMP_TXT"
|
|
||||||
|
|
||||||
# 2) Root-Helper ausführen (hängt Key ein, pflegt Key/SigningTable, kopiert TXT)
|
|
||||||
if [[ -x /usr/local/sbin/mailwolt-install-dkim ]]; then
|
|
||||||
/usr/local/sbin/mailwolt-install-dkim "${SYSMAIL_DOMAIN}" "${DKIM_SELECTOR}" "${PRIV_PATH}" "${TMP_TXT}"
|
|
||||||
else
|
|
||||||
echo "[!] Helper /usr/local/sbin/mailwolt-install-dkim fehlt oder ist nicht ausführbar." >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$TMP_TXT" || true
|
|
||||||
|
|
||||||
# 3) OpenDKIM neu laden
|
|
||||||
touch /run/mailwolt.need-opendkim-reload || true
|
|
||||||
else
|
|
||||||
log "DKIM übersprungen (DKIM_ENABLE=${DKIM_ENABLE}, SYSMAIL_DOMAIN='${SYSMAIL_DOMAIN}')."
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# --- TLSA aus App heraus (idempotent; läuft, wenn Zert lesbar ist) ----------
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan dns:tlsa:refresh || true"
|
|
||||||
|
|
||||||
# --- Build Frontend (nur wenn nötig) ----------------------------------------
|
|
||||||
if [[ -f "${APP_DIR}/package.json" && ! -f "${APP_DIR}/public/build/manifest.json" ]]; then
|
|
||||||
safe_frontend_build
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Abschluss: Caches + Rechte + Reloads -----------------------------------
|
|
||||||
sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan optimize:clear && php artisan config:cache && php artisan optimize:clear"
|
|
||||||
|
|
||||||
# Konsistente Rechte/ACL für das gesamte App-Verzeichnis
|
|
||||||
chown -R "$APP_USER":"$APP_GROUP" "$APP_DIR"
|
|
||||||
find "$APP_DIR" -type d -exec chmod 2775 {} \;
|
|
||||||
find "$APP_DIR" -type f -exec chmod 664 {} \;
|
|
||||||
setfacl -R -m g:"$APP_GROUP":rwX -m d:g:"$APP_GROUP":rwX "$APP_DIR" || true
|
|
||||||
|
|
||||||
# Laravel-Write-Dirs sicherstellen (mit setgid & ACL)
|
|
||||||
install -d -m 2775 -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
|
|
||||||
chgrp -R www-data "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
|
||||||
find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type d -exec chmod 2775 {} \; || true
|
|
||||||
find "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" -type f -exec chmod 0664 {} \; || true
|
|
||||||
setfacl -R -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
|
||||||
setfacl -dR -m u:www-data:rwx,u:${APP_USER}:rwx "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" || true
|
|
||||||
|
|
@ -1,264 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Update-Wrapper & Sudoers …"
|
|
||||||
|
|
||||||
WRAPPER="/usr/local/sbin/mailwolt-update"
|
|
||||||
LOGFILE="/var/log/mailwolt-update.log"
|
|
||||||
STATEDIR="/var/lib/mailwolt/update"
|
|
||||||
SUDOERS="/etc/sudoers.d/mailwolt-update"
|
|
||||||
VERSION_FILE="/var/lib/mailwolt/version"
|
|
||||||
SUDOERS_SERVICES="/etc/sudoers.d/mailwolt-services"
|
|
||||||
SUDOERS_ARTISAN="/etc/sudoers.d/mailwolt-artisan"
|
|
||||||
|
|
||||||
# Kandidaten: wo liegt update.sh?
|
|
||||||
CANDIDATES=(
|
|
||||||
/opt/mailwolt-installer/scripts/update.sh
|
|
||||||
/mailwolt-installer/scripts/update.sh
|
|
||||||
/usr/local/lib/mailwolt/update.sh
|
|
||||||
)
|
|
||||||
|
|
||||||
# State/Log vorbereiten
|
|
||||||
install -d -m 0755 "$(dirname "$LOGFILE")"
|
|
||||||
install -d -m 0755 "$STATEDIR"
|
|
||||||
: > "$LOGFILE" || true
|
|
||||||
chmod 0644 "$LOGFILE"
|
|
||||||
|
|
||||||
# Wrapper erzeugen
|
|
||||||
cat > "$WRAPPER" <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
LOG="/var/log/mailwolt-update.log"
|
|
||||||
STATE_DIR="/var/lib/mailwolt/update"
|
|
||||||
APP_DIR="/var/www/mailwolt"
|
|
||||||
WEB_USER="www-data"
|
|
||||||
|
|
||||||
CANDIDATES=(
|
|
||||||
/opt/mailwolt-installer/scripts/update.sh
|
|
||||||
/mailwolt-installer/scripts/update.sh
|
|
||||||
/usr/local/lib/mailwolt/update.sh
|
|
||||||
)
|
|
||||||
|
|
||||||
install -d -m 0755 "$(dirname "$LOG")" "$STATE_DIR" /var/lib/mailwolt
|
|
||||||
: > "$LOG" || true
|
|
||||||
chmod 0644 "$LOG"
|
|
||||||
|
|
||||||
echo "running" > "$STATE_DIR/state"
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "===== $(date -Is) :: Update gestartet ====="
|
|
||||||
|
|
||||||
# --- Update-Script finden --------------------------------------------------
|
|
||||||
SCRIPT=""
|
|
||||||
for p in "${CANDIDATES[@]}"; do
|
|
||||||
if [[ -x "$p" ]]; then SCRIPT="$p"; break; fi
|
|
||||||
if [[ -f "$p" && -r "$p" ]]; then SCRIPT="$p"; break; fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$SCRIPT" ]]; then
|
|
||||||
echo "[!] update.sh nicht gefunden (versucht: ${CANDIDATES[*]})"
|
|
||||||
rc=127
|
|
||||||
else
|
|
||||||
echo "[i] benutze: $SCRIPT"
|
|
||||||
if [[ "$(id -u)" -ne 0 ]]; then
|
|
||||||
echo "[!] Bitte als root ausführen"
|
|
||||||
rc=1
|
|
||||||
else
|
|
||||||
if [[ -x "$SCRIPT" ]]; then
|
|
||||||
ALLOW_DIRTY=1 "$SCRIPT"
|
|
||||||
else
|
|
||||||
ALLOW_DIRTY=1 bash "$SCRIPT"
|
|
||||||
fi
|
|
||||||
rc=$?
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "===== $(date -Is) :: Update-Script beendet (rc=$rc) ====="
|
|
||||||
|
|
||||||
# --- Nach dem Update: Assets neu bauen & Laravel optimieren ---------------
|
|
||||||
if [ -d "$APP_DIR" ]; then
|
|
||||||
cd "$APP_DIR" || exit 1
|
|
||||||
|
|
||||||
echo "[i] Führe Composer aus (falls vorhanden) ..."
|
|
||||||
if [ -f composer.json ]; then
|
|
||||||
sudo -u "$WEB_USER" composer install --no-dev --prefer-dist --no-interaction -q || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[i] Baue Frontend-Assets neu ..."
|
|
||||||
if command -v npm >/dev/null 2>&1 && [ -f package.json ]; then
|
|
||||||
sudo -u "$WEB_USER" npm ci --silent || true
|
|
||||||
sudo -u "$WEB_USER" npm run build --silent || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[i] Führe Migrationen & Cache-Optimierungen durch ..."
|
|
||||||
sudo -u "$WEB_USER" php artisan migrate --force || true
|
|
||||||
sudo -u "$WEB_USER" php artisan config:cache || true
|
|
||||||
sudo -u "$WEB_USER" php artisan optimize:clear || true
|
|
||||||
sudo -u "$WEB_USER" php artisan route:cache || true
|
|
||||||
sudo -u "$WEB_USER" php artisan view:cache || true
|
|
||||||
|
|
||||||
echo "[i] Hebe Wartungsmodus auf ..."
|
|
||||||
sudo -u "$WEB_USER" php artisan up >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Version aktualisieren -------------------------------------------------
|
|
||||||
echo "[i] Aktualisiere Version ..."
|
|
||||||
if command -v git >/dev/null 2>&1; then
|
|
||||||
SRC="/var/www/mailwolt"
|
|
||||||
if [ ! -d "$SRC/.git" ]; then
|
|
||||||
SRC="/opt/mailwolt-installer"
|
|
||||||
fi
|
|
||||||
|
|
||||||
git config --global --add safe.directory "$SRC" || true
|
|
||||||
|
|
||||||
if [ -f "$SRC/.git/shallow" ]; then
|
|
||||||
git -C "$SRC" fetch --unshallow --quiet || true
|
|
||||||
fi
|
|
||||||
git -C "$SRC" fetch --tags --quiet origin || true
|
|
||||||
|
|
||||||
raw="$(git -C "$SRC" describe --tags --always --dirty 2>/dev/null || echo "unknown")"
|
|
||||||
norm="$(printf '%s' "$raw" | sed -E 's/^[vV]//; s/-.*$//')"
|
|
||||||
|
|
||||||
printf '%s\n' "$raw" > /var/lib/mailwolt/version_raw
|
|
||||||
printf '%s\n' "$norm" > /var/lib/mailwolt/version
|
|
||||||
chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
|
|
||||||
|
|
||||||
echo "[i] Version aktualisiert: raw=$raw norm=$norm (Quelle: $SRC)"
|
|
||||||
else
|
|
||||||
echo "unknown" > /var/lib/mailwolt/version_raw
|
|
||||||
echo "0.0.0" > /var/lib/mailwolt/version
|
|
||||||
chmod 0644 /var/lib/mailwolt/version_raw /var/lib/mailwolt/version
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Services neu starten --------------------------------------------------
|
|
||||||
echo "[i] Starte MailWolt-Dienste neu ..."
|
|
||||||
sudo -u "$WEB_USER" php artisan mailwolt:restart-services || true
|
|
||||||
|
|
||||||
# --- Abschluss -------------------------------------------------------------
|
|
||||||
printf '%s\n' "$rc" > "$STATE_DIR/rc"
|
|
||||||
echo "done" > "$STATE_DIR/state"
|
|
||||||
echo "===== $(date -Is) :: Update beendet ====="
|
|
||||||
exit "$rc"
|
|
||||||
|
|
||||||
} | tee -a "$LOG"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 0755 "$WRAPPER"
|
|
||||||
chown root:root "$WRAPPER"
|
|
||||||
|
|
||||||
# Sudoers: www-data (Laravel) & mailwolt dürfen den Wrapper laufen lassen
|
|
||||||
cat > "$SUDOERS" <<'EOF'
|
|
||||||
Defaults!/usr/local/sbin/mailwolt-update !requiretty
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-update
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chown root:root "$SUDOERS"
|
|
||||||
chmod 440 "$SUDOERS"
|
|
||||||
|
|
||||||
if ! visudo -c -f "$SUDOERS" >/dev/null 2>&1; then
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS – entferne Datei."
|
|
||||||
rm -f "$SUDOERS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "$SUDOERS_SERVICES" <<'EOF'
|
|
||||||
Defaults!/usr/bin/systemctl !requiretty
|
|
||||||
|
|
||||||
Cmnd_Alias MW_SERVICES = \
|
|
||||||
/usr/bin/systemctl reload nginx.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart nginx.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart postfix.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart dovecot.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart rspamd.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart opendkim.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart opendmarc.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart clamav-daemon.service, \
|
|
||||||
/usr/bin/systemctl try-reload-or-restart redis-server.service
|
|
||||||
|
|
||||||
www-data ALL=(root) NOPASSWD: MW_SERVICES
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 440 "$SUDOERS_SERVICES"
|
|
||||||
chown root:root "$SUDOERS_SERVICES"
|
|
||||||
|
|
||||||
# Prüfen, ob Syntax gültig ist
|
|
||||||
if ! visudo -c -f "$SUDOERS_SERVICES" >/dev/null 2>&1; then
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_SERVICES – entferne Datei."
|
|
||||||
rm -f "$SUDOERS_SERVICES"
|
|
||||||
else
|
|
||||||
echo "[✓] Sudoers für Dienststeuerung angelegt: $SUDOERS_SERVICES"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Version-File initial anlegen, falls nicht existiert
|
|
||||||
if [[ ! -f "$VERSION_FILE" ]]; then
|
|
||||||
echo "unknown" > "$VERSION_FILE"
|
|
||||||
chmod 0644 "$VERSION_FILE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "$SUDOERS_ARTISAN" <<'EOF'
|
|
||||||
# mailwolt darf artisan im App-Verzeichnis als www-data ausführen (ohne Passwort)
|
|
||||||
mailwolt ALL=(www-data) NOPASSWD: /usr/bin/php /var/www/mailwolt/artisan *
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chown root:root "$SUDOERS_ARTISAN"
|
|
||||||
chmod 440 "$SUDOERS_ARTISAN"
|
|
||||||
|
|
||||||
if ! visudo -c -f "$SUDOERS_ARTISAN" >/dev/null 2>&1; then
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in $SUDOERS_ARTISAN – entferne Datei."
|
|
||||||
rm -f "$SUDOERS_ARTISAN"
|
|
||||||
else
|
|
||||||
echo "[✓] Sudoers für Artisan-Kommandos angelegt: $SUDOERS_ARTISAN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "[✓] Update-Wrapper bereit: $WRAPPER"
|
|
||||||
log "[✓] Version wird unter $VERSION_FILE gespeichert"
|
|
||||||
|
|
||||||
# ─── Installer-Wrapper ────────────────────────────────────────────────────────
|
|
||||||
INSTALL_WRAPPER="/usr/local/sbin/mailwolt-install"
|
|
||||||
INSTALL_SUDOERS="/etc/sudoers.d/mailwolt-install"
|
|
||||||
INSTALL_STATE_DIR="/var/lib/mailwolt/install"
|
|
||||||
INSTALL_LOG="/var/log/mailwolt-install.log"
|
|
||||||
|
|
||||||
# State/Log vorbereiten
|
|
||||||
install -d -m 0755 "$INSTALL_STATE_DIR"
|
|
||||||
: > "$INSTALL_LOG" || true
|
|
||||||
chmod 0644 "$INSTALL_LOG"
|
|
||||||
|
|
||||||
# Installer-Wrapper aus scripts/install-wrapper.sh kopieren
|
|
||||||
INSTALL_SRC=""
|
|
||||||
for candidate in \
|
|
||||||
"$(dirname "$0")/install-wrapper.sh" \
|
|
||||||
/opt/mailwolt-installer/scripts/install-wrapper.sh \
|
|
||||||
/var/www/mailwolt/mailwolt-installer/scripts/install-wrapper.sh; do
|
|
||||||
[[ -f "$candidate" ]] && INSTALL_SRC="$candidate" && break
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -n "$INSTALL_SRC" ]]; then
|
|
||||||
cp "$INSTALL_SRC" "$INSTALL_WRAPPER"
|
|
||||||
chmod 0755 "$INSTALL_WRAPPER"
|
|
||||||
chown root:root "$INSTALL_WRAPPER"
|
|
||||||
echo "[✓] Installer-Wrapper angelegt: $INSTALL_WRAPPER"
|
|
||||||
else
|
|
||||||
echo "[!] install-wrapper.sh nicht gefunden – Installer-Wrapper wird übersprungen."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sudoers: www-data & mailwolt dürfen den Installer-Wrapper laufen lassen
|
|
||||||
cat > "$INSTALL_SUDOERS" <<'SUDOEOF'
|
|
||||||
Defaults!/usr/local/sbin/mailwolt-install !requiretty
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-install *
|
|
||||||
SUDOEOF
|
|
||||||
|
|
||||||
chown root:root "$INSTALL_SUDOERS"
|
|
||||||
chmod 440 "$INSTALL_SUDOERS"
|
|
||||||
|
|
||||||
if ! visudo -c -f "$INSTALL_SUDOERS" >/dev/null 2>&1; then
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in $INSTALL_SUDOERS – entferne Datei."
|
|
||||||
rm -f "$INSTALL_SUDOERS"
|
|
||||||
else
|
|
||||||
echo "[✓] Sudoers für Installer angelegt: $INSTALL_SUDOERS"
|
|
||||||
fi
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "systemd Units (Reverb / Scheduler / Queue / Mail) …"
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/${APP_USER}-ws.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=${APP_NAME} WebSocket Backend
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
Environment=NODE_ENV=production WS_PORT=8080
|
|
||||||
User=${APP_USER}
|
|
||||||
Group=${APP_GROUP}
|
|
||||||
WorkingDirectory=${APP_DIR}
|
|
||||||
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
|
||||||
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
|
||||||
ExecStart=/usr/bin/php artisan reverb:start --host=127.0.0.1 --port=8080 --no-interaction
|
|
||||||
Restart=always
|
|
||||||
RestartSec=2
|
|
||||||
StandardOutput=append:/var/log/${APP_USER}-ws.log
|
|
||||||
StandardError=append:/var/log/${APP_USER}-ws.log
|
|
||||||
KillSignal=SIGINT
|
|
||||||
TimeoutStopSec=15
|
|
||||||
UMask=0002
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/${APP_USER}-schedule.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=${APP_NAME} Laravel Scheduler
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=${APP_USER}
|
|
||||||
Group=${APP_GROUP}
|
|
||||||
WorkingDirectory=${APP_DIR}
|
|
||||||
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
|
||||||
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
|
||||||
ExecStart=/usr/bin/php artisan schedule:work
|
|
||||||
Restart=always
|
|
||||||
RestartSec=2
|
|
||||||
StandardOutput=append:/var/log/${APP_USER}-schedule.log
|
|
||||||
StandardError=append:/var/log/${APP_USER}-schedule.log
|
|
||||||
KillSignal=SIGINT
|
|
||||||
TimeoutStopSec=15
|
|
||||||
UMask=0002
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > /etc/systemd/system/${APP_USER}-queue.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=${APP_NAME} Queue Worker
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=${APP_USER}
|
|
||||||
Group=${APP_GROUP}
|
|
||||||
WorkingDirectory=${APP_DIR}
|
|
||||||
ExecStartPre=/usr/bin/bash -lc 'test -f .env'
|
|
||||||
ExecStartPre=/usr/bin/bash -lc 'test -d vendor'
|
|
||||||
ExecStart=/usr/bin/php artisan queue:work --queue=default,notify --tries=1
|
|
||||||
Restart=always
|
|
||||||
RestartSec=2
|
|
||||||
StandardOutput=append:/var/log/${APP_USER}-queue.log
|
|
||||||
StandardError=append:/var/log/${APP_USER}-queue.log
|
|
||||||
KillSignal=SIGINT
|
|
||||||
TimeoutStopSec=15
|
|
||||||
UMask=0002
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chown root:root /etc/systemd/system/${APP_USER}-*.service
|
|
||||||
chmod 644 /etc/systemd/system/${APP_USER}-*.service
|
|
||||||
touch /var/log/${APP_USER}-ws.log /var/log/${APP_USER}-schedule.log /var/log/${APP_USER}-queue.log
|
|
||||||
chown ${APP_USER}:${APP_GROUP} /var/log/${APP_USER}-*.log
|
|
||||||
chmod 664 /var/log/${APP_USER}-*.log
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
# App-Dienste
|
|
||||||
if sudo -u "$APP_USER" -H bash -lc "cd ${APP_DIR} && php artisan list --no-ansi | grep -qE '(^| )reverb:start( |$)'"; then
|
|
||||||
systemctl enable --now ${APP_USER}-ws
|
|
||||||
else
|
|
||||||
systemctl disable --now ${APP_USER}-ws >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
systemctl enable --now ${APP_USER}-schedule
|
|
||||||
systemctl enable --now ${APP_USER}-queue
|
|
||||||
|
|
||||||
# Mail-Dienste starten
|
|
||||||
systemctl enable --now rspamd opendkim postfix dovecot || true
|
|
||||||
|
|
||||||
# PHP-FPM: Unit erkennen, enable + (re)load
|
|
||||||
enable_and_touch_php_fpm() {
|
|
||||||
for u in php8.3-fpm php8.2-fpm php8.1-fpm php8.0-fpm php7.4-fpm php-fpm; do
|
|
||||||
if systemctl list-unit-files | grep -q "^${u}\.service"; then
|
|
||||||
systemctl enable --now "$u" || true
|
|
||||||
systemctl reload "$u" || systemctl restart "$u" || true
|
|
||||||
echo "[i] PHP-FPM unit: $u"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "[!] Keine passende php-fpm Unit gefunden."
|
|
||||||
}
|
|
||||||
enable_and_touch_php_fpm
|
|
||||||
|
|
||||||
# Falls in 80-app.sh DKIM installiert wurde: jetzt einmal reloaden
|
|
||||||
if [[ -e /run/mailwolt.need-opendkim-reload ]]; then
|
|
||||||
systemctl reload opendkim || true
|
|
||||||
rm -f /run/mailwolt.need-opendkim-reload || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Falls Zert-Fix markiert ist: Dovecot neu laden
|
|
||||||
if [[ -e /run/mailwolt.need-dovecot-reload ]]; then
|
|
||||||
systemctl reload dovecot || true
|
|
||||||
rm -f /run/mailwolt.need-dovecot-reload || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Falls DB-Migration schon durch: einmal reload
|
|
||||||
db_ready(){ mysql -u"${DB_USER}" -p"${DB_PASS}" -h 127.0.0.1 -D "${DB_NAME}" -e "SHOW TABLES LIKE 'migrations'\G" >/dev/null 2>&1; }
|
|
||||||
if db_ready; then
|
|
||||||
systemctl reload postfix || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mini-Portcheck (hilft beim Installer-Output)
|
|
||||||
echo "Listening (25/465/587):"
|
|
||||||
ss -ltnp | awk '$4 ~ /:(25|465|587)$/ {print " " $0}'
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Sudoers: npm-Build ohne Passwort für user 'mailwolt' …"
|
|
||||||
|
|
||||||
# 1) npm-Binary ermitteln (normal: /usr/bin/npm)
|
|
||||||
NPM_BIN="$(command -v npm || true)"
|
|
||||||
|
|
||||||
if [[ -z "$NPM_BIN" ]]; then
|
|
||||||
warn "npm wurde nicht gefunden – sudoers wird vorbereitet, aber ohne Validierung. Stelle sicher, dass Node/npm installiert ist."
|
|
||||||
# Fallback – die meisten Distros legen hier an
|
|
||||||
NPM_BIN="/usr/bin/npm"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SUDOERS_FILE="/etc/sudoers.d/mailwolt-npm"
|
|
||||||
|
|
||||||
# 2) Sudoers-Datei schreiben
|
|
||||||
cat > "$SUDOERS_FILE" <<EOF
|
|
||||||
Defaults!${NPM_BIN} !requiretty
|
|
||||||
mailwolt ALL=(root) NOPASSWD: ${NPM_BIN}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chown root:root "$SUDOERS_FILE"
|
|
||||||
chmod 440 "$SUDOERS_FILE"
|
|
||||||
|
|
||||||
# 3) Validieren
|
|
||||||
if visudo -c -f "$SUDOERS_FILE" >/dev/null 2>&1; then
|
|
||||||
log "[✓] sudoers OK: ${SUDOERS_FILE} erlaubt 'mailwolt' → ${NPM_BIN} ohne Passwort."
|
|
||||||
else
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_FILE} – entferne Datei."
|
|
||||||
rm -f "$SUDOERS_FILE"
|
|
||||||
fi
|
|
||||||
|
|
@ -1,284 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Backup/Restore – Tools, Config & Timer (installer.env) …"
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# 1) installer.env laden (ENV > installer.env > Defaults)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
if [[ -f /etc/mailwolt/installer.env ]]; then
|
|
||||||
# automatisch exportieren, damit ${VAR} später überall wirkt
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
source /etc/mailwolt/installer.env
|
|
||||||
set +a
|
|
||||||
else
|
|
||||||
log "[i] /etc/mailwolt/installer.env nicht gefunden – nutze Defaults."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# 2) Pfade & Defaults (werden durch ENV/installer.env überschrieben)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
CONF_DIR="/etc/mailwolt"
|
|
||||||
CONF_FILE="${CONF_DIR}/backup.conf"
|
|
||||||
BIN_DIR="/usr/local/sbin"
|
|
||||||
UNIT_DIR="/etc/systemd/system"
|
|
||||||
|
|
||||||
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
|
||||||
|
|
||||||
# DB-Parameter aus installer.env (bzw. ENV) oder Fallbacks
|
|
||||||
DB_HOST="${DB_HOST:-127.0.0.1}"
|
|
||||||
DB_NAME="${DB_NAME:-mailwolt}"
|
|
||||||
DB_USER="${DB_USER:-mailwolt}"
|
|
||||||
DB_PASS="${DB_PASS:-}"
|
|
||||||
|
|
||||||
# Backup-Settings aus installer.env (bzw. ENV)
|
|
||||||
BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
|
|
||||||
BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
|
|
||||||
BACKUP_USE_ZSTD="${BACKUP_USE_ZSTD:-1}"
|
|
||||||
BACKUP_ENABLED="${BACKUP_ENABLED:-0}" # 0|1
|
|
||||||
BACKUP_INTERVAL="${BACKUP_INTERVAL:-daily}" # daily|weekly|monthly
|
|
||||||
|
|
||||||
install -d -m 0755 "$CONF_DIR" "$BACKUP_DIR"
|
|
||||||
|
|
||||||
|
|
||||||
SUDOERS_BACKUP_FILE="/etc/sudoers.d/mailwolt-backup"
|
|
||||||
# 2) Sudoers-Datei schreiben
|
|
||||||
cat > "${SUDOERS_BACKUP_FILE} " <<EOF
|
|
||||||
Defaults!/usr/local/sbin/mailwolt-backup !requiretty
|
|
||||||
www-data ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-backup
|
|
||||||
mailwolt ALL=(root) NOPASSWD: /usr/local/sbin/mailwolt-backup
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chown root:root "${SUDOERS_BACKUP_FILE}"
|
|
||||||
chmod 440 "${SUDOERS_BACKUP_FILE}"
|
|
||||||
if ! visudo -c -f "${SUDOERS_BACKUP_FILE}" >/dev/null 2>&1; then
|
|
||||||
echo "[!] Ungültiger sudoers-Eintrag in ${SUDOERS_BACKUP_FILE} – entferne Datei."
|
|
||||||
rm -f "${SUDOERS_BACKUP_FILE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# 3) /etc/mailwolt/backup.conf (von UI/APP überschreibbar)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
cat > "$CONF_FILE" <<EOF
|
|
||||||
# MailWolt Backup Konfiguration (UI kann überschreiben)
|
|
||||||
APP_DIR="$APP_DIR"
|
|
||||||
BACKUP_DIR="$BACKUP_DIR"
|
|
||||||
RETENTION_DAYS="$BACKUP_RETENTION_DAYS"
|
|
||||||
USE_ZSTD="$BACKUP_USE_ZSTD"
|
|
||||||
|
|
||||||
# DB-Parameter
|
|
||||||
MYSQL_DB="$DB_NAME"
|
|
||||||
MYSQL_USER="$DB_USER"
|
|
||||||
MYSQL_PASS="$DB_PASS"
|
|
||||||
MYSQL_HOST="$DB_HOST"
|
|
||||||
MYSQL_PORT="3306"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod 0644 "$CONF_FILE"
|
|
||||||
log "[✓] config geschrieben: $CONF_FILE"
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# 4) /usr/local/sbin/mailwolt-backup (schreibt backup.status)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
cat > "${BIN_DIR}/mailwolt-backup" <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
log(){ echo "[$(date -Is)] $*"; }
|
|
||||||
|
|
||||||
# Konfiguration laden (ENV > Datei)
|
|
||||||
CONF="/etc/mailwolt/backup.conf"
|
|
||||||
[[ -f "$CONF" ]] && # shellcheck disable=SC1090
|
|
||||||
source "$CONF"
|
|
||||||
|
|
||||||
APP_DIR="${APP_DIR:-/var/www/mailwolt}"
|
|
||||||
BACKUP_DIR="${BACKUP_DIR:-/var/backups/mailwolt}"
|
|
||||||
RETENTION_DAYS="${RETENTION_DAYS:-7}"
|
|
||||||
USE_ZSTD="${USE_ZSTD:-1}"
|
|
||||||
|
|
||||||
MYSQL_DB="${MYSQL_DB:-mailwolt}"
|
|
||||||
MYSQL_USER="${MYSQL_USER:-mailwolt}"
|
|
||||||
MYSQL_PASS="${MYSQL_PASS:-}"
|
|
||||||
MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}"
|
|
||||||
MYSQL_PORT="${MYSQL_PORT:-3306}"
|
|
||||||
|
|
||||||
STATE_DIR="/var/lib/mailwolt"
|
|
||||||
STATUS_FILE="${STATE_DIR}/backup.status"
|
|
||||||
install -d -m 0755 "$STATE_DIR" "$BACKUP_DIR"
|
|
||||||
|
|
||||||
START_TS="$(date +%s)"
|
|
||||||
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
||||||
TMP="$(mktemp -d /tmp/mwbackup.XXXXXX)"
|
|
||||||
trap 'rm -rf "$TMP"' EXIT
|
|
||||||
|
|
||||||
fail(){
|
|
||||||
local msg="${1:-backup failed}"
|
|
||||||
local now="$(date -Is)"
|
|
||||||
{
|
|
||||||
echo "time=${now}"
|
|
||||||
echo "size=0"
|
|
||||||
echo "dur=$(( $(date +%s) - START_TS ))s"
|
|
||||||
echo "ok=0"
|
|
||||||
echo "error=${msg}"
|
|
||||||
} > "$STATUS_FILE"
|
|
||||||
echo "[$now] ${msg}" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
trap 'fail "unexpected error (exit $?)"' ERR
|
|
||||||
|
|
||||||
OUT="${BACKUP_DIR}/mailwolt-${TS}.tar"
|
|
||||||
log "⇒ starte Backup in $OUT …"
|
|
||||||
|
|
||||||
# 1) DB
|
|
||||||
log " • mysqldump …"
|
|
||||||
MYSQL_PWD="$MYSQL_PASS" mysqldump \
|
|
||||||
-h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" \
|
|
||||||
--single-transaction --routines --events --triggers \
|
|
||||||
"$MYSQL_DB" > "$TMP/mysql.sql"
|
|
||||||
|
|
||||||
# 2) Maildir
|
|
||||||
log " • Maildir …"
|
|
||||||
tar -C / -cf "$TMP/mail.tar" var/mail/vhosts 2>/dev/null || true
|
|
||||||
|
|
||||||
# 3) App (ohne heavy dirs)
|
|
||||||
log " • App …"
|
|
||||||
tar -C / -cf "$TMP/app.tar" \
|
|
||||||
--exclude='var/www/mailwolt/vendor' \
|
|
||||||
--exclude='var/www/mailwolt/node_modules' \
|
|
||||||
--exclude='var/www/mailwolt/public/build' \
|
|
||||||
var/www/mailwolt
|
|
||||||
|
|
||||||
# 4) Configs
|
|
||||||
log " • Configs …"
|
|
||||||
mkdir -p "$TMP/files"
|
|
||||||
cp -a /etc/mailwolt "$TMP/files/" 2>/dev/null || true
|
|
||||||
cp -a /etc/postfix "$TMP/files/" 2>/dev/null || true
|
|
||||||
cp -a /etc/dovecot "$TMP/files/" 2>/dev/null || true
|
|
||||||
cp -a /etc/opendkim "$TMP/files/" 2>/dev/null || true
|
|
||||||
cp -a /etc/opendmarc "$TMP/files/" 2>/dev/null || true
|
|
||||||
cp -a /etc/rspamd "$TMP/files/" 2>/dev/null || true
|
|
||||||
cp -a /etc/ssl/ui "$TMP/files/" 2>/dev/null || true
|
|
||||||
tar -C "$TMP" -cf "$TMP/files.tar" files
|
|
||||||
|
|
||||||
# 5) Paket
|
|
||||||
log " • Archiviere …"
|
|
||||||
tar -C "$TMP" -cf "$OUT" mysql.sql mail.tar app.tar files.tar
|
|
||||||
|
|
||||||
# 6) Komprimieren (optional)
|
|
||||||
if [[ "${USE_ZSTD:-1}" = "1" ]] && command -v zstd >/dev/null 2>&1; then
|
|
||||||
log " • komprimiere (zstd) …"
|
|
||||||
zstd -f --rm -19 "$OUT"
|
|
||||||
OUT="${OUT}.zst"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 7) Retention
|
|
||||||
if [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]]; then
|
|
||||||
log " • Retention: lösche älter als ${RETENTION_DAYS} Tage …"
|
|
||||||
find "$BACKUP_DIR" -type f -mtime +"$RETENTION_DAYS" -name 'mailwolt-*' -delete || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 8) Statusfile fürs UI
|
|
||||||
SIZE_BYTES="$(stat -c '%s' "$OUT" 2>/dev/null || echo 0)"
|
|
||||||
{
|
|
||||||
echo "time=$(date -Is)"
|
|
||||||
echo "size=${SIZE_BYTES}"
|
|
||||||
echo "dur=$(( $(date +%s) - START_TS ))s"
|
|
||||||
echo "ok=1"
|
|
||||||
echo "file=${OUT}"
|
|
||||||
} > "$STATUS_FILE"
|
|
||||||
chmod 0644 "$STATUS_FILE" 2>/dev/null || true
|
|
||||||
|
|
||||||
log "[✓] Backup fertig: $OUT"
|
|
||||||
EOSH
|
|
||||||
chmod 0755 "${BIN_DIR}/mailwolt-backup"
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# 5) /usr/local/sbin/mailwolt-restore
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
cat > "${BIN_DIR}/mailwolt-restore" <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
log(){ echo "[$(date -Is)] $*"; }
|
|
||||||
|
|
||||||
ARCHIVE="${1:-}"
|
|
||||||
[[ -n "$ARCHIVE" ]] || { echo "Usage: mailwolt-restore <backup.tar[.zst]>"; exit 1; }
|
|
||||||
[[ -f "$ARCHIVE" ]] || { echo "Backup nicht gefunden: $ARCHIVE"; exit 1; }
|
|
||||||
|
|
||||||
TMP="$(mktemp -d /tmp/mwrestore.XXXXXX)"
|
|
||||||
trap 'rm -rf "$TMP"' EXIT
|
|
||||||
|
|
||||||
case "$ARCHIVE" in
|
|
||||||
*.zst) zstd -d -c "$ARCHIVE" > "$TMP/backup.tar" ;;
|
|
||||||
*) cp -a "$ARCHIVE" "$TMP/backup.tar" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
log "⇒ entpacke …"
|
|
||||||
tar -C "$TMP" -xf "$TMP/backup.tar"
|
|
||||||
|
|
||||||
# Reihenfolge: DB → App → Mail → Config
|
|
||||||
if [[ -f "$TMP/mysql.sql" ]]; then
|
|
||||||
log " • MySQL wiederherstellen …"
|
|
||||||
mysql < "$TMP/mysql.sql"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$TMP/app.tar" ]]; then
|
|
||||||
log " • App → /var/www/mailwolt …"
|
|
||||||
tar -C / -xf "$TMP/app.tar"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$TMP/mail.tar" ]]; then
|
|
||||||
log " • Maildir → /var/mail/vhosts …"
|
|
||||||
tar -C / -xf "$TMP/mail.tar"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -f "$TMP/files.tar" ]]; then
|
|
||||||
log " • Configs → /etc/* …"
|
|
||||||
tar -C / -xf "$TMP/files.tar"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "[✓] Restore abgeschlossen."
|
|
||||||
EOSH
|
|
||||||
chmod 0755 "${BIN_DIR}/mailwolt-restore"
|
|
||||||
|
|
||||||
log "[✓] Tools installiert: ${BIN_DIR}/mailwolt-backup, mailwolt-restore"
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# 6) systemd Service + Timer (Timer default via installer.env)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
cat > "${UNIT_DIR}/mailwolt-backup.service" <<'EOSVC'
|
|
||||||
[Unit]
|
|
||||||
Description=MailWolt Backup
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/sbin/mailwolt-backup
|
|
||||||
Nice=10
|
|
||||||
IOSchedulingClass=best-effort
|
|
||||||
IOSchedulingPriority=7
|
|
||||||
EOSVC
|
|
||||||
|
|
||||||
cat > "${UNIT_DIR}/mailwolt-backup.timer" <<EOTIM
|
|
||||||
[Unit]
|
|
||||||
Description=MailWolt Backup Timer
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
OnCalendar=${BACKUP_ONCALENDAR:-*-*-* 03:00:00}
|
|
||||||
Persistent=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
EOTIM
|
|
||||||
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
if [[ "${BACKUP_ENABLED}" = "1" ]]; then
|
|
||||||
log "Aktiviere Backup-Timer (${BACKUP_ONCALENDAR}) …"
|
|
||||||
systemctl enable --now mailwolt-backup.timer
|
|
||||||
else
|
|
||||||
log "Timer bleibt deaktiviert (BACKUP_ENABLED=0)."
|
|
||||||
systemctl disable --now mailwolt-backup.timer >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "[✓] Backup-Setup abgeschlossen."
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "Monit konfigurieren …"
|
|
||||||
cat > /etc/monit/monitrc <<'EOF'
|
|
||||||
set daemon 60
|
|
||||||
set logfile syslog facility log_daemon
|
|
||||||
|
|
||||||
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
|
||||||
start program = "/bin/systemctl start postfix"
|
|
||||||
stop program = "/bin/systemctl stop postfix"
|
|
||||||
if failed port 25 protocol smtp then restart
|
|
||||||
if failed port 465 type tcp ssl then restart
|
|
||||||
if failed port 587 type tcp then restart
|
|
||||||
|
|
||||||
check process dovecot with pidfile /run/dovecot/master.pid
|
|
||||||
start program = "/bin/systemctl start dovecot"
|
|
||||||
stop program = "/bin/systemctl stop dovecot"
|
|
||||||
if failed port 143 type tcp then restart
|
|
||||||
if failed port 993 type tcp ssl then restart
|
|
||||||
|
|
||||||
check process mariadb with pidfile /run/mysqld/mysqld.pid
|
|
||||||
start program = "/bin/systemctl start mariadb"
|
|
||||||
stop program = "/bin/systemctl stop mariadb"
|
|
||||||
if failed port 3306 type tcp then restart
|
|
||||||
|
|
||||||
check process redis-server with pidfile /run/redis/redis-server.pid
|
|
||||||
start program = "/bin/systemctl start redis-server"
|
|
||||||
stop program = "/bin/systemctl stop redis-server"
|
|
||||||
if failed port 6379 type tcp then restart
|
|
||||||
|
|
||||||
check process nginx with pidfile /run/nginx.pid
|
|
||||||
start program = "/bin/systemctl start nginx"
|
|
||||||
stop program = "/bin/systemctl stop nginx"
|
|
||||||
if failed port 80 type tcp then restart
|
|
||||||
if failed port 443 type tcp ssl then restart
|
|
||||||
EOF
|
|
||||||
chmod 600 /etc/monit/monitrc
|
|
||||||
monit -t && systemctl enable --now monit
|
|
||||||
monit reload || true
|
|
||||||
|
|
||||||
log "[✓] Monit konfiguriert und gestartet"
|
|
||||||
|
|
||||||
# ── mailwolt-update ins System kopieren ─────────────────────────────
|
|
||||||
install -m 0750 -o root -g root scripts/update.sh /usr/local/sbin/mailwolt-update
|
|
||||||
log "[✓] mailwolt-update installiert → ausführbar via 'sudo mailwolt-update'"
|
|
||||||
|
|
@ -1,491 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "WoltGuard (Monit + Self-Heal) einrichten …"
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Env nur nachladen, wenn Flags nicht bereits exportiert sind
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
: "${CLAMAV_ENABLE:=}" ; : "${OPENDMARC_ENABLE:=}" ; : "${FAIL2BAN_ENABLE:=}"
|
|
||||||
if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$INSTALLER_ENV"
|
|
||||||
fi
|
|
||||||
CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
|
||||||
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
|
|
||||||
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Monit installieren & aktivieren
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
|
|
||||||
systemctl enable --now monit
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Helper-Skripte (laufen später eigenständig → Env selbst laden)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
install -d -m 0755 /usr/local/sbin
|
|
||||||
|
|
||||||
# Redis-Ping (nimmt REDIS_PASSWORD aus installer.env oder .env)
|
|
||||||
cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
APP_ENV="/var/www/mailwolt/.env"
|
|
||||||
|
|
||||||
# Defaults
|
|
||||||
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
||||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
|
||||||
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
|
||||||
REDIS_PASS="${REDIS_PASS:-}" # Legacy
|
|
||||||
|
|
||||||
# Installer-Env (falls vorhanden)
|
|
||||||
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
|
||||||
|
|
||||||
# Falls .env existiert: Werte ergänzen, die noch leer sind
|
|
||||||
if [[ -r "$APP_ENV" ]]; then
|
|
||||||
[[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 -E '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
[[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 -E '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
[[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 -E '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Legacy-Fallback: wenn PASSWORD leer, aber PASS gesetzt → übernehmen
|
|
||||||
[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
|
||||||
|
|
||||||
# Quotes strippen
|
|
||||||
strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'; }
|
|
||||||
REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
|
||||||
REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
|
||||||
REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
|
||||||
|
|
||||||
# redis-cli muss vorhanden sein
|
|
||||||
command -v redis-cli >/dev/null 2>&1 || exit 1
|
|
||||||
|
|
||||||
BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
|
||||||
if [[ -n "$REDIS_PASSWORD" ]]; then
|
|
||||||
CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping)
|
|
||||||
else
|
|
||||||
CMD=("${BASE[@]}" ping)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Erfolgreich nur bei exakt "PONG"
|
|
||||||
[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
|
||||||
EOSH
|
|
||||||
chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
|
||||||
|
|
||||||
# Rspamd-Heal (setzt Laufzeitverzeichnis, leert alte Socke, restarts rspamd)
|
|
||||||
cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
APP_ENV="/var/www/mailwolt/.env"
|
|
||||||
|
|
||||||
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
||||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
|
||||||
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
|
||||||
|
|
||||||
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
|
||||||
if [[ -z "${REDIS_PASSWORD}" && -r "$APP_ENV" ]]; then
|
|
||||||
REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rspamd Runtime fixen
|
|
||||||
install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
|
||||||
[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
|
||||||
|
|
||||||
# Neustart
|
|
||||||
systemctl restart rspamd
|
|
||||||
|
|
||||||
# Mini-Healthcheck
|
|
||||||
sleep 2
|
|
||||||
ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
EOSH
|
|
||||||
chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# WoltGuard Wrapper + Unit
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
cat >/usr/local/bin/woltguard <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
case "${1:-status}" in
|
|
||||||
start) systemctl enable --now monit ;;
|
|
||||||
stop) systemctl stop monit ;;
|
|
||||||
status) monit summary || systemctl status monit || true ;;
|
|
||||||
heal) monit reload || true; sleep 1; monit restart all || true ;;
|
|
||||||
monitor) monit monitor all || true ;;
|
|
||||||
unmonitor) monit unmonitor all || true ;;
|
|
||||||
*) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
|
|
||||||
esac
|
|
||||||
EOSH
|
|
||||||
chmod 0755 /usr/local/bin/woltguard
|
|
||||||
|
|
||||||
cat >/etc/systemd/system/woltguard.service <<'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=WoltGuard – Self-Healing Monitor for MailWolt
|
|
||||||
After=network.target
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/usr/local/bin/woltguard start
|
|
||||||
ExecStop=/usr/local/bin/woltguard stop
|
|
||||||
RemainAfterExit=yes
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now woltguard
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Monit Basis + includes
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
|
||||||
grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
|
|
||||||
install -d -m 0755 /etc/monit/conf.d
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Monit Checks
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# 10 – Redis zuerst (abhängig für rspamd)
|
|
||||||
cat >/etc/monit/conf.d/10-redis.conf <<'EOF'
|
|
||||||
check process redis with pidfile /run/redis/redis-server.pid
|
|
||||||
start program = "/bin/systemctl start redis-server"
|
|
||||||
stop program = "/bin/systemctl stop redis-server"
|
|
||||||
if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
|
|
||||||
check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
|
||||||
if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 20 – Rspamd (hängt von Redis ab), robust über process-matching
|
|
||||||
cat >/etc/monit/conf.d/20-rspamd.conf <<'EOF'
|
|
||||||
check process rspamd matching "/usr/bin/rspamd"
|
|
||||||
start program = "/bin/systemctl start rspamd"
|
|
||||||
stop program = "/bin/systemctl stop rspamd"
|
|
||||||
depends on redis
|
|
||||||
if failed host 127.0.0.1 port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
if failed host 127.0.0.1 port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 30 – Maildienste
|
|
||||||
cat >/etc/monit/conf.d/30-postfix.conf <<'EOF'
|
|
||||||
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
|
||||||
start program = "/bin/systemctl start postfix"
|
|
||||||
stop program = "/bin/systemctl stop postfix"
|
|
||||||
if failed port 25 protocol smtp then restart
|
|
||||||
if failed port 465 type tcpssl then restart
|
|
||||||
if failed port 587 type tcp then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >/etc/monit/conf.d/30-dovecot.conf <<'EOF'
|
|
||||||
check process dovecot with pidfile /run/dovecot/master.pid
|
|
||||||
start program = "/bin/systemctl start dovecot"
|
|
||||||
stop program = "/bin/systemctl stop dovecot"
|
|
||||||
if failed port 993 type tcpssl for 2 cycles then restart
|
|
||||||
if failed port 24 protocol lmtp for 2 cycles then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 40 – Web/PHP
|
|
||||||
cat >/etc/monit/conf.d/40-nginx.conf <<'EOF'
|
|
||||||
check process nginx with pidfile /run/nginx.pid
|
|
||||||
start program = "/bin/systemctl start nginx"
|
|
||||||
stop program = "/bin/systemctl stop nginx"
|
|
||||||
if failed port 80 type tcp then restart
|
|
||||||
if failed port 443 type tcpssl then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 50 – DKIM/DMARC
|
|
||||||
cat >/etc/monit/conf.d/50-opendkim.conf <<'EOF'
|
|
||||||
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
|
||||||
start program = "/bin/systemctl start opendkim"
|
|
||||||
stop program = "/bin/systemctl stop opendkim"
|
|
||||||
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# optional: OpenDMARC
|
|
||||||
if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
|
||||||
cat >/etc/monit/conf.d/55-opendmarc.conf <<'EOF'
|
|
||||||
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
|
||||||
start program = "/bin/systemctl start opendmarc"
|
|
||||||
stop program = "/bin/systemctl stop opendmarc"
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
rm -f /etc/monit/conf.d/55-opendmarc.conf || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 60 – optional: ClamAV
|
|
||||||
if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
|
||||||
cat >/etc/monit/conf.d/60-clamav.conf <<'EOF'
|
|
||||||
check process clamd with pidfile /run/clamav/clamd.pid
|
|
||||||
start program = "/bin/systemctl start clamav-daemon"
|
|
||||||
stop program = "/bin/systemctl stop clamav-daemon"
|
|
||||||
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
|
||||||
if 5 restarts within 5 cycles then timeout
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
rm -f /etc/monit/conf.d/60-clamav.conf || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 70 – Fail2Ban (optional, standardmäßig aktiv)
|
|
||||||
if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
|
||||||
cat >/etc/monit/conf.d/70-fail2ban.conf <<'EOF'
|
|
||||||
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
|
||||||
start program = "/bin/systemctl start fail2ban"
|
|
||||||
stop program = "/bin/systemctl stop fail2ban"
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
rm -f /etc/monit/conf.d/70-fail2ban.conf || true
|
|
||||||
fi
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# Monit neu laden
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
monit -t
|
|
||||||
systemctl reload monit || systemctl restart monit
|
|
||||||
systemctl status monit --no-pager || true
|
|
||||||
log "[✓] WoltGuard aktiv."
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#source ./lib.sh
|
|
||||||
#
|
|
||||||
#log "WoltGuard (Monit + Self-Heal) einrichten …"
|
|
||||||
#
|
|
||||||
#set +u
|
|
||||||
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
#set -u
|
|
||||||
#CLAMAV_ENABLE="${CLAMAV_ENABLE:-0}"
|
|
||||||
#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-0}"
|
|
||||||
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
#
|
|
||||||
## Pakete sicherstellen
|
|
||||||
#command -v monit >/dev/null || { apt-get update -qq; apt-get install -y monit; }
|
|
||||||
#systemctl enable --now monit
|
|
||||||
#
|
|
||||||
## Helper-Skripte
|
|
||||||
#install -d -m 0755 /usr/local/sbin
|
|
||||||
#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#PASS=""
|
|
||||||
#[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
|
|
||||||
#if command -v redis-cli >/dev/null 2>&1; then
|
|
||||||
# [[ -n "${REDIS_PASS:-}" ]] \
|
|
||||||
# && redis-cli -h 127.0.0.1 -p 6379 -a "$REDIS_PASS" ping | grep -q PONG \
|
|
||||||
# || redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG
|
|
||||||
#else
|
|
||||||
# exit 1
|
|
||||||
#fi
|
|
||||||
#EOSH
|
|
||||||
#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
|
||||||
#
|
|
||||||
#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
||||||
#REDIS_PORT="${REDIS_PORT:-6379}"
|
|
||||||
#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
|
||||||
#
|
|
||||||
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
#APP_ENV="/var/www/mailwolt/.env"
|
|
||||||
#REDIS_CLI="$(command -v redis-cli || true)"
|
|
||||||
#SYSTEMCTL="$(command -v systemctl || true)"
|
|
||||||
#RSPAMD_SERVICE="rspamd"
|
|
||||||
#
|
|
||||||
#if [ -r "$INSTALLER_ENV" ]; then . "$INSTALLER_ENV"; fi
|
|
||||||
#if [ -z "${REDIS_PASSWORD}" ] && [ -r "$APP_ENV" ]; then
|
|
||||||
# REDIS_PASSWORD="$(grep -E '^REDIS_PASSWORD=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#if [ -n "$REDIS_CLI" ]; then
|
|
||||||
# echo "[INFO] Prüfe Redis Verbindung..."
|
|
||||||
# if [ -n "${REDIS_PASSWORD}" ]; then
|
|
||||||
# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" -a "$REDIS_PASSWORD" ping | grep -q '^PONG$'; then
|
|
||||||
# echo "[WARN] Redis antwortet nicht oder Passwort falsch!"
|
|
||||||
# else
|
|
||||||
# echo "[OK] Redis antwortet (auth ok)."
|
|
||||||
# fi
|
|
||||||
# else
|
|
||||||
# if ! "$REDIS_CLI" -h "$REDIS_HOST" -p "$REDIS_PORT" ping | grep -q '^PONG$'; then
|
|
||||||
# echo "[WARN] Redis antwortet nicht (ohne Passwort)."
|
|
||||||
# else
|
|
||||||
# echo "[OK] Redis antwortet (kein Passwort)."
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
#else
|
|
||||||
# echo "[WARN] redis-cli nicht gefunden – überspringe Test."
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#echo "[INFO] Prüfe Rspamd Socket & Verzeichnis..."
|
|
||||||
#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
|
||||||
#[ -S /var/lib/rspamd/rspamd.sock ] && rm -f /var/lib/rspamd/rspamd.sock || true
|
|
||||||
#
|
|
||||||
#echo "[INFO] Starte Rspamd neu..."
|
|
||||||
#if [ -n "$SYSTEMCTL" ]; then
|
|
||||||
# "$SYSTEMCTL" restart "$RSPAMD_SERVICE"
|
|
||||||
# echo "[OK] Rspamd erfolgreich neu gestartet."
|
|
||||||
#else
|
|
||||||
# echo "[ERROR] systemctl nicht gefunden – kein Neustart möglich."
|
|
||||||
# exit 1
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#echo "[INFO] Healthcheck (Port 11334)..."
|
|
||||||
#sleep 3
|
|
||||||
#if ss -tln | grep -q ':11334'; then
|
|
||||||
# echo "[OK] Rspamd Controller läuft auf Port 11334."
|
|
||||||
#else
|
|
||||||
# echo "[WARN] Rspamd Controller Port 11334 nicht erreichbar."
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#echo "[DONE] Mailwolt Rspamd-Heal abgeschlossen."
|
|
||||||
#exit 0
|
|
||||||
#EOSH
|
|
||||||
#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
|
||||||
#
|
|
||||||
## WoltGuard Wrapper + Unit
|
|
||||||
#cat >/usr/local/bin/woltguard <<'EOSH'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#case "${1:-status}" in
|
|
||||||
# start) systemctl enable --now monit ;;
|
|
||||||
# stop) systemctl stop monit ;;
|
|
||||||
# status) monit summary || systemctl status monit || true ;;
|
|
||||||
# heal) monit reload || true; sleep 1; monit restart all || true ;;
|
|
||||||
# monitor) monit monitor all || true ;;
|
|
||||||
# unmonitor) monit unmonitor all || true ;;
|
|
||||||
# *) echo "Usage: woltguard {start|stop|status|heal|monitor|unmonitor}"; exit 2;;
|
|
||||||
#esac
|
|
||||||
#EOSH
|
|
||||||
#chmod 0755 /usr/local/bin/woltguard
|
|
||||||
#
|
|
||||||
#cat >/etc/systemd/system/woltguard.service <<'EOF'
|
|
||||||
#[Unit]
|
|
||||||
#Description=WoltGuard – Self-Healing Monitor for MailWolt
|
|
||||||
#After=network.target
|
|
||||||
#[Service]
|
|
||||||
#Type=oneshot
|
|
||||||
#ExecStart=/usr/local/bin/woltguard start
|
|
||||||
#ExecStop=/usr/local/bin/woltguard stop
|
|
||||||
#RemainAfterExit=yes
|
|
||||||
#[Install]
|
|
||||||
#WantedBy=multi-user.target
|
|
||||||
#EOF
|
|
||||||
#systemctl daemon-reload
|
|
||||||
#systemctl enable --now woltguard
|
|
||||||
#
|
|
||||||
## Monit Basis + include
|
|
||||||
#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
|
||||||
#grep -q 'include /etc/monit/conf.d/*' /etc/monit/monitrc || echo 'include /etc/monit/conf.d/*' >>/etc/monit/monitrc
|
|
||||||
#install -d -m 0755 /etc/monit/conf.d
|
|
||||||
#
|
|
||||||
## Checks
|
|
||||||
#cat >/etc/monit/conf.d/postfix.conf <<'EOF'
|
|
||||||
#check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
|
||||||
# start program = "/bin/systemctl start postfix"
|
|
||||||
# stop program = "/bin/systemctl stop postfix"
|
|
||||||
# if failed port 25 protocol smtp then restart
|
|
||||||
# if failed port 465 type tcpssl then restart
|
|
||||||
# if failed port 587 type tcp then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#cat >/etc/monit/conf.d/dovecot.conf <<'EOF'
|
|
||||||
#check process dovecot with pidfile /run/dovecot/master.pid
|
|
||||||
# start program = "/bin/systemctl start dovecot"
|
|
||||||
# stop program = "/bin/systemctl stop dovecot"
|
|
||||||
# if failed port 993 type tcpssl for 2 cycles then restart
|
|
||||||
# if failed port 24 protocol lmtp for 2 cycles then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#cat >/etc/monit/conf.d/nginx.conf <<'EOF'
|
|
||||||
#check process nginx with pidfile /run/nginx.pid
|
|
||||||
# start program = "/bin/systemctl start nginx"
|
|
||||||
# stop program = "/bin/systemctl stop nginx"
|
|
||||||
# if failed port 80 type tcp then restart
|
|
||||||
# if failed port 443 type tcpssl then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#cat >/etc/monit/conf.d/redis.conf <<'EOF'
|
|
||||||
#check process redis with pidfile /run/redis/redis-server.pid
|
|
||||||
# start program = "/bin/systemctl start redis-server"
|
|
||||||
# stop program = "/bin/systemctl stop redis-server"
|
|
||||||
# if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#
|
|
||||||
#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
|
||||||
# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#cat >/etc/monit/conf.d/rspamd.conf <<'EOF'
|
|
||||||
#check process rspamd with pidfile /run/rspamd/rspamd.pid
|
|
||||||
# start program = "/bin/systemctl start rspamd"
|
|
||||||
# stop program = "/bin/systemctl stop rspamd"
|
|
||||||
# if failed port 11333 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
# if failed port 11334 for 2 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#cat >/etc/monit/conf.d/opendkim.conf <<'EOF'
|
|
||||||
#check process opendkim with pidfile /run/opendkim/opendkim.pid
|
|
||||||
# start program = "/bin/systemctl start opendkim"
|
|
||||||
# stop program = "/bin/systemctl stop opendkim"
|
|
||||||
# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## optional: OpenDMARC
|
|
||||||
#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
|
||||||
# cat >/etc/monit/conf.d/opendmarc.conf <<'EOF'
|
|
||||||
#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
|
||||||
# start program = "/bin/systemctl start opendmarc"
|
|
||||||
# stop program = "/bin/systemctl stop opendmarc"
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#else
|
|
||||||
# rm -f /etc/monit/conf.d/opendmarc.conf || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## optional: ClamAV
|
|
||||||
#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
|
||||||
# cat >/etc/monit/conf.d/clamav.conf <<'EOF'
|
|
||||||
#check process clamd with pidfile /run/clamav/clamd.pid
|
|
||||||
# start program = "/bin/systemctl start clamav-daemon"
|
|
||||||
# stop program = "/bin/systemctl stop clamav-daemon"
|
|
||||||
# if failed unixsocket /run/clamav/clamd.ctl then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#else
|
|
||||||
# rm -f /etc/monit/conf.d/clamav.conf || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## optional: Fail2Ban
|
|
||||||
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
|
||||||
# cat >/etc/monit/conf.d/fail2ban.conf <<'EOF'
|
|
||||||
#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
|
||||||
# start program = "/bin/systemctl start fail2ban"
|
|
||||||
# stop program = "/bin/systemctl stop fail2ban"
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#else
|
|
||||||
# rm -f /etc/monit/conf.d/fail2ban.conf || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#monit -t
|
|
||||||
#systemctl reload monit || systemctl restart monit
|
|
||||||
#systemctl status monit --no-pager || true
|
|
||||||
#log "[✓] WoltGuard aktiv."
|
|
||||||
|
|
@ -1,439 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Flags laden (falls vorhanden)
|
|
||||||
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
|
|
||||||
if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
|
||||||
. "$INSTALLER_ENV"
|
|
||||||
fi
|
|
||||||
CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
|
||||||
OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
|
||||||
FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
MONIT_HTTP="${MONIT_HTTP:-1}"
|
|
||||||
|
|
||||||
# ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
|
|
||||||
install -d -m 0755 /etc/monit/monitrc.d
|
|
||||||
install -d -m 0755 /etc/monit/conf.d # passiver Ablageort (NICHT includiert)
|
|
||||||
|
|
||||||
# Poll-Intervall (30s)
|
|
||||||
sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
|
||||||
# alle alten include-Zeilen raus und monitrc.d setzen
|
|
||||||
sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
|
|
||||||
grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
|
|
||||||
|| echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
|
|
||||||
|
|
||||||
# Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
|
|
||||||
if [[ "$MONIT_HTTP" = "1" ]]; then
|
|
||||||
grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
|
|
||||||
set httpd port 2812 and
|
|
||||||
use address localhost
|
|
||||||
allow localhost
|
|
||||||
HTTP
|
|
||||||
fi
|
|
||||||
|
|
||||||
# KEIN Löschen mehr der Dateien – wir verschieben je nach Status
|
|
||||||
# (vorher stand hier rm -rf /etc/monit/monitrc.d/* und rm -f /etc/monit/conf.d/*.conf)
|
|
||||||
|
|
||||||
# ── Helper-Skripte ──────────────────────────────────────────────────────────
|
|
||||||
install -d -m 0755 /usr/local/sbin
|
|
||||||
|
|
||||||
# Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
|
|
||||||
cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
APP_ENV="/var/www/mailwolt/.env"
|
|
||||||
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
||||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
|
||||||
REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
|
||||||
REDIS_PASS="${REDIS_PASS:-}"
|
|
||||||
|
|
||||||
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
|
||||||
if [[ -r "$APP_ENV" ]]; then
|
|
||||||
[[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
[[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
[[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
fi
|
|
||||||
[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
|
||||||
|
|
||||||
strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
|
|
||||||
REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
|
||||||
REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
|
||||||
REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
|
||||||
|
|
||||||
command -v redis-cli >/dev/null 2>&1 || exit 1
|
|
||||||
BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
|
||||||
[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
|
|
||||||
[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
|
||||||
EOSH
|
|
||||||
chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
|
||||||
|
|
||||||
# Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
|
|
||||||
cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
APP_ENV="/var/www/mailwolt/.env"
|
|
||||||
|
|
||||||
REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
||||||
REDIS_PORT="${REDIS_PORT:-6379}"
|
|
||||||
REDIS_PASS="${REDIS_PASS:-}"
|
|
||||||
|
|
||||||
[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
|
||||||
if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
|
|
||||||
REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Rspamd Runtime fixen
|
|
||||||
install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
|
||||||
[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
|
||||||
|
|
||||||
echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
|
|
||||||
|
|
||||||
# Neustart
|
|
||||||
systemctl restart rspamd
|
|
||||||
|
|
||||||
# Mini-Healthcheck
|
|
||||||
sleep 2
|
|
||||||
ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
EOSH
|
|
||||||
chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
|
||||||
|
|
||||||
# ── Monit-Checks (nummeriert) – fixe Dienste immer aktiv ────────────────────
|
|
||||||
# 10 – Redis
|
|
||||||
cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
|
|
||||||
check process redis with pidfile /run/redis/redis-server.pid
|
|
||||||
start program = "/bin/systemctl start redis-server"
|
|
||||||
stop program = "/bin/systemctl stop redis-server"
|
|
||||||
if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
|
|
||||||
check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
|
||||||
if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 20 – Rspamd (robust via process-matching + Heal)
|
|
||||||
cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
|
|
||||||
check process rspamd matching "rspamd: main process"
|
|
||||||
start program = "/bin/systemctl start rspamd" with timeout 120 seconds
|
|
||||||
stop program = "/bin/systemctl stop rspamd"
|
|
||||||
depends on redis
|
|
||||||
if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
if does not exist for 2 cycles then restart
|
|
||||||
if 5 restarts within 10 cycles then unmonitor
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 30 – Postfix
|
|
||||||
cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
|
|
||||||
check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
|
||||||
start program = "/bin/systemctl start postfix"
|
|
||||||
stop program = "/bin/systemctl stop postfix"
|
|
||||||
if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
|
|
||||||
if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
|
|
||||||
if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
|
|
||||||
cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
|
|
||||||
check process dovecot with pidfile /run/dovecot/master.pid
|
|
||||||
start program = "/bin/systemctl start dovecot"
|
|
||||||
stop program = "/bin/systemctl stop dovecot"
|
|
||||||
if failed port 993 type tcpssl for 3 cycles then restart
|
|
||||||
if 5 restarts within 10 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 40 – Nginx
|
|
||||||
cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
|
|
||||||
check process nginx with pidfile /run/nginx.pid
|
|
||||||
start program = "/bin/systemctl start nginx"
|
|
||||||
stop program = "/bin/systemctl stop nginx"
|
|
||||||
if failed port 80 type tcp then restart
|
|
||||||
if failed port 443 type tcpssl then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# 50 – OpenDKIM
|
|
||||||
cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
|
|
||||||
check process opendkim with pidfile /run/opendkim/opendkim.pid
|
|
||||||
start program = "/bin/systemctl start opendkim"
|
|
||||||
stop program = "/bin/systemctl stop opendkim"
|
|
||||||
if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
move_monit_conf() {
|
|
||||||
local name="$1" # z.B. 55-opendmarc
|
|
||||||
local enabled="$2" # "0" oder "1"
|
|
||||||
local src="/etc/monit/conf.d/${name}.conf"
|
|
||||||
local dst="/etc/monit/monitrc.d/${name}.conf"
|
|
||||||
|
|
||||||
mkdir -p /etc/monit/conf.d /etc/monit/monitrc.d
|
|
||||||
|
|
||||||
# Falls Datei nirgends existiert → in conf.d anlegen (lesbare Quelle)
|
|
||||||
if [[ ! -f "$src" && ! -f "$dst" ]]; then
|
|
||||||
cat >"$src" <<'EOF_PAYLOAD'
|
|
||||||
__PAYLOAD__
|
|
||||||
EOF_PAYLOAD
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$enabled" = "1" ]]; then
|
|
||||||
# Aktiv: in monitrc.d haben
|
|
||||||
if [[ -f "$src" && ! -f "$dst" ]]; then
|
|
||||||
mv -f "$src" "$dst"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Inaktiv: in conf.d haben
|
|
||||||
if [[ -f "$dst" && ! -f "$src" ]]; then
|
|
||||||
mv -f "$dst" "$src"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
move_monit_conf "55-opendmarc" "${OPENDMARC_ENABLE:-0}" <<'EOF'
|
|
||||||
check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
|
||||||
start program = "/bin/systemctl start opendmarc"
|
|
||||||
stop program = "/bin/systemctl stop opendmarc"
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
move_monit_conf "60-clamav" "${CLAMAV_ENABLE:-0}" <<'EOF'
|
|
||||||
check process clamd matching "clamd"
|
|
||||||
start program = "/bin/systemctl start clamav-daemon"
|
|
||||||
stop program = "/bin/systemctl stop clamav-daemon"
|
|
||||||
if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
|
||||||
if 5 restarts within 10 cycles then unmonitor
|
|
||||||
EOF
|
|
||||||
|
|
||||||
move_monit_conf "70-fail2ban" "${FAIL2BAN_ENABLE:-0}" <<'EOF'
|
|
||||||
check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
|
||||||
start program = "/bin/systemctl start fail2ban"
|
|
||||||
stop program = "/bin/systemctl stop fail2ban"
|
|
||||||
if 5 restarts within 5 cycles then alert
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# ── Monit neu laden ─────────────────────────────────────────────────────────
|
|
||||||
monit -t
|
|
||||||
systemctl reload monit || systemctl restart monit
|
|
||||||
|
|
||||||
# Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
|
|
||||||
# sleep 2
|
|
||||||
# monit summary || true
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
## Flags laden (falls vorhanden)
|
|
||||||
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
#: "${CLAMAV_ENABLE:=}"; : "${OPENDMARC_ENABLE:=}"; : "${FAIL2BAN_ENABLE:=}"; : "${MONIT_HTTP:=}"
|
|
||||||
#if [[ -z "${CLAMAV_ENABLE}${OPENDMARC_ENABLE}${FAIL2BAN_ENABLE}" && -r "$INSTALLER_ENV" ]]; then
|
|
||||||
# . "$INSTALLER_ENV"
|
|
||||||
#fi
|
|
||||||
#CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
|
||||||
#OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
|
||||||
#FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
#MONIT_HTTP="${MONIT_HTTP:-1}"
|
|
||||||
#
|
|
||||||
## ── Monit so konfigurieren, dass NUR monitrc.d/* geladen wird ────────────────
|
|
||||||
#install -d -m 0755 /etc/monit/monitrc.d
|
|
||||||
## Poll-Intervall (30s)
|
|
||||||
#sed -i 's/^set daemon .*/set daemon 30/' /etc/monit/monitrc || true
|
|
||||||
## alle alten include-Zeilen raus und monitrc.d setzen
|
|
||||||
#sed -i 's|^#\?\s*include .*$||g' /etc/monit/monitrc
|
|
||||||
#grep -q '^include /etc/monit/monitrc.d/\*' /etc/monit/monitrc \
|
|
||||||
# || echo 'include /etc/monit/monitrc.d/*' >> /etc/monit/monitrc
|
|
||||||
#
|
|
||||||
## Optional: HTTP-UI nur einschalten, wenn explizit gewünscht
|
|
||||||
#if [[ "$MONIT_HTTP" = "1" ]]; then
|
|
||||||
# grep -q '^set httpd port 2812' /etc/monit/monitrc || cat >>/etc/monit/monitrc <<'HTTP'
|
|
||||||
#set httpd port 2812 and
|
|
||||||
# use address localhost
|
|
||||||
# allow localhost
|
|
||||||
#HTTP
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#sudo mkdir -p /etc/monit/monitrc.d
|
|
||||||
#sudo rm -rf /etc/monit/monitrc.d/* 2>/dev/null || true
|
|
||||||
#sudo rm -f /etc/monit/conf.d/*.conf 2>/dev/null || true
|
|
||||||
#
|
|
||||||
## ── Helper-Skripte ──────────────────────────────────────────────────────────
|
|
||||||
#install -d -m 0755 /usr/local/sbin
|
|
||||||
#
|
|
||||||
## Redis-Ping (Password: REDIS_PASSWORD aus installer.env oder .env)
|
|
||||||
#cat >/usr/local/sbin/mailwolt-redis-ping.sh <<'EOSH'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
#APP_ENV="/var/www/mailwolt/.env"
|
|
||||||
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
||||||
#REDIS_PORT="${REDIS_PORT:-6379}"
|
|
||||||
#REDIS_PASSWORD="${REDIS_PASSWORD:-}"
|
|
||||||
#REDIS_PASS="${REDIS_PASS:-}"
|
|
||||||
#
|
|
||||||
#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV" || true
|
|
||||||
#if [[ -r "$APP_ENV" ]]; then
|
|
||||||
# [[ -z "${REDIS_HOST}" ]] && REDIS_HOST="$(grep -m1 '^REDIS_HOST=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
# [[ -z "${REDIS_PORT}" ]] && REDIS_PORT="$(grep -m1 '^REDIS_PORT=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
# [[ -z "${REDIS_PASSWORD}" ]] && REDIS_PASSWORD="$(grep -m1 '^REDIS_PASSWORD=' "$APP_ENV" | cut -d= -f2- || true)"
|
|
||||||
#fi
|
|
||||||
#[[ -z "${REDIS_PASSWORD}" && -n "${REDIS_PASS}" ]] && REDIS_PASSWORD="$REDIS_PASS"
|
|
||||||
#
|
|
||||||
#strip(){ printf '%s' "$1" | sed -E 's/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/'; }
|
|
||||||
#REDIS_HOST="$(strip "${REDIS_HOST:-}")"
|
|
||||||
#REDIS_PORT="$(strip "${REDIS_PORT:-}")"
|
|
||||||
#REDIS_PASSWORD="$(strip "${REDIS_PASSWORD:-}")"
|
|
||||||
#
|
|
||||||
#command -v redis-cli >/dev/null 2>&1 || exit 1
|
|
||||||
#BASE=(timeout 2 redis-cli --no-auth-warning --raw -h "$REDIS_HOST" -p "$REDIS_PORT")
|
|
||||||
#[[ -n "$REDIS_PASSWORD" ]] && CMD=("${BASE[@]}" -a "$REDIS_PASSWORD" ping) || CMD=("${BASE[@]}" ping)
|
|
||||||
#[[ "$("${CMD[@]}" 2>/dev/null || true)" == "PONG" ]]
|
|
||||||
#EOSH
|
|
||||||
#chmod 0755 /usr/local/sbin/mailwolt-redis-ping.sh
|
|
||||||
#
|
|
||||||
## Rspamd-Heal (Socke aufräumen, restart, Mini-Port-Check)
|
|
||||||
#cat >/usr/local/sbin/mailwolt-rspamd-heal.sh <<'EOSH'
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
#INSTALLER_ENV="/etc/mailwolt/installer.env"
|
|
||||||
#APP_ENV="/var/www/mailwolt/.env"
|
|
||||||
#
|
|
||||||
#REDIS_HOST="${REDIS_HOST:-127.0.0.1}"
|
|
||||||
#REDIS_PORT="${REDIS_PORT:-6379}"
|
|
||||||
#REDIS_PASS="${REDIS_PASS:-}"
|
|
||||||
#
|
|
||||||
#[[ -r "$INSTALLER_ENV" ]] && . "$INSTALLER_ENV"
|
|
||||||
#if [[ -z "${REDIS_PASS}" && -r "$APP_ENV" ]]; then
|
|
||||||
# REDIS_PASS="$(grep -E '^REDIS_PASS=' "$APP_ENV" | head -n1 | cut -d= -f2- || true)"
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## Rspamd Runtime fixen
|
|
||||||
#install -d -m 0755 -o _rspamd -g _rspamd /run/rspamd || true
|
|
||||||
#[[ -S /var/lib/rspamd/rspamd.sock ]] && rm -f /var/lib/rspamd/rspamd.sock || true
|
|
||||||
#
|
|
||||||
#echo "$(date '+%F %T') heal run" >> /var/log/rspamd-heal.log
|
|
||||||
#
|
|
||||||
## Neustart
|
|
||||||
#systemctl restart rspamd
|
|
||||||
#
|
|
||||||
## Mini-Healthcheck
|
|
||||||
#sleep 2
|
|
||||||
#ss -tln | grep -q ':11334' || echo "[WARN] Rspamd Controller Port 11334 nicht sichtbar"
|
|
||||||
#
|
|
||||||
#exit 0
|
|
||||||
#EOSH
|
|
||||||
#chmod 0755 /usr/local/sbin/mailwolt-rspamd-heal.sh
|
|
||||||
#
|
|
||||||
## ── Monit-Checks (nummeriert) ───────────────────────────────────────────────
|
|
||||||
## 10 – Redis
|
|
||||||
#cat >/etc/monit/monitrc.d/10-redis.conf <<'EOF'
|
|
||||||
#check process redis with pidfile /run/redis/redis-server.pid
|
|
||||||
# start program = "/bin/systemctl start redis-server"
|
|
||||||
# stop program = "/bin/systemctl stop redis-server"
|
|
||||||
# if failed host 127.0.0.1 port 6379 for 2 cycles then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#
|
|
||||||
#check program redis_ping path "/usr/local/sbin/mailwolt-redis-ping.sh"
|
|
||||||
# if status != 0 for 2 cycles then exec "/bin/systemctl restart redis-server"
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## 20 – Rspamd (robust via process-matching + Heal)
|
|
||||||
#cat >/etc/monit/monitrc.d/20-rspamd.conf <<'EOF'
|
|
||||||
#check process rspamd matching "rspamd: main process"
|
|
||||||
# start program = "/bin/systemctl start rspamd" with timeout 120 seconds
|
|
||||||
# stop program = "/bin/systemctl stop rspamd"
|
|
||||||
# depends on redis
|
|
||||||
# if failed host 127.0.0.1 port 11333 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
# if failed host 127.0.0.1 port 11334 for 3 cycles then exec "/usr/local/sbin/mailwolt-rspamd-heal.sh"
|
|
||||||
# if does not exist for 2 cycles then restart
|
|
||||||
# if 5 restarts within 10 cycles then unmonitor
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## 30 – Postfix
|
|
||||||
#cat >/etc/monit/monitrc.d/30-postfix.conf <<'EOF'
|
|
||||||
#check process postfix with pidfile /var/spool/postfix/pid/master.pid
|
|
||||||
# start program = "/bin/systemctl start postfix"
|
|
||||||
# stop program = "/bin/systemctl stop postfix"
|
|
||||||
# if failed host 127.0.0.1 port 25 type tcp with timeout 15 seconds for 3 cycles then restart
|
|
||||||
# if failed host 127.0.0.1 port 465 type tcpssl with timeout 10 seconds then restart
|
|
||||||
# if failed host 127.0.0.1 port 587 type tcp with timeout 10 seconds then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## 30 – Dovecot (IMAPS; LMTP oft Unix-Socket → kein TCP-Fehlalarm)
|
|
||||||
#cat >/etc/monit/monitrc.d/30-dovecot.conf <<'EOF'
|
|
||||||
#check process dovecot with pidfile /run/dovecot/master.pid
|
|
||||||
# start program = "/bin/systemctl start dovecot"
|
|
||||||
# stop program = "/bin/systemctl stop dovecot"
|
|
||||||
# if failed port 993 type tcpssl for 3 cycles then restart
|
|
||||||
# if 5 restarts within 10 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## 40 – Nginx
|
|
||||||
#cat >/etc/monit/monitrc.d/40-nginx.conf <<'EOF'
|
|
||||||
#check process nginx with pidfile /run/nginx.pid
|
|
||||||
# start program = "/bin/systemctl start nginx"
|
|
||||||
# stop program = "/bin/systemctl stop nginx"
|
|
||||||
# if failed port 80 type tcp then restart
|
|
||||||
# if failed port 443 type tcpssl then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## 50 – OpenDKIM
|
|
||||||
#cat >/etc/monit/monitrc.d/50-opendkim.conf <<'EOF'
|
|
||||||
#check process opendkim with pidfile /run/opendkim/opendkim.pid
|
|
||||||
# start program = "/bin/systemctl start opendkim"
|
|
||||||
# stop program = "/bin/systemctl stop opendkim"
|
|
||||||
# if failed host 127.0.0.1 port 8891 type tcp for 2 cycles then restart
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
## 55 – OpenDMARC (optional)
|
|
||||||
#if [[ "$OPENDMARC_ENABLE" = "1" ]]; then
|
|
||||||
# cat >/etc/monit/monitrc.d/55-opendmarc.conf <<'EOF'
|
|
||||||
#check process opendmarc with pidfile /run/opendmarc/opendmarc.pid
|
|
||||||
# start program = "/bin/systemctl start opendmarc"
|
|
||||||
# stop program = "/bin/systemctl stop opendmarc"
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#else
|
|
||||||
# rm -f /etc/monit/monitrc.d/55-opendmarc.conf || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## 60 – ClamAV (über Socket)
|
|
||||||
#if [[ "$CLAMAV_ENABLE" = "1" ]]; then
|
|
||||||
# cat >/etc/monit/monitrc.d/60-clamav.conf <<'EOF'
|
|
||||||
#check process clamd matching "clamd"
|
|
||||||
# start program = "/bin/systemctl start clamav-daemon"
|
|
||||||
# stop program = "/bin/systemctl stop clamav-daemon"
|
|
||||||
# if failed unixsocket /run/clamav/clamd.ctl for 3 cycles then restart
|
|
||||||
# if 5 restarts within 10 cycles then unmonitor
|
|
||||||
#EOF
|
|
||||||
#else
|
|
||||||
# rm -f /etc/monit/monitrc.d/60-clamav.conf || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## 70 – Fail2Ban (optional)
|
|
||||||
#if [[ "$FAIL2BAN_ENABLE" = "1" ]]; then
|
|
||||||
# cat >/etc/monit/monitrc.d/70-fail2ban.conf <<'EOF'
|
|
||||||
#check process fail2ban with pidfile /run/fail2ban/fail2ban.pid
|
|
||||||
# start program = "/bin/systemctl start fail2ban"
|
|
||||||
# stop program = "/bin/systemctl stop fail2ban"
|
|
||||||
# if 5 restarts within 5 cycles then alert
|
|
||||||
#EOF
|
|
||||||
#else
|
|
||||||
# rm -f /etc/monit/monitrc.d/70-fail2ban.conf || true
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
## ── Monit neu laden ─────────────────────────────────────────────────────────
|
|
||||||
#monit -t
|
|
||||||
#systemctl reload monit || systemctl restart monit
|
|
||||||
#
|
|
||||||
## Optionaler Sichttest (CLI funktioniert auch ohne HTTP-UI)
|
|
||||||
##sleep 2
|
|
||||||
##monit summary || true
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
log "MOTD installieren …"
|
|
||||||
install -d /usr/local/bin
|
|
||||||
|
|
||||||
cat >/usr/local/bin/mw-motd <<'SH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
# MOTD – MailWolt
|
|
||||||
# bewusst KEIN "set -e" und KEIN pipefail; MOTD darf nie hart abbrechen
|
|
||||||
set -u
|
|
||||||
|
|
||||||
# ---------- Farben ----------
|
|
||||||
NC="\033[0m"; WH="\033[1;37m"; CY="\033[1;36m"; GY="\033[0;90m"
|
|
||||||
GR="\033[1;32m"; YE="\033[1;33m"; RD="\033[1;31m"
|
|
||||||
|
|
||||||
# ---------- Breite / Zentrierung ----------
|
|
||||||
W=110
|
|
||||||
term_cols=$(tput cols 2>/dev/null || echo $W)
|
|
||||||
[ "$term_cols" -gt "$W" ] && pad=$(( (term_cols - W)/2 )) || pad=0
|
|
||||||
sp(){ [ "$1" -gt 0 ] && printf "%${1}s" " " || true; }
|
|
||||||
center() { local s="$1"; local n=$(( (W - ${#s})/2 )); sp $((pad+n)); printf "%s\n" "$s"; }
|
|
||||||
rule(){ sp "$pad"; printf "%0.s=" $(seq 1 "$W"); printf "\n"; }
|
|
||||||
title(){ sp "$pad"; local t="$1"; local lf=$(( (W - ${#t} - 2)/2 )); local rf=$(( W - ${#t} - 2 - lf )); \
|
|
||||||
printf "%s" "$(printf '─%.0s' $(seq 1 $lf))"; printf " %s " "$t"; printf "%s\n" "$(printf '─%.0s' $(seq 1 $rf))"; }
|
|
||||||
kv(){ sp "$pad"; printf "%-12s: %s\n" "$1" "$2"; }
|
|
||||||
|
|
||||||
# ---------- Installer-/App-Variablen ----------
|
|
||||||
UI_HOST=""; WEBMAIL_HOST=""; MAIL_HOSTNAME=""
|
|
||||||
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env || true
|
|
||||||
|
|
||||||
# ---------- Systemdaten ----------
|
|
||||||
now="$(date '+%Y-%m-%d %H:%M:%S %Z' 2>/dev/null || echo '-')"
|
|
||||||
upt="$(uptime -p 2>/dev/null || echo '-')"
|
|
||||||
cores="$(nproc 2>/dev/null || echo 1)"
|
|
||||||
load_raw="$(awk '{printf "%s / %s / %s",$1,$2,$3}' /proc/loadavg 2>/dev/null || echo '0.00 / 0.00 / 0.00')"
|
|
||||||
load1="$(awk '{print $1}' /proc/loadavg 2>/dev/null || echo 0)"
|
|
||||||
|
|
||||||
# RAM/SWAP
|
|
||||||
mem_total="$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
|
||||||
mem_avail="$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
|
||||||
mem_used=$(( mem_total - mem_avail ))
|
|
||||||
swap_total="$(awk '/SwapTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
|
||||||
swap_free="$(awk '/SwapFree/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo 0)"
|
|
||||||
swap_used=$(( swap_total - swap_free ))
|
|
||||||
|
|
||||||
pct(){ local u="$1" t="$2"; [ "$t" -gt 0 ] || { echo 0; return; }; awk -v u="$u" -v t="$t" 'BEGIN{printf "%d",(u*100)/t}' ; }
|
|
||||||
ram_pct=$(pct "$mem_used" "$mem_total")
|
|
||||||
swap_pct=$(pct "$swap_used" "$swap_total")
|
|
||||||
|
|
||||||
# Disks
|
|
||||||
df_line(){ df -hP "$1" 2>/dev/null | awk 'NR==2{printf "%s / %s (%s)",$3,$2,$5}'; }
|
|
||||||
df_pct(){ df -P "$1" 2>/dev/null | awk 'NR==2{gsub("%","",$5);print $5+0}'; }
|
|
||||||
disk_root="$(df_line /)"; pct_root="$(df_pct /)"
|
|
||||||
disk_var="$(df_line /var 2>/dev/null)"; [ -n "$disk_var" ] || disk_var="-"
|
|
||||||
pct_var="$(df_pct /var 2>/dev/null)"; [ -n "$pct_var" ] || pct_var=0
|
|
||||||
|
|
||||||
# IPs (int/ext)
|
|
||||||
ipv4_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i!~/:/){print $i;exit}}')"
|
|
||||||
ipv6_int="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i~/:/){print $i;exit}}')"
|
|
||||||
ipv4_ext="$(curl -4fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
|
|
||||||
ipv6_ext="$(curl -6fsS --max-time 1 https://ifconfig.me 2>/dev/null || true)"
|
|
||||||
|
|
||||||
# ---------- Status-Farben ----------
|
|
||||||
mark(){ # value thresholdY thresholdR
|
|
||||||
local v="$1" y="$2" r="$3"
|
|
||||||
if [ "$v" -ge "$r" ]; then printf "${RD}[HIGH]${NC}"
|
|
||||||
elif [ "$v" -ge "$y" ]; then printf "${YE}[WARN]${NC}"
|
|
||||||
else printf "${GR}[OK]${NC}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
# Load/CPU-Schwellen (pro Core)
|
|
||||||
load_pct=$(awk -v l="$load1" -v c="$cores" 'BEGIN{if(c<1)c=1; printf "%d", (l/c)*100}')
|
|
||||||
m_load="$(mark "$load_pct" 70 100)"
|
|
||||||
m_ram="$(mark "$ram_pct" 75 90)"
|
|
||||||
m_swap="$(mark "$swap_pct" 10 50)"
|
|
||||||
m_root="$(mark "$pct_root" 75 90)"
|
|
||||||
m_var="$(mark "$pct_var" 75 90)"
|
|
||||||
|
|
||||||
# ---------- Header ----------
|
|
||||||
rule
|
|
||||||
center ""
|
|
||||||
center ":::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::"
|
|
||||||
center ":+:+:+ :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+: "
|
|
||||||
center ":+: +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ "
|
|
||||||
center "+#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+ "
|
|
||||||
center "+#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+ "
|
|
||||||
center "#+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+# "
|
|
||||||
center "### ### ### ### ########### ########## ### ### ######## ########## ### "
|
|
||||||
center ""
|
|
||||||
rule
|
|
||||||
|
|
||||||
# ---------- System ----------
|
|
||||||
kv "Date / Time" "${YE}${now}${NC}"
|
|
||||||
sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv4" "${ipv4_int:--}" "${ipv4_ext:--}"
|
|
||||||
sp "$pad"; printf "%-12s: int %-40s ext %s\n" "IPv6" "${ipv6_int:--}" "${ipv6_ext:--}"
|
|
||||||
kv "Uptime" "$upt"
|
|
||||||
sp "$pad"; printf "%-12s: %s cores, load %s %b\n" "CPU" "$cores" "$load_raw" "$m_load"
|
|
||||||
sp "$pad"; printf "%-12s: %s MiB / %s MiB (%d%%) %b %-5s %s MiB / %s MiB (%d%%) %b\n" \
|
|
||||||
"RAM" "$mem_used" "$mem_total" "$ram_pct" "$m_ram" "SWAP:" "$swap_used" "$swap_total" "$swap_pct" "$m_swap"
|
|
||||||
sp "$pad"; printf "%-12s: / %s %b %-5s %s %b\n" \
|
|
||||||
"Disk" "$disk_root" "$m_root" "/var:" "$disk_var" "$m_var"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ---------- Domains ----------
|
|
||||||
title "Domains"
|
|
||||||
[ -n "${UI_HOST:-}" ] && kv "UI" "${UI_HOST}"
|
|
||||||
[ -n "${WEBMAIL_HOST:-}" ] && kv "Webmail" "${WEBMAIL_HOST}"
|
|
||||||
[ -n "${MAIL_HOSTNAME:-}" ]&& kv "MX" "${MAIL_HOSTNAME}"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ---------- Services (4 Spalten, bündig) ----------
|
|
||||||
title "Services"
|
|
||||||
svc_state(){ systemctl is-active --quiet "$1" && printf "${GR}[OK]${NC}" || printf "${RD}[FAIL]${NC}"; }
|
|
||||||
SVC=( nginx mariadb redis-server postfix dovecot rspamd opendkim opendmarc clamav-daemon fail2ban mailwolt-ws mailwolt-queue mailwolt-schedule )
|
|
||||||
|
|
||||||
i=0; line=""
|
|
||||||
for s in "${SVC[@]}"; do
|
|
||||||
st="$(svc_state "$s")"
|
|
||||||
seg="$(printf "%-18s %-7s" "$s" "$st")"
|
|
||||||
line="$line$seg"
|
|
||||||
i=$((i+1))
|
|
||||||
if [ $((i%4)) -eq 0 ]; then sp "$pad"; echo "$line"; line=""; else line="$line "; fi
|
|
||||||
done
|
|
||||||
[ -n "$line" ] && { sp "$pad"; echo "$line"; }
|
|
||||||
echo
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
SH
|
|
||||||
|
|
||||||
chmod 755 /usr/local/bin/mw-motd
|
|
||||||
|
|
||||||
# update-motd Hook
|
|
||||||
if [[ -d /etc/update-motd.d ]]; then
|
|
||||||
cat >/etc/update-motd.d/10-mailwolt <<'SH'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
/usr/local/bin/mw-motd
|
|
||||||
SH
|
|
||||||
chmod +x /etc/update-motd.d/10-mailwolt
|
|
||||||
[[ -f /etc/update-motd.d/50-motd-news ]] && chmod -x /etc/update-motd.d/50-motd-news || true
|
|
||||||
else
|
|
||||||
# Fallback für Systeme ohne dynamic MOTD
|
|
||||||
cat >/etc/profile.d/10-mailwolt-motd.sh <<'SH'
|
|
||||||
case "$-" in *i*) /usr/local/bin/mw-motd ;; esac
|
|
||||||
SH
|
|
||||||
fi
|
|
||||||
|
|
||||||
: > /etc/motd 2>/dev/null || true
|
|
||||||
log "[✓] MOTD installiert."
|
|
||||||
|
|
@ -1,212 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
source ./lib.sh
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
# MailWolt – Abschluss / Summary (Dienste, Zertifikate, Smoke-Test)
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# Farben & Deko
|
|
||||||
NC="\033[0m"; BOLD="\033[1m"; DIM="\033[2m"
|
|
||||||
GREEN="\033[1;32m"; RED="\033[1;31m"; YELLOW="\033[1;33m"; CYAN="\033[1;36m"; GREY="\033[0;90m"
|
|
||||||
OKS="${GREEN}OK${NC}"; FAILS="${RED}FAIL${NC}"
|
|
||||||
bar(){ printf "${CYAN}%s${NC}\n" "──────────────────────────────────────────────────────────────────────────────"; }
|
|
||||||
ok(){ printf " [${OKS}]\n"; }
|
|
||||||
fail(){ printf " [${FAILS}]\n"; }
|
|
||||||
|
|
||||||
# Installer-Variablen laden (falls vorhanden)
|
|
||||||
set +u
|
|
||||||
[ -r /etc/mailwolt/installer.env ] && . /etc/mailwolt/installer.env
|
|
||||||
set -u
|
|
||||||
|
|
||||||
# Defaults / Umgebung
|
|
||||||
APP_USER="${APP_USER:-mailwolt}"
|
|
||||||
APP_GROUP="${APP_GROUP:-www-data}"
|
|
||||||
APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
|
||||||
|
|
||||||
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
|
||||||
UI_HOST="${UI_HOST:-}"
|
|
||||||
WEBMAIL_HOST="${WEBMAIL_HOST:-}"
|
|
||||||
MAIL_HOSTNAME="${MAIL_HOSTNAME:-}"
|
|
||||||
|
|
||||||
APP_ENV="${APP_ENV:-production}"
|
|
||||||
PROXY_MODE="${PROXY_MODE:-}" # leer = nicht anzeigen; "1"=Proxy, "dev"=Dev, sonst "nein"
|
|
||||||
NPM_IP="${NPM_IP:-}"
|
|
||||||
|
|
||||||
LE_EMAIL="${LE_EMAIL:-admin@${BASE_DOMAIN}}"
|
|
||||||
ACME_WEBROOT="/var/www/letsencrypt"
|
|
||||||
|
|
||||||
# Zert-Pfade (werden via Hook nach /etc/ssl/* verlinkt)
|
|
||||||
UI_CERT="/etc/ssl/ui/fullchain.pem"
|
|
||||||
UI_KEY="/etc/ssl/ui/privkey.pem"
|
|
||||||
WEBMAIL_CERT="/etc/ssl/webmail/fullchain.pem"
|
|
||||||
MAIL_CERT="/etc/ssl/mail/fullchain.pem"
|
|
||||||
|
|
||||||
# IPs (aus lib.sh)
|
|
||||||
SERVER_PUBLIC_IPV4="${SERVER_PUBLIC_IPV4:-$(detect_ip)}"
|
|
||||||
SERVER_PUBLIC_IPV6="${SERVER_PUBLIC_IPV6:-$(detect_ipv6)}"
|
|
||||||
|
|
||||||
# URLs (https nur, wenn UI-Cert+Key vorhanden)
|
|
||||||
SCHEME="http"
|
|
||||||
[[ -s "$UI_CERT" && -s "$UI_KEY" ]] && SCHEME="https"
|
|
||||||
APP_URL="${SCHEME}://${UI_HOST:-$SERVER_PUBLIC_IPV4}"
|
|
||||||
WEBMAIL_URL="${SCHEME}://${WEBMAIL_HOST:-$SERVER_PUBLIC_IPV4}"
|
|
||||||
|
|
||||||
# Ziel eines Symlinks auflösen
|
|
||||||
real_target(){ readlink -f -- "$1" 2>/dev/null || true; }
|
|
||||||
|
|
||||||
# "LE" werten, wenn live/* ODER archive/* (auch fullchainN.pem) getroffen wird
|
|
||||||
is_le_path(){
|
|
||||||
local p="$1"
|
|
||||||
[[ "$p" == /etc/letsencrypt/live/*/fullchain.pem || "$p" == /etc/letsencrypt/archive/*/fullchain*.pem ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
UI_CERT_TARGET="$(real_target "$UI_CERT")"
|
|
||||||
WEBMAIL_CERT_TARGET="$(real_target "$WEBMAIL_CERT")"
|
|
||||||
MAIL_CERT_TARGET="$(real_target "$MAIL_CERT")"
|
|
||||||
|
|
||||||
is_le_path() {
|
|
||||||
case "$1" in
|
|
||||||
/etc/letsencrypt/live/*) return 0 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# robust gegen set -u: immer ${var:-}
|
|
||||||
UI_LE="self-signed/none"
|
|
||||||
if [ -s "${UI_CERT:-}" ] && [ -n "${UI_CERT_TARGET:-}" ] && is_le_path "${UI_CERT_TARGET:-}"; then
|
|
||||||
UI_LE="LE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
WEBMAIL_LE="self-signed/none"
|
|
||||||
if [ -s "${WEBMAIL_CERT:-}" ] && [ -n "${WEBMAIL_CERT_TARGET:-}" ] && is_le_path "${WEBMAIL_CERT_TARGET:-}"; then
|
|
||||||
WEBMAIL_LE="LE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
MAIL_LE="self-signed/none"
|
|
||||||
if [ -s "${MAIL_CERT:-}" ] && [ -n "${MAIL_CERT_TARGET:-}" ] && is_le_path "${MAIL_CERT_TARGET:-}"; then
|
|
||||||
MAIL_LE="LE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
bar
|
|
||||||
printf " %s\n" "✔ MailWolt Bootstrap fertig"
|
|
||||||
bar
|
|
||||||
|
|
||||||
# Kopf-Infos
|
|
||||||
printf " %-14s %s\n" "Aufruf UI:" "${APP_URL}"
|
|
||||||
printf " %-14s %s\n" "Webmail:" "${WEBMAIL_URL}"
|
|
||||||
printf " %-14s %s\n" "App Root:" "${APP_DIR}"
|
|
||||||
printf " %-14s %s\n" "Mail-FQDN:" "${MAIL_HOSTNAME:-$SERVER_PUBLIC_IPV4}"
|
|
||||||
printf " %-14s %s\n" "BASE_DOMAIN:" "${BASE_DOMAIN}"
|
|
||||||
printf " %-14s %s\n" "LE-Email:" "${LE_EMAIL}"
|
|
||||||
printf " %-14s %s\n" "APP_ENV:" "${APP_ENV}"
|
|
||||||
# Proxy-Block nur anzeigen, wenn Variable gesetzt ist
|
|
||||||
if [[ -n "$PROXY_MODE" ]]; then
|
|
||||||
if [[ "$PROXY_MODE" == "1" ]]; then
|
|
||||||
printf " %-14s %s\n" "Proxy-Mode:" "ja (NPM: ${NPM_IP:-unbekannt})"
|
|
||||||
elif [[ "$PROXY_MODE" == "dev" ]]; then
|
|
||||||
printf " %-14s %s\n" "Proxy-Mode:" "Entwicklungsmodus"
|
|
||||||
else
|
|
||||||
printf " %-14s %s\n" "Proxy-Mode:" "nein"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
printf " %-14s %s\n" "Server IPv6:" "${SERVER_PUBLIC_IPV6:-–}"
|
|
||||||
printf " %-14s %s\n" "ACME Webroot:" "${ACME_WEBROOT}"
|
|
||||||
|
|
||||||
echo
|
|
||||||
printf " %-14s UI=%s, Webmail=%s, MX=%s\n" "Zertifikate:" "$UI_LE" "$WEBMAIL_LE" "$MAIL_LE"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo " Anmeldung: Keine vordefinierten Admin-Daten."
|
|
||||||
echo " Bitte zuerst registrieren (Erst-User wird Admin, danach"
|
|
||||||
echo " wird die Registrierung automatisch gesperrt)."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ── Dienste ────────────────────────────────────────────────────────────────
|
|
||||||
bar
|
|
||||||
echo " Services"
|
|
||||||
bar
|
|
||||||
|
|
||||||
OK_LIST=()
|
|
||||||
FAIL_LIST=()
|
|
||||||
|
|
||||||
svc(){
|
|
||||||
local unit="$1" label="${2:-$1}"
|
|
||||||
printf " • %-18s … " "$label"
|
|
||||||
if systemctl is-active --quiet "$unit"; then
|
|
||||||
ok
|
|
||||||
OK_LIST+=("$label")
|
|
||||||
else
|
|
||||||
fail
|
|
||||||
FAIL_LIST+=("$label")
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Kern-Services
|
|
||||||
svc nginx
|
|
||||||
svc mariadb
|
|
||||||
svc redis-server
|
|
||||||
svc postfix
|
|
||||||
svc dovecot
|
|
||||||
# App-Worker (tolerant)
|
|
||||||
svc "${APP_USER}-ws" "mailwolt-ws" || true
|
|
||||||
svc "${APP_USER}-schedule" "mailwolt-schedule" || true
|
|
||||||
svc "${APP_USER}-queue" "mailwolt-queue" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
if ((${#OK_LIST[@]})); then
|
|
||||||
printf " ${GREEN}OK:${NC} %s\n" "$(IFS=', '; echo "${OK_LIST[*]}")"
|
|
||||||
fi
|
|
||||||
if ((${#FAIL_LIST[@]})); then
|
|
||||||
printf " ${RED}FAIL:${NC} %s\n" "$(IFS=', '; echo "${FAIL_LIST[*]}")"
|
|
||||||
echo " ${YELLOW}Hinweis:${NC} Details mit: journalctl -u <dienst> -b --no-pager"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# ── Smoke-Test ─────────────────────────────────────────────────────────────
|
|
||||||
bar
|
|
||||||
echo " Smoke-Test (SMTP/IMAP/POP3 mit/ohne TLS)"
|
|
||||||
bar
|
|
||||||
|
|
||||||
check_port(){
|
|
||||||
local tag="$1" cmd="$2" desc="$3"
|
|
||||||
printf " [%-3s] %-35s … " "$tag" "$desc"
|
|
||||||
if timeout 8s bash -lc "$cmd" >/dev/null 2>&1; then ok; else fail; fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# kleines Delay nach Erststart
|
|
||||||
sleep 6 || true
|
|
||||||
|
|
||||||
# SMTP
|
|
||||||
check_port "25" 'printf "EHLO x\r\nQUIT\r\n" | nc -w 3 127.0.0.1 25' \
|
|
||||||
"SMTP (EHLO)"
|
|
||||||
check_port "465" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:465 -quiet -ign_eof' \
|
|
||||||
"SMTPS (TLS + EHLO)"
|
|
||||||
check_port "587" 'printf "EHLO x\r\nSTARTTLS\r\nQUIT\r\n" | openssl s_client -starttls smtp -connect 127.0.0.1:587 -quiet -ign_eof' \
|
|
||||||
"Submission (STARTTLS)"
|
|
||||||
|
|
||||||
# POP/IMAP
|
|
||||||
check_port "110" 'printf "QUIT\r\n" | nc -w 3 127.0.0.1 110' \
|
|
||||||
"POP3 (QUIT)"
|
|
||||||
check_port "995" 'printf "QUIT\r\n" | openssl s_client -connect 127.0.0.1:995 -quiet -ign_eof' \
|
|
||||||
"POP3S (TLS + QUIT)"
|
|
||||||
check_port "143" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | nc -w 3 127.0.0.1 143' \
|
|
||||||
"IMAP (CAPABILITY/LOGOUT)"
|
|
||||||
check_port "993" 'printf ". CAPABILITY\r\n. LOGOUT\r\n" | openssl s_client -connect 127.0.0.1:993 -quiet -ign_eof' \
|
|
||||||
"IMAPS (TLS + CAPABILITY/LOGOUT)"
|
|
||||||
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Hinweise nur ausgeben, wenn wirklich kein LE für UI/Webmail
|
|
||||||
if [[ "$UI_LE" != "LE" || "$WEBMAIL_LE" != "LE" ]]; then
|
|
||||||
echo -e " ${YELLOW}Hinweis:${NC} UI/Webmail verwenden noch kein Let's-Encrypt-Zertifikat."
|
|
||||||
echo -e " Prüfe Symlinks unter /etc/ssl/{ui,webmail} und den LE-Hook (21/75-Skripte)."
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Proxy-Info (optional)
|
|
||||||
if [[ "$PROXY_MODE" == "1" ]]; then
|
|
||||||
echo -e " ${GREY}Proxy-Hinweis:${NC} App erwartet TLS am Proxy (Backend ohne https-Redirects)."
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
|
|
@ -1,633 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# --- Farbschema für whiptail (libnewt) – hohe Lesbarkeit (dunkler Input, schwarze Schrift) ---
|
|
||||||
export NEWT_COLORS='
|
|
||||||
root=,blue
|
|
||||||
border=black,lightgray
|
|
||||||
window=black,lightgray
|
|
||||||
textbox=black,lightgray
|
|
||||||
label=black,lightgray
|
|
||||||
entry=black,cyan
|
|
||||||
button=black,cyan
|
|
||||||
actlistbox=black,cyan
|
|
||||||
actsellistbox=black,cyan
|
|
||||||
'
|
|
||||||
|
|
||||||
# optionales Backtitle (erscheint oben)
|
|
||||||
export DIALOGOPTS="--backtitle MailWolt Setup"
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
# MailWolt – Interaktiver Bootstrap (whiptail + Fallback)
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
DEV_MODE=0
|
|
||||||
PROXY_MODE=0
|
|
||||||
NPM_IP=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
-dev) DEV_MODE=1 ;;
|
|
||||||
-proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
|
||||||
esac
|
|
||||||
shift
|
|
||||||
done
|
|
||||||
|
|
||||||
APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
|
||||||
APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
|
||||||
export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
|
||||||
|
|
||||||
DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
export DB_PASS REDIS_PASS
|
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
source ./lib.sh
|
|
||||||
require_root
|
|
||||||
header
|
|
||||||
|
|
||||||
# ── Defaults ──────────────────────────────────────────────────
|
|
||||||
APP_NAME="${APP_NAME:-MailWolt}"
|
|
||||||
APP_USER="${APP_USER:-mailwolt}"
|
|
||||||
APP_GROUP="${APP_GROUP:-www-data}"
|
|
||||||
APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
|
|
||||||
APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
|
||||||
|
|
||||||
BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
|
||||||
UI_SUB="${UI_SUB:-ui}"
|
|
||||||
WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
|
|
||||||
MTA_SUB="${MTA_SUB:-mx}"
|
|
||||||
|
|
||||||
DB_NAME="${DB_NAME:-${APP_USER}}"
|
|
||||||
DB_USER="${DB_USER:-${APP_USER}}"
|
|
||||||
|
|
||||||
SERVER_PUBLIC_IPV4="$(detect_ip)"
|
|
||||||
SERVER_PUBLIC_IPV6="$(detect_ipv6)"
|
|
||||||
DEFAULT_TZ="$(detect_timezone)"
|
|
||||||
DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
|
|
||||||
|
|
||||||
echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────
|
|
||||||
have_whiptail(){ command -v whiptail >/dev/null 2>&1; }
|
|
||||||
|
|
||||||
#valid_fqdn(){
|
|
||||||
# [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]
|
|
||||||
#}
|
|
||||||
|
|
||||||
# ── Host-Validierung & DEV-Erkennung ────────────────────────────────────────
|
|
||||||
valid_fqdn_prod(){ [[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$ ]]; }
|
|
||||||
valid_host_dev(){
|
|
||||||
# erlaubt: single-label (ui, webmail), FQDNs, IPv4
|
|
||||||
[[ "$1" =~ ^([a-z0-9]([-a-z0-9]*[a-z0-9])?)(\.[a-z0-9-]+)*$ ]] || [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
|
|
||||||
}
|
|
||||||
is_local_like(){
|
|
||||||
local h="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
|
|
||||||
[[ "$h" =~ \.local$ || "$h" =~ \.loc$ || "$h" =~ \.dev$ || "$h" =~ \.test$ || "$h" = "localhost" ]] && return 0
|
|
||||||
[[ "$h" =~ ^10\. || "$h" =~ ^192\.168\. || "$h" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. || "$h" =~ ^127\. ]] && return 0
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
normalize_host(){
|
|
||||||
# $1=input $2=default (nutzt DEV_MODE für die passende Prüflogik)
|
|
||||||
local inp="$1" def="$2"
|
|
||||||
if [[ "${DEV_MODE}" = "1" ]]; then
|
|
||||||
valid_host_dev "$inp" && { echo "$inp"; return; }
|
|
||||||
else
|
|
||||||
valid_fqdn_prod "$inp" && { echo "$inp"; return; }
|
|
||||||
fi
|
|
||||||
echo "$def"
|
|
||||||
}
|
|
||||||
|
|
||||||
ask_tty_domain(){
|
|
||||||
local label="$1" example="$2" def="$3" outvar="$4" inp
|
|
||||||
echo -e "${CYAN}${label}${NC}"
|
|
||||||
echo -e " z.B. ${YELLOW}${example}${NC}"
|
|
||||||
echo -e " Default: ${GREY}${def}${NC}"
|
|
||||||
read -r -p " Eingabe (Enter=Default): " inp || true
|
|
||||||
inp="${inp:-$def}"
|
|
||||||
if ! valid_fqdn "$inp"; then
|
|
||||||
echo -e "${YELLOW}[!] Ungültiger FQDN, nehme Default: ${def}${NC}"
|
|
||||||
inp="$def"
|
|
||||||
fi
|
|
||||||
eval "$outvar='$inp'"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Interaktive Eingaben (whiptail oder Fallback) ─────────────
|
|
||||||
MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
|
|
||||||
UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
|
|
||||||
WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
|
|
||||||
|
|
||||||
CLAMAV_ENABLE=1
|
|
||||||
OPENDMARC_ENABLE=1
|
|
||||||
FAIL2BAN_ENABLE=1
|
|
||||||
|
|
||||||
if command -v whiptail >/dev/null 2>&1; then
|
|
||||||
TITLE="MailWolt Setup"
|
|
||||||
|
|
||||||
# Hinweise zu erlaubten DEV-Hosts
|
|
||||||
MSG_SUFFIX="\n\nHinweis: Im DEV-Modus sind auch single-label Hosts (z.B. ui, webmail), *.local/*.dev und IPs erlaubt."
|
|
||||||
|
|
||||||
_mta_in="$(whiptail --title "$TITLE" --inputbox "Mailserver-Host (MX)\nBeispiele: mx.domain.tld | mx.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
|
||||||
_ui_in="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel Host\nBeispiele: ui.domain.tld | ui.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
|
||||||
_wm_in="$(whiptail --title "$TITLE" --inputbox "Webmail Host\nBeispiele: webmail.domain.tld | web.local | 10.0.0.10${MSG_SUFFIX}" 13 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
|
||||||
|
|
||||||
# ZUERST provisorisch prüfen, ob „lokal“ → DEV erzwingen
|
|
||||||
if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
|
|
||||||
DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
|
|
||||||
fi
|
|
||||||
export DEV_MODE APP_ENV APP_DEBUG
|
|
||||||
|
|
||||||
# Jetzt mit passender Logik normalisieren
|
|
||||||
MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
|
|
||||||
UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
|
|
||||||
WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
|
|
||||||
|
|
||||||
CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
|
|
||||||
"ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
|
|
||||||
"OpenDMARC" "DMARC-Auswertung" ON \
|
|
||||||
"Fail2Ban" "Brute-Force-Schutz" ON \
|
|
||||||
3>&1 1>&2 2>&3)" || true
|
|
||||||
CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
|
|
||||||
OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
|
|
||||||
FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
|
|
||||||
|
|
||||||
else
|
|
||||||
echo -e "${GREY}[i] whiptail nicht gefunden – TTY-Fallback.${NC}\n"
|
|
||||||
read -r -p "Mailserver-Host (MX) [${MTA_DEFAULT}]: " _mta_in; _mta_in="${_mta_in:-$MTA_DEFAULT}"
|
|
||||||
read -r -p "UI / Admin-Panel Host [${UI_DEFAULT}]: " _ui_in; _ui_in="${_ui_in:-$UI_DEFAULT}"
|
|
||||||
read -r -p "Webmail Host [${WEBMAIL_DEFAULT}]: " _wm_in; _wm_in="${_wm_in:-$WEBMAIL_DEFAULT}"
|
|
||||||
|
|
||||||
if is_local_like "$_mta_in" || is_local_like "$_ui_in" || is_local_like "$_wm_in"; then
|
|
||||||
DEV_MODE=1; APP_ENV="local"; APP_DEBUG="true"
|
|
||||||
fi
|
|
||||||
export DEV_MODE APP_ENV APP_DEBUG
|
|
||||||
|
|
||||||
MTA_FQDN="$(normalize_host "$_mta_in" "$MTA_DEFAULT")"
|
|
||||||
UI_FQDN="$(normalize_host "$_ui_in" "$UI_DEFAULT")"
|
|
||||||
WEBMAIL_FQDN="$(normalize_host "$_wm_in" "$WEBMAIL_DEFAULT")"
|
|
||||||
|
|
||||||
read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
|
||||||
read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
|
||||||
read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#if have_whiptail; then
|
|
||||||
# TITLE="MailWolt Setup"
|
|
||||||
#
|
|
||||||
# MTA_FQDN="$(whiptail --title "$TITLE" --inputbox "Mailserver-FQDN (MX)\nBeispiel: mx.domain.tld" 11 70 "$MTA_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
|
||||||
# valid_fqdn "$MTA_FQDN" || MTA_FQDN="$MTA_DEFAULT"
|
|
||||||
#
|
|
||||||
# UI_FQDN="$(whiptail --title "$TITLE" --inputbox "UI / Admin-Panel FQDN\nBeispiel: ui.domain.tld" 11 70 "$UI_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
|
||||||
# valid_fqdn "$UI_FQDN" || UI_FQDN="$UI_DEFAULT"
|
|
||||||
#
|
|
||||||
# WEBMAIL_FQDN="$(whiptail --title "$TITLE" --inputbox "Webmail FQDN\nBeispiel: webmail.domain.tld" 11 70 "$WEBMAIL_DEFAULT" 3>&1 1>&2 2>&3)" || exit 1
|
|
||||||
# valid_fqdn "$WEBMAIL_FQDN" || WEBMAIL_FQDN="$WEBMAIL_DEFAULT"
|
|
||||||
#
|
|
||||||
# CHOICES="$(whiptail --title "$TITLE" --checklist "Optionale Dienste aktivieren" 15 70 6 \
|
|
||||||
# "ClamAV" "Virenscan (clamd/clamav-daemon)" ON \
|
|
||||||
# "OpenDMARC" "DMARC-Auswertung" ON \
|
|
||||||
# "Fail2Ban" "Brute-Force-Schutz" ON \
|
|
||||||
# 3>&1 1>&2 2>&3)" || true
|
|
||||||
#
|
|
||||||
# CLAMAV_ENABLE=0; [[ "$CHOICES" == *"ClamAV"* ]] && CLAMAV_ENABLE=1
|
|
||||||
# OPENDMARC_ENABLE=0; [[ "$CHOICES" == *"OpenDMARC"* ]] && OPENDMARC_ENABLE=1
|
|
||||||
# FAIL2BAN_ENABLE=0; [[ "$CHOICES" == *"Fail2Ban"* ]] && FAIL2BAN_ENABLE=1
|
|
||||||
#
|
|
||||||
# whiptail --title "$TITLE" --msgbox "Zusammenfassung:
|
|
||||||
#
|
|
||||||
#MX : $MTA_FQDN
|
|
||||||
#UI : $UI_FQDN
|
|
||||||
#Webmail : $WEBMAIL_FQDN
|
|
||||||
#
|
|
||||||
#ClamAV : $([[ $CLAMAV_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
|
||||||
#OpenDMARC : $([[ $OPENDMARC_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
|
||||||
#Fail2Ban : $([[ $FAIL2BAN_ENABLE -eq 1 ]] && echo Aktiv || echo Deaktiv)
|
|
||||||
#" 16 70
|
|
||||||
#
|
|
||||||
#else
|
|
||||||
# echo -e "${GREY}[i] whiptail nicht gefunden – nutze TTY-Prompts.${NC}\n"
|
|
||||||
# ask_tty_domain "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT" MTA_FQDN
|
|
||||||
# ask_tty_domain "UI / Admin-Panel FQDN" "ui.domain.tld" "$UI_DEFAULT" UI_FQDN
|
|
||||||
# ask_tty_domain "Webmail FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT" WEBMAIL_FQDN
|
|
||||||
#
|
|
||||||
# read -r -p "ClamAV aktivieren? (1/0, Enter=1): " CLAMAV_ENABLE; CLAMAV_ENABLE="${CLAMAV_ENABLE:-1}"
|
|
||||||
# read -r -p "OpenDMARC aktivieren? (1/0, Enter=1): " OPENDMARC_ENABLE; OPENDMARC_ENABLE="${OPENDMARC_ENABLE:-1}"
|
|
||||||
# read -r -p "Fail2Ban aktivieren? (1/0, Enter=1): " FAIL2BAN_ENABLE; FAIL2BAN_ENABLE="${FAIL2BAN_ENABLE:-1}"
|
|
||||||
#fi
|
|
||||||
|
|
||||||
# ── Defaults/Kompatibilität ──────────────────────────────────
|
|
||||||
MTA_FQDN="${MTA_FQDN:-${MTA_DEFAULT}}"
|
|
||||||
UI_FQDN="${UI_FQDN:-${UI_DEFAULT}}"
|
|
||||||
WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_DEFAULT}}"
|
|
||||||
DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
|
||||||
DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
|
||||||
DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
|
||||||
|
|
||||||
# BASE_DOMAIN/Subs aus FQDNs ableiten
|
|
||||||
if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then MTA_SUB="${BASH_REMATCH[1]}"; BASE_DOMAIN="${BASH_REMATCH[2]}"; fi
|
|
||||||
if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then UI_SUB="${BASH_REMATCH[1]}"; fi
|
|
||||||
if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then WEBMAIL_SUB="${BASH_REMATCH[1]}"; fi
|
|
||||||
|
|
||||||
SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
|
||||||
SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
|
||||||
|
|
||||||
MAIL_HOSTNAME="${MTA_FQDN}"
|
|
||||||
UI_HOST="${UI_FQDN}"
|
|
||||||
WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
|
||||||
|
|
||||||
APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
|
||||||
APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
|
||||||
|
|
||||||
# ── Export & persist ─────────────────────────────────────────
|
|
||||||
export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
|
||||||
export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
|
||||||
export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
|
||||||
export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
|
||||||
export DB_NAME DB_USER
|
|
||||||
export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
|
||||||
export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
|
|
||||||
|
|
||||||
install -d -m 0755 /etc/mailwolt
|
|
||||||
cat >/etc/mailwolt/installer.env <<EOF
|
|
||||||
SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
|
||||||
SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
|
||||||
APP_TZ=${APP_TZ}
|
|
||||||
APP_LOCALE=${APP_LOCALE}
|
|
||||||
BASE_DOMAIN=${BASE_DOMAIN}
|
|
||||||
MTA_SUB=${MTA_SUB}
|
|
||||||
UI_SUB=${UI_SUB}
|
|
||||||
WEBMAIL_SUB=${WEBMAIL_SUB}
|
|
||||||
|
|
||||||
MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
|
||||||
UI_HOST=${UI_HOST}
|
|
||||||
WEBMAIL_HOST=${WEBMAIL_HOST}
|
|
||||||
|
|
||||||
LE_EMAIL=${LE_EMAIL:-admin@${BASE_DOMAIN}}
|
|
||||||
SYSMAIL_SUB=${SYSMAIL_SUB}
|
|
||||||
SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
|
||||||
|
|
||||||
DKIM_ENABLE=${DKIM_ENABLE}
|
|
||||||
DKIM_SELECTOR=${DKIM_SELECTOR}
|
|
||||||
DKIM_GENERATE=${DKIM_GENERATE}
|
|
||||||
|
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_NAME=${DB_NAME}
|
|
||||||
DB_USER=${DB_USER}
|
|
||||||
DB_PASS=${DB_PASS}
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASS=${REDIS_PASS}
|
|
||||||
|
|
||||||
SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
|
||||||
SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
|
||||||
APP_ENV=${APP_ENV}
|
|
||||||
|
|
||||||
CLAMAV_ENABLE=${CLAMAV_ENABLE}
|
|
||||||
OPENDMARC_ENABLE=${OPENDMARC_ENABLE}
|
|
||||||
FAIL2BAN_ENABLE=${FAIL2BAN_ENABLE}
|
|
||||||
|
|
||||||
BACKUP_ONCALENDAR="${BACKUP_ONCALENDAR:-*-*-* 03:00:00}"
|
|
||||||
BACKUP_ENABLED=0
|
|
||||||
BACKUP_INTERVAL=daily
|
|
||||||
BACKUP_RETENTION_DAYS=7
|
|
||||||
BACKUP_DIR=/var/backups/mailwolt
|
|
||||||
BACKUP_USE_ZSTD=1
|
|
||||||
EOF
|
|
||||||
chmod 600 /etc/mailwolt/installer.env
|
|
||||||
|
|
||||||
# ── Installer-Sequenz ────────────────────────────────────────
|
|
||||||
for STEP in \
|
|
||||||
10-provision \
|
|
||||||
20-ssl 21-le-deploy-hook 22-dkim-helper \
|
|
||||||
30-db 40-postfix 50-dovecot \
|
|
||||||
60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 64-apply-milters \
|
|
||||||
70-nginx 75-le-issue 80-app 88-update-wrapper 90-services \
|
|
||||||
92-sudoers-npm 93-backup-tools 95-woltguard 98-motd 99-summary
|
|
||||||
do
|
|
||||||
log ">>> Running ${STEP}.sh"
|
|
||||||
bash "./${STEP}.sh"
|
|
||||||
done
|
|
||||||
|
|
||||||
##!/usr/bin/env bash
|
|
||||||
#set -euo pipefail
|
|
||||||
#
|
|
||||||
## --- Flags / Modi ---
|
|
||||||
#DEV_MODE=0
|
|
||||||
#PROXY_MODE=0
|
|
||||||
#NPM_IP=""
|
|
||||||
#
|
|
||||||
#while [[ $# -gt 0 ]]; do
|
|
||||||
# case "$1" in
|
|
||||||
# -dev) DEV_MODE=1 ;;
|
|
||||||
# -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
|
||||||
# esac
|
|
||||||
# shift
|
|
||||||
#done
|
|
||||||
#
|
|
||||||
#APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
|
||||||
#APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
|
||||||
#export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
|
||||||
#
|
|
||||||
#DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
#REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
#
|
|
||||||
#export DB_PASS REDIS_PASS
|
|
||||||
#
|
|
||||||
#cd "$(dirname "$0")"
|
|
||||||
#source ./lib.sh
|
|
||||||
#require_root
|
|
||||||
#header
|
|
||||||
#
|
|
||||||
## ── Defaults ────────────────────────────────────────────────────────────────
|
|
||||||
#APP_NAME="${APP_NAME:-MailWolt}"
|
|
||||||
#APP_USER="${APP_USER:-mailwolt}"
|
|
||||||
#APP_GROUP="${APP_GROUP:-www-data}"
|
|
||||||
#APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
|
|
||||||
#APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
|
||||||
#
|
|
||||||
#BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
|
||||||
#UI_SUB="${UI_SUB:-ui}"
|
|
||||||
#WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
|
|
||||||
#MTA_SUB="${MTA_SUB:-mx}"
|
|
||||||
#
|
|
||||||
#DB_NAME="${DB_NAME:-${APP_USER}}"
|
|
||||||
#DB_USER="${DB_USER:-${APP_USER}}"
|
|
||||||
#
|
|
||||||
#SERVER_PUBLIC_IPV4="$(detect_ip)"
|
|
||||||
#SERVER_PUBLIC_IPV6="$(detect_ipv6)"
|
|
||||||
#DEFAULT_TZ="$(detect_timezone)"
|
|
||||||
#DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
|
|
||||||
#
|
|
||||||
#echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
|
|
||||||
#
|
|
||||||
## ── Schöne, farbige Abfragen ────────────────────────────────────────────────
|
|
||||||
#echo -e "${CYAN}"
|
|
||||||
#echo "──────────────────────────────────────────────"
|
|
||||||
#echo -e " 📧 MailWolt Setup – Domain Konfiguration"
|
|
||||||
#echo "──────────────────────────────────────────────"
|
|
||||||
#echo -e "${NC}"
|
|
||||||
#
|
|
||||||
#MTA_DEFAULT="${MTA_SUB}.${BASE_DOMAIN}"
|
|
||||||
#UI_DEFAULT="${UI_SUB}.${BASE_DOMAIN}"
|
|
||||||
#WEBMAIL_DEFAULT="${WEBMAIL_SUB}.${BASE_DOMAIN}"
|
|
||||||
#
|
|
||||||
#ask_domain() {
|
|
||||||
# local __outvar="$1" label="$2" example="$3" defval="$4" input=""
|
|
||||||
# echo -e "${GREEN}[?]${NC} ${label}"
|
|
||||||
# echo -e " z.B. ${YELLOW}${example}${NC}"
|
|
||||||
# echo -e " Default: ${CYAN}${defval}${NC}"
|
|
||||||
# echo -ne " → Eingabe: ${CYAN}"
|
|
||||||
# read -r input
|
|
||||||
# echo -e "${NC}"
|
|
||||||
# if [[ -z "$input" ]]; then
|
|
||||||
# eval "$__outvar='$defval'"
|
|
||||||
# else
|
|
||||||
# eval "$__outvar='$input'"
|
|
||||||
# fi
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
#ask_toggle() {
|
|
||||||
# local __outvar="$1" label="$2" defval="${3:-1}" input=""
|
|
||||||
# echo -ne "${GREEN}[?]${NC} ${label} (${CYAN}1${NC}=Ja / ${YELLOW}0${NC}=Nein) [Enter=${defval}]: "
|
|
||||||
# read -r input
|
|
||||||
# input="${input:-$defval}"
|
|
||||||
# case "$input" in
|
|
||||||
# 1|0) ;;
|
|
||||||
# *) echo -e "${YELLOW}Ungültig, nehme Default=${defval}.${NC}"; input="$defval" ;;
|
|
||||||
# esac
|
|
||||||
# eval "$__outvar='$input'"
|
|
||||||
#}
|
|
||||||
#
|
|
||||||
#ask_domain "MTA_FQDN" "Mailserver-FQDN (MX)" "mx.domain.tld" "$MTA_DEFAULT"
|
|
||||||
#ask_domain "UI_FQDN" "UI / Admin-Panel" "ui.domain.tld" "$UI_DEFAULT"
|
|
||||||
#ask_domain "WEBMAIL_FQDN" "Webmail-FQDN" "webmail.domain.tld" "$WEBMAIL_DEFAULT"
|
|
||||||
#
|
|
||||||
#echo -e "${CYAN}"
|
|
||||||
#echo "──────────────────────────────────────────────"
|
|
||||||
#echo -e " 🛡 Optionale Dienste"
|
|
||||||
#echo "──────────────────────────────────────────────"
|
|
||||||
#echo -e "${NC}"
|
|
||||||
#
|
|
||||||
#ask_toggle "CLAMAV_ENABLE" "ClamAV Virenscan aktivieren?" 1
|
|
||||||
#ask_toggle "OPENDMARC_ENABLE" "OpenDMARC auswerten?" 1
|
|
||||||
#ask_toggle "FAIL2BAN_ENABLE" "Fail2Ban aktivieren?" 1
|
|
||||||
#echo
|
|
||||||
#
|
|
||||||
## Defaults, wenn Enter gedrückt (Abwärtskompatibilität)
|
|
||||||
#MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
|
|
||||||
#UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
|
|
||||||
#WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
|
|
||||||
#DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
|
||||||
#DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
|
||||||
#DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
|
||||||
#
|
|
||||||
## BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
|
|
||||||
#if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
|
||||||
# MTA_SUB="${BASH_REMATCH[1]}"
|
|
||||||
# BASE_DOMAIN="${BASH_REMATCH[2]}"
|
|
||||||
#fi
|
|
||||||
#if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
|
||||||
# UI_SUB="${BASH_REMATCH[1]}"
|
|
||||||
#fi
|
|
||||||
#if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
|
||||||
# WEBMAIL_SUB="${BASH_REMATCH[1]}"
|
|
||||||
#fi
|
|
||||||
#
|
|
||||||
#SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
|
||||||
#SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
|
||||||
## Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
|
|
||||||
#MAIL_HOSTNAME="${MTA_FQDN}"
|
|
||||||
#UI_HOST="${UI_FQDN}"
|
|
||||||
#WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
|
||||||
#
|
|
||||||
## Zeitzone/Locale sinnvoll setzen
|
|
||||||
#APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
|
||||||
#APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
|
||||||
#
|
|
||||||
## ── Variablen exportieren ───────────────────────────────────────────────────
|
|
||||||
#export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
|
||||||
#export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
|
||||||
#export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
|
||||||
#export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
|
||||||
#export DB_NAME DB_USER
|
|
||||||
#export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
|
||||||
#export CLAMAV_ENABLE OPENDMARC_ENABLE FAIL2BAN_ENABLE
|
|
||||||
#
|
|
||||||
#install -d -m 0755 /etc/mailwolt
|
|
||||||
#cat >/etc/mailwolt/installer.env <<EOF
|
|
||||||
#BASE_DOMAIN=${BASE_DOMAIN}
|
|
||||||
#MTA_SUB=${MTA_SUB}
|
|
||||||
#UI_SUB=${UI_SUB}
|
|
||||||
#WEBMAIL_SUB=${WEBMAIL_SUB}
|
|
||||||
#
|
|
||||||
#MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
|
||||||
#UI_HOST=${UI_HOST}
|
|
||||||
#WEBMAIL_HOST=${WEBMAIL_HOST}
|
|
||||||
#
|
|
||||||
#SYSMAIL_SUB=${SYSMAIL_SUB}
|
|
||||||
#SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
|
||||||
#
|
|
||||||
#DKIM_ENABLE=${DKIM_ENABLE}
|
|
||||||
#DKIM_SELECTOR=${DKIM_SELECTOR}
|
|
||||||
#DKIM_GENERATE=${DKIM_GENERATE}
|
|
||||||
#
|
|
||||||
#DB_HOST=127.0.0.1
|
|
||||||
#DB_NAME=${DB_NAME}
|
|
||||||
#DB_USER=${DB_USER}
|
|
||||||
#DB_PASS=${DB_PASS}
|
|
||||||
#REDIS_PASS=${REDIS_PASS}
|
|
||||||
#
|
|
||||||
#SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
|
||||||
#SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
|
||||||
#APP_ENV=${APP_ENV}
|
|
||||||
#
|
|
||||||
#CLAMAV_ENABLE=${CLAMAV_ENABLE}
|
|
||||||
#OPENDMARC_ENABLE=${OPENDMARC_ENABLE}
|
|
||||||
#FAIL2BAN_ENABLE=${FAIL2BAN_ENABLE}
|
|
||||||
#EOF
|
|
||||||
#
|
|
||||||
#chmod 600 /etc/mailwolt/installer.env
|
|
||||||
#
|
|
||||||
## ── Sequenz ────────────────────────────────────────────────────────────────
|
|
||||||
#for STEP in 10-provision 20-ssl 21-le-deploy-hook 22-dkim-helper 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 70-nginx 75-le-issue 80-app 90-services 95-woltguard 98-motd 99-summary
|
|
||||||
#do
|
|
||||||
# log ">>> Running ${STEP}.sh"
|
|
||||||
# bash "./${STEP}.sh"
|
|
||||||
#done
|
|
||||||
###!/usr/bin/env bash
|
|
||||||
##set -euo pipefail
|
|
||||||
##
|
|
||||||
### --- Flags / Modi ---
|
|
||||||
##DEV_MODE=0
|
|
||||||
##PROXY_MODE=0
|
|
||||||
##NPM_IP=""
|
|
||||||
##
|
|
||||||
##while [[ $# -gt 0 ]]; do
|
|
||||||
## case "$1" in
|
|
||||||
## -dev) DEV_MODE=1 ;;
|
|
||||||
## -proxy) PROXY_MODE=1; NPM_IP="${2:-}"; shift ;;
|
|
||||||
## esac
|
|
||||||
## shift
|
|
||||||
##done
|
|
||||||
##
|
|
||||||
##APP_ENV="${APP_ENV:-$([[ $DEV_MODE -eq 1 ]] && echo local || echo production)}"
|
|
||||||
##APP_DEBUG="${APP_DEBUG:-$([[ $DEV_MODE -eq 1 ]] && echo true || echo false)}"
|
|
||||||
##export DEV_MODE PROXY_MODE NPM_IP APP_ENV APP_DEBUG
|
|
||||||
##
|
|
||||||
##DB_PASS="${DB_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
##REDIS_PASS="${REDIS_PASS:-$(openssl rand -hex 16)}"
|
|
||||||
##
|
|
||||||
##export DB_PASS REDIS_PASS
|
|
||||||
##
|
|
||||||
##cd "$(dirname "$0")"
|
|
||||||
##source ./lib.sh
|
|
||||||
##require_root
|
|
||||||
##header
|
|
||||||
##
|
|
||||||
### ── Defaults ────────────────────────────────────────────────────────────────
|
|
||||||
##APP_NAME="${APP_NAME:-MailWolt}"
|
|
||||||
##APP_USER="${APP_USER:-mailwolt}"
|
|
||||||
##APP_GROUP="${APP_GROUP:-www-data}"
|
|
||||||
##APP_USER_PREFIX="${APP_USER_PREFIX:-mw}"
|
|
||||||
##APP_DIR="${APP_DIR:-/var/www/${APP_USER}}"
|
|
||||||
##
|
|
||||||
##BASE_DOMAIN="${BASE_DOMAIN:-example.com}"
|
|
||||||
##UI_SUB="${UI_SUB:-ui}"
|
|
||||||
##WEBMAIL_SUB="${WEBMAIL_SUB:-webmail}"
|
|
||||||
##MTA_SUB="${MTA_SUB:-mx}"
|
|
||||||
##
|
|
||||||
##DB_NAME="${DB_NAME:-${APP_USER}}"
|
|
||||||
##DB_USER="${DB_USER:-${APP_USER}}"
|
|
||||||
##
|
|
||||||
##SERVER_PUBLIC_IPV4="$(detect_ip)"
|
|
||||||
##SERVER_PUBLIC_IPV6="$(detect_ipv6)"
|
|
||||||
##DEFAULT_TZ="$(detect_timezone)"
|
|
||||||
##DEFAULT_LOCALE="$(guess_locale_from_tz "$DEFAULT_TZ")"
|
|
||||||
##
|
|
||||||
##echo -e "${GREY}Erkannte IP (v4): ${SERVER_PUBLIC_IPV4} v6: ${SERVER_PUBLIC_IPV6:-–}${NC}"
|
|
||||||
##
|
|
||||||
### ── FQDNs abfragen ───────────────────────────────────────────────────────────
|
|
||||||
##read -r -p "Mailserver FQDN (MX, z.B. mx.domain.tld) [Enter=${MTA_SUB}.${BASE_DOMAIN}]: " MTA_FQDN
|
|
||||||
##read -r -p "UI / Admin-Panel FQDN (z.B. ui.domain.tld) [Enter=${UI_SUB}.${BASE_DOMAIN}]: " UI_FQDN
|
|
||||||
##read -r -p "Webmail FQDN (z.B. webmail.domain.tld) [Enter=${WEBMAIL_SUB}.${BASE_DOMAIN}]: " WEBMAIL_FQDN
|
|
||||||
##
|
|
||||||
### Defaults, wenn Enter gedrückt
|
|
||||||
##MTA_FQDN="${MTA_FQDN:-${MTA_SUB}.${BASE_DOMAIN}}"
|
|
||||||
##UI_FQDN="${UI_FQDN:-${UI_SUB}.${BASE_DOMAIN}}"
|
|
||||||
##WEBMAIL_FQDN="${WEBMAIL_FQDN:-${WEBMAIL_SUB}.${BASE_DOMAIN}}"
|
|
||||||
##DKIM_ENABLE="${DKIM_ENABLE:-1}"
|
|
||||||
##DKIM_SELECTOR="${DKIM_SELECTOR:-mwl1}"
|
|
||||||
##DKIM_GENERATE="${DKIM_GENERATE:-1}"
|
|
||||||
##
|
|
||||||
### BASE_DOMAIN und Sub-Labels aus MTA/UI/WEBMAIL ableiten (robust)
|
|
||||||
##if [[ "$MTA_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
|
||||||
## MTA_SUB="${BASH_REMATCH[1]}"
|
|
||||||
## BASE_DOMAIN="${BASH_REMATCH[2]}"
|
|
||||||
##fi
|
|
||||||
##if [[ "$UI_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
|
||||||
## UI_SUB="${BASH_REMATCH[1]}"
|
|
||||||
##fi
|
|
||||||
##if [[ "$WEBMAIL_FQDN" =~ ^([^.]+)\.(.+)$ ]]; then
|
|
||||||
## WEBMAIL_SUB="${BASH_REMATCH[1]}"
|
|
||||||
##fi
|
|
||||||
##
|
|
||||||
##SYSMAIL_SUB="${SYSMAIL_SUB:-sysmail}"
|
|
||||||
##SYSMAIL_DOMAIN="${SYSMAIL_SUB}.${BASE_DOMAIN}"
|
|
||||||
### Kanonische Host-Variablen (NIE wieder zusammenbauen – nimm die FQDNs)
|
|
||||||
##MAIL_HOSTNAME="${MTA_FQDN}"
|
|
||||||
##UI_HOST="${UI_FQDN}"
|
|
||||||
##WEBMAIL_HOST="${WEBMAIL_FQDN}"
|
|
||||||
##
|
|
||||||
### Zeitzone/Locale sinnvoll setzen (könntest du auch noch abfragen)
|
|
||||||
##APP_TZ="${APP_TZ:-$DEFAULT_TZ}"
|
|
||||||
##APP_LOCALE="${APP_LOCALE:-$DEFAULT_LOCALE}"
|
|
||||||
##
|
|
||||||
### ── Variablen exportieren ───────────────────────────────────────────────────
|
|
||||||
##export APP_NAME APP_USER APP_GROUP APP_USER_PREFIX APP_DIR
|
|
||||||
##export BASE_DOMAIN UI_SUB WEBMAIL_SUB MTA_SUB
|
|
||||||
##export SYSMAIL_SUB SYSMAIL_DOMAIN DKIM_ENABLE DKIM_SELECTOR DKIM_GENERATE
|
|
||||||
##export UI_HOST WEBMAIL_HOST MAIL_HOSTNAME
|
|
||||||
##export DB_NAME DB_USER
|
|
||||||
##export SERVER_PUBLIC_IPV4 SERVER_PUBLIC_IPV6 APP_TZ APP_LOCALE
|
|
||||||
##
|
|
||||||
##install -d -m 0755 /etc/mailwolt
|
|
||||||
##cat >/etc/mailwolt/installer.env <<EOF
|
|
||||||
##BASE_DOMAIN=${BASE_DOMAIN}
|
|
||||||
##MTA_SUB=${MTA_SUB}
|
|
||||||
##UI_SUB=${UI_SUB}
|
|
||||||
##WEBMAIL_SUB=${WEBMAIL_SUB}
|
|
||||||
##
|
|
||||||
##MAIL_HOSTNAME=${MAIL_HOSTNAME}
|
|
||||||
##UI_HOST=${UI_HOST}
|
|
||||||
##WEBMAIL_HOST=${WEBMAIL_HOST}
|
|
||||||
##
|
|
||||||
##SYSMAIL_SUB=${SYSMAIL_SUB}
|
|
||||||
##SYSMAIL_DOMAIN=${SYSMAIL_DOMAIN}
|
|
||||||
##
|
|
||||||
##DKIM_ENABLE=${DKIM_ENABLE}
|
|
||||||
##DKIM_SELECTOR=${DKIM_SELECTOR}
|
|
||||||
##DKIM_GENERATE=${DKIM_GENERATE}
|
|
||||||
##
|
|
||||||
##DB_HOST=127.0.0.1
|
|
||||||
##DB_NAME=${DB_NAME}
|
|
||||||
##DB_USER=${DB_USER}
|
|
||||||
##DB_PASS=${DB_PASS}
|
|
||||||
##REDIS_PASS=${REDIS_PASS}
|
|
||||||
##
|
|
||||||
##SERVER_PUBLIC_IPV4=${SERVER_PUBLIC_IPV4}
|
|
||||||
##SERVER_PUBLIC_IPV6=${SERVER_PUBLIC_IPV6}
|
|
||||||
##APP_ENV=${APP_ENV}
|
|
||||||
##
|
|
||||||
##CLAMAV_ENABLE=1
|
|
||||||
##OPENDMARC_ENABLE=1
|
|
||||||
##FAIL2BAN_ENABLE=1
|
|
||||||
##EOF
|
|
||||||
##
|
|
||||||
##chmod 600 /etc/mailwolt/installer.env
|
|
||||||
##
|
|
||||||
### ── Sequenz ────────────────────────────────────────────────────────────────
|
|
||||||
##for STEP in 10-provision 20-ssl 21-le-deploy-hook 22-dkim-helper 30-db 40-postfix 50-dovecot 60-rspamd-opendkim 61-opendmarc 62-clamav 63-fail2ban 70-nginx 75-le-issue 80-app 90-services 95-woltguard 98-motd 99-summary
|
|
||||||
##do
|
|
||||||
## log ">>> Running ${STEP}.sh"
|
|
||||||
## bash "./${STEP}.sh"
|
|
||||||
##done
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Mailwolt Installer-Wrapper
|
|
||||||
# Deploy to: /usr/local/sbin/mailwolt-install
|
|
||||||
# Permissions: chmod 0755, chown root:root
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
LOG="/var/log/mailwolt-install.log"
|
|
||||||
STATE_DIR="/var/lib/mailwolt/install"
|
|
||||||
INSTALLER_SCRIPT="/var/www/mailwolt/mailwolt-installer/install.sh"
|
|
||||||
APP_DIR="/var/www/mailwolt"
|
|
||||||
|
|
||||||
install -d -m 0755 "$STATE_DIR" /var/lib/mailwolt /var/lib/mailwolt/wizard
|
|
||||||
chown www-data:www-data /var/lib/mailwolt/wizard
|
|
||||||
: > "$LOG"
|
|
||||||
chmod 0644 "$LOG"
|
|
||||||
|
|
||||||
echo "running" > "$STATE_DIR/state"
|
|
||||||
: > "$STATE_DIR/rc"
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "===== $(date -Is) :: Installation gestartet ====="
|
|
||||||
|
|
||||||
if [[ "$(id -u)" -ne 0 ]]; then
|
|
||||||
echo "[!] Muss als root laufen"
|
|
||||||
printf '1\n' > "$STATE_DIR/rc"
|
|
||||||
echo "done" > "$STATE_DIR/state"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Komponente aus $1, falls übergeben (z.B. "nginx", "postfix", "dovecot", "all")
|
|
||||||
COMPONENT="${1:-all}"
|
|
||||||
echo "[i] Komponente: $COMPONENT"
|
|
||||||
|
|
||||||
RC=0
|
|
||||||
if [[ -f "$INSTALLER_SCRIPT" ]]; then
|
|
||||||
APP_DIR="$APP_DIR" COMPONENT="$COMPONENT" bash "$INSTALLER_SCRIPT" || RC=$?
|
|
||||||
else
|
|
||||||
echo "[!] installer script nicht gefunden: $INSTALLER_SCRIPT"
|
|
||||||
RC=127
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "===== $(date -Is) :: Installation beendet (rc=$RC) ====="
|
|
||||||
printf '%s\n' "$RC" > "$STATE_DIR/rc"
|
|
||||||
echo "done" > "$STATE_DIR/state"
|
|
||||||
exit "$RC"
|
|
||||||
} 2>&1 | tee -a "$LOG"
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [ -f /etc/mailwolt/installer.env ]; then
|
|
||||||
set -a
|
|
||||||
. /etc/mailwolt/installer.env
|
|
||||||
set +a
|
|
||||||
fi
|
|
||||||
# ── Styling ────────────────────────────────────────────────────────────────
|
|
||||||
GREEN="$(printf '\033[1;32m')"; YELLOW="$(printf '\033[1;33m')"
|
|
||||||
RED="$(printf '\033[1;31m')"; CYAN="$(printf '\033[1;36m')"
|
|
||||||
GREY="$(printf '\033[0;90m')"; NC="$(printf '\033[0m')"
|
|
||||||
BAR="──────────────────────────────────────────────────────────────────────────────"
|
|
||||||
log(){ echo -e "${GREEN}[+]${NC} $*"; }
|
|
||||||
warn(){ echo -e "${YELLOW}[!]${NC} $*"; }
|
|
||||||
err(){ echo -e "${RED}[x]${NC} $*"; }
|
|
||||||
die(){ err "$*"; exit 1; }
|
|
||||||
require_root(){ [[ "$(id -u)" -eq 0 ]] || die "Bitte als root ausführen."; }
|
|
||||||
|
|
||||||
# --- Defaults, nur wenn noch nicht gesetzt ---------------------------------
|
|
||||||
: "${APP_USER:=mailwolt}"
|
|
||||||
: "${APP_GROUP:=www-data}"
|
|
||||||
: "${APP_DIR:=/var/www/${APP_USER}}"
|
|
||||||
|
|
||||||
: "${APP_NAME:=MailWolt}"
|
|
||||||
|
|
||||||
: "${BASE_DOMAIN:=example.com}"
|
|
||||||
: "${UI_SUB:=ui}"
|
|
||||||
: "${WEBMAIL_SUB:=webmail}"
|
|
||||||
: "${MTA_SUB:=mx}"
|
|
||||||
|
|
||||||
# DB / Redis (werden später durch .env überschrieben)
|
|
||||||
: "${DB_NAME:=${APP_USER}}"
|
|
||||||
: "${DB_USER:=${APP_USER}}"
|
|
||||||
: "${DB_PASS:=}"
|
|
||||||
: "${REDIS_PASS:=}"
|
|
||||||
|
|
||||||
# Stabile Zert-Pfade (UI/WEBMAIL/MX → symlinked via 20-ssl.sh)
|
|
||||||
: "${MAIL_SSL_DIR:=/etc/ssl/mail}"
|
|
||||||
: "${UI_SSL_DIR:=/etc/ssl/ui}"
|
|
||||||
: "${WEBMAIL_SSL_DIR:=/etc/ssl/webmail}"
|
|
||||||
: "${UI_CERT:=${UI_SSL_DIR}/fullchain.pem}"
|
|
||||||
: "${UI_KEY:=${UI_SSL_DIR}/privkey.pem}"
|
|
||||||
|
|
||||||
# Optional: E-Mail für LE
|
|
||||||
: "${LE_EMAIL:=admin@${BASE_DOMAIN}}"
|
|
||||||
|
|
||||||
load_env_file(){
|
|
||||||
local f="$1"
|
|
||||||
[[ -f "$f" ]] || return 0
|
|
||||||
while IFS='=' read -r k v; do
|
|
||||||
[[ "$k" =~ ^[A-Z0-9_]+$ ]] || continue
|
|
||||||
export "$k=$v"
|
|
||||||
done < <(grep -E '^[A-Z0-9_]+=' "$f")
|
|
||||||
}
|
|
||||||
|
|
||||||
header(){ echo -e "${CYAN}${BAR}${NC}
|
|
||||||
${CYAN} :::: :::: ::: ::::::::::: ::: ::: ::: :::::::: ::: :::::::::::
|
|
||||||
${CYAN} +:+:+: :+:+:+ :+: :+: :+: :+: :+: :+: :+: :+: :+: :+:
|
|
||||||
${CYAN} +:+ +:+:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+ +:+
|
|
||||||
${CYAN} +#+ +:+ +#+ +#++:++#++: +#+ +#+ +#+ +:+ +#+ +#+ +:+ +#+ +#+
|
|
||||||
${CYAN} +#+ +#+ +#+ +#+ +#+ +#+ +#+ +#+#+ +#+ +#+ +#+ +#+ +#+
|
|
||||||
${CYAN} #+# #+# #+# #+# #+# #+# #+#+# #+#+# #+# #+# #+# #+#
|
|
||||||
${CYAN} ### ### ### ### ########### ########## ### ### ######## ########## ###
|
|
||||||
${CYAN} ${CYAN}${BAR}${NC}\n"; }
|
|
||||||
|
|
||||||
#header(){ echo -e "${CYAN}${BAR}${NC}
|
|
||||||
#${CYAN} 888b d888 d8b 888 888 888 888 888
|
|
||||||
#${CYAN} 8888b d8888 Y8P 888 888 o 888 888 888
|
|
||||||
#${CYAN} 88888b.d88888 888 888 d8b 888 888 888
|
|
||||||
#${CYAN} 888Y88888P888 8888b. 888 888 888 d888b 888 .d88b. 888 888888
|
|
||||||
#${CYAN} 888 Y888P 888 '88b 888 888 888d88888b888 d88''88b 888 888
|
|
||||||
#${CYAN} 888 Y8P 888 .d888888 888 888 88888P Y88888 888 888 888 888
|
|
||||||
#${CYAN} 888 ' 888 888 888 888 888 8888P Y8888 Y88..88P 888 Y88b.
|
|
||||||
#${CYAN} 888 888 'Y888888 888 888 888P Y888 'Y88P' 888 'Y888
|
|
||||||
#${CYAN}${BAR}${NC}\n"; }
|
|
||||||
|
|
||||||
detect_ip(){
|
|
||||||
local ip
|
|
||||||
ip="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')" || true
|
|
||||||
[[ -n "${ip:-}" ]] || ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
|
||||||
[[ -n "${ip:-}" ]] || die "Konnte Server-IP nicht ermitteln."
|
|
||||||
echo "$ip"
|
|
||||||
}
|
|
||||||
detect_ipv4() {
|
|
||||||
local ext=""
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
ext="$(curl -fsS --max-time 2 https://ifconfig.me 2>/dev/null || true)"
|
|
||||||
[[ "$ext" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || ext=""
|
|
||||||
fi
|
|
||||||
echo "$ext"
|
|
||||||
}
|
|
||||||
detect_ipv6(){
|
|
||||||
local ip6
|
|
||||||
ip6="$(ip -6 addr show scope global 2>/dev/null | awk '/inet6/{print $2}' | cut -d/ -f1 | head -n1)" || true
|
|
||||||
[[ -n "${ip6:-}" ]] || ip6="$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /:/){print $i; exit}}')" || true
|
|
||||||
echo "${ip6:-}"
|
|
||||||
}
|
|
||||||
detect_timezone(){
|
|
||||||
local tz
|
|
||||||
if command -v timedatectl >/dev/null 2>&1; then
|
|
||||||
tz="$(timedatectl show -p Timezone --value 2>/dev/null | tr -d '[:space:]')" || true
|
|
||||||
[[ -n "${tz:-}" && "$tz" == */* ]] && { echo "$tz"; return; }
|
|
||||||
fi
|
|
||||||
[[ -r /etc/timezone ]] && { tz="$(sed -n '1p' /etc/timezone | tr -d '[:space:]')" || true; [[ "$tz" == */* ]] && { echo "$tz"; return; }; }
|
|
||||||
if [[ -L /etc/localtime ]]; then
|
|
||||||
tz="$(readlink -f /etc/localtime 2>/dev/null || true)"; tz="${tz#/usr/share/zoneinfo/}"
|
|
||||||
[[ "$tz" == */* ]] && { echo "$tz"; return; }
|
|
||||||
fi
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
tz="$(curl -fsSL --max-time 3 https://ipapi.co/timezone 2>/dev/null || true)"; [[ "$tz" == */* ]] && { echo "$tz"; return; }
|
|
||||||
fi
|
|
||||||
echo "UTC"
|
|
||||||
}
|
|
||||||
guess_locale_from_tz(){ case "${1:-UTC}" in
|
|
||||||
Europe/Berlin|Europe/Vienna|Europe/Zurich|Europe/Luxembourg|Europe/Brussels|Europe/Amsterdam) echo "de";;
|
|
||||||
*) echo "en";; esac; }
|
|
||||||
|
|
||||||
resolve_ok(){ local host="$1"; getent ahosts "$host" | awk '{print $1}' | sort -u | grep -q -F "${SERVER_PUBLIC_IPV4:-}" ; }
|
|
||||||
join_host(){ local sub="$1" base="$2"; [[ -z "$sub" ]] && echo "$base" || echo "$sub.$base"; }
|
|
||||||
|
|
||||||
# dns_preflight HOST [HOST2 ...]
|
|
||||||
# Prüft: A-Record → SERVER_PUBLIC_IPV4, MX (nur wenn HOST == MAIL_HOSTNAME), PTR.
|
|
||||||
# Gibt strukturierte Zeilen aus: OK|WARN|FAIL <host> <check> <detail>
|
|
||||||
# Rückgabe 0 = alles OK; 1 = mind. ein FAIL.
|
|
||||||
dns_preflight(){
|
|
||||||
local overall=0
|
|
||||||
local server_ip="${SERVER_PUBLIC_IPV4:-}"
|
|
||||||
|
|
||||||
_dns_line(){ local level="$1" host="$2" check="$3" detail="$4"
|
|
||||||
case "$level" in
|
|
||||||
OK) echo -e "${GREEN}[DNS OK ]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
|
|
||||||
WARN) echo -e "${YELLOW}[DNS WARN]${NC} ${host} ${GREY}${check}${NC} → ${detail}" ;;
|
|
||||||
FAIL) echo -e "${RED}[DNS FAIL]${NC} ${host} ${GREY}${check}${NC} → ${detail}"; overall=1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
for host in "$@"; do
|
|
||||||
[[ -z "$host" || "$host" == "example.com" ]] && continue
|
|
||||||
|
|
||||||
# A-Record
|
|
||||||
local a_ip
|
|
||||||
a_ip="$(dig +short A "$host" @1.1.1.1 2>/dev/null | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1)"
|
|
||||||
if [[ -z "$a_ip" ]]; then
|
|
||||||
_dns_line FAIL "$host" "A-Record" "kein Eintrag gefunden"
|
|
||||||
elif [[ -n "$server_ip" && "$a_ip" != "$server_ip" ]]; then
|
|
||||||
_dns_line FAIL "$host" "A-Record" "${a_ip} ≠ ${server_ip} (Server-IP)"
|
|
||||||
else
|
|
||||||
_dns_line OK "$host" "A-Record" "${a_ip}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# MX (nur für MAIL_HOSTNAME)
|
|
||||||
if [[ "$host" == "${MAIL_HOSTNAME:-}" ]]; then
|
|
||||||
local mx
|
|
||||||
mx="$(dig +short MX "$host" @1.1.1.1 2>/dev/null | awk '{print $2}' | head -n1)"
|
|
||||||
if [[ -z "$mx" ]]; then
|
|
||||||
_dns_line WARN "$host" "MX-Record" "kein Eintrag – ausgehende Mail ggf. eingeschränkt"
|
|
||||||
else
|
|
||||||
_dns_line OK "$host" "MX-Record" "${mx}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# PTR (nur wenn IP bekannt)
|
|
||||||
if [[ -n "$server_ip" && -n "$a_ip" && "$a_ip" == "$server_ip" ]]; then
|
|
||||||
local ptr
|
|
||||||
ptr="$(dig +short -x "$server_ip" @1.1.1.1 2>/dev/null | head -n1 | sed 's/\.$//')"
|
|
||||||
if [[ -z "$ptr" ]]; then
|
|
||||||
_dns_line WARN "$host" "PTR-Record" "kein Reverse-DNS – kann Spam-Score erhöhen"
|
|
||||||
elif [[ "$ptr" != "$host" && "$ptr" != "${MAIL_HOSTNAME:-}" ]]; then
|
|
||||||
_dns_line WARN "$host" "PTR-Record" "${ptr} (zeigt nicht auf ${host})"
|
|
||||||
else
|
|
||||||
_dns_line OK "$host" "PTR-Record" "${ptr}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
return $overall
|
|
||||||
}
|
|
||||||
|
|
||||||
upsert_env(){ # upsert in $ENV_FILE
|
|
||||||
local k="$1" v="$2" ek ev
|
|
||||||
ek="$(printf '%s' "$k" | sed -e 's/[.[\*^$(){}+?|/]/\\&/g')"
|
|
||||||
ev="$(printf '%s' "$v" | sed -e 's/[&/]/\\&/g')"
|
|
||||||
if grep -qE "^[#[:space:]]*${ek}=" "$ENV_FILE" 2>/dev/null; then
|
|
||||||
sed -Ei "s|^[#[:space:]]*${ek}=.*|${k}=${ev}|g" "$ENV_FILE"
|
|
||||||
else
|
|
||||||
printf '%s=%s\n' "$k" "$v" >> "$ENV_FILE"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue