fix mailing an email

This commit is contained in:
2026-06-23 08:05:51 +02:00
parent c5127da750
commit def8d22544
2479 changed files with 325393 additions and 781 deletions
+339
View File
@@ -26,6 +26,20 @@ App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi.
- **2026-06-17**: Google OAuth 2.0 unificato (revert Socialite, unified OAuth per Email/Drive/Calendar, XOAUTH2 SMTP+IMAP, UI impostazioni)
- **2026-06-17**: Fix CalendarioConnessione — `encryptAndSetConfig()` perdeva campi sensibili in edit, `is_active` checkbox senza hidden fallback, query duplicata nella view
- **2026-06-18**: Fix import CSV — `declare(strict_types=1)` mancante in IndividuoController, header check assente, Log::warning() su errori; fix freeze server (set_time_limit, session_write_close, DB::transaction, fgetcsv length illimitato) in GruppoController e IndividuoController
- **2026-06-23**: Email compose: firma spostata dopo il corpo; Mailing list: aggiunto `sender_account_id` (mittente predefinito salvato in creazione, usato all'invio come fallback)
- **2026-06-23**: Fix formati nome: responsabili gruppi e individui email ora mostrano "Nome Cognome"; mailing list edit: aggiunto pulsante "Rimuovi selezionati"
- **2026-06-22**: Aggiunto supporto Email Mittente Ufficiale (from_email/from_name) per invio con indirizzo diverso dalle credenziali SMTP/IMAP
- Migration: `from_email`, `from_name` columns su `email_settings`
- `EmailSetting`: metodi `getEffectiveFromAddress()`, `getEffectiveFromName()`, `getEffectiveReplyTo()`
- Tutti i metodi di invio (`sendViaImap`, `sendViaSystem`, `sendPasswordResetNotification`, `storeDraft`, `storeSentMessage`, `testSmtp`) ora usano `getEffectiveFromAddress()`/`getEffectiveFromName()` per il mittente
- Reply-To automatico impostato su `from_email` quando disponibile
- UI: nuovi campi "Email Mittente Ufficiale" e "Nome Mittente Ufficiale" in entrambe le pagine impostazioni email
- **2026-06-22**: Backup system enhancements:
- `BackupService::run()` ora accetta `$options` (include_files, include_env) per override temporanei senza persistere su DB
- `BackupRunCommand --no-files/--no-env` non salva più permanentemente la configurazione (era un bug)
- `manifest.json` ora include `included_components` array (database, env, files)
- Vista backup: badge .env (grigio) e Files (giallo) nella colonna "Contenuto" basati sul manifest
- `run()` restituisce `steps` array; mostrato in flash message (controller) e output (command)
- **2026-06-18**: Diagnose script (`diagnose.php`), .env.example aggiornato (SESSION_SECURE_COOKIE, FORCE_HTTPS, TRUSTED_PROXIES), build-dist.sh aggiornato con passo diagnose
## Funzionalità Implementate
@@ -751,3 +765,328 @@ Aggiunte variabili mancanti:
### Verifica
- `php -l diagnose.php`: OK
- `php diagnose.php`: eseguito con 58/68 check superati su server locale
## 2026-06-19 — Fix 419: Duplicated middleware + session env vars + diagnose.php fixes
### Problema
`POST /login` restituiva **419 Page Expired** su HTTP. La causa era doppia:
1. **Critico — Middleware duplicato in `bootstrap/app.php`**: `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, `ShareErrorsFromSession` erano appesi al web group via `$middleware->web(append: [...])`. Questi middleware sono **già attivi di default** in Laravel 13. Appenderli una seconda volta causava doppia crittografia/decrittografia dei cookie di sessione → session ID corrotto → token CSRF non riconosciuto → 419.
2. **`SESSION_SECURE_COOKIE` non impostato** nel `.env` — con `APP_URL=https` + `FORCE_HTTPS=true`, Laravel impostava il flag `Secure` sul cookie di sessione. Poiché il server serviva solo HTTP, il browser non inviava il cookie → session persa → 419.
### Fix apportati
**`bootstrap/app.php`** (righe 22-27):
- Rimosso l'intero blocco `$middleware->web(append: [...])` che duplicava EncryptCookies, AddQueuedCookiesToResponse, StartSession, ShareErrorsFromSession.
- Sostituito con commento esplicativo per prevenire future reintroduzioni.
**`.env`**:
- Aggiunto `SESSION_SECURE_COOKIE=false` — impedisce il flag Secure su HTTP
- Aggiunto `SESSION_SAME_SITE=lax` — esplicito per consistenza cross-protocol
- Cambiato `APP_URL` da HTTPS a HTTP (ambiente locale senza HTTPS)
- Cambiato `APP_PROTOCOL` da `https` a `http`
- Cambiato `FORCE_HTTPS` da `true` a `false`
- Cambiato `TRUSTED_PROXIES` da `*` a CIDR espliciti
**`diagnose.php`** — 4 bug fix:
1. **`unlink()` senza `file_exists()`** nelle linee 863 e 900: causava PHP Warning quando curl non creava il cookie jar (es. HTTPS non raggiungibile). Aggiunto `file_exists($cookieJar) && unlink($cookieJar)`.
2. **Regex middleware duplicato non matchava array multi-riga**: il pattern `[^\]]*` non matcha newline. Sostituito con `(.+?)` con flag `s`. Classe name extraction semplificata a `([a-zA-Z]+)::class`.
3. **`CURLOPT_COOKIEJAR` non funzionante** in questo ambiente (curl 8.14.1 + PHP 8.4 — il file non viene creato da `curl_close()`). Riscritto `testCsrfLogin()` per parsare manualmente gli header `Set-Cookie` dalla prima risposta e reinviarli via `CURLOPT_COOKIE` nella POST. Eliminata dipendenza da `COOKIEJAR`/`COOKIEFILE`.
4. Aggiunto verbose debug per codici HTTP POST anomali (≠ 302/200).
### Risultato finale
```
✅ Passed: 63
⚠️ Warnings: 8 (upload/post_max_size, engine opzionali mancanti)
❌ Failed: 1 (spazio disco 94.6% — server admin)
```
- Test CSRF: ✅ POST login → HTTP 302 (nessun 419)
- Solo il disco (94.6%) è critico — non risolvibile via script.
### File modificati
- `bootstrap/app.php` — rimosso middleware web duplicato
- `.env` — aggiunte SESSION_SECURE_COOKIE, SESSION_SAME_SITE; APP_URL/APP_PROTOCOL/FORCE_HTTPS adeguati
- `diagnose.php` — fix unlink warning, regex middleware multi-line, CSRF test manual-cookie, verbose debug
## 2026-06-19 — diagnose.php: storage false positivo + ForceHttps registration check
### Fix 1 — Storage permission false positive
**Problema**: `diagnose.php` segnava 15 storage directory come ❌ NON SCRIVIBILE quando `php diagnose.php` veniva eseguito da CLI (utente `opencode`). Le directory sono di proprietà `www-data:www-data` con permessi `775`, quindi il web server **può** scrivere — il falso positivo generava confusione.
**Diagnosi**:
- CLI esegue come `opencode``is_writable()` controlla i permessi "other" (5 = r-x) → `false`
- Web server esegue come `www-data` → owner `rwx``true`
- Questo è normale su server condivisi/Debian standard
**Fix** (`diagnose.php:659-660`):
- Aggiunto `elseif ($owner === WEB_USER && in_array($perms, ['775', '777', '755', '770']))`
- Se il proprietario è `www-data` e permessi sono `775`/`777`/`755`/`770`, mostra ⚠️ **warning** invece di ❌ **fail**, con messaggio "NON scrivibile da CLI ma WEB server sì"
- Altrimenti mostra fail come prima
### Fix 2 — ForceHttps registration check
**Problema**: Il controllo ForceHttps usava `str_contains($appContent, 'ForceHttps')` che matchava anche il semplice `use App\Http\Middleware\ForceHttps` (import), segnando ✅ "registrato" anche quando il middleware non era attivo in alcun gruppo.
**Stato attuale**: `ForceHttps::class` è **importato** ma **non registrato** in nessun middleware group (`$middleware->append()`, `->web()`, ecc.) in `bootstrap/app.php`.
**Fix** (`diagnose.php:871-879`):
- Sostituito con `preg_match('/\$middleware\s*->\s*\w+\s*\(.*?ForceHttps::class/s', $appContent)`
- 3 stati:
1.`"registrato correttamente"` — se ForceHttps::class è argomento di una chiamata `$middleware->xxx()`
2. ⚠️ `"IMPORTATO ma NON registrato"` — se la stringa ForceHttps esiste (use import) ma non in una chiamata
3. ⚠️ `"NON presente"` — se ForceHttps non esiste nel file
### Verifica
- `php -l diagnose.php`: No syntax errors detected
- `php diagnose.php` su locale: ForceHttps ora segnala ⚠️ "IMPORTATO ma NON registrato" (corretto)
### File modificati
- `diagnose.php` — storage false positive + ForceHttps registration check
## 2026-06-22 — build-dist.sh: genera post-deploy.sh per upgrade/install automatico
**Obiettivo**: Includere nell'archivio di distribuzione uno script `post-deploy.sh` che esegue controllo e setup dell'installazione sul server target, sia per fresh install che per upgrade.
### Cosa è stato fatto
**`build-dist.sh`** riscritto:
- Prima del `tar`, genera `post-deploy.sh` (heredoc) e lo include nell'archivio
- Dopo il `tar`, `post-deploy.sh` viene rimosso localmente (non sporca la working dir)
- Istruzioni finali aggiornate: mostrano i tre comandi principali
**`post-deploy.sh`** (generato, non versionato):
Tre modalità d'uso:
| Comando | Cosa fa |
|---------|---------|
| `bash post-deploy.sh check` | Diagnostica stato installazione: migration pendenti, permessi directory, symlink, cache. Nessuna modifica. |
| `bash post-deploy.sh upgrade` | Check + upgrade esistente: crea directory, permessi, symlink, `migrate --force`, `key:generate` (se mancante), cache clear, verifica finale. Chiede conferma prima di eseguire. |
| `bash post-deploy.sh install` | Fresh install completo: come upgrade + copia `.env.example → .env` con pausa per modifica utente. |
| `bash post-deploy.sh upgrade --yes` | Batch mode (nessuna conferma richiesta) |
Fasi upgrade (8 step): directory → .gitignore → permessi → symlink → migrazioni → key → cache → diagnose
Fasi install (9 step): come upgrade + setup .env con pausa interattiva
### Vantaggi
- **Idempotente**: `migrate --force` esegue solo migration pendenti
- **Sicuro**: modalità `check` non modifica nulla
- **Interattivo**: chiede conferma prima di azioni distruttive
- **Batch**: `--yes` per automazione CI/CD
- **Compatibile**: integra `diagnose.php` già esistente (check + fix)
- **Autopulente**: `post-deploy.sh` non resta nella working dir di build
### Verifica
- `bash -n build-dist.sh`: syntax OK
- `bash -n post-deploy.sh`: syntax OK
- `bash post-deploy.sh check`: eseguito correttamente
- `tar tzf glastree-*.tar.gz | grep post-deploy`: presente nell'archivio
- `rm -f post-deploy.sh` dopo tar: pulizia OK
## 2026-06-22 — Estensione Gruppi: contatti propri + diocesi multiple
**Obiettivo**: Aggiungere contatti (email, telefono) ai gruppi e supportare assegnazione di più diocesi.
### Cosa è stato fatto
**Migration** (2 nuove tabelle):
- `2026_06_22_000002_create_gruppo_contatti_table.php``gruppo_contatti` con: id, gruppo_id (FK cascade), tipo, valore, etichetta, is_primary, timestamps
- `2026_06_22_000003_create_diocesi_gruppo_table.php` — pivot `diocesi_gruppo` con: id, gruppo_id (FK cascade), diocesi_id (FK cascade), unique(gruppo_id, diocesi_id)
**Model `GruppoContatto.php`** (nuovo):
- `$table = 'gruppo_contatti'`, fillable: gruppo_id, tipo, valore, etichetta, is_primary
- `gruppo()`: BelongsTo Gruppo
**Model `Gruppo.php`**:
- `diocesi()`: cambiata da `BelongsTo` a `BelongsToMany` via `diocesi_gruppo`
- `gruppoContatti()`: nuova `HasMany` relation
- Accessor `getEmailPrimariaAttribute()`: primo contatto email (is_primary preferito)
- Accessor `getTelefonoPrimarioAttribute()`: primo contatto telefono/cellulare (is_primary preferito)
- `$appends`: già presente `email_primaria`, `telefono_primario` (invariato)
- `$fillable`: `diocesi_id` mantenuto (nullable) per backward compat con import CSV
**Model `Diocesi.php`**:
- `gruppi()`: cambiata da `HasMany` a `BelongsToMany` via `diocesi_gruppo`
**`GruppoController.php`**:
- `store()`/`update()`: validazione cambiata da `diocesi_id` (nullable|exists) a `diocesi_ids` (nullable|array|exists)
- `store()`/`update()`: nuova validazione `contatti` array con tipo (in:email,telefono,cellulare), valore, etichetta, is_primary
- `store()`: crea contatti dopo create; sync diocesi
- `update()`: cancella+ricrea contatti se presenti; sync diocesi (o detach se assenti)
- `edit()`: eager-load `gruppoContatti`, passa `$selectedDiocesiIds`
- `show()`: eager-load `gruppoContatti`
- `index()`: eager-load `gruppoContatti`
- `create()`: invariato (passa già `$diocesi`)
**`ReportController.php`**:
- 4 occorrenze `$g->diocesi?->nome` → collection pattern `$g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-'`
**View `gruppi/create.blade.php`**:
- Select diocesi: cambiato da single `select name="diocesi_id"` a multiple `select name="diocesi_ids[]" class="select2-multi"`
- Aggiunta card "Contatti del Gruppo" con tabella inline (tipo select, valore input, etichetta, checkbox primario, elimina)
- Select2 CSS/JS caricati via CDN
**View `gruppi/edit.blade.php`**:
- Stessa modifica diocesi → Select2 multi con `$selectedDiocesiIds` pre-selezionati
- Aggiunta card "Contatti del Gruppo" con righe pre-popolate da `$gruppo->gruppoContatti`
- Select2 CSS/JS caricati via CDN
**View `gruppi/show.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
- Aggiunte righe Email/Telefono (da accessor) e "Altri Contatti" nella card info
**View `gruppi/index.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
- Telefono/Email: da hardcoded `-` a `$gruppo->telefono_primario` / `$gruppo->email_primaria`
**View `gruppi/partials/tree-item.blade.php`**:
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
**View `individui/show.blade.php` + `individui/edit.blade.php`**:
- Diocesi nei gruppi dell'individuo: da `$gruppo->diocesi?->nome` a collection implode
### Backward compatibility
- Colonna `diocesi_id` su `gruppi` mantenuta (nullable) per import CSV
- CSV import (`importStore()`) usa ancora `diocesi_id` → singola diocesi
- `$fillable` include ancora `diocesi_id`
- Nessuna modifica a `Individuo` o `Contatto` esistenti
### Verifica
- `php -l` su tutti i file modificati: OK
- Migrations eseguite: ✅
- Relazioni verificate: `diocesi()` BelongsToMany, `gruppoContatti()` HasMany, `email_primaria`/`telefono_primario` accessors funzionanti
## 2026-06-22 — Fix produzione: 500 pagina gruppi (migration pending + autoloader stale)
**Problema**: Dopo deploy su server produzione (`192.168.222.177`):
- Migration `diocesi_gruppo` non eseguita (PENDING)
- `GruppoContatto.php` non nell'autoloader (`composer dump-autoload` non eseguito)
- `storage/logs/laravel.log` non scrivibile da www-data
- 3 migration pre-esistenti non marcate come "ran" (google_oauth_connections, add_auth_method, add_google_id) — tabelle/colonne già esistenti ma migration record mancanti → bloccavano `migrate --force`
**Fix**:
1. `sudo chmod -R 775 storage bootstrap/cache` — permessi
2. `sudo usermod -a -G www-data opencode` — utente CLI nel gruppo www-data
3. Inseriti manualmente 3 migration record mancanti in `migrations` table (già eseguite in passato ma mai registrate)
4. `php artisan migrate --force` — eseguita `2026_06_22_000003_create_diocesi_gruppo_table`
5. `composer dump-autoload` — autoloader rigenerato (41464 classi)
6. `php artisan view:clear && php artisan route:clear && php artisan config:clear`
**Risultato**:
- `class_exists(App\Models\GruppoContatto)` → ✅
- `gruppi` page HTTP 200 (after login redirect 302) — 500 risolto
- Tutte le migration marked as Ran (batch 40-42)
- Views/routes/config cache pulite
**Lezione**: `build-dist.sh` fa `composer install --no-scripts` che non rigenera autoloader ottimizzato per il target. `post-deploy.sh upgrade` dovrebbe includere `composer dump-autoload` (o almeno `composer install --no-dev --optimize-autoloader`).
## 2026-06-22 — Fix: Select2 search field visibile sotto le diocesi selezionate
**Problema**: In create/edit gruppi, il `<textarea class="select2-search__field">` creato da Select2 in modalità multiple era visibile sotto i tag delle diocesi selezionate, con altezza 65px (ereditata da Bootstrap form-control).
**Causa**:
1. `allowClear: true` in Select2 multiple mode non serve (ogni tag ha già la X per rimuoverlo) e causa conflitti di rendering
2. Bootstrap 4 applica `height: 65px` al textarea del search field
3. Nessun CSS specifico per normalizzare il search field inline
**Fix**:
1. Rimosso `allowClear: true` da entrambi i file (`create.blade.php`, `edit.blade.php`)
2. Aggiunto CSS per normalizzare `height: 28px`, `border: none`, `background: transparent`, `width: auto` con `min-width: 30px`
**File modificati**:
- `resources/views/gruppi/create.blade.php` — rimosso allowClear, aggiunto CSS search field
- `resources/views/gruppi/edit.blade.php` — rimosso allowClear, aggiunto CSS search field
## 2026-06-23 — Fix: Select2 counter rimosso (utente vuole vedere tutti i nomi)
**Problema**: Il `templateSelection` con counter mostrava "5 diocesi selezionate" invece dei nomi.
**Fix**: Rimosso l'intero blocco `templateSelection` da entrambi create/edit. Ogni diocesi selezionata mostra ora il proprio nome.
**File modificati**:
- `resources/views/gruppi/create.blade.php` — rimosso templateSelection
- `resources/views/gruppi/edit.blade.php` — rimosso templateSelection
## 2026-06-23 — Fix: DiocesiSeeder + build-dist.sh per deploy
**Problema**: La tabella `diocesi` con 225 record importati da ODS non veniva popolata sul server target dopo deploy. Il seeder `DiocesiSeeder.php` aveva solo ~100 nomi obsoleti/inaccurati (es. "Mongolia", "Donegal", "Tirana").
**Fix**:
1. **`DiocesiSeeder.php`** riscritto completamente:
- 225 nomi corretti (Arcidiocesi/Diocesi/Sede/Patriarcato/Abbazia/Eparachia)
- Encoding: 7 nomi con mojibake da ODS fixati via `where('id', ...)->update()` (Trinità, Cefalù, Città, Forlì, Mondovì, Nardò, Perugia-Città)
- Apostrofo: "Val d Elsa" → "Val d'Elsa"
- `declare(strict_types=1)` aggiunto
- `firstOrCreate` per idempotenza
2. **`build-dist.sh`** (`post-deploy.sh` generato):
- Upgrade: aggiunto step `[6/10] Seed diocesi` dopo migrazioni (rinumerati da [1-5/9] → [1-10/10])
- Install: aggiunto step `[9/11] Seed diocesi` dopo migrazioni (rinumerato da [1-8/10] → [1-11/11])
- Usa `--force` per bypassare conferma in produzione
**File modificati**:
- `database/seeders/DiocesiSeeder.php` — riscritto con 225 nomi + fix encoding
- `build-dist.sh` — aggiunto step seed diocesi in upgrade/install
## 2026-06-23 — Fix 419: SESSION_DRIVER=database + diagnose CSRF su IP locale
**Problema**: Accesso via IP di rete locale (`http://192.168.222.174`) poteva causare 419 Page Expired. Sessione su file vulnerabile a permessi/LOCK del filesystem.
**Fix**:
1. **`SESSION_DRIVER=database`**: cambiato da `file` a `database`. La sessione su DB non soffre di:
- Permessi filesystem errati
- Lock concorrente su file
- Pulizia sessioni scadute lottery-based
2. **Migration `2026_06_23_000001_create_sessions_table.php`**: con guard `Schema::hasTable` per idempotenza cross-DB
3. **`diagnose.php`**: aggiunto test CSRF su IP locale automatico. Rileva `hostname -I` ed esegue POST login → verifica 419 su `http://<lan-ip>/login`
4. **`.env` e `.env.example`**: `SESSION_DRIVER=file``SESSION_DRIVER=database`
5. Vecchi file di sessione in `storage/framework/sessions/` eliminati
**Risultato**:
- diagnose.php: `✅ HTTP: POST login → HTTP 302 (nessun 419)`
- diagnose.php: `✅ LOCAL IP: POST login → HTTP 302 (nessun 419)`
- Sessioni persistono in tabella `sessions` (DB) invece di file
**File modificati**:
- `database/migrations/2026_06_23_000001_create_sessions_table.php` (nuovo)
- `.env` — SESSION_DRIVER=database
- `.env.example` — SESSION_DRIVER=database
- `diagnose.php` — test CSRF su IP locale
## 2026-06-23 — Email compose: firma dopo corpo + Mailing list: mittente e firma
**Obiettivo**: Spostare la select firma dopo il corpo email nella pagina di composizione email. Aggiungere mittente predefinito (`sender_account_id`) alle mailing list (salvato in creazione/edit, usato come fallback all'invio).
### Cosa è stato fatto
**Migration** (`2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php`):
- Aggiunta colonna `sender_account_id` (FK → sender_accounts, nullOnDelete) a `mailing_lists`
**Model `MailingList.php`**:
- `sender_account_id` aggiunto a `$fillable`
- `senderAccount()`: nuova relazione BelongsTo → `SenderAccount`
**Controller `MailingListController.php`**:
- `create()`: passa `$senderAccounts` (SenderAccount::active()->get()) alla view
- `edit()`: passa `$senderAccounts` alla view, eager-load `senderAccount`
- `store()`: validazione `sender_account_id` nullable|exists, salvato in create
- `update()`: validazione `sender_account_id` nullable|exists, salvato in update
**View `mailing-liste/create.blade.php`**:
- Aggiunto select per `sender_account_id` dopo il select firma
**View `mailing-liste/edit.blade.php`**:
- Aggiunto select per `sender_account_id` dopo il select firma, con `selected` se uguale a `$mailingList->sender_account_id`
**View `email/compose.blade.php`**:
- Spostato blocco firma (prima del body) → dopo il textarea body (prima della sezione allegati)
**Controller `MailingController.php`**:
- `invia()`: fallback a `$lista->senderAccount` se `mittente_id` non fornito esplicitamente (stesso pattern del firma_id fallback già esistente)
- `invioElabora()`: fallback a `mittente_id`/`firma_id` dalla mailing list se non forniti esplicitamente (solo quando una singola lista è selezionata)
**File modificati**:
- `database/migrations/2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php` (nuovo)
- `app/Models/MailingList.php` — fillable + senderAccount relazione
- `app/Http/Controllers/MailingListController.php` — sender_account in create/edit/store/update
- `app/Http/Controllers/MailingController.php` — fallback mittente/firma da mailing list
- `resources/views/email/compose.blade.php` — firma spostata dopo il body
- `resources/views/mailing-liste/create.blade.php` — select mittente aggiunto
- `resources/views/mailing-liste/edit.blade.php` — select mittente aggiunto