Commit 4942a125 authored by Anthony Gautreau's avatar Anthony Gautreau
Browse files

Bundle unicaen/mail-bundle : portage Symfony de unicaen/mail

parents
Loading
Loading
Loading
Loading

.php-cs-fixer.dist.php

0 → 100644
+13 −0
Changes for .php-cs-fixer.dist.php: 13 added lines, 0 removed lines.
Original line number Diff line number Diff line
<?php

$finder = (new PhpCsFixer\Finder())
    ->in(__DIR__.'/src')
    ->exclude('vendor')
;

return (new PhpCsFixer\Config())
    ->setRules([
        '@Symfony' => true,
    ])
    ->setFinder($finder)
;

.version

0 → 100644
+1 −0
Changes for .version: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
0.0.0

README.md

0 → 100644
+191 −0
Changes for README.md: 191 added lines, 0 removed lines.
Original line number Diff line number Diff line
# unicaen/mail-bundle

Envoi des mails par le mailer de Symfony, avec profils d'envoi, redirection hors production
et journal en base. Expose une API d'administration sécurisée par unicaen/privilege-bundle.

## Installation

```bash
composer require unicaen/mail-bundle
```

Sans Symfony Flex, déclarer le bundle dans `config/bundles.php` :

```php
Unicaen\Mail\UnicaenMailBundle::class => ['all' => true],
```

Le transport est celui de Symfony, à configurer dans `config/packages/mailer.yaml` :

```yaml
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'   # smtp://smtp.exemple.fr:25, smtp://user:pass@smtp.exemple.fr:465, null://null
```

La table `unicaen_mail_mail` se crée par une migration de l'application
(`doctrine:migrations:diff`). Pour reprendre la table de unicaen/mail (Laminas), voir
[Migration depuis unicaen/mail](#migration-depuis-unicaenmail).

## Configuration

Un exemple commenté est livré avec le paquet :

```bash
cp vendor/unicaen/mail-bundle/config/unicaen_mail.yaml.dist config/packages/unicaen_mail.yaml
```

```yaml
unicaen_mail:
    conservation: P1Y          # durée de conservation, lue par unicaen:mail:purge
    journaliser_tout: true     # journalise et redirige aussi les mails envoyés sans MailService

    profils:
        default:
            expediteur: { email: '%env(MAIL_FROM)%', nom: 'Mon application' }
            reply_to: ~                              # adresse ou liste
            prefixe_sujet: 'Mon application'         # « [Mon application] Sujet »
            mention_automatique: true                # « Ce courrier électronique vous a été adressé automatiquement… »
            envoi: '%env(bool:MAIL_ENVOI)%'          # false : journalisé au statut NOT SENT, pas envoyé
            redirection: '%env(MAIL_REDIRECTION)%'   # liste ou « a@x.fr,b@x.fr », vide : pas de redirection
        assistance:
            expediteur: { email: assistance@exemple.fr, nom: Assistance }
```

Une clé absente d'un profil est reprise du profil `default`. L'expéditeur se reprend d'un
bloc : un profil qui fixe son adresse ne garde pas le nom du profil `default`. Pour qu'un
profil échappe à la redirection du profil `default`, lui donner `redirection: []`.

La redirection remplace To, Cc, Bcc et Reply-To par les adresses de redirection, en-têtes
et enveloppe SMTP, ajoute `{REDIR}` au sujet et la liste des destinataires prévus en pied.

## Usage

```php
use Unicaen\Mail\Service\MailService;

// Mail construit par l'application, TemplatedEmail compris : sans expéditeur, celui du profil est posé.
$mail = $mailService->envoyer(
    (new TemplatedEmail())->to($individu->getEmail())->subject('Votre dossier')
        ->htmlTemplate('mail/dossier.html.twig')->context(['dossier' => $dossier]),
    profil: 'assistance',
    motsClefs: ['DOSSIER_'.$dossier->getId()],
);

// Forme courte, proche de MailService::sendMail() de unicaen/mail.
$mail = $mailService->envoyerSimple(
    ['a@exemple.fr', 'b@exemple.fr'], 'Réunion', '<p>Bonjour…</p>',
    piecesJointes: ['/var/data/convocation.pdf'], copies: 'c@exemple.fr',
);
```

Une erreur du transport ne lève pas d'exception : le mail revient au statut `FAILED`
(`StatutMail::Echec`), l'erreur dans `getLog()`. Un mail invalide (sans destinataire) est
journalisé en échec et l'exception est relancée.

Le journal est écrit par un `flush` de l'EntityManager : envoyer après avoir enregistré ses
propres données, sous peine de les enregistrer en même temps.

Les mots-clefs servent à retrouver les mails d'une entité ou d'un modèle (filtre exact
`motClef` de l'API). Une adresse ne peut contenir de virgule ni un mot-clef de `||`,
séparateurs de stockage hérités de unicaen/mail.

### Réenvoi et pièces jointes

`MailService::reenvoyer($mail)` renvoie le mail aux destinataires prévus, avec la redirection
de la configuration actuelle, et le lie au mail d'origine. Seules les pièces jointes passées
par `envoyerSimple(piecesJointes: …)` sont réenvoyables, tant que le fichier existe : leur
chemin est journalisé. Celles jointes à un `Email` passé à `envoyer()` ne sont journalisées
que par leur nom et leur type, ce mail ne peut donc pas être réenvoyé.

### Monolog

Le handler natif `symfony_mailer` de Monolog remplace le `MailWriter` de unicaen/mail. Ses
mails passent par le mailer, ils sont donc journalisés et redirigés comme les autres :

```yaml
monolog:
    handlers:
        erreurs:
            type: fingers_crossed
            action_level: critical
            handler: mail_erreurs
        mail_erreurs:
            type: symfony_mailer
            from_email: '%env(MAIL_FROM)%'
            to_email: ['equipe@exemple.fr']
            subject: 'Erreur critique'
            level: debug
```

## API

Toutes les opérations sont protégées par `is_granted('UNICAEN_PRIVILEGE', object)` et
documentées sous le tag OpenAPI « Unicaen Mail ».

| Opération | Route | Privilège |
|---|---|---|
| Liste paginée, filtres `statut[]`, `depuis`, `jusqua`, `profil`, `destinataire`, `sujet`, `motClef` | `GET /mails` | `mail_voir` |
| Détail, corps compris | `GET /mails/{id}` | `mail_voir` |
| Suppression | `DELETE /mails/{id}` | `mail_supprimer` |
| Réenvoi, 409 si une pièce jointe manque | `POST /mails/{id}/reenvoi` | `mail_reenvoi_creer` |
| Mail de test `{ destinataire, profil? }` | `POST /mails/test` | `mail_test_creer` |
| Profils résolus, sans le DSN | `GET /mails/configuration` | `mail_configuration_voir` |

Les codes suivent la convention de `unicaen:privilege:sync-routes --assign` : une fois les
privilèges créés, la commande relie chaque route au sien. Un référentiel prêt à copier est
fourni dans [docs/data/25_privilege_mail.yaml](../docs/data/25_privilege_mail.yaml), avec la
catégorie `unicaen_mail` à déclarer.

## Commandes

```bash
# Supprime les mails plus anciens que unicaen_mail.conservation, ou que la durée donnée
bin/console unicaen:mail:purge [--conservation=P2Y] [--dry-run]

# Envoie un mail de test et affiche son statut
bin/console unicaen:mail:test jean.dupont@exemple.fr [--profil=assistance]
```

`mail:purge` reste disponible en alias.

## Surcharger l'entité

```yaml
unicaen_mail:
    classes:
        mail: App\Entity\Mail
```

`App\Entity\Mail` étend `Unicaen\Mail\Entity\AbstractMail`, porte `#[ORM\Entity]` et
`#[ORM\Table(name: 'unicaen_mail_mail')]`. Les relations vers `MailInterface` sont résolues
vers cette classe.

## Migration depuis unicaen/mail

| unicaen/mail (Laminas) | unicaen/mail-bundle |
|---|---|
| `transport_options` (host, port, tls, auto_tls, use_auth) | `MAILER_DSN` de Symfony |
| `module` et `default` | `profils` et `default` |
| `from_email`, `from_name` | `expediteur.email`, `expediteur.nom` |
| `subject_prefix` | `prefixe_sujet`, sans crochets |
| `redirect` + `redirect_to` | `redirection` : vide ou liste |
| `do_not_send` | `envoi: false` |
| `sendMail($to, $sujet, $texte, $module, $pj, $copie, $replyTo)` | `envoyerSimple(...)`, arguments nommés |
| `send(Email $mail, $module)` | `envoyer(Email $email, $profil)`, journalisé |
| `MailWriter` | handler Monolog `symfony_mailer` |
| `mail:purge --time=P2Y` | `unicaen:mail:purge --conservation=P2Y` |
| privilèges `mail-mail_*` | `mail_voir`, `mail_supprimer`, `mail_reenvoi_creer`, `mail_test_creer`, `mail_configuration_voir` |

La table est reprise par [docs/SQL/migration-depuis-unicaen-mail.sql](../docs/SQL/migration-depuis-unicaen-mail.sql)
(PostgreSQL) : nouvelles colonnes, pièces jointes converties en JSON, identifiant passé en
IDENTITY. Les données existantes restent lisibles telles quelles. L'ancienne `date_envoi`,
posée à la création, devient `date_creation`.

`HasMailsTrait` n'est pas repris : les mots-clefs remplissent ce rôle sans relation.

## Limites

- Envoi synchrone : avec un transport Messenger asynchrone, le mail reste `PENDING` jusqu'au
  passage du worker, qui le met en forme et le journalise à ce moment.
- `journaliser_tout: false` laisse passer sans redirection les mails envoyés hors de `MailService`.

composer.json

0 → 100644
+52 −0
Changes for composer.json: 52 added lines, 0 removed lines.
Original line number Diff line number Diff line
{
    "name": "unicaen/mail-bundle",
    "type": "symfony-bundle",
    "description": "Bundle Unicaen Mail : envoi, journal et administration des courriers électroniques",
    "repositories": [
        {
            "type": "composer",
            "url": "https://packagist.unicaen.fr"
        }
    ],
    "license": "MIT",
    "autoload": {
        "psr-4": {
            "Unicaen\\Mail\\": "src/"
        }
    },
    "require": {
        "php": "^8.3",
        "api-platform/core": "^4.2",
        "doctrine/orm": "^3.3",
        "symfony/config": "^7.3 || ^8.1",
        "symfony/console": "^7.3 || ^8.1",
        "symfony/dependency-injection": "^7.3 || ^8.1",
        "symfony/event-dispatcher": "^7.3 || ^8.1",
        "symfony/expression-language": "^7.3 || ^8.1",
        "symfony/http-kernel": "^7.3 || ^8.1",
        "symfony/mailer": "^7.3 || ^8.1",
        "symfony/mime": "^7.3 || ^8.1",
        "symfony/serializer": "^7.3 || ^8.1",
        "symfony/validator": "^7.3 || ^8.1",
        "symfony/yaml": "^7.3 || ^8.1",
        "unicaen/privilege-bundle": "^2.5"
    },
    "suggest": {
        "symfony/twig-bundle": "Pour envoyer des TemplatedEmail rendus par Twig"
    },
    "require-dev": {
        "friendsofphp/php-cs-fixer": "^3.75"
    },
    "extra": {
        "symfony": {
            "bundle": {
                "Unicaen\\Mail\\UnicaenMailBundle": "all"
            }
        }
    },
    "config": {
        "allow-plugins": {
            "symfony/runtime": true
        }
    }
}

config/services.yaml

0 → 100644
+18 −0
Changes for config/services.yaml: 18 added lines, 0 removed lines.
Original line number Diff line number Diff line
services:
  _defaults:
    autowire: true
    autoconfigure: true
    bind:
      string $mailClass: '%unicaen.mail_class%'
      array $profils: '%unicaen_mail.profils%'
      string $conservation: '%unicaen_mail.conservation%'
      bool $journaliserTout: '%unicaen_mail.journaliser_tout%'

  Unicaen\Mail\:
    resource: '../src/*'
    exclude:
      - '../src/ApiResource/'
      - '../src/DependencyInjection/'
      - '../src/Entity/'
      - '../src/Model/'
      - '../src/UnicaenMailBundle.php'
Loading