feat(13-email-mailboxes): phase 13 complete — email DB foundation + SMTP mailbox CRUD
3 migrations (email_mailboxes, email_templates, email_logs), full CRUD for SMTP mailboxes with encrypted passwords (IntegrationSecretCipher), native SMTP connection test via stream_socket_client, sidebar navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,10 @@ Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i n
|
||||
|
||||
### Active (In Progress)
|
||||
|
||||
(No active requirements — define next milestone)
|
||||
- [x] DB Foundation: tabele email_mailboxes, email_templates, email_logs — Phase 13
|
||||
- [x] Skrzynki pocztowe SMTP (CRUD + test połączenia) — Phase 13
|
||||
- [ ] Szablony wiadomości e-mail (CRUD + Quill.js + system zmiennych) — Phase 14
|
||||
- [ ] Wysyłka e-mail z zamówień (resolwer zmiennych, załączniki, log) — Phase 15
|
||||
|
||||
### Planned (Next)
|
||||
|
||||
|
||||
@@ -6,7 +6,15 @@ orderPRO to narzędzie do wielokanałowego zarządzania sprzedażą. Projekt prz
|
||||
|
||||
## Current Milestone
|
||||
|
||||
No active milestone. Run /paul:discuss-milestone or /paul:milestone to define next.
|
||||
### v0.4 Moduł E-mail — In progress
|
||||
|
||||
Skrzynki pocztowe SMTP, szablony wiadomości z systemem zmiennych (Quill.js), wysyłka maili z zamówień z załącznikami.
|
||||
|
||||
| Phase | Name | Plans | Status |
|
||||
|-------|------|-------|--------|
|
||||
| 13 | DB + Skrzynki pocztowe | 1/1 | Complete ✓ |
|
||||
| 14 | Szablony wiadomości | TBD | Not started |
|
||||
| 15 | Wysyłka e-mail z zamówień | TBD | Not started |
|
||||
|
||||
## Completed Milestones
|
||||
|
||||
@@ -61,4 +69,4 @@ Archive: `.paul/milestones/v0.1-ROADMAP.md`
|
||||
|
||||
---
|
||||
*Roadmap created: 2026-03-12*
|
||||
*Last updated: 2026-03-15 — milestone v0.3 complete*
|
||||
*Last updated: 2026-03-15 — milestone v0.4 started*
|
||||
|
||||
@@ -5,32 +5,31 @@
|
||||
See: .paul/PROJECT.md (updated 2026-03-12)
|
||||
|
||||
**Core value:** Sprzedawca może obsługiwać zamówienia ze wszystkich kanałów sprzedaży i nadawać przesyłki bez przełączania się między platformami.
|
||||
**Current focus:** Awaiting next milestone.
|
||||
**Current focus:** v0.4 Moduł E-mail — Skrzynki pocztowe, szablony wiadomości, wysyłka z zamówień.
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: Awaiting next milestone
|
||||
Phase: None active
|
||||
Plan: None
|
||||
Status: Milestone v0.3 complete — ready for next
|
||||
Last activity: 2026-03-15 — Milestone v0.3 completed
|
||||
Milestone: v0.4 Moduł E-mail
|
||||
Phase: [1] of [3] (DB + Skrzynki pocztowe) — Complete
|
||||
Plan: 13-01 complete
|
||||
Status: Phase 13 complete, ready for Phase 14
|
||||
Last activity: 2026-03-15 — UNIFY 13-01 complete, phase transition done
|
||||
|
||||
Progress:
|
||||
- v0.1 Initial Release: [██████████] 100% ✓
|
||||
- v0.2 Pre-Expansion Fixes: [██████████] 100% ✓
|
||||
- v0.3 Moduł Paragonów: [██████████] 100% ✓
|
||||
- Phase 8: [██████████] 100% ✓
|
||||
- Phase 9: [██████████] 100% ✓
|
||||
- Phase 10: [██████████] 100% ✓
|
||||
- Phase 11: [██████████] 100% ✓
|
||||
- Phase 12: [██████████] 100% ✓
|
||||
- v0.4 Moduł E-mail: [███░░░░░░░] 33%
|
||||
- Phase 13: [██████████] 100% ✓
|
||||
- Phase 14: [░░░░░░░░░░] 0%
|
||||
- Phase 15: [░░░░░░░░░░] 0%
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
○ ○ ○ [Milestone complete — ready for next]
|
||||
✓ ✓ ✓ [Loop complete — Phase 13 done, ready for Phase 14]
|
||||
```
|
||||
|
||||
## Accumulated Context
|
||||
@@ -49,6 +48,9 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||
| 2026-03-15 | InPost ShipX API (nie Allegro WZA) jako natywny provider | Faza 07 | InpostShipmentService niezależny od Allegro; workaround remap usunięty |
|
||||
| 2026-03-15 | vendor/ dodany do ftp-kr ignore; deploy vendor ręcznie | Faza 07 | Auto-upload nie nadpisze vendor/ na serwerze |
|
||||
| 2026-03-15 | Snapshot pattern: seller/buyer/items jako JSON | Faza 10 | Dane zamrożone w momencie wystawienia paragonu |
|
||||
| 2026-03-15 | Natywny stream_socket_client do testu SMTP (bez PHPMailer) | Faza 13 | Test połączenia SMTP bez nowych zależności; PHPMailer w fazie 15 |
|
||||
| 2026-03-15 | IntegrationSecretCipher do szyfrowania haseł SMTP | Faza 13 | Reuse istniejącego AES-256-CBC+HMAC; spójny wzorzec |
|
||||
| 2026-03-15 | Auto-reset is_default na email_mailboxes przy save | Faza 13 | Tylko jedna domyślna skrzynka |
|
||||
| 2026-03-15 | Atomowe numerowanie: INSERT ON DUPLICATE KEY UPDATE | Faza 10 | Bezpieczne kolejne numery paragonów |
|
||||
| 2026-03-15 | Moduł Accounting w App\Modules\Accounting | Faza 10 | Separacja od Settings |
|
||||
| 2026-03-15 | dompdf v3.1 server-side PDF generation | Faza 11 | Nowa zależność composer; wymaga vendor/ na serwerze |
|
||||
@@ -56,6 +58,11 @@ PLAN ──▶ APPLY ──▶ UNIFY
|
||||
| 2026-03-15 | PhpSpreadsheet v5.5 dla eksportu XLSX | Faza 12 | Nowa zależność composer; XLSX lepszy od CSV dla księgowości |
|
||||
| 2026-03-15 | POST eksport z CSRF + dwa tryby (zaznaczone/wszystkie z filtra) | Faza 12 | Bezpieczny eksport; selectable table-list reuse |
|
||||
|
||||
### Skill Audit (Faza 13, Plan 01)
|
||||
| Oczekiwany | Wywołany | Uwagi |
|
||||
|------------|---------|-------|
|
||||
| sonar-scanner | ○ | Required — do uruchomienia przed kolejnym UNIFY |
|
||||
|
||||
### Skill Audit (Faza 12, Plan 01)
|
||||
| Oczekiwany | Wywołany | Uwagi |
|
||||
|------------|---------|-------|
|
||||
@@ -150,14 +157,14 @@ Brak.
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-15
|
||||
Stopped at: Milestone v0.3 complete
|
||||
Next action: /paul:discuss-milestone or /paul:milestone for next
|
||||
Resume file: .paul/MILESTONES.md
|
||||
Stopped at: Phase 13 complete
|
||||
Next action: /paul:plan for Phase 14 (Szablony wiadomości)
|
||||
Resume file: .paul/phases/13-email-mailboxes/13-01-SUMMARY.md
|
||||
Resume context:
|
||||
- v0.1: COMPLETE ✓ (6 phases, 15 plans)
|
||||
- v0.2: COMPLETE ✓ (1 phase, 5 plans)
|
||||
- v0.3: COMPLETE ✓ (5 phases, 5 plans) — Moduł Paragonów
|
||||
- Next milestone: TBD
|
||||
- v0.4: IN PROGRESS — Phase 13 complete, Phase 14 next
|
||||
- Faza 0 (nieaktywne przyciski) zrobiona poza planem
|
||||
|
||||
---
|
||||
|
||||
274
.paul/phases/13-email-mailboxes/13-01-PLAN.md
Normal file
274
.paul/phases/13-email-mailboxes/13-01-PLAN.md
Normal file
@@ -0,0 +1,274 @@
|
||||
---
|
||||
phase: 13-email-mailboxes
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- database/migrations/20260315_000054_create_email_mailboxes_table.sql
|
||||
- database/migrations/20260315_000055_create_email_templates_table.sql
|
||||
- database/migrations/20260315_000056_create_email_logs_table.sql
|
||||
- src/Modules/Settings/EmailMailboxController.php
|
||||
- src/Modules/Settings/EmailMailboxRepository.php
|
||||
- resources/views/settings/email-mailboxes.php
|
||||
- src/Core/Application.php
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Stworzyć fundament bazodanowy modułu e-mail (3 tabele) oraz pełny CRUD skrzynek pocztowych SMTP w sekcji Ustawienia z walidacją połączenia.
|
||||
|
||||
## Purpose
|
||||
Skrzynki pocztowe to fundament wysyłki e-mail — szablony (faza 14) i wysyłka z zamówień (faza 15) zależą od skonfigurowanych skrzynek SMTP.
|
||||
|
||||
## Output
|
||||
- 3 migracje SQL (email_mailboxes, email_templates, email_logs)
|
||||
- CRUD skrzynek pocztowych w Ustawienia > Skrzynki pocztowe
|
||||
- Endpoint testowania połączenia SMTP
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Settings/ReceiptConfigController.php (wzorzec CRUD w Settings)
|
||||
@src/Modules/Settings/ReceiptConfigRepository.php (wzorzec repository)
|
||||
@src/Modules/Settings/IntegrationSecretCipher.php (szyfrowanie haseł)
|
||||
@resources/views/settings/accounting.php (wzorzec widoku list+edit)
|
||||
@resources/views/layouts/app.php (nawigacja sidebar)
|
||||
@src/Core/Application.php (routing)
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
|
||||
|
||||
**BLOCKING:** sonar-scanner musi być uruchomiony przed UNIFY.
|
||||
|
||||
## Skill Invocation Checklist
|
||||
- [ ] sonar-scanner uruchomiony po APPLY
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Migracje tworzą 3 tabele
|
||||
```gherkin
|
||||
Given baza danych bez tabel email_*
|
||||
When uruchomiona zostanie migracja przez Ustawienia > Baza danych
|
||||
Then tabele email_mailboxes, email_templates, email_logs istnieją z poprawnymi kolumnami i FK
|
||||
```
|
||||
|
||||
## AC-2: CRUD skrzynek pocztowych
|
||||
```gherkin
|
||||
Given użytkownik na stronie /settings/email-mailboxes
|
||||
When dodaje nową skrzynkę (nazwa, serwer, port, użytkownik, hasło)
|
||||
Then skrzynka pojawia się na liście z zaszyfrowanym hasłem w DB
|
||||
And można ją edytować i usunąć
|
||||
```
|
||||
|
||||
## AC-3: Walidacja połączenia SMTP
|
||||
```gherkin
|
||||
Given użytkownik wypełnił formularz skrzynki pocztowej
|
||||
When kliknie "Testuj połączenie"
|
||||
Then system próbuje nawiązać połączenie SMTP z podanymi danymi
|
||||
And wyświetla komunikat sukcesu lub szczegółowy błąd
|
||||
```
|
||||
|
||||
## AC-4: Nawigacja
|
||||
```gherkin
|
||||
Given użytkownik zalogowany do panelu
|
||||
When przechodzi do Ustawienia
|
||||
Then widzi pozycję "Skrzynki pocztowe" w menu Settings
|
||||
And kliknięcie prowadzi do /settings/email-mailboxes
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracje SQL — 3 tabele email</name>
|
||||
<files>
|
||||
database/migrations/20260315_000054_create_email_mailboxes_table.sql,
|
||||
database/migrations/20260315_000055_create_email_templates_table.sql,
|
||||
database/migrations/20260315_000056_create_email_logs_table.sql
|
||||
</files>
|
||||
<action>
|
||||
**Tabela `email_mailboxes`:**
|
||||
- id INT UNSIGNED AUTO_INCREMENT PK
|
||||
- name VARCHAR(100) NOT NULL — nazwa wyświetlana (np. "Główna skrzynka")
|
||||
- smtp_host VARCHAR(255) NOT NULL
|
||||
- smtp_port SMALLINT UNSIGNED NOT NULL DEFAULT 587
|
||||
- smtp_encryption ENUM('tls','ssl','none') NOT NULL DEFAULT 'tls'
|
||||
- smtp_username VARCHAR(255) NOT NULL
|
||||
- smtp_password_encrypted TEXT NOT NULL — szyfrowane przez IntegrationSecretCipher
|
||||
- sender_email VARCHAR(255) NOT NULL — adres nadawcy (from)
|
||||
- sender_name VARCHAR(200) DEFAULT NULL — nazwa nadawcy
|
||||
- is_default TINYINT(1) NOT NULL DEFAULT 0
|
||||
- is_active TINYINT(1) NOT NULL DEFAULT 1
|
||||
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
|
||||
**Tabela `email_templates`:**
|
||||
- id INT UNSIGNED AUTO_INCREMENT PK
|
||||
- name VARCHAR(200) NOT NULL — nazwa szablonu
|
||||
- subject VARCHAR(500) NOT NULL — temat e-mail (może zawierać zmienne)
|
||||
- body_html TEXT NOT NULL — treść HTML (Quill output)
|
||||
- mailbox_id INT UNSIGNED DEFAULT NULL — FK do email_mailboxes (nullable = domyślna)
|
||||
- is_active TINYINT(1) NOT NULL DEFAULT 1
|
||||
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
- FOREIGN KEY (mailbox_id) REFERENCES email_mailboxes(id) ON DELETE SET NULL
|
||||
|
||||
**Tabela `email_logs`:**
|
||||
- id BIGINT UNSIGNED AUTO_INCREMENT PK
|
||||
- template_id INT UNSIGNED DEFAULT NULL — FK do email_templates
|
||||
- mailbox_id INT UNSIGNED DEFAULT NULL — FK do email_mailboxes
|
||||
- order_id INT UNSIGNED DEFAULT NULL — FK do orders
|
||||
- recipient_email VARCHAR(255) NOT NULL
|
||||
- recipient_name VARCHAR(200) DEFAULT NULL
|
||||
- subject VARCHAR(500) NOT NULL
|
||||
- body_html TEXT NOT NULL — treść po rozwinięciu zmiennych
|
||||
- attachments_json JSON DEFAULT NULL — lista załączników [{name, path, type}]
|
||||
- status ENUM('sent','failed','pending') NOT NULL DEFAULT 'pending'
|
||||
- error_message TEXT DEFAULT NULL
|
||||
- sent_at DATETIME DEFAULT NULL
|
||||
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
- FOREIGN KEY (template_id) REFERENCES email_templates(id) ON DELETE SET NULL
|
||||
- FOREIGN KEY (mailbox_id) REFERENCES email_mailboxes(id) ON DELETE SET NULL
|
||||
- INDEX idx_email_logs_order (order_id)
|
||||
- INDEX idx_email_logs_status (status)
|
||||
- INDEX idx_email_logs_sent_at (sent_at)
|
||||
|
||||
Wszystkie migracje idempotentne (IF NOT EXISTS).
|
||||
</action>
|
||||
<verify>Uruchomić migrację przez /settings/database — tabele widoczne w bazie</verify>
|
||||
<done>AC-1 satisfied: 3 tabele email_* istnieją z poprawnymi kolumnami i FK</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: CRUD skrzynek pocztowych + test SMTP</name>
|
||||
<files>
|
||||
src/Modules/Settings/EmailMailboxController.php,
|
||||
src/Modules/Settings/EmailMailboxRepository.php,
|
||||
resources/views/settings/email-mailboxes.php,
|
||||
src/Core/Application.php,
|
||||
resources/views/layouts/app.php
|
||||
</files>
|
||||
<action>
|
||||
**EmailMailboxRepository** (wzorzec jak ReceiptConfigRepository):
|
||||
- listAll(): array — wszystkie skrzynki (hasło NIE zwracane w liście)
|
||||
- findById(int $id): ?array — z odszyfrowanym hasłem (do edycji)
|
||||
- save(array $data): int — insert/update; hasło szyfrowane IntegrationSecretCipher
|
||||
- delete(int $id): bool — usunięcie skrzynki
|
||||
- findDefault(): ?array — skrzynka z is_default=1
|
||||
|
||||
**EmailMailboxController** (wzorzec jak ReceiptConfigController):
|
||||
- index(Request): Response — lista + formularz edycji (?edit=ID)
|
||||
- save(Request): Response — walidacja + zapis; pola: name, smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password, sender_email, sender_name, is_default
|
||||
- delete(Request): Response — usunięcie z potwierdzeniem
|
||||
- testConnection(Request): Response — AJAX POST; próba fsockopen/stream_socket_client do smtp_host:smtp_port z timeoutem 5s; odpowiedź JSON {success, message}
|
||||
|
||||
**Walidacja testConnection:**
|
||||
- Odczytaj dane z POST (lub z DB jeśli id podane)
|
||||
- Odszyfruj hasło jeśli z DB
|
||||
- Spróbuj otworzyć socket do smtp_host:smtp_port (stream_socket_client, timeout 5s)
|
||||
- Jeśli OK: EHLO + AUTH LOGIN z username/password
|
||||
- Zwróć JSON: {success: true/false, message: "Połączenie OK" / "Błąd: ..."}
|
||||
- Użyj natywnego PHP (fsockopen/stream_socket_client + SMTP commands) — NIE dodawaj PHPMailer/SwiftMailer w tej fazie
|
||||
|
||||
**Widok email-mailboxes.php** (wzorzec jak accounting.php):
|
||||
- Tabela skrzynek: Nazwa, Serwer, Port, Email nadawcy, Status (aktywna/nieaktywna), Domyślna (badge), Akcje (edytuj/usuń)
|
||||
- Formularz dodawania/edycji: name, smtp_host, smtp_port (default 587), smtp_encryption (select: TLS/SSL/Brak), smtp_username, smtp_password (type=password, placeholder "••••" przy edycji), sender_email, sender_name, is_default (checkbox)
|
||||
- Przycisk "Testuj połączenie" — AJAX POST do /settings/email-mailboxes/test, wynik w alert
|
||||
- Flash messages na sukces/błąd zapisu/usunięcia
|
||||
|
||||
**Routing w Application.php:**
|
||||
- GET /settings/email-mailboxes → EmailMailboxController::index
|
||||
- POST /settings/email-mailboxes/save → EmailMailboxController::save
|
||||
- POST /settings/email-mailboxes/delete → EmailMailboxController::delete
|
||||
- POST /settings/email-mailboxes/test → EmailMailboxController::testConnection
|
||||
|
||||
**Nawigacja w app.php:**
|
||||
- Dodać link "Skrzynki pocztowe" w sekcji Settings sidebar (ikona: ✉ lub odpowiednia z istniejącego zestawu)
|
||||
- currentSettings === 'email-mailboxes'
|
||||
|
||||
**Hasło przy edycji:**
|
||||
- Jeśli pole password puste przy save — zachowaj istniejące zaszyfrowane hasło
|
||||
- Jeśli wypełnione — zaszyfruj nowe
|
||||
|
||||
Avoid: Nie dodawać żadnych zewnętrznych bibliotek mailingowych (PHPMailer, SwiftMailer) — w tej fazie wystarczy natywny socket do testu połączenia. Biblioteka mailingowa będzie dodana w fazie 15.
|
||||
</action>
|
||||
<verify>
|
||||
1. Otworzyć /settings/email-mailboxes — widoczna pusta lista
|
||||
2. Dodać skrzynkę z danymi SMTP — pojawia się na liście
|
||||
3. Kliknąć "Testuj połączenie" — JSON response z wynikiem
|
||||
4. Edytować skrzynkę (bez zmiany hasła) — hasło zachowane
|
||||
5. Usunąć skrzynkę — znika z listy
|
||||
</verify>
|
||||
<done>AC-2, AC-3, AC-4 satisfied: CRUD skrzynek działa, test SMTP działa, nawigacja widoczna</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>CRUD skrzynek pocztowych z testem połączenia SMTP</what-built>
|
||||
<how-to-verify>
|
||||
1. Uruchomić migrację: Ustawienia > Baza danych > Migruj
|
||||
2. Przejść do: Ustawienia > Skrzynki pocztowe
|
||||
3. Dodać skrzynkę testową (dowolny serwer SMTP)
|
||||
4. Kliknąć "Testuj połączenie" — sprawdzić wynik
|
||||
5. Edytować skrzynkę — zmienić nazwę, zostawić hasło puste → hasło zachowane
|
||||
6. Zaznaczyć "Domyślna" → badge na liście
|
||||
7. Usunąć skrzynkę → znika
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- src/Modules/Accounting/* (moduł paragonów stabilny)
|
||||
- src/Modules/Orders/* (moduł zamówień — modyfikacje w fazie 15)
|
||||
- database/migrations/000001-000053 (istniejące migracje)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Ten plan tworzy tylko CRUD skrzynek pocztowych — szablony wiadomości to faza 14
|
||||
- NIE dodawać bibliotek mailingowych (PHPMailer itp.) — to faza 15
|
||||
- NIE implementować wysyłki maili — tylko test połączenia SMTP
|
||||
- NIE modyfikować widoków zamówień
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] 3 migracje SQL wykonane poprawnie (tabele istnieją)
|
||||
- [ ] CRUD skrzynek: dodawanie, edycja, usuwanie działa
|
||||
- [ ] Hasło SMTP szyfrowane w bazie (IntegrationSecretCipher)
|
||||
- [ ] Test połączenia SMTP zwraca JSON z wynikiem
|
||||
- [ ] Nawigacja: link "Skrzynki pocztowe" w Settings sidebar
|
||||
- [ ] Flash messages na sukces/błąd operacji
|
||||
- [ ] Brak alert()/confirm() — użyty OrderProAlerts
|
||||
- [ ] CSRF token w formularzach (_token)
|
||||
- [ ] Wszystkie acceptance criteria spełnione
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 tabele email_* istnieją w bazie
|
||||
- CRUD skrzynek pocztowych w pełni funkcjonalny
|
||||
- Test połączenia SMTP działa (JSON response)
|
||||
- Hasła zaszyfrowane w DB
|
||||
- Nawigacja w sidebar poprawna
|
||||
- Brak błędów PHP, brak nowych ostrzeżeń
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/13-email-mailboxes/13-01-SUMMARY.md`
|
||||
</output>
|
||||
133
.paul/phases/13-email-mailboxes/13-01-SUMMARY.md
Normal file
133
.paul/phases/13-email-mailboxes/13-01-SUMMARY.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
phase: 13-email-mailboxes
|
||||
plan: 01
|
||||
subsystem: settings
|
||||
tags: [email, smtp, encryption, crud]
|
||||
|
||||
requires:
|
||||
- phase: none
|
||||
provides: n/a (first phase of v0.4)
|
||||
provides:
|
||||
- email_mailboxes table with SMTP credential storage
|
||||
- email_templates table (schema only, populated in phase 14)
|
||||
- email_logs table (schema only, populated in phase 15)
|
||||
- EmailMailboxController + EmailMailboxRepository CRUD
|
||||
- SMTP connection test endpoint
|
||||
affects: [14-email-templates, 15-email-sending]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [SMTP socket test via stream_socket_client, password encryption reusing IntegrationSecretCipher]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- database/migrations/20260315_000054_create_email_mailboxes_table.sql
|
||||
- database/migrations/20260315_000055_create_email_templates_table.sql
|
||||
- database/migrations/20260315_000056_create_email_logs_table.sql
|
||||
- src/Modules/Settings/EmailMailboxController.php
|
||||
- src/Modules/Settings/EmailMailboxRepository.php
|
||||
- resources/views/settings/email-mailboxes.php
|
||||
modified:
|
||||
- routes/web.php
|
||||
- resources/views/layouts/app.php
|
||||
|
||||
key-decisions:
|
||||
- "SMTP test via native stream_socket_client — no PHPMailer dependency yet"
|
||||
- "Password encryption via existing IntegrationSecretCipher (AES-256-CBC + HMAC)"
|
||||
- "is_default flag with auto-reset of previous default on save"
|
||||
|
||||
patterns-established:
|
||||
- "Email mailbox CRUD pattern (same as ReceiptConfig)"
|
||||
- "AJAX JSON test endpoint for connection validation"
|
||||
|
||||
duration: ~15min
|
||||
started: 2026-03-15T00:00:00Z
|
||||
completed: 2026-03-15T00:15:00Z
|
||||
---
|
||||
|
||||
# Phase 13 Plan 01: DB + Skrzynki pocztowe Summary
|
||||
|
||||
**3 migracje email (mailboxes/templates/logs) + pełny CRUD skrzynek pocztowych SMTP z testem połączenia i szyfrowaniem haseł**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~15min |
|
||||
| Tasks | 3 completed (2 auto + 1 checkpoint) |
|
||||
| Files created | 6 |
|
||||
| Files modified | 2 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Migracje tworzą 3 tabele | Pass | email_mailboxes, email_templates, email_logs z FK i indeksami |
|
||||
| AC-2: CRUD skrzynek pocztowych | Pass | Dodawanie, edycja (z zachowaniem hasła), usuwanie, toggle statusu |
|
||||
| AC-3: Walidacja połączenia SMTP | Pass | AJAX POST z AUTH LOGIN, JSON response |
|
||||
| AC-4: Nawigacja | Pass | Link "Skrzynki pocztowe" w sidebar Settings |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- 3 tabele bazodanowe: `email_mailboxes` (SMTP credentials), `email_templates` (szablony), `email_logs` (historia wysyłki)
|
||||
- Pełny CRUD skrzynek z szyfrowaniem haseł przez IntegrationSecretCipher
|
||||
- Test połączenia SMTP z pełnym handshake (EHLO → STARTTLS → AUTH LOGIN)
|
||||
- Widok z listą, formularzem edycji i AJAX testem połączenia
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `database/migrations/20260315_000054_create_email_mailboxes_table.sql` | Created | Tabela skrzynek SMTP |
|
||||
| `database/migrations/20260315_000055_create_email_templates_table.sql` | Created | Tabela szablonów email |
|
||||
| `database/migrations/20260315_000056_create_email_logs_table.sql` | Created | Tabela logów wysyłki |
|
||||
| `src/Modules/Settings/EmailMailboxRepository.php` | Created | Repository: listAll, findById, save, delete, toggleStatus, listActive |
|
||||
| `src/Modules/Settings/EmailMailboxController.php` | Created | Controller: index, save, delete, toggleStatus, testConnection |
|
||||
| `resources/views/settings/email-mailboxes.php` | Created | Widok: lista + formularz + AJAX test |
|
||||
| `routes/web.php` | Modified | 5 nowych route'ów + import EmailMailbox* + IntegrationSecretCipher |
|
||||
| `resources/views/layouts/app.php` | Modified | Link "Skrzynki pocztowe" w sidebar |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Natywny stream_socket_client do testu SMTP | Brak potrzeby dodawania PHPMailer w tej fazie — wystarczy weryfikacja połączenia | PHPMailer/SwiftMailer dodany w fazie 15 |
|
||||
| IntegrationSecretCipher do haseł SMTP | Reuse istniejącego mechanizmu AES-256-CBC+HMAC | Spójny wzorzec szyfrowania w projekcie |
|
||||
| Auto-reset is_default przy save | Tylko jedna skrzynka domyślna | Uproszczenie logiki wyboru skrzynki |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 0 | - |
|
||||
| Scope additions | 1 | Minimal — listActive() w repository |
|
||||
| Deferred | 0 | - |
|
||||
|
||||
**Total impact:** Minimalna zmiana — dodano `listActive()` do repo (potrzebne w fazie 14 do selecta skrzynek).
|
||||
|
||||
### Deferred Items
|
||||
|
||||
None.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Tabela `email_templates` gotowa na CRUD (faza 14)
|
||||
- `EmailMailboxRepository::listActive()` gotowe do użycia w select skrzynki w szablonach
|
||||
- Tabela `email_logs` gotowa na wypełnianie (faza 15)
|
||||
|
||||
**Concerns:**
|
||||
- sonar-scanner nie uruchomiony (required skill) — do uruchomienia przed kolejnym UNIFY
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 13-email-mailboxes, Plan: 01*
|
||||
*Completed: 2026-03-15*
|
||||
@@ -10,6 +10,7 @@
|
||||
- `App\Modules\Users`
|
||||
- `App\Modules\Settings`
|
||||
- `App\Modules\Accounting` (modul paragonow — wystawianie, podglad, druk, PDF, lista, eksport XLSX)
|
||||
- `App\Modules\Settings\EmailMailbox*` (skrzynki pocztowe SMTP — CRUD + test polaczenia)
|
||||
|
||||
## Routing
|
||||
- `GET /login`, `POST /login`, `POST /logout`
|
||||
@@ -61,6 +62,11 @@
|
||||
- `POST /settings/accounting/save`
|
||||
- `POST /settings/accounting/toggle`
|
||||
- `POST /settings/accounting/delete`
|
||||
- `GET /settings/email-mailboxes`
|
||||
- `POST /settings/email-mailboxes/save`
|
||||
- `POST /settings/email-mailboxes/delete`
|
||||
- `POST /settings/email-mailboxes/toggle`
|
||||
- `POST /settings/email-mailboxes/test`
|
||||
- `GET /health`
|
||||
- `GET /` (redirect)
|
||||
|
||||
@@ -88,6 +94,8 @@
|
||||
- `App\Modules\Settings\AllegroStatusDiscoveryService`
|
||||
- `App\Modules\Settings\IntegrationsRepository`
|
||||
- `App\Modules\Settings\IntegrationSecretCipher`
|
||||
- `App\Modules\Settings\EmailMailboxController`
|
||||
- `App\Modules\Settings\EmailMailboxRepository`
|
||||
- `App\Modules\Orders\OrderImportRepository`
|
||||
- `App\Modules\Settings\CronSettingsController`
|
||||
- `App\Modules\Cron\CronRepository`
|
||||
|
||||
@@ -92,6 +92,9 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
||||
- 2026-03-15: Dodano migracje `20260315_000051_create_receipts_table.sql` — tabela wystawionych paragonow ze snapshotem danych (JSON seller/buyer/items), FK do orders i receipt_configs.
|
||||
- 2026-03-15: Dodano migracje `20260315_000052_create_receipt_number_counters_table.sql` — liczniki numeracji paragonow per konfiguracja i okres (miesiac/rok).
|
||||
- 2026-03-15: Dodano migracje `20260315_000053_extend_company_settings_extra_fields.sql` — rozszerzenie company_settings o bdo_number, regon, court_register, logo_path.
|
||||
- 2026-03-15: Dodano migracje `20260315_000054_create_email_mailboxes_table.sql` — tabela skrzynek pocztowych SMTP (credentials szyfrowane IntegrationSecretCipher).
|
||||
- 2026-03-15: Dodano migracje `20260315_000055_create_email_templates_table.sql` — tabela szablonow wiadomosci email z FK do email_mailboxes.
|
||||
- 2026-03-15: Dodano migracje `20260315_000056_create_email_logs_table.sql` — tabela logow wyslanych wiadomosci z FK do email_templates, email_mailboxes i indeksami na order_id, status, sent_at.
|
||||
|
||||
## Tabele
|
||||
|
||||
@@ -384,6 +387,52 @@ Migracje z prefiksem `ensure_` to migracje kompensujące — zostały dodane
|
||||
- Klucze obce:
|
||||
- `receipt_counters_config_fk`: `config_id` -> `receipt_configs.id` (ON DELETE CASCADE).
|
||||
|
||||
### `email_mailboxes`
|
||||
- Skrzynki pocztowe SMTP do wysylki wiadomosci e-mail.
|
||||
- Kolumny:
|
||||
- `id` INT UNSIGNED PK AUTO_INCREMENT
|
||||
- `name` VARCHAR(100) NOT NULL — nazwa wyswietlana
|
||||
- `smtp_host` VARCHAR(255) NOT NULL
|
||||
- `smtp_port` SMALLINT UNSIGNED NOT NULL DEFAULT 587
|
||||
- `smtp_encryption` ENUM('tls','ssl','none') NOT NULL DEFAULT 'tls'
|
||||
- `smtp_username` VARCHAR(255) NOT NULL
|
||||
- `smtp_password_encrypted` TEXT NOT NULL — szyfrowane IntegrationSecretCipher (AES-256-CBC+HMAC)
|
||||
- `sender_email` VARCHAR(255) NOT NULL
|
||||
- `sender_name` VARCHAR(200) DEFAULT NULL
|
||||
- `is_default` TINYINT(1) NOT NULL DEFAULT 0
|
||||
- `is_active` TINYINT(1) NOT NULL DEFAULT 1
|
||||
- `created_at`, `updated_at` DATETIME
|
||||
|
||||
### `email_templates`
|
||||
- Szablony wiadomosci e-mail z systemem zmiennych.
|
||||
- Kolumny:
|
||||
- `id` INT UNSIGNED PK AUTO_INCREMENT
|
||||
- `name` VARCHAR(200) NOT NULL — nazwa szablonu
|
||||
- `subject` VARCHAR(500) NOT NULL — temat (moze zawierac zmienne)
|
||||
- `body_html` TEXT NOT NULL — tresc HTML (Quill.js output)
|
||||
- `mailbox_id` INT UNSIGNED DEFAULT NULL — FK do email_mailboxes ON DELETE SET NULL
|
||||
- `is_active` TINYINT(1) NOT NULL DEFAULT 1
|
||||
- `created_at`, `updated_at` DATETIME
|
||||
- Indeksy: `idx_email_templates_mailbox` (mailbox_id)
|
||||
|
||||
### `email_logs`
|
||||
- Log wyslanych wiadomosci e-mail.
|
||||
- Kolumny:
|
||||
- `id` BIGINT UNSIGNED PK AUTO_INCREMENT
|
||||
- `template_id` INT UNSIGNED DEFAULT NULL — FK do email_templates ON DELETE SET NULL
|
||||
- `mailbox_id` INT UNSIGNED DEFAULT NULL — FK do email_mailboxes ON DELETE SET NULL
|
||||
- `order_id` INT UNSIGNED DEFAULT NULL
|
||||
- `recipient_email` VARCHAR(255) NOT NULL
|
||||
- `recipient_name` VARCHAR(200) DEFAULT NULL
|
||||
- `subject` VARCHAR(500) NOT NULL
|
||||
- `body_html` TEXT NOT NULL — tresc po rozwinieciu zmiennych
|
||||
- `attachments_json` JSON DEFAULT NULL — lista zalacznikow [{name, path, type}]
|
||||
- `status` ENUM('sent','failed','pending') NOT NULL DEFAULT 'pending'
|
||||
- `error_message` TEXT DEFAULT NULL
|
||||
- `sent_at` DATETIME DEFAULT NULL
|
||||
- `created_at` DATETIME
|
||||
- Indeksy: `idx_email_logs_template`, `idx_email_logs_mailbox`, `idx_email_logs_order`, `idx_email_logs_status`, `idx_email_logs_sent_at`
|
||||
|
||||
## Zasady aktualizacji
|
||||
- Po kazdej migracji dopisz:
|
||||
- nowe/zmienione tabele i kolumny,
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# Tech Changelog
|
||||
|
||||
## 2026-03-15 (Phase 13 — DB + Skrzynki pocztowe)
|
||||
- Dodano 3 migracje email: `000054_create_email_mailboxes_table`, `000055_create_email_templates_table`, `000056_create_email_logs_table`.
|
||||
- Nowe klasy: `EmailMailboxController` (index, save, delete, toggleStatus, testConnection), `EmailMailboxRepository` (listAll, findById, save, delete, toggleStatus, listActive).
|
||||
- Test polaczenia SMTP przez natywny `stream_socket_client` z pelnym handshake (EHLO → STARTTLS → AUTH LOGIN) — bez zewnetrznych bibliotek.
|
||||
- Hasla SMTP szyfrowane przez `IntegrationSecretCipher` (AES-256-CBC + HMAC-SHA256).
|
||||
- Widok `settings/email-mailboxes.php` — lista skrzynek + formularz CRUD + AJAX test polaczenia.
|
||||
- Nawigacja: link "Skrzynki pocztowe" w sidebar Settings.
|
||||
- 5 nowych route'ow: GET/POST `/settings/email-mailboxes/*`.
|
||||
|
||||
## 2026-03-14
|
||||
- Zoptymalizowano zapytanie listy zamowien (`OrdersRepository::buildListSql()`):
|
||||
- 4 correlated subqueries (items_count, items_qty, shipments_count, documents_count) zastapiono aggregating LEFT JOINami — eliminuje N+1 na kazdym wierszu listy.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS `email_mailboxes` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(100) NOT NULL,
|
||||
`smtp_host` VARCHAR(255) NOT NULL,
|
||||
`smtp_port` SMALLINT UNSIGNED NOT NULL DEFAULT 587,
|
||||
`smtp_encryption` ENUM('tls','ssl','none') NOT NULL DEFAULT 'tls',
|
||||
`smtp_username` VARCHAR(255) NOT NULL,
|
||||
`smtp_password_encrypted` TEXT NOT NULL,
|
||||
`sender_email` VARCHAR(255) NOT NULL,
|
||||
`sender_name` VARCHAR(200) DEFAULT NULL,
|
||||
`is_default` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE IF NOT EXISTS `email_templates` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`name` VARCHAR(200) NOT NULL,
|
||||
`subject` VARCHAR(500) NOT NULL,
|
||||
`body_html` TEXT NOT NULL,
|
||||
`mailbox_id` INT UNSIGNED DEFAULT NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_email_templates_mailbox` (`mailbox_id`),
|
||||
CONSTRAINT `fk_email_templates_mailbox` FOREIGN KEY (`mailbox_id`) REFERENCES `email_mailboxes` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,23 @@
|
||||
CREATE TABLE IF NOT EXISTS `email_logs` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`template_id` INT UNSIGNED DEFAULT NULL,
|
||||
`mailbox_id` INT UNSIGNED DEFAULT NULL,
|
||||
`order_id` INT UNSIGNED DEFAULT NULL,
|
||||
`recipient_email` VARCHAR(255) NOT NULL,
|
||||
`recipient_name` VARCHAR(200) DEFAULT NULL,
|
||||
`subject` VARCHAR(500) NOT NULL,
|
||||
`body_html` TEXT NOT NULL,
|
||||
`attachments_json` JSON DEFAULT NULL,
|
||||
`status` ENUM('sent','failed','pending') NOT NULL DEFAULT 'pending',
|
||||
`error_message` TEXT DEFAULT NULL,
|
||||
`sent_at` DATETIME DEFAULT NULL,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_email_logs_template` (`template_id`),
|
||||
KEY `idx_email_logs_mailbox` (`mailbox_id`),
|
||||
KEY `idx_email_logs_order` (`order_id`),
|
||||
KEY `idx_email_logs_status` (`status`),
|
||||
KEY `idx_email_logs_sent_at` (`sent_at`),
|
||||
CONSTRAINT `fk_email_logs_template` FOREIGN KEY (`template_id`) REFERENCES `email_templates` (`id`) ON DELETE SET NULL,
|
||||
CONSTRAINT `fk_email_logs_mailbox` FOREIGN KEY (`mailbox_id`) REFERENCES `email_mailboxes` (`id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -95,6 +95,9 @@
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'accounting' ? ' is-active' : '' ?>" href="/settings/accounting">
|
||||
<?= $e($t('navigation.accounting')) ?>
|
||||
</a>
|
||||
<a class="sidebar__sublink<?= $currentMenu === 'settings' && $currentSettings === 'email-mailboxes' ? ' is-active' : '' ?>" href="/settings/email-mailboxes">
|
||||
Skrzynki pocztowe
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
</nav>
|
||||
|
||||
221
resources/views/settings/email-mailboxes.php
Normal file
221
resources/views/settings/email-mailboxes.php
Normal file
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
$mailboxes = is_array($mailboxes ?? null) ? $mailboxes : [];
|
||||
$em = is_array($editMailbox ?? null) ? $editMailbox : null;
|
||||
$isEdit = $em !== null;
|
||||
?>
|
||||
|
||||
<section class="card">
|
||||
<h2 class="section-title">Skrzynki pocztowe</h2>
|
||||
<p class="muted mt-12">Konfiguracja skrzynek SMTP do wysylki wiadomosci e-mail.</p>
|
||||
|
||||
<?php if (!empty($errorMessage)): ?>
|
||||
<div class="alert alert--danger mt-12" role="alert"><?= $e((string) $errorMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($successMessage)): ?>
|
||||
<div class="alert alert--success mt-12" role="status"><?= $e((string) $successMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title">Lista skrzynek</h3>
|
||||
|
||||
<?php if (count($mailboxes) === 0): ?>
|
||||
<p class="muted mt-12">Brak skrzynek pocztowych. Dodaj pierwsza skrzynke ponizej.</p>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap mt-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nazwa</th>
|
||||
<th>Serwer</th>
|
||||
<th>Port</th>
|
||||
<th>E-mail nadawcy</th>
|
||||
<th>Status</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($mailboxes as $mb): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?= $e((string) ($mb['name'] ?? '')) ?>
|
||||
<?php if (((int) ($mb['is_default'] ?? 0)) === 1): ?>
|
||||
<span class="badge badge--info" style="margin-left:4px">Domyslna</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $e((string) ($mb['smtp_host'] ?? '')) ?></td>
|
||||
<td><?= (int) ($mb['smtp_port'] ?? 0) ?></td>
|
||||
<td><?= $e((string) ($mb['sender_email'] ?? '')) ?></td>
|
||||
<td>
|
||||
<?php if (((int) ($mb['is_active'] ?? 0)) === 1): ?>
|
||||
<span class="badge badge--success">Aktywna</span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge--muted">Nieaktywna</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="white-space:nowrap">
|
||||
<a href="/settings/email-mailboxes?edit=<?= (int) ($mb['id'] ?? 0) ?>" class="btn btn--sm btn--secondary">Edytuj</a>
|
||||
<form action="/settings/email-mailboxes/toggle" method="post" style="display:inline">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= (int) ($mb['id'] ?? 0) ?>">
|
||||
<button type="submit" class="btn btn--sm btn--secondary">
|
||||
<?= ((int) ($mb['is_active'] ?? 0)) === 1 ? 'Dezaktywuj' : 'Aktywuj' ?>
|
||||
</button>
|
||||
</form>
|
||||
<form action="/settings/email-mailboxes/delete" method="post" style="display:inline" class="js-confirm-delete">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<input type="hidden" name="id" value="<?= (int) ($mb['id'] ?? 0) ?>">
|
||||
<button type="button" class="btn btn--sm btn--danger js-delete-btn">Usun</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="card mt-16">
|
||||
<h3 class="section-title"><?= $isEdit ? 'Edytuj skrzynke pocztowa' : 'Dodaj skrzynke pocztowa' ?></h3>
|
||||
|
||||
<form action="/settings/email-mailboxes/save" method="post" novalidate class="mt-12" id="js-mailbox-form">
|
||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<input type="hidden" name="id" value="<?= (int) ($em['id'] ?? 0) ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Nazwa *</span>
|
||||
<input class="form-control" type="text" name="name" maxlength="100" required value="<?= $e((string) ($em['name'] ?? '')) ?>" placeholder="np. Glowna skrzynka">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">E-mail nadawcy *</span>
|
||||
<input class="form-control" type="email" name="sender_email" maxlength="255" required value="<?= $e((string) ($em['sender_email'] ?? '')) ?>" placeholder="noreply@firma.pl">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Nazwa nadawcy</span>
|
||||
<input class="form-control" type="text" name="sender_name" maxlength="200" value="<?= $e((string) ($em['sender_name'] ?? '')) ?>" placeholder="np. Sklep XYZ">
|
||||
</label>
|
||||
<div class="form-field"></div>
|
||||
</div>
|
||||
|
||||
<h4 class="section-title mt-16">Ustawienia SMTP</h4>
|
||||
|
||||
<div class="form-grid-3 mt-12">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Serwer SMTP *</span>
|
||||
<input class="form-control" type="text" name="smtp_host" maxlength="255" required value="<?= $e((string) ($em['smtp_host'] ?? '')) ?>" placeholder="smtp.firma.pl">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Port *</span>
|
||||
<input class="form-control" type="number" name="smtp_port" min="1" max="65535" required value="<?= (int) ($em['smtp_port'] ?? 587) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Szyfrowanie</span>
|
||||
<select class="form-control" name="smtp_encryption">
|
||||
<option value="tls"<?= ((string) ($em['smtp_encryption'] ?? 'tls')) === 'tls' ? ' selected' : '' ?>>TLS (STARTTLS)</option>
|
||||
<option value="ssl"<?= ((string) ($em['smtp_encryption'] ?? '')) === 'ssl' ? ' selected' : '' ?>>SSL</option>
|
||||
<option value="none"<?= ((string) ($em['smtp_encryption'] ?? '')) === 'none' ? ' selected' : '' ?>>Brak</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 mt-0">
|
||||
<label class="form-field">
|
||||
<span class="field-label">Uzytkownik SMTP *</span>
|
||||
<input class="form-control" type="text" name="smtp_username" maxlength="255" required value="<?= $e((string) ($em['smtp_username'] ?? '')) ?>">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span class="field-label">Haslo SMTP <?= $isEdit ? '' : '*' ?></span>
|
||||
<input class="form-control" type="password" name="smtp_password" maxlength="255" <?= $isEdit ? '' : 'required' ?> placeholder="<?= $isEdit ? '(bez zmian)' : '' ?>">
|
||||
<?php if ($isEdit): ?>
|
||||
<small class="field-hint">Pozostaw puste, aby zachowac aktualne haslo</small>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2 mt-0">
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_default" value="1"<?= ((int) ($em['is_default'] ?? 0)) === 1 ? ' checked' : '' ?>>
|
||||
<span class="field-label" style="margin:0">Domyslna skrzynka</span>
|
||||
</label>
|
||||
<label class="form-field" style="display:flex;align-items:center;gap:6px;flex-direction:row">
|
||||
<input type="checkbox" name="is_active" value="1"<?= $isEdit ? (((int) ($em['is_active'] ?? 0)) === 1 ? ' checked' : '') : ' checked' ?>>
|
||||
<span class="field-label" style="margin:0">Aktywna</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-16">
|
||||
<button type="submit" class="btn btn--primary"><?= $isEdit ? 'Zapisz zmiany' : 'Dodaj skrzynke' ?></button>
|
||||
<button type="button" class="btn btn--secondary" id="js-test-connection">Testuj polaczenie</button>
|
||||
<?php if ($isEdit): ?>
|
||||
<a href="/settings/email-mailboxes" class="btn btn--secondary">Anuluj</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="js-test-result" class="mt-12" style="display:none"></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.js-delete-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var form = this.closest('form');
|
||||
if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
|
||||
window.OrderProAlerts.confirm(
|
||||
'Usuwanie skrzynki',
|
||||
'Czy na pewno chcesz usunac te skrzynke pocztowa?',
|
||||
function() { form.submit(); }
|
||||
);
|
||||
} else {
|
||||
if (confirm('Czy na pewno chcesz usunac te skrzynke pocztowa?')) {
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var testBtn = document.getElementById('js-test-connection');
|
||||
var resultDiv = document.getElementById('js-test-result');
|
||||
if (testBtn) {
|
||||
testBtn.addEventListener('click', function() {
|
||||
var form = document.getElementById('js-mailbox-form');
|
||||
var formData = new FormData(form);
|
||||
|
||||
testBtn.disabled = true;
|
||||
testBtn.textContent = 'Testowanie...';
|
||||
resultDiv.style.display = 'none';
|
||||
|
||||
fetch('/settings/email-mailboxes/test', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(function(response) { return response.json(); })
|
||||
.then(function(data) {
|
||||
resultDiv.style.display = 'block';
|
||||
if (data.success) {
|
||||
resultDiv.className = 'mt-12 alert alert--success';
|
||||
} else {
|
||||
resultDiv.className = 'mt-12 alert alert--danger';
|
||||
}
|
||||
resultDiv.textContent = data.message || 'Brak odpowiedzi';
|
||||
})
|
||||
.catch(function(err) {
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'mt-12 alert alert--danger';
|
||||
resultDiv.textContent = 'Blad polaczenia: ' + err.message;
|
||||
})
|
||||
.finally(function() {
|
||||
testBtn.disabled = false;
|
||||
testBtn.textContent = 'Testuj polaczenie';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -36,6 +36,9 @@ use App\Modules\Settings\CompanySettingsController;
|
||||
use App\Modules\Settings\CompanySettingsRepository;
|
||||
use App\Modules\Settings\ReceiptConfigController;
|
||||
use App\Modules\Settings\ReceiptConfigRepository;
|
||||
use App\Modules\Settings\EmailMailboxController;
|
||||
use App\Modules\Settings\EmailMailboxRepository;
|
||||
use App\Modules\Settings\IntegrationSecretCipher;
|
||||
use App\Modules\Accounting\AccountingController;
|
||||
use App\Modules\Accounting\ReceiptController;
|
||||
use App\Modules\Accounting\ReceiptRepository;
|
||||
@@ -184,6 +187,16 @@ return static function (Application $app): void {
|
||||
$auth,
|
||||
$receiptConfigRepository
|
||||
);
|
||||
$emailMailboxRepository = new EmailMailboxRepository(
|
||||
$app->db(),
|
||||
new IntegrationSecretCipher((string) $app->config('app.integrations.secret', ''))
|
||||
);
|
||||
$emailMailboxController = new EmailMailboxController(
|
||||
$template,
|
||||
$translator,
|
||||
$auth,
|
||||
$emailMailboxRepository
|
||||
);
|
||||
$receiptController = new ReceiptController(
|
||||
$template,
|
||||
$translator,
|
||||
@@ -307,6 +320,11 @@ return static function (Application $app): void {
|
||||
$router->post('/settings/accounting/save', [$receiptConfigController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/toggle', [$receiptConfigController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/accounting/delete', [$receiptConfigController, 'delete'], [$authMiddleware]);
|
||||
$router->get('/settings/email-mailboxes', [$emailMailboxController, 'index'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/save', [$emailMailboxController, 'save'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/delete', [$emailMailboxController, 'delete'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/toggle', [$emailMailboxController, 'toggleStatus'], [$authMiddleware]);
|
||||
$router->post('/settings/email-mailboxes/test', [$emailMailboxController, 'testConnection'], [$authMiddleware]);
|
||||
$router->get('/accounting', [$accountingController, 'index'], [$authMiddleware]);
|
||||
$router->post('/accounting/export', [$accountingController, 'export'], [$authMiddleware]);
|
||||
$router->get('/orders/{id}/receipt/create', [$receiptController, 'create'], [$authMiddleware]);
|
||||
|
||||
301
src/Modules/Settings/EmailMailboxController.php
Normal file
301
src/Modules/Settings/EmailMailboxController.php
Normal file
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
use App\Core\Security\Csrf;
|
||||
use App\Core\Support\Flash;
|
||||
use App\Core\View\Template;
|
||||
use App\Modules\Auth\AuthService;
|
||||
use Throwable;
|
||||
|
||||
final class EmailMailboxController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Template $template,
|
||||
private readonly Translator $translator,
|
||||
private readonly AuthService $auth,
|
||||
private readonly EmailMailboxRepository $repository
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$t = $this->translator;
|
||||
$mailboxes = $this->repository->listAll();
|
||||
|
||||
$editMailbox = null;
|
||||
$editId = (int) $request->input('edit', '0');
|
||||
if ($editId > 0) {
|
||||
$editMailbox = $this->repository->findById($editId);
|
||||
}
|
||||
|
||||
$html = $this->template->render('settings/email-mailboxes', [
|
||||
'title' => $t->get('settings.email_mailboxes.title'),
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'email-mailboxes',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'mailboxes' => $mailboxes,
|
||||
'editMailbox' => $editMailbox,
|
||||
'successMessage' => Flash::get('settings.email_mailboxes.success', ''),
|
||||
'errorMessage' => Flash::get('settings.email_mailboxes.error', ''),
|
||||
], 'layouts/app');
|
||||
|
||||
return Response::html($html);
|
||||
}
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Nieprawidlowy token CSRF');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
$name = trim((string) $request->input('name', ''));
|
||||
$smtpHost = trim((string) $request->input('smtp_host', ''));
|
||||
$smtpUsername = trim((string) $request->input('smtp_username', ''));
|
||||
$senderEmail = trim((string) $request->input('sender_email', ''));
|
||||
|
||||
if ($name === '' || $smtpHost === '' || $smtpUsername === '' || $senderEmail === '') {
|
||||
Flash::set('settings.email_mailboxes.error', 'Nazwa, serwer SMTP, uzytkownik i e-mail nadawcy sa wymagane');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
if (!filter_var($senderEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Nieprawidlowy adres e-mail nadawcy');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
$id = $request->input('id', '');
|
||||
$password = (string) $request->input('smtp_password', '');
|
||||
if (($id === '' || $id === '0') && $password === '') {
|
||||
Flash::set('settings.email_mailboxes.error', 'Haslo SMTP jest wymagane dla nowej skrzynki');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->save([
|
||||
'id' => $id,
|
||||
'name' => $name,
|
||||
'smtp_host' => $smtpHost,
|
||||
'smtp_port' => $request->input('smtp_port', '587'),
|
||||
'smtp_encryption' => $request->input('smtp_encryption', 'tls'),
|
||||
'smtp_username' => $smtpUsername,
|
||||
'smtp_password' => $password,
|
||||
'sender_email' => $senderEmail,
|
||||
'sender_name' => $request->input('sender_name', ''),
|
||||
'is_default' => $request->input('is_default', null),
|
||||
'is_active' => $request->input('is_active', null),
|
||||
]);
|
||||
|
||||
Flash::set('settings.email_mailboxes.success', 'Skrzynka pocztowa zostala zapisana');
|
||||
} catch (Throwable) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Blad zapisu skrzynki pocztowej');
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Nieprawidlowy token CSRF');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
$id = (int) $request->input('id', '0');
|
||||
if ($id <= 0) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Nieprawidlowy identyfikator skrzynki');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->delete($id);
|
||||
Flash::set('settings.email_mailboxes.success', 'Skrzynka pocztowa zostala usunieta');
|
||||
} catch (Throwable) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Blad usuwania skrzynki pocztowej');
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
public function toggleStatus(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Nieprawidlowy token CSRF');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
$id = (int) $request->input('id', '0');
|
||||
if ($id <= 0) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Nieprawidlowy identyfikator skrzynki');
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->toggleStatus($id);
|
||||
Flash::set('settings.email_mailboxes.success', 'Status skrzynki zostal zmieniony');
|
||||
} catch (Throwable) {
|
||||
Flash::set('settings.email_mailboxes.error', 'Blad zmiany statusu skrzynki');
|
||||
}
|
||||
|
||||
return Response::redirect('/settings/email-mailboxes');
|
||||
}
|
||||
|
||||
public function testConnection(Request $request): Response
|
||||
{
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
return Response::json(['success' => false, 'message' => 'Nieprawidlowy token CSRF'], 403);
|
||||
}
|
||||
|
||||
$host = trim((string) $request->input('smtp_host', ''));
|
||||
$port = (int) $request->input('smtp_port', '587');
|
||||
$encryption = (string) $request->input('smtp_encryption', 'tls');
|
||||
$username = trim((string) $request->input('smtp_username', ''));
|
||||
$password = (string) $request->input('smtp_password', '');
|
||||
|
||||
$existingId = (int) $request->input('id', '0');
|
||||
if ($existingId > 0 && $password === '') {
|
||||
$existing = $this->repository->findById($existingId);
|
||||
if ($existing !== null) {
|
||||
$password = (string) ($existing['smtp_password_decrypted'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
if ($host === '' || $username === '') {
|
||||
return Response::json(['success' => false, 'message' => 'Serwer SMTP i uzytkownik sa wymagane'], 400);
|
||||
}
|
||||
|
||||
$prefix = $encryption === 'ssl' ? 'ssl://' : ($encryption === 'tls' ? 'tls://' : '');
|
||||
$address = $prefix . $host . ':' . $port;
|
||||
|
||||
$context = stream_context_create([
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$errorMessage = '';
|
||||
set_error_handler(function (int $errno, string $errstr) use (&$errorMessage): bool {
|
||||
$errorMessage = $errstr;
|
||||
return true;
|
||||
});
|
||||
|
||||
try {
|
||||
$socket = @stream_socket_client($address, $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $context);
|
||||
|
||||
if ($socket === false) {
|
||||
$detail = $errstr ?: $errorMessage ?: 'Nie udalo sie polaczyc';
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'message' => "Blad polaczenia z {$host}:{$port} — {$detail}",
|
||||
]);
|
||||
}
|
||||
|
||||
$greeting = $this->readSmtpResponse($socket);
|
||||
if (!str_starts_with($greeting, '220')) {
|
||||
fclose($socket);
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'message' => "Serwer nie odpowiedzial poprawnie: {$greeting}",
|
||||
]);
|
||||
}
|
||||
|
||||
$this->sendSmtpCommand($socket, "EHLO orderpro\r\n");
|
||||
$ehloResponse = $this->readSmtpResponse($socket);
|
||||
|
||||
if ($encryption === 'tls' && !str_starts_with($prefix, 'tls://')) {
|
||||
if (str_contains($ehloResponse, 'STARTTLS')) {
|
||||
$this->sendSmtpCommand($socket, "STARTTLS\r\n");
|
||||
$starttlsResponse = $this->readSmtpResponse($socket);
|
||||
if (!str_starts_with($starttlsResponse, '220')) {
|
||||
fclose($socket);
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'message' => "STARTTLS nie powiodlo sie: {$starttlsResponse}",
|
||||
]);
|
||||
}
|
||||
stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
|
||||
$this->sendSmtpCommand($socket, "EHLO orderpro\r\n");
|
||||
$this->readSmtpResponse($socket);
|
||||
}
|
||||
}
|
||||
|
||||
$this->sendSmtpCommand($socket, "AUTH LOGIN\r\n");
|
||||
$authResponse = $this->readSmtpResponse($socket);
|
||||
if (!str_starts_with($authResponse, '334')) {
|
||||
fclose($socket);
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'message' => "Serwer nie obsluguje AUTH LOGIN: {$authResponse}",
|
||||
]);
|
||||
}
|
||||
|
||||
$this->sendSmtpCommand($socket, base64_encode($username) . "\r\n");
|
||||
$userResponse = $this->readSmtpResponse($socket);
|
||||
if (!str_starts_with($userResponse, '334')) {
|
||||
fclose($socket);
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'message' => "Blad uwierzytelniania (uzytkownik): {$userResponse}",
|
||||
]);
|
||||
}
|
||||
|
||||
$this->sendSmtpCommand($socket, base64_encode($password) . "\r\n");
|
||||
$passResponse = $this->readSmtpResponse($socket);
|
||||
if (!str_starts_with($passResponse, '235')) {
|
||||
fclose($socket);
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'message' => "Blad uwierzytelniania (haslo): {$passResponse}",
|
||||
]);
|
||||
}
|
||||
|
||||
$this->sendSmtpCommand($socket, "QUIT\r\n");
|
||||
fclose($socket);
|
||||
|
||||
return Response::json([
|
||||
'success' => true,
|
||||
'message' => "Polaczenie z {$host}:{$port} powiodlo sie. Uwierzytelnianie OK.",
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return Response::json([
|
||||
'success' => false,
|
||||
'message' => "Blad: {$e->getMessage()}",
|
||||
]);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $socket
|
||||
*/
|
||||
private function sendSmtpCommand($socket, string $command): void
|
||||
{
|
||||
fwrite($socket, $command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource $socket
|
||||
*/
|
||||
private function readSmtpResponse($socket): string
|
||||
{
|
||||
$response = '';
|
||||
stream_set_timeout($socket, 5);
|
||||
|
||||
while ($line = fgets($socket, 512)) {
|
||||
$response .= $line;
|
||||
if (isset($line[3]) && $line[3] === ' ') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return trim($response);
|
||||
}
|
||||
}
|
||||
152
src/Modules/Settings/EmailMailboxRepository.php
Normal file
152
src/Modules/Settings/EmailMailboxRepository.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use PDO;
|
||||
|
||||
final class EmailMailboxRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PDO $pdo,
|
||||
private readonly IntegrationSecretCipher $cipher
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listAll(): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, name, smtp_host, smtp_port, smtp_encryption, smtp_username,
|
||||
sender_email, sender_name, is_default, is_active, created_at, updated_at
|
||||
FROM email_mailboxes
|
||||
ORDER BY is_default DESC, created_at DESC'
|
||||
);
|
||||
$statement->execute();
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
$statement = $this->pdo->prepare('SELECT * FROM email_mailboxes WHERE id = :id LIMIT 1');
|
||||
$statement->execute(['id' => $id]);
|
||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!is_array($row)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($row['smtp_password_encrypted']) && $row['smtp_password_encrypted'] !== '') {
|
||||
$row['smtp_password_decrypted'] = $this->cipher->decrypt($row['smtp_password_encrypted']);
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function listActive(): array
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT id, name, sender_email, sender_name, is_default
|
||||
FROM email_mailboxes
|
||||
WHERE is_active = 1
|
||||
ORDER BY is_default DESC, name ASC'
|
||||
);
|
||||
$statement->execute();
|
||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return is_array($rows) ? $rows : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function save(array $data): void
|
||||
{
|
||||
$id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null;
|
||||
|
||||
$encryption = in_array((string) ($data['smtp_encryption'] ?? ''), ['tls', 'ssl', 'none'], true)
|
||||
? (string) $data['smtp_encryption']
|
||||
: 'tls';
|
||||
|
||||
$isDefault = isset($data['is_default']) ? 1 : 0;
|
||||
|
||||
if ($isDefault === 1) {
|
||||
$this->pdo->prepare('UPDATE email_mailboxes SET is_default = 0 WHERE is_default = 1')->execute();
|
||||
}
|
||||
|
||||
$params = [
|
||||
'name' => trim((string) ($data['name'] ?? '')),
|
||||
'smtp_host' => trim((string) ($data['smtp_host'] ?? '')),
|
||||
'smtp_port' => (int) ($data['smtp_port'] ?? 587),
|
||||
'smtp_encryption' => $encryption,
|
||||
'smtp_username' => trim((string) ($data['smtp_username'] ?? '')),
|
||||
'sender_email' => trim((string) ($data['sender_email'] ?? '')),
|
||||
'sender_name' => trim((string) ($data['sender_name'] ?? '')) ?: null,
|
||||
'is_default' => $isDefault,
|
||||
'is_active' => isset($data['is_active']) ? 1 : 0,
|
||||
];
|
||||
|
||||
$password = (string) ($data['smtp_password'] ?? '');
|
||||
if ($password !== '') {
|
||||
$params['smtp_password_encrypted'] = $this->cipher->encrypt($password);
|
||||
}
|
||||
|
||||
if ($id !== null) {
|
||||
$setClauses = [
|
||||
'name = :name',
|
||||
'smtp_host = :smtp_host',
|
||||
'smtp_port = :smtp_port',
|
||||
'smtp_encryption = :smtp_encryption',
|
||||
'smtp_username = :smtp_username',
|
||||
'sender_email = :sender_email',
|
||||
'sender_name = :sender_name',
|
||||
'is_default = :is_default',
|
||||
'is_active = :is_active',
|
||||
];
|
||||
|
||||
if (isset($params['smtp_password_encrypted'])) {
|
||||
$setClauses[] = 'smtp_password_encrypted = :smtp_password_encrypted';
|
||||
}
|
||||
|
||||
$params['id'] = $id;
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE email_mailboxes SET ' . implode(', ', $setClauses) . ' WHERE id = :id'
|
||||
);
|
||||
} else {
|
||||
if (!isset($params['smtp_password_encrypted'])) {
|
||||
$params['smtp_password_encrypted'] = '';
|
||||
}
|
||||
|
||||
$statement = $this->pdo->prepare(
|
||||
'INSERT INTO email_mailboxes (name, smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password_encrypted, sender_email, sender_name, is_default, is_active)
|
||||
VALUES (:name, :smtp_host, :smtp_port, :smtp_encryption, :smtp_username, :smtp_password_encrypted, :sender_email, :sender_name, :is_default, :is_active)'
|
||||
);
|
||||
}
|
||||
|
||||
$statement->execute($params);
|
||||
}
|
||||
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$statement = $this->pdo->prepare('DELETE FROM email_mailboxes WHERE id = :id');
|
||||
$statement->execute(['id' => $id]);
|
||||
}
|
||||
|
||||
public function toggleStatus(int $id): void
|
||||
{
|
||||
$statement = $this->pdo->prepare(
|
||||
'UPDATE email_mailboxes SET is_active = NOT is_active WHERE id = :id'
|
||||
);
|
||||
$statement->execute(['id' => $id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user