diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing-0.2.png b/doc/fr/Sil/Component/Emailing/domain/emailing-0.2.png
new file mode 100644
index 00000000..b246cab3
Binary files /dev/null and b/doc/fr/Sil/Component/Emailing/domain/emailing-0.2.png differ
diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing.uxf b/doc/fr/Sil/Component/Emailing/domain/emailing.uxf
new file mode 100644
index 00000000..1e503515
--- /dev/null
+++ b/doc/fr/Sil/Component/Emailing/domain/emailing.uxf
@@ -0,0 +1,458 @@
+
+
+ 10
+
+ UMLFrame
+
+ 70
+ 20
+ 1210
+ 650
+
+ EmailingComponent
+
+
+
+ UMLClass
+
+ 580
+ 150
+ 160
+ 60
+
+ /AbstractMessage/
+--
+#title :string
+#content: string
+--
+
+
+
+ UMLClass
+
+ 460
+ 340
+ 160
+ 50
+
+ template=Resource
+SimpleMessage
+--
+
+
+
+
+ UMLClass
+
+ 720
+ 340
+ 160
+ 50
+
+ template=Resource
+GroupedMessage
+--
+
+
+
+ UMLClass
+
+ 560
+ 430
+ 200
+ 90
+
+ template=Resource
+MailingList
+--
+#name: string
+#description: string
+#enabled: bool
+
+
+
+ UMLClass
+
+ 560
+ 590
+ 200
+ 60
+
+ template=Resource
+Recipient
+--
+#valid: bool
+
+
+
+ UMLClass
+
+ 580
+ 70
+ 160
+ 40
+
+ <<Interface>>
+MessageInterface
+--
+
+
+
+
+ UMLClass
+
+ 90
+ 70
+ 180
+ 40
+
+ <<Interface>>
+AttachmentInterface
+--
+
+
+
+
+ Relation
+
+ 260
+ 70
+ 340
+ 120
+
+ lt=-
+r1=0..1
+m1=#message
+r2=0..n
+m2=#attachments
+ 320.0;90.0;130.0;90.0;130.0;20.0;10.0;20.0
+
+
+ Relation
+
+ 650
+ 100
+ 30
+ 70
+
+ lt=<<.
+ 10.0;10.0;10.0;50.0
+
+
+ Relation
+
+ 510
+ 200
+ 170
+ 170
+
+ lt=<<-
+ 150.0;10.0;150.0;70.0;10.0;70.0;10.0;150.0
+
+
+ Relation
+
+ 650
+ 260
+ 150
+ 110
+
+ lt=-
+ 10.0;10.0;130.0;10.0;130.0;90.0
+
+
+ Relation
+
+ 710
+ 360
+ 200
+ 140
+
+ lt=-
+r1=0..n
+m1=#lists
+ 10.0;110.0;180.0;110.0;180.0;10.0;130.0;10.0
+
+
+ Relation
+
+ 710
+ 490
+ 200
+ 170
+
+ lt=-
+r1=1..n
+m1=#lists
+r2=1..n
+m2=#recipients
+ 10.0;20.0;180.0;20.0;180.0;140.0;10.0;140.0
+
+
+ UMLClass
+
+ 90
+ 330
+ 190
+ 220
+
+ template=Resource
+EmailAddress
+--
+-value: string
+
+
+
+ Relation
+
+ 230
+ 510
+ 350
+ 140
+
+ lt=-
+m2=email
+r2=1..1
+ 330.0;120.0;100.0;120.0;100.0;20.0;10.0;20.0
+
+
+ Relation
+
+ 230
+ 360
+ 250
+ 160
+
+ lt=-
+m2=#from
+r2=1..1
+ 230.0;10.0;100.0;10.0;100.0;130.0;10.0;130.0
+
+
+ Relation
+
+ 230
+ 430
+ 120
+ 50
+
+ lt=-
+m2=#to
+r2=1..n
+ 100.0;20.0;10.0;20.0
+
+
+ Relation
+
+ 230
+ 390
+ 120
+ 50
+
+ lt=-
+m2=#cc
+r2=0..n
+ 100.0;20.0;10.0;20.0
+
+
+ Relation
+
+ 230
+ 350
+ 120
+ 50
+
+ lt=-
+m2=#bcc
+r2=0..n
+ 100.0;20.0;10.0;20.0
+
+
+ UMLClass
+
+ 970
+ 50
+ 190
+ 70
+
+ template=Resource
+MessageTemplate
+--
+#name: string
+#content: string
+
+
+
+
+ UMLClass
+
+ 970
+ 290
+ 190
+ 70
+
+ template=Resource
+ContentToken
+--
+#value: mixed
+
+
+
+
+ Relation
+
+ 1110
+ 60
+ 170
+ 150
+
+ lt=-
+r1=0..n
+m1=#tokenTypes
+r2=1..1
+m2=#template
+ 10.0;120.0;150.0;120.0;150.0;20.0;10.0;20.0
+
+
+ Relation
+
+ 730
+ 60
+ 260
+ 130
+
+ lt=-
+r2=0..n
+m2=#messages
+r1=0..1
+m1=#template
+ 240.0;20.0;140.0;20.0;140.0;100.0;10.0;100.0
+
+
+ Relation
+
+ 730
+ 180
+ 260
+ 160
+
+ lt=-
+r2=1..1
+m2=#message
+r1=0..n
+m1=#tokens
+ 240.0;130.0;140.0;130.0;140.0;20.0;10.0;20.0
+
+
+ UMLClass
+
+ 970
+ 140
+ 190
+ 110
+
+ template=Resource
+ContentTokenType
+--
+#name: string
+
+
+
+
+ Relation
+
+ 1110
+ 220
+ 120
+ 120
+
+ lt=-
+r2=1..1
+m2=#type
+ 10.0;100.0;100.0;100.0;100.0;20.0;10.0;20.0
+
+
+ UMLClass
+
+ 960
+ 420
+ 170
+ 130
+
+ <<enum>>
+ContentTokenDataType
+--
+BOOLEAN
+STRING
+INTEGER
+FLOAT
+DATE
+DATETIME
+
+
+
+
+ Relation
+
+ 1110
+ 200
+ 170
+ 270
+
+ lt=-
+r1=1..1
+m1=#dataType
+ 20.0;240.0;150.0;240.0;150.0;10.0;10.0;10.0
+
+
+ UMLClass
+
+ 90
+ 180
+ 180
+ 40
+
+ <<Interface>>
+MessageStateInterface
+--
+
+
+
+
+ UMLClass
+
+ 90
+ 250
+ 180
+ 30
+
+ MessageState
+--
+
+
+
+
+ Relation
+
+ 170
+ 210
+ 30
+ 60
+
+ lt=<<.
+ 10.0;10.0;10.0;40.0
+
+
+ Relation
+
+ 260
+ 180
+ 340
+ 50
+
+ lt=-
+r2=1..1
+m2=#state
+ 320.0;20.0;10.0;20.0
+
+
diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing_message_states-0.1.png b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states-0.1.png
new file mode 100644
index 00000000..e81a0640
Binary files /dev/null and b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states-0.1.png differ
diff --git a/doc/fr/Sil/Component/Emailing/domain/emailing_message_states.uxf b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states.uxf
new file mode 100644
index 00000000..01c52e41
--- /dev/null
+++ b/doc/fr/Sil/Component/Emailing/domain/emailing_message_states.uxf
@@ -0,0 +1,175 @@
+
+
+ 10
+
+ UMLSpecialState
+
+ 650
+ 210
+ 20
+ 20
+
+ type=initial
+
+
+
+ Relation
+
+ 650
+ 220
+ 70
+ 90
+
+ lt=->
+create
+ 10.0;10.0;10.0;70.0
+
+
+ UMLState
+
+ 610
+ 290
+ 100
+ 40
+
+ DRAFT
+
+
+
+ UMLState
+
+ 610
+ 410
+ 100
+ 40
+
+ VALIDATED
+
+
+
+ Relation
+
+ 650
+ 320
+ 80
+ 110
+
+ lt=->
+validate
+ 10.0;10.0;10.0;90.0
+
+
+ UMLState
+
+ 610
+ 530
+ 100
+ 40
+
+ SENT
+
+
+
+ Relation
+
+ 650
+ 440
+ 60
+ 110
+
+ lt=->
+send
+ 10.0;10.0;10.0;90.0
+
+
+ Relation
+
+ 700
+ 410
+ 150
+ 40
+
+ lt=-
+delete
+ 10.0;20.0;130.0;20.0
+
+
+ Relation
+
+ 700
+ 290
+ 150
+ 260
+
+ lt=->
+delete
+ 10.0;20.0;130.0;20.0;130.0;240.0
+
+
+ UMLSpecialState
+
+ 650
+ 630
+ 20
+ 20
+
+ type=final
+
+
+
+ UMLFrame
+
+ 460
+ 170
+ 460
+ 490
+
+ Mailing State Machine
+
+
+
+ UMLState
+
+ 780
+ 530
+ 100
+ 40
+
+ DELETED
+
+
+
+ Relation
+
+ 650
+ 560
+ 200
+ 60
+
+ lt=-
+ 180.0;10.0;180.0;40.0;10.0;40.0
+
+
+ Relation
+
+ 650
+ 560
+ 30
+ 90
+
+ lt=->
+ 10.0;10.0;10.0;70.0
+
+
+ Relation
+
+ 540
+ 490
+ 110
+ 80
+
+ lt=->
+re-send
+ 70.0;60.0;10.0;60.0;10.0;10.0;90.0;10.0;90.0;40.0
+
+
diff --git a/doc/fr/Sil/Component/Emailing/domain/index.rst b/doc/fr/Sil/Component/Emailing/domain/index.rst
new file mode 100644
index 00000000..44764d3a
--- /dev/null
+++ b/doc/fr/Sil/Component/Emailing/domain/index.rst
@@ -0,0 +1,214 @@
+========
+Emailing
+========
+
+----------------------
+Description du domaine
+----------------------
+
+L'**emailing** consiste à **préparer** des envois d'emails en masse.
+
+Cela consiste à créer une contenu textuel riche (HTML) et le diffuser à une liste de destinataires appelée **liste de diffusion**.
+
+L'utilisation du mailing se fait principalement dans un cadre marketing, pour informer sa clientèle ou sa communauté, réaliser des prospections et démarchages, ou communiquer sur des actions commerciales promotionnelles.
+
+Il peut également servir pour des envois ponctuels (sans passer par des listes de diffusions).
+
+----------------------
+Fonctionnalités cibles
+----------------------
+
+- `Gestion du contenu`_
+- `Gestion des listes de diffusion`_
+- `Paramétrage des envois`_
+- `Gestion d'envoi simple`_
+- `Gestion de modèles`_
+
+Gestion du contenu
+==================
+
+Saisir le titre du message, son contenu (via éditeur de texte riche (éditeur WYSIWYG)), ses pièces jointes.
+
+Il faut également gérer son cycle de vie. Selon son état, un mailing ne pourra être envoyé, modifié ou archivé.
+
+Gestion des listes de diffusion
+===============================
+
+Le composant doit pouvoir proposer une gestion de liste de diffusion. une liste de diffusion est nommée et est composée d'un ou plusieurs destinataires.
+
+Paramétrage des envois
+======================
+
+Pour chaque message simple, il doit être possible de paramétrer des informations liées à l'envoi : expéditeur, répondre à, copie.
+
+Gestion d'envoi simple
+======================
+
+Pouvoir gérer des envois de mail simples : A destination d'une seul contact. Il faut également conserver l'historique des envois par contacts.
+
+Gestion de modèles
+==================
+
+Une gestion de modèle de contenu doit permettre la création rapide de campagne d'emailing. Il faudra également prévoir un système de remplacement de jeton [1]_ pour permettre de pré-remplir certaines informations.
+
+.. note::
+
+ .. [1] Un jeton est un emplacement dans un contenu texte qui sera substitué par une valeur lors de la construction du contenu
+
+-------
+Domaine
+-------
+
+Message groupé
+==============
+
+Un **message groupé** est définit de cette manière :
+
++-------------+---------------------------------------------------------+--------+
+| Propriété | Description | Oblig. |
++=============+=========================================================+========+
+| title | Le titre du message. Sera également l'objet de l'email. | x |
++-------------+---------------------------------------------------------+--------+
+| content | Le contenu du message. | |
++-------------+---------------------------------------------------------+--------+
+| attachments | Les pièces jointes du message. | |
++-------------+---------------------------------------------------------+--------+
+| lists | Les listes de diffusion qui seront utilisées. | |
++-------------+---------------------------------------------------------+--------+
+
+Message simple
+==============
+
+Un **message simple** est définit de cette manière :
+
++-------------+---------------------------------------------------------+--------+
+| Propriété | Description | Oblig. |
++=============+=========================================================+========+
+| title | Le titre du message. Sera également l'objet de l'email. | x |
++-------------+---------------------------------------------------------+--------+
+| content | Le contenu du message. | |
++-------------+---------------------------------------------------------+--------+
+| attachments | Les pièces jointes du message. | |
++-------------+---------------------------------------------------------+--------+
+| config | La configuration d'expédition du message. | |
++-------------+---------------------------------------------------------+--------+
+
+Configuration de message
+========================
+
+Une **configuration de message** gère les paramètres suivants :
+
++-----------+-----------------------------------------------+
+| Propriété | Description |
++===========+===============================================+
+| from | L'expéditeur qui sera définit pour le message |
++-----------+-----------------------------------------------+
+| to | Le destinataire du message [2]_ |
++-----------+-----------------------------------------------+
+| cc | Une adresse email qui sera mise en copie [2]_ |
++-----------+-----------------------------------------------+
+| bcc | Une adresse email en copie cachée [2]_ |
++-----------+-----------------------------------------------+
+
+.. note::
+
+ .. [2] Ce paramètre du message sera utilisé que lors d'envoi de message simple (hors listes de diffusion)
+
+Liste de diffusion
+==================
+
+Une **liste de diffusion** se définit par un titre et une collection de **destinataires**.
+
++-------------+--------------------------------+--------+
+| Propriété | Description | Oblig. |
++=============+================================+========+
+| title | Le titre de la liste | x |
++-------------+--------------------------------+--------+
+| description | Une description optionnelle | |
++-------------+--------------------------------+--------+
+| enabled | La list est utilisable ou non | |
++-------------+--------------------------------+--------+
+| recipients | Une collection de destinataire | |
++-------------+--------------------------------+--------+
+
+Destinataire
+============
+
+Un **destinataire** est une représentation d'une adresse email.
+
++-----------+----------------------------------------------------+--------+
+| Propriété | Description | Oblig. |
++===========+====================================================+========+
+| email | L'adresse email du destinataire | x |
++-----------+----------------------------------------------------+--------+
+| valid | Un indicateur d'état de validité de l'adresse [3]_ | |
++-----------+----------------------------------------------------+--------+
+
+.. note::
+
+ .. [3] Cet indicateur sera à mettre à jour en fonction des retours après envoi. (voir https://en.wikipedia.org/wiki/Bounce_message)
+
+Pièce jointe
+============
+
+Une pièce jointe représentera un fichier à joindre au message.
+
+Modèle de message
+=================
+
+Un **modèle de message** permet de définir une mise en page de base pour les messages ainsi que la définition de jetons de substitution pour faciliter la saisie des messages utilisant un modèle.
+
++-----------+-----------------------------------------------+--------+
+| Propriété | Description | Oblig. |
++===========+===============================================+========+
+| content | Contenu du modèle | |
++-----------+-----------------------------------------------+--------+
+| tokens | Collection de types de jetons de substitution | |
++-----------+-----------------------------------------------+--------+
+
+Type de jeton de substitution
+=============================
+
+Un **type de jeton** permet de définir quelle donnée sera affichée dans un modèle de message.
+
++-----------+-----------------------+--------+
+| Propriété | Description | Oblig. |
++===========+=======================+========+
+| name | Nom du type de donnée | |
++-----------+-----------------------+--------+
+| dataType | Type de donnée cible | |
++-----------+-----------------------+--------+
+
+Jeton de substitution
+=====================
+
+Un **jeton de substitution** permet de remplacer des emplacements définis depuis un modèle par des valeurs de substitution.
+
++-----------+---------------------------------+--------+
+| Propriété | Description | Oblig. |
++===========+=================================+========+
+| value | Valeur du jeton de substitution | |
++-----------+---------------------------------+--------+
+| type | Type de jeton | x |
++-----------+---------------------------------+--------+
+
+-----------------
+Modèle du domaine
+-----------------
+
+.. image:: emailing-0.2.png
+
+------------
+Cycle de vie
+------------
+
+Message
+=======
+
+Un message, qu'il soit simple ou groupé, suivra le même cycle de vie suivant :
+
+.. image:: emailing_message_states-0.1.png
+
+.. note::
+
+ Un message envoyé pourra être envoyé à nouveau, ceci sans limite. Il sera à la charge des implémentations de limiter ou non cette particularité.
diff --git a/doc/fr/Sil/Component/Emailing/index.rst b/doc/fr/Sil/Component/Emailing/index.rst
new file mode 100644
index 00000000..1ec1d93c
--- /dev/null
+++ b/doc/fr/Sil/Component/Emailing/index.rst
@@ -0,0 +1,8 @@
+Composants Emailing
+===================
+
+.. toctree::
+ :maxdepth: 2
+
+ installation
+ domain/index
diff --git a/doc/fr/Sil/Component/Emailing/installation.rst b/doc/fr/Sil/Component/Emailing/installation.rst
new file mode 100644
index 00000000..11e44375
--- /dev/null
+++ b/doc/fr/Sil/Component/Emailing/installation.rst
@@ -0,0 +1,2 @@
+Installation
+============
diff --git a/doc/fr/Sil/Component/map.rst.inc b/doc/fr/Sil/Component/map.rst.inc
index ec3f23a8..a8a8376a 100644
--- a/doc/fr/Sil/Component/map.rst.inc
+++ b/doc/fr/Sil/Component/map.rst.inc
@@ -8,4 +8,5 @@
* :doc:`/Sil/Component/Product/index`
* :doc:`/Sil/Component/Stock/index`
* :doc:`/Sil/Component/Uom/index`
+* :doc:`/Sil/Component/Emailing/index`
* :doc:`/Sil/Component/User/index`
diff --git a/doc/fr/conf.py b/doc/fr/conf.py
index a99b1797..dbba8534 100644
--- a/doc/fr/conf.py
+++ b/doc/fr/conf.py
@@ -20,7 +20,7 @@
master_doc = 'index'
project = u'Sil & Blast Projects'
-copyright = u'2017, Libre-Informatique'
+copyright = u'2018, Libre-Informatique'
version = ''
release = ''
diff --git a/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php b/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php
index f7af66ff..6ab740bf 100644
--- a/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php
+++ b/src/Blast/Bundle/TestsBundle/Api/BlastApiTestCase.php
@@ -1,7 +1,7 @@
shuf.nbr
+fi
+
+#RND contain branch name which may contain '-' which may not work as database name for postgre
+
+RND=$(echo $RND|sed -e s/-/_/g|tr '[:upper:]' '[:lower:]')$(echo -n $(cat shuf.nbr ))
+
+SILURL="/sil"
+SERVERADDR="127.0.0.1:8042"
+SERVERENV="test"
+
+DBHOST=postgres.host
+DBROOTUSER=postgres
+DBROOTPASSWORD=postgres24
+DBAPPNAME=sil_db_${RND}
+DBAPPUSER=sil_user_${RND}
+DBAPPPASSWORD=sil_password
+
+ELHOST=elk.host
+ELALIAS=sil_${RND}
+
+PHPUNITCMD="bin/phpunit --verbose --debug -c phpunit.xml.dist --coverage-html build/coverage --coverage-clover build/clover.xml --coverage-crap4j build/crap4j.xml --log-junit build/junit.xml" #--testdox
+#CODECEPTCMD="bin/codecept run -vvv --debug --steps --fail-fast --no-interaction --xml --html"
+CODECEPTENV="firefox" #chrome firefox,lisem ...
+CODECEPTOUTPUT="src/Tests/_output/"
+
+
+DISPLAY=:99
diff --git a/src/Sil/Component/Emailing/.env.travis b/src/Sil/Component/Emailing/.env.travis
new file mode 100644
index 00000000..0b042b50
--- /dev/null
+++ b/src/Sil/Component/Emailing/.env.travis
@@ -0,0 +1,23 @@
+
+
+SILURL="/sil"
+SERVERADDR="127.0.0.1:8042"
+SERVERENV="test"
+
+DBHOST=localhost
+DBROOTUSER=postgres
+DBROOTPASSWORD=postgres24
+DBAPPNAME=test_sil_db
+DBAPPUSER=test_sil_user
+DBAPPPASSWORD=test_sil_password
+
+ELHOST=localhost
+ELALIAS=Sil
+
+PHPUNITCMD="bin/phpunit --verbose --debug -c phpunit.xml.dist --coverage-clover build/logs/clover.xml" #--testdox
+#CODECEPTCMD="bin/codecept run -vvv --debug --steps --fail-fast --no-interaction --xml --html"
+CODECEPTENV="firefox" #chrome firefox,lisem ...
+CODECEPTOUTPUT="src/Tests/_output/"
+
+
+DISPLAY=:99
diff --git a/src/Sil/Component/Emailing/.github/ISSUE_TEMPLATE.md b/src/Sil/Component/Emailing/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..7209c517
--- /dev/null
+++ b/src/Sil/Component/Emailing/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,11 @@
+| Q | A
+| ---------------- | -----
+| Bug report? | yes/no
+| Feature request? | yes/no
+| BC Break report? | yes/no
+| Version | x.y.z
+
+
diff --git a/src/Sil/Component/Emailing/.github/PULL_REQUEST_TEMPLATE.md b/src/Sil/Component/Emailing/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..b92f1610
--- /dev/null
+++ b/src/Sil/Component/Emailing/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,15 @@
+| Q | A
+| ------------- | ---
+| Branch? | master
+| Bug fix? | yes/no
+| New feature? | yes/no
+| BC breaks? | yes/no
+| Tests pass? | yes/no
+| Fixed tickets | fixes #X, partially #Y, mentioned in #Z
+| License | LGPL
+
+
+
diff --git a/src/Sil/Component/Emailing/.gitignore b/src/Sil/Component/Emailing/.gitignore
new file mode 100644
index 00000000..6be7c114
--- /dev/null
+++ b/src/Sil/Component/Emailing/.gitignore
@@ -0,0 +1,11 @@
+/nbproject
+/.php_cs.cache
+/composer.phar
+/composer.lock
+/vendor/
+/Resources/doc/_build
+*~
+/bin/*
+!/bin/console
+!/bin/ci-scripts
+!/bin/git-script
\ No newline at end of file
diff --git a/src/Sil/Component/Emailing/.gitrepo b/src/Sil/Component/Emailing/.gitrepo
new file mode 100644
index 00000000..98603c72
--- /dev/null
+++ b/src/Sil/Component/Emailing/.gitrepo
@@ -0,0 +1,12 @@
+; DO NOT EDIT (unless you know what you are doing)
+;
+; This subdirectory is a git "subrepo", and this file is maintained by the
+; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme
+;
+[subrepo]
+ remote = git@github.com:sil-project/Order.git
+ branch = wip-platform
+ commit = 4e6c89333ba3b70c866553de3777a438fd8401a0
+ method = merge
+ cmdver = 0.4.0
+ parent = 50e93e7427a5053c19cebb6beb86860b5e318920
diff --git a/src/Sil/Component/Emailing/.php_cs b/src/Sil/Component/Emailing/.php_cs
new file mode 100644
index 00000000..e41052d3
--- /dev/null
+++ b/src/Sil/Component/Emailing/.php_cs
@@ -0,0 +1,47 @@
+in(__DIR__)
+;
+
+$config = PhpCsFixer\Config::create()
+ ->setRules(array(
+ '@Symfony' => true,
+ 'binary_operator_spaces' => ['align_double_arrow' => true],
+ 'concat_space' => ['spacing'=>'one'],
+ 'yoda_style' => null,
+ 'increment_style' => ['style' => 'post'],
+ ))
+ ->setFinder($finder);
+
+// PHP-CS-Fixer 2.x
+if (method_exists($config, 'setRules')) {
+ $config->setRules(array_merge($config->getRules(), array(
+ 'header_comment' => array('header' => $header),
+ )));
+}
+
+return $config;
diff --git a/src/Sil/Component/Emailing/.scrutinizer.yml b/src/Sil/Component/Emailing/.scrutinizer.yml
new file mode 100644
index 00000000..08151b1a
--- /dev/null
+++ b/src/Sil/Component/Emailing/.scrutinizer.yml
@@ -0,0 +1,16 @@
+filter:
+ excluded_paths:
+ - 'vendor/*'
+ - 'bin/*'
+ - '*.min.js'
+ - 'Tests/*'
+checks:
+ php:
+ code_rating: true
+ duplication: true
+ javascript: true
+coding_style:
+ php:
+ spaces:
+ around_operators:
+ concatenation: true
diff --git a/src/Sil/Component/Emailing/.styleci.yml b/src/Sil/Component/Emailing/.styleci.yml
new file mode 100644
index 00000000..977c5a87
--- /dev/null
+++ b/src/Sil/Component/Emailing/.styleci.yml
@@ -0,0 +1,11 @@
+# Package `sllh/php-cs-fixer-styleci-bridge` is required to get it working.
+
+preset: psr2
+
+#disabled:
+# - concat_without_spaces
+# - single_quote
+
+enabled:
+ - no_php4_constructor
+ - concat_with_spaces
diff --git a/src/Sil/Component/Emailing/.travis.yml b/src/Sil/Component/Emailing/.travis.yml
new file mode 100644
index 00000000..bd1d3462
--- /dev/null
+++ b/src/Sil/Component/Emailing/.travis.yml
@@ -0,0 +1,54 @@
+language: php
+
+php:
+ - '7.1'
+ - nightly
+
+services:
+ - mysql
+ - postgresql
+
+cache:
+ directories:
+ - $HOME/.composer/cache/files
+
+env:
+ global:
+ - PATH="$HOME/.local/bin:$PATH"
+ - SCRIPTS_FOLDER=bin/ci-scripts
+ - SYMFONY_DEPRECATIONS_HELPER=weak
+ - TARGET=test
+ - XMLLINT_INDENT=" "
+
+matrix:
+ fast_finish: true
+ include:
+ - php: '7.1'
+ env: TARGET=docs
+ - php: '7.1'
+ env: TARGET=lint
+ allow_failures:
+ - php: nightly
+ - env: SYMFONY_DEPRECATIONS_HELPER=0
+ - env: SYMFONY=dev-master@dev
+ - env: SONATA_CORE=dev-master@dev
+ - env: SONATA_BLOCK=dev-master@dev
+
+before_install:
+ - if [ -x ${SCRIPTS_FOLDER}/before_install_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/before_install_${TARGET}.sh; fi;
+ - if [ -x ${SCRIPTS_FOLDER}/create_database_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/create_database_${TARGET}.sh; fi;
+
+install:
+ - if [ -x ${SCRIPTS_FOLDER}/install_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/install_${TARGET}.sh; fi;
+
+before_script:
+ - if [ -x ${SCRIPTS_FOLDER}/before_script_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/before_script_${TARGET}.sh; fi;
+
+script:
+ - if [ -x ${SCRIPTS_FOLDER}/run_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/run_${TARGET}.sh; fi;
+
+after_success:
+ - if [ -x ${SCRIPTS_FOLDER}/after_success_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/after_success_${TARGET}.sh; fi;
+
+after_failure:
+ - if [ -x ${SCRIPTS_FOLDER}/after_failure_${TARGET}.sh ]; then ${SCRIPTS_FOLDER}/after_failure_${TARGET}.sh; fi;
diff --git a/src/Sil/Component/Emailing/LICENCE.md b/src/Sil/Component/Emailing/LICENCE.md
new file mode 100644
index 00000000..408c98d4
--- /dev/null
+++ b/src/Sil/Component/Emailing/LICENCE.md
@@ -0,0 +1,157 @@
+### GNU LESSER GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+This version of the GNU Lesser General Public License incorporates the
+terms and conditions of version 3 of the GNU General Public License,
+supplemented by the additional permissions listed below.
+
+#### 0. Additional Definitions.
+
+As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the
+GNU General Public License.
+
+"The Library" refers to a covered work governed by this License, other
+than an Application or a Combined Work as defined below.
+
+An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+#### 1. Exception to Section 3 of the GNU GPL.
+
+You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+#### 2. Conveying Modified Versions.
+
+If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+- a) under this License, provided that you make a good faith effort
+ to ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+- b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+#### 3. Object Code Incorporating Material from Library Header Files.
+
+The object code form of an Application may incorporate material from a
+header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+- a) Give prominent notice with each copy of the object code that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+- b) Accompany the object code with a copy of the GNU GPL and this
+ license document.
+
+#### 4. Combined Works.
+
+You may convey a Combined Work under terms of your choice that, taken
+together, effectively do not restrict modification of the portions of
+the Library contained in the Combined Work and reverse engineering for
+debugging such modifications, if you also do each of the following:
+
+- a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+- b) Accompany the Combined Work with a copy of the GNU GPL and this
+ license document.
+- c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+- d) Do one of the following:
+ - 0) Convey the Minimal Corresponding Source under the terms of
+ this License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+ - 1) Use a suitable shared library mechanism for linking with
+ the Library. A suitable mechanism is one that (a) uses at run
+ time a copy of the Library already present on the user's
+ computer system, and (b) will operate properly with a modified
+ version of the Library that is interface-compatible with the
+ Linked Version.
+- e) Provide Installation Information, but only if you would
+ otherwise be required to provide such information under section 6
+ of the GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the Application
+ with a modified version of the Linked Version. (If you use option
+ 4d0, the Installation Information must accompany the Minimal
+ Corresponding Source and Corresponding Application Code. If you
+ use option 4d1, you must provide the Installation Information in
+ the manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.)
+
+#### 5. Combined Libraries.
+
+You may place library facilities that are a work based on the Library
+side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+- a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities, conveyed under the terms of this License.
+- b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+#### 6. Revised Versions of the GNU Lesser General Public License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+as you received it specifies that a certain numbered version of the
+GNU Lesser General Public License "or any later version" applies to
+it, you have the option of following the terms and conditions either
+of that published version or of any later version published by the
+Free Software Foundation. If the Library as you received it does not
+specify a version number of the GNU Lesser General Public License, you
+may choose any version of the GNU Lesser General Public License ever
+published by the Free Software Foundation.
+
+If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
\ No newline at end of file
diff --git a/src/Sil/Component/Emailing/Model/AbstractMessage.php b/src/Sil/Component/Emailing/Model/AbstractMessage.php
new file mode 100644
index 00000000..fa073124
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/AbstractMessage.php
@@ -0,0 +1,191 @@
+title = $title;
+ $this->content = trim($content);
+
+ $this->state = MessageState::draft();
+
+ $this->attachments = new ArrayCollection();
+ $this->tokens = new ArrayCollection();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTitle(string $title): void
+ {
+ $this->title = $title;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setContent(string $content): void
+ {
+ $this->content = trim($content);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAttachments(): array
+ {
+ return $this->attachments->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addAttachment(AttachmentInterface $attachment): void
+ {
+ if ($this->attachments->contains($attachment)) {
+ throw new InvalidArgumentException(sprintf('Attachment « %s » is already attached to message « %s »', $attachment->getName(), $this->getTitle()));
+ }
+ $this->attachments->add($attachment);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeAttachment(AttachmentInterface $attachment): void
+ {
+ if (!$this->attachments->contains($attachment)) {
+ throw new InvalidArgumentException(sprintf('Attachment « %s » is not attached to message « %s »', $attachment->getName(), $this->getTitle()));
+ }
+ $this->attachments->removeElement($attachment);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTemplate(): ?MessageTemplateInterface
+ {
+ return $this->template;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTemplate(?MessageTemplateInterface $template): void
+ {
+ $this->template = $template;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTokens(): array
+ {
+ return $this->tokens->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addToken(ContentTokenInterface $token): void
+ {
+ if ($this->tokens->contains($token)) {
+ throw new InvalidArgumentException(sprintf('Token « %s : %s » is already assigned to the message « %s »', $token->getName(), $token->getValue(), $this->getTitle()));
+ }
+ $this->tokens->add($token);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeToken(ContentTokenInterface $token): void
+ {
+ if (!$this->tokens->contains($token)) {
+ throw new InvalidArgumentException(sprintf('Token « %s : %s » is not assigned to the message « %s »', $token->getName(), $token->getValue(), $this->getTitle()));
+ }
+ $this->tokens->removeElement($token);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clearTokens(): void
+ {
+ $this->tokens->clear();
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/AttachmentInterface.php b/src/Sil/Component/Emailing/Model/AttachmentInterface.php
new file mode 100644
index 00000000..4851fd5b
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/AttachmentInterface.php
@@ -0,0 +1,17 @@
+tokenType = $tokenType;
+ $this->message = $message;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return $this->getTokenType()->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMessage(): MessageInterface
+ {
+ return $this->message;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValue($value): void
+ {
+ $typeIsValid = false;
+
+ switch ($this->getTokenType()->getDataType()->getValue()) {
+ case ContentTokenDataType::TYPE_BOOLEAN:
+ $typeIsValid = is_bool($value);
+ break;
+
+ case ContentTokenDataType::TYPE_DATE:
+ case ContentTokenDataType::TYPE_DATETIME:
+ $typeIsValid = get_class($value) === 'DateTime';
+ break;
+
+ case ContentTokenDataType::TYPE_STRING:
+ $typeIsValid = is_string($value);
+ break;
+
+ case ContentTokenDataType::TYPE_INTEGER:
+ $typeIsValid = is_int($value);
+ break;
+
+ case ContentTokenDataType::TYPE_FLOAT:
+ $typeIsValid = is_float($value);
+ break;
+
+ default:
+ throw new InvalidArgumentException(sprintf('The type of the value (%s) of ContentToken « %s » is not managed', $this->getName(), gettype($value)));
+ }
+
+ if (!$typeIsValid) {
+ throw new InvalidArgumentException(sprintf('Value of ContentToken « %s » must be of type « %s », « %s » given', $this->getName(), $this->getTokenType()->getDataType(), gettype($value)));
+ }
+
+ $this->value = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTokenType(): ContentTokenTypeInterface
+ {
+ return $this->tokenType;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValueAsString(): string
+ {
+ $stringValue = '';
+
+ switch ($this->getTokenType()->getDataType()->getValue()) {
+ case ContentTokenDataType::TYPE_BOOLEAN:
+ $stringValue = $this->value === true ? 'Yes' : 'No';
+ break;
+
+ case ContentTokenDataType::TYPE_DATE:
+ $stringValue = $this->value->format('Y-m-d');
+ break;
+
+ case ContentTokenDataType::TYPE_DATETIME:
+ $stringValue = $this->value->format('Y-m-d H:i:s');
+ break;
+
+ case ContentTokenDataType::TYPE_STRING:
+ case ContentTokenDataType::TYPE_INTEGER:
+ case ContentTokenDataType::TYPE_FLOAT:
+ default:
+ $stringValue = (string) $this->value;
+ }
+
+ return $stringValue;
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/ContentTokenDataType.php b/src/Sil/Component/Emailing/Model/ContentTokenDataType.php
new file mode 100644
index 00000000..60c1bc10
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/ContentTokenDataType.php
@@ -0,0 +1,60 @@
+getTypes())) {
+ throw new InvalidArgumentException(sprintf('Type « %s » is not managed. Managed types are : %s', $value, implode(', ', $this->getTypes())));
+ }
+ $this->value = $value;
+ }
+
+ /**
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ public function getTypes(): array
+ {
+ return [
+ 'TYPE_BOOLEAN' => static::TYPE_BOOLEAN,
+ 'TYPE_STRING' => static::TYPE_STRING,
+ 'TYPE_INTEGER' => static::TYPE_INTEGER,
+ 'TYPE_FLOAT' => static::TYPE_FLOAT,
+ 'TYPE_DATE' => static::TYPE_DATE,
+ 'TYPE_DATETIME' => static::TYPE_DATETIME,
+ ];
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/ContentTokenInterface.php b/src/Sil/Component/Emailing/Model/ContentTokenInterface.php
new file mode 100644
index 00000000..13db8a36
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/ContentTokenInterface.php
@@ -0,0 +1,62 @@
+name = $name;
+ $this->dataType = $dataType;
+ $this->template = $template;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataType(): ContentTokenDataType
+ {
+ return $this->dataType;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTemplate(): MessageTemplateInterface
+ {
+ return $this->template;
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/ContentTokenTypeInterface.php b/src/Sil/Component/Emailing/Model/ContentTokenTypeInterface.php
new file mode 100644
index 00000000..a9914f09
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/ContentTokenTypeInterface.php
@@ -0,0 +1,37 @@
+isValid($value)) {
+ throw new InvalidArgumentException(sprintf('The string « %s » is not a valid email address', $value));
+ }
+
+ $this->value = $value;
+ }
+
+ /**
+ * Validate if given string is an email.
+ *
+ * @param string $value
+ */
+ public function isValid(?string $value = null): bool
+ {
+ if ($value === null) {
+ $value = $this->value;
+ }
+
+ return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
+ }
+
+ /**
+ * Gets the email as string.
+ *
+ * @return string
+ */
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ public function __toString(): string
+ {
+ return $this->getValue();
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/EmailAddressInterface.php b/src/Sil/Component/Emailing/Model/EmailAddressInterface.php
new file mode 100644
index 00000000..8159d93e
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/EmailAddressInterface.php
@@ -0,0 +1,37 @@
+lists = new ArrayCollection();
+ }
+
+ /**
+ * @return array|MailingListInterface[]
+ */
+ public function getLists(): array
+ {
+ return $this->lists->getValues();
+ }
+
+ /**
+ * @param MailingListInterface $list
+ *
+ * @throws InvalidArgumentException
+ */
+ public function addList(MailingListInterface $list): void
+ {
+ if ($this->lists->contains($list)) {
+ throw new InvalidArgumentException(sprintf('List « %s » is already used by message « %s »', $list->getName(), $this->getTitle()));
+ }
+ $this->lists->add($list);
+ }
+
+ /**
+ * @param MailingListInterface $list
+ *
+ * @throws InvalidArgumentException
+ */
+ public function removeList(MailingListInterface $list): void
+ {
+ if (!$this->lists->contains($list)) {
+ throw new InvalidArgumentException(sprintf('List « %s » is not used by message « %s »', $list->getName(), $this->getTitle()));
+ }
+ $this->lists->removeElement($list);
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/GroupedMessageInterface.php b/src/Sil/Component/Emailing/Model/GroupedMessageInterface.php
new file mode 100644
index 00000000..21637b10
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/GroupedMessageInterface.php
@@ -0,0 +1,37 @@
+name = $name;
+ $this->description = $description;
+
+ $this->recipients = new ArrayCollection();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDescription(?string $description): void
+ {
+ $this->description = $description;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setEnabled(bool $enabled): void
+ {
+ if ($this->recipients->count() === 0) {
+ throw new DomainException(sprintf('MailingList « %s » cannot be enabled because it has no recipients', $this->getName()));
+ }
+ $this->enabled = $enabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isEnabled(): bool
+ {
+ return $this->enabled;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRecipients(): array
+ {
+ return $this->recipients->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addRecipient(RecipientInterface $recipient): void
+ {
+ if ($this->recipients->contains($recipient)) {
+ throw new InvalidArgumentException(sprintf('Recipient « %s » already belong to list « %s »', $recipient->getEmail(), $this->getName()));
+ }
+ $this->recipients->add($recipient);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeRecipient(RecipientInterface $recipient): void
+ {
+ if (!$this->recipients->contains($recipient)) {
+ throw new InvalidArgumentException(sprintf('Recipient « %s » does not belongs to list « %s »', $recipient->getEmail(), $this->getName()));
+ }
+ $this->recipients->removeElement($recipient);
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/MailingListInterface.php b/src/Sil/Component/Emailing/Model/MailingListInterface.php
new file mode 100644
index 00000000..40ce42f8
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/MailingListInterface.php
@@ -0,0 +1,80 @@
+setValue($value);
+ $this->stateMachine = new MessageStateMachine($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ /**
+ * @internal
+ *
+ * @param string $value
+ */
+ public function setValue(string $value): void
+ {
+ if (!in_array($value, static::getStates())) {
+ throw new InvalidArgumentException(sprintf('The state %s is not a valid state, valids are : %s', $value, implode(', ', static::getStates())));
+ }
+ $this->value = $value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getStates(): array
+ {
+ return [
+ 'DRAFT' => static::DRAFT,
+ 'VALIDATED' => static::VALIDATED,
+ 'SENT' => static::SENT,
+ 'DELETED' => static::DELETED,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function draft(): MessageStateInterface
+ {
+ return new self(static::DRAFT);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function validated(): MessageStateInterface
+ {
+ return new self(static::VALIDATED);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function sent(): MessageStateInterface
+ {
+ return new self(static::SENT);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function deleted(): MessageStateInterface
+ {
+ return new self(static::DELETED);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDraft(): bool
+ {
+ return $this->value === static::DRAFT;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValidated(): bool
+ {
+ return $this->value === static::VALIDATED;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSent(): bool
+ {
+ return $this->value === static::SENT;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDeleted(): bool
+ {
+ return $this->value === static::DELETED;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toDelete(): MessageStateInterface
+ {
+ $this->stateMachine->apply('delete');
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toValidate(): MessageStateInterface
+ {
+ $this->stateMachine->apply('validate');
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toSent(): MessageStateInterface
+ {
+ $this->stateMachine->apply('sent');
+
+ return $this;
+ }
+
+ public function __toString(): string
+ {
+ return $this->value;
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/MessageStateAwareInterface.php b/src/Sil/Component/Emailing/Model/MessageStateAwareInterface.php
new file mode 100644
index 00000000..e22cb867
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/MessageStateAwareInterface.php
@@ -0,0 +1,66 @@
+state;
+ }
+
+ /**
+ * @internal
+ *
+ * @param MessageStateInterface $state
+ */
+ public function setState(MessageStateInterface $state): void
+ {
+ $this->state = $state;
+ }
+
+ /**
+ * Sets current state to validated.
+ */
+ public function beValidated(): void
+ {
+ $this->getState()->toValidate();
+ }
+
+ /**
+ * Sets current state to sent.
+ */
+ public function beSent(): void
+ {
+ $this->getState()->toSent();
+ }
+
+ /**
+ * Sets current state to deleted.
+ */
+ public function beDeleted(): void
+ {
+ $this->getState()->toDelete();
+ }
+
+ /**
+ * Checks if current state is draft.
+ *
+ * @return bool
+ */
+ public function isDraft(): bool
+ {
+ return $this->getState()->isDraft();
+ }
+
+ /**
+ * Checks if current state is validated.
+ *
+ * @return bool
+ */
+ public function isValidated(): bool
+ {
+ return $this->getState()->isValidated();
+ }
+
+ /**
+ * Checks if current state is sent.
+ *
+ * @return bool
+ */
+ public function isSent(): bool
+ {
+ return $this->getState()->isSent();
+ }
+
+ /**
+ * Checks if current state is deleted.
+ *
+ * @return bool
+ */
+ public function isDeleted(): bool
+ {
+ return $this->getState()->isDeleted();
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/MessageStateInterface.php b/src/Sil/Component/Emailing/Model/MessageStateInterface.php
new file mode 100644
index 00000000..6891ddc6
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/MessageStateInterface.php
@@ -0,0 +1,107 @@
+ static::STATE,
+ * [...]
+ * ].
+ *
+ * @return array
+ */
+ public static function getStates(): array;
+
+ /**
+ * Gets current state value.
+ *
+ * @return string
+ */
+ public function getValue(): string;
+
+ /**
+ * Draft state constructor.
+ *
+ * @return MessageStateInterface
+ */
+ public static function draft(): self;
+
+ /**
+ * Validated state constructor.
+ *
+ * @return MessageStateInterface
+ */
+ public static function validated(): self;
+
+ /**
+ * Sent state constructor.
+ *
+ * @return MessageStateInterface
+ */
+ public static function sent(): self;
+
+ /**
+ * Deleted state constructor.
+ *
+ * @return MessageStateInterface
+ */
+ public static function deleted(): self;
+
+ /**
+ * Current state is Draft.
+ *
+ * @return bool
+ */
+ public function isDraft(): bool;
+
+ /**
+ * Current state is Validated.
+ *
+ * @return bool
+ */
+ public function isValidated(): bool;
+
+ /**
+ * Current state is Sent.
+ *
+ * @return bool
+ */
+ public function isSent(): bool;
+
+ /**
+ * Current state is Deleted.
+ *
+ * @return bool
+ */
+ public function isDeleted(): bool;
+
+ /**
+ * Apply transition delete.
+ */
+ public function toDelete(): self;
+
+ /**
+ * Apply transition validate.
+ */
+ public function toValidate(): self;
+
+ /**
+ * Apply transition cancel.
+ */
+ public function toSent(): self;
+}
diff --git a/src/Sil/Component/Emailing/Model/MessageTemplate.php b/src/Sil/Component/Emailing/Model/MessageTemplate.php
new file mode 100644
index 00000000..33311f48
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/MessageTemplate.php
@@ -0,0 +1,138 @@
+name = $name;
+ $this->content = $content;
+
+ $this->tokenTypes = new ArrayCollection();
+ $this->messages = new ArrayCollection();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTokenTypes(): array
+ {
+ return $this->tokenTypes->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTokenType(ContentTokenTypeInterface $token): void
+ {
+ if ($this->tokenTypes->contains($token)) {
+ throw new InvalidArgumentException(sprintf('TokenType « %s » is already associated to template « %s »', $token->getName(), $this->getName()));
+ }
+ $this->tokenTypes->add($token);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeTokenType(ContentTokenTypeInterface $token): void
+ {
+ if (!$this->tokenTypes->contains($token)) {
+ throw new InvalidArgumentException(sprintf('TokenType « %s » is not associated to template « %s »', $token->getName(), $this->getName()));
+ }
+ $this->tokenTypes->removeElement($token);
+ }
+
+ /**
+ * @return array|MessageInterface[]
+ */
+ public function getMessages(): array
+ {
+ return $this->messages->getValues();
+ }
+
+ /**
+ * @param MessageInterface $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public function addMessage(MessageInterface $message): void
+ {
+ if ($this->messages->contains($message)) {
+ throw new InvalidArgumentException(sprintf('Message « %s » is already using template « %s »', $message->getTitle(), $this->getName()));
+ }
+ $this->messages->add($message);
+ }
+
+ /**
+ * @param MessageInterface $message
+ *
+ * @throws InvalidArgumentException
+ */
+ public function removeMessage(MessageInterface $message): void
+ {
+ if (!$this->messages->contains($message)) {
+ throw new InvalidArgumentException(sprintf('Message « %s » is not using template « %s »', $message->getTitle(), $this->getName()));
+ }
+ $this->messages->removeElement($message);
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/MessageTemplateInterface.php b/src/Sil/Component/Emailing/Model/MessageTemplateInterface.php
new file mode 100644
index 00000000..2c79abfb
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/MessageTemplateInterface.php
@@ -0,0 +1,57 @@
+email = $email;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createFromEmailAsString(string $email): RecipientInterface
+ {
+ $email = new EmailAddress($email);
+
+ return new self($email);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValid(): bool
+ {
+ return $this->valid;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValid(bool $valid): void
+ {
+ $this->valid = $valid;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEmail(): EmailAddressInterface
+ {
+ return $this->email;
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/RecipientInterface.php b/src/Sil/Component/Emailing/Model/RecipientInterface.php
new file mode 100644
index 00000000..c885a2c7
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/RecipientInterface.php
@@ -0,0 +1,46 @@
+to = new ArrayCollection();
+ $this->cc = new ArrayCollection();
+ $this->bcc = new ArrayCollection();
+
+ $this->from = $from;
+ $this->addTo($to);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFrom(): EmailAddressInterface
+ {
+ return $this->from;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTo(): array
+ {
+ return $this->to->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTo(EmailAddressInterface $to): void
+ {
+ if ($this->to->contains($to)) {
+ throw new InvalidArgumentException(sprintf('To « %s » is already set for message « %s »', $to, $this->getTitle()));
+ }
+ $this->to->add($to);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeTo(EmailAddressInterface $to): void
+ {
+ if (!$this->to->contains($to)) {
+ throw new InvalidArgumentException(sprintf('To « %s » is not set for message « %s »', $to, $this->getTitle()));
+ }
+ if ($this->to->count() === 1) {
+ throw new DomainException(
+ sprintf(
+ 'Message « %s » must have at least one recipient in To field.
+ Removing « %s » will result in an empty to field, wich is not a valid state for a message',
+ $this->getTitle(),
+ $to
+ )
+ );
+ }
+ $this->to->removeElement($to);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCc(): array
+ {
+ return $this->cc->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addCc(EmailAddressInterface $cc): void
+ {
+ if ($this->cc->contains($cc)) {
+ throw new InvalidArgumentException(sprintf('Cc address « %s » is already set for message « %s »', $cc, $this->getTitle()));
+ }
+ $this->cc->add($cc);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeCc(EmailAddressInterface $cc): void
+ {
+ if (!$this->cc->contains($cc)) {
+ throw new InvalidArgumentException(sprintf('Cc address « %s » does not exists for message « %s »', $cc, $this->getTitle()));
+ }
+ $this->cc->removeElement($cc);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBcc(): array
+ {
+ return $this->bcc->getValues();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addBcc(EmailAddressInterface $bcc): void
+ {
+ if ($this->bcc->contains($bcc)) {
+ throw new InvalidArgumentException(sprintf('Bcc address « %s » is already set for message « %s »', $bcc, $this->getTitle()));
+ }
+ $this->bcc->add($bcc);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeBcc(EmailAddressInterface $bcc): void
+ {
+ if (!$this->bcc->contains($bcc)) {
+ throw new InvalidArgumentException(sprintf('Bcc address « %s » does not exists for message « %s »', $bcc, $this->getTitle()));
+ }
+ $this->bcc->removeElement($bcc);
+ }
+}
diff --git a/src/Sil/Component/Emailing/Model/SimpleMessageInterface.php b/src/Sil/Component/Emailing/Model/SimpleMessageInterface.php
new file mode 100644
index 00000000..7641ab3d
--- /dev/null
+++ b/src/Sil/Component/Emailing/Model/SimpleMessageInterface.php
@@ -0,0 +1,98 @@
+clearTokens();
+ $message->setTemplate($template);
+ $message->setContent($template->getContent());
+
+ foreach ($template->getTokenTypes() as $tokenType) {
+ $token = new ContentToken($message, $tokenType);
+ $message->addToken($token);
+ }
+ }
+
+ /**
+ * Replaces all token placeholder with values (if they are set) in message content.
+ *
+ * @param MessageInterface $message
+ */
+ public function replaceTokensOfMessage(MessageInterface $message): void
+ {
+ foreach ($message->getTokens() as $token) {
+ if ($token->getValue() === null) {
+ continue;
+ }
+
+ $rawContent = $message->getContent();
+
+ $regexExpression = sprintf('/%s/', preg_quote('%%' . $token->getName() . '%%'));
+
+ $rawContent = preg_replace($regexExpression, $token->getValueAsString(), $rawContent);
+
+ $message->setContent($rawContent);
+ }
+ }
+}
diff --git a/src/Sil/Component/Emailing/StateMachine/MessageStateMachine.php b/src/Sil/Component/Emailing/StateMachine/MessageStateMachine.php
new file mode 100644
index 00000000..96084eb4
--- /dev/null
+++ b/src/Sil/Component/Emailing/StateMachine/MessageStateMachine.php
@@ -0,0 +1,49 @@
+ 'message_state',
+ 'property_path' => 'value',
+ 'states' => [
+ MessageState::DRAFT,
+ MessageState::VALIDATED,
+ MessageState::SENT,
+ MessageState::DELETED,
+ ],
+ 'transitions' => [
+ 'delete' => [
+ 'from' => [MessageState::DRAFT, MessageState::VALIDATED],
+ 'to' => MessageState::DELETED,
+ ],
+ 'validate' => [
+ 'from' => [MessageState::DRAFT],
+ 'to' => MessageState::VALIDATED,
+ ],
+ 'sent' => [
+ 'from' => [MessageState::VALIDATED, MessageState::SENT],
+ 'to' => MessageState::SENT,
+ ],
+ ],
+ ];
+
+ public function __construct($object)
+ {
+ parent::__construct($object, $this->config);
+ }
+}
diff --git a/src/Sil/Component/Emailing/Tests/.gitkeep b/src/Sil/Component/Emailing/Tests/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/Sil/Component/Emailing/Tests/Unit.suite.yml b/src/Sil/Component/Emailing/Tests/Unit.suite.yml
new file mode 100644
index 00000000..026c91db
--- /dev/null
+++ b/src/Sil/Component/Emailing/Tests/Unit.suite.yml
@@ -0,0 +1,9 @@
+# Codeception Test Suite Configuration
+#
+# Suite for unit or integration tests.
+
+actor: UnitTester
+modules:
+ enabled:
+ - Asserts
+ - \Helper\Unit
\ No newline at end of file
diff --git a/src/Sil/Component/Emailing/Tests/Unit/.gitkeep b/src/Sil/Component/Emailing/Tests/Unit/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/Sil/Component/Emailing/Tests/Unit/EmailAddressTest.php b/src/Sil/Component/Emailing/Tests/Unit/EmailAddressTest.php
new file mode 100644
index 00000000..c3a5188e
--- /dev/null
+++ b/src/Sil/Component/Emailing/Tests/Unit/EmailAddressTest.php
@@ -0,0 +1,41 @@
+fixtures = new Fixtures();
+ }
+
+ public function test_valid_email_address_creation()
+ {
+ $emailAddress = new EmailAddress('valid@sil.eu');
+
+ $this->assertTrue($emailAddress->isValid());
+ }
+
+ public function test_invalid_email_address_creation()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $emailAddress = new EmailAddress('not.a.valid.address');
+ }
+}
diff --git a/src/Sil/Component/Emailing/Tests/Unit/Fixtures/Fixtures.php b/src/Sil/Component/Emailing/Tests/Unit/Fixtures/Fixtures.php
new file mode 100644
index 00000000..6095483f
--- /dev/null
+++ b/src/Sil/Component/Emailing/Tests/Unit/Fixtures/Fixtures.php
@@ -0,0 +1,366 @@
+rawData = [
+ 'Templates' => [
+ 'basic' => [
+ 'name' => 'basic',
+ 'content' => 'A template without token placeholder',
+ 'tokens' => [],
+ ],
+ 'rich' => [
+ 'name' => 'rich',
+ 'content' => 'A template with token placeholders :
+ - %%A_BOOLEAN_TOKEN%%
+ - %%A_STRING_TOKEN%%
+ - %%A_INTEGER_TOKEN%%
+ - %%A_FLOAT_TOKEN%%
+ - %%A_DATE_TOKEN%%
+ - %%A_DATETIME_TOKEN%%
+ ',
+ 'tokens' => [
+ [
+ 'name' => 'A_BOOLEAN_TOKEN',
+ 'type' => 'BOOLEAN',
+ ],
+ [
+ 'name' => 'A_STRING_TOKEN',
+ 'type' => 'STRING',
+ ],
+ [
+ 'name' => 'A_INTEGER_TOKEN',
+ 'type' => 'INTEGER',
+ ],
+ [
+ 'name' => 'A_FLOAT_TOKEN',
+ 'type' => 'FLOAT',
+ ],
+ [
+ 'name' => 'A_DATE_TOKEN',
+ 'type' => 'DATE',
+ ],
+ [
+ 'name' => 'A_DATETIME_TOKEN',
+ 'type' => 'DATETIME',
+ ],
+ ],
+ ],
+ ],
+ 'Mailing lists' => [
+ 'ListOne' => [
+ 'name' => 'ListOne',
+ 'description' => null,
+ 'recipients' => [
+ 'recipient1@sil.eu',
+ 'recipient2@sil.eu',
+ 'recipient3@sil.eu',
+ 'recipient4@sil.eu',
+ 'recipient5@sil.eu',
+ ],
+ ],
+ 'ListTwo' => [
+ 'name' => 'ListTwo',
+ 'description' => 'A small description of list',
+ 'recipients' => [
+ 'recipient6@sil.eu',
+ 'recipient7@sil.eu',
+ 'recipient8@sil.eu',
+ 'recipient9@sil.eu',
+ 'recipient10@sil.eu',
+ ],
+ ],
+ ],
+ 'Simple messages' => [
+ [
+ 'title' => 'Simple message One',
+ 'content' => 'A rich content without lorem.',
+ 'template' => 'rich',
+ 'from' => 'sender1@sil.eu',
+ 'to' => ['recipient1@sil.eu', 'recipient2@sil.eu'],
+ 'cc' => ['cc1@sil.eu', 'cc2@sil.eu', 'cc3@sil.eu', 'cc4@sil.eu'],
+ 'bcc' => ['bcc1@sil.eu'],
+ ],
+ [
+ 'title' => 'Simple message Two',
+ 'content' => 'A text content without lorem.',
+ 'template' => null,
+ 'from' => 'sender2@sil.eu',
+ 'to' => ['recipient3@sil.eu', 'recipient4@sil.eu'],
+ 'cc' => [],
+ 'bcc' => [],
+ ],
+ ],
+ 'Grouped messages' => [
+ [
+ 'title' => 'Grouped message One',
+ 'content' => 'A rich content without lorem.',
+ 'template' => 'basic',
+ 'lists' => [
+ 'ListOne',
+ ],
+ ],
+ [
+ 'title' => 'Grouped message Two',
+ 'content' => 'A text content without lorem.',
+ 'template' => null,
+ 'lists' => [
+ 'ListOne',
+ 'ListTwo',
+ ],
+ ],
+ ],
+ ];
+
+ $this->simpleMessageRepository = new InMemoryRepository(SimpleMessageInterface::class);
+ $this->groupedMessageRepository = new InMemoryRepository(GroupedMessageInterface::class);
+ $this->mailingListRepository = new InMemoryRepository(MailingListInterface::class);
+ $this->recipientRepository = new InMemoryRepository(RecipientInterface::class);
+ $this->emailAddressRepository = new InMemoryRepository(EmailAddressInterface::class);
+ $this->messageTemplateRepository = new InMemoryRepository(MessageTemplateInterface::class);
+ $this->contentTokenTypeRepository = new InMemoryRepository(ContentTokenTypeInterface::class);
+ $this->contentTokenRepository = new InMemoryRepository(ContentTokenInterface::class);
+
+ $this->generateFixtures();
+ }
+
+ private function generateFixtures(): void
+ {
+ $this->loadTemplates();
+ $this->loadMailingLists();
+ $this->loadSimpleMessages();
+ $this->loadGroupedMessages();
+ }
+
+ private function loadtemplates()
+ {
+ foreach ($this->rawData['Templates'] as $templateData) {
+ $template = new MessageTemplate($templateData['name'], $templateData['content']);
+
+ foreach ($templateData['tokens'] as $tokenData) {
+ $tokenType = new ContentTokenType($template, $tokenData['name'], new ContentTokenDataType(constant(ContentTokenDataType::class . '::TYPE_' . $tokenData['type'])));
+ $template->addTokenType($tokenType);
+ $this->contentTokenTypeRepository->add($tokenType);
+ }
+
+ $this->messageTemplateRepository->add($template);
+ }
+ }
+
+ private function loadMailingLists()
+ {
+ foreach ($this->rawData['Mailing lists'] as $mailingListData) {
+ $mailingList = new MailingList($mailingListData['name']);
+
+ foreach ($mailingListData['recipients'] as $recipientEmail) {
+ $recipient = Recipient::createFromEmailAsString($recipientEmail);
+ $this->recipientRepository->add($recipient);
+ $mailingList->addRecipient($recipient);
+ }
+
+ $this->mailingListRepository->add($mailingList);
+ }
+ }
+
+ private function loadSimpleMessages()
+ {
+ foreach ($this->rawData['Simple messages'] as $messageData) {
+ $from = new EmailAddress($messageData['from']);
+ $to = new EmailAddress($messageData['to'][0]);
+
+ $this->emailAddressRepository->add($from);
+ $this->emailAddressRepository->add($to);
+
+ $simpleMessage = new SimpleMessage($messageData['title'], $messageData['content'], $from, $to);
+
+ if ($messageData['template'] !== null) {
+ $template = $this->messageTemplateRepository->findOneBy(['name' => $messageData['template']]);
+
+ $templateHandler = new TemplateHandler();
+ $templateHandler->applyTemplateToMessage($template, $simpleMessage);
+ }
+
+ foreach ($messageData['to'] as $tos) {
+ $toAddress = new EmailAddress($tos);
+ if (!in_array($toAddress, $simpleMessage->getTo())) {
+ $simpleMessage->addTo($toAddress);
+ $this->emailAddressRepository->add($toAddress);
+ }
+ }
+
+ foreach ($messageData['cc'] as $ccs) {
+ $ccAddress = new EmailAddress($ccs);
+ if (!in_array($ccAddress, $simpleMessage->getCc())) {
+ $simpleMessage->addCc($ccAddress);
+ $this->emailAddressRepository->add($ccAddress);
+ }
+ }
+
+ foreach ($messageData['bcc'] as $bccs) {
+ $bccAddress = new EmailAddress($bccs);
+ if (!in_array($bccAddress, $simpleMessage->getBcc())) {
+ $simpleMessage->addBcc($bccAddress);
+ $this->emailAddressRepository->add($bccAddress);
+ }
+ }
+
+ $this->simpleMessageRepository->add($simpleMessage);
+ }
+ }
+
+ private function loadGroupedMessages()
+ {
+ foreach ($this->rawData['Grouped messages'] as $messageData) {
+ $groupedMessage = new GroupedMessage($messageData['title'], $messageData['content']);
+
+ if ($messageData['template'] !== null) {
+ $template = $this->messageTemplateRepository->findOneBy(['name' => $messageData['template']]);
+
+ $templateHandler = new TemplateHandler();
+ $templateHandler->applyTemplateToMessage($template, $groupedMessage);
+ }
+
+ foreach ($messageData['lists'] as $listName) {
+ $list = $this->mailingListRepository->findOneBy(['name' => $listName]);
+ $groupedMessage->addList($list);
+ }
+
+ $this->groupedMessageRepository->add($groupedMessage);
+ }
+ }
+
+ /**
+ * @return InMemoryRepository
+ */
+ public function getSimpleMessageRepository(): InMemoryRepository
+ {
+ return $this->simpleMessageRepository;
+ }
+
+ /**
+ * @return InMemoryRepository
+ */
+ public function getGroupedMessageRepository(): InMemoryRepository
+ {
+ return $this->groupedMessageRepository;
+ }
+
+ /**
+ * @return InMemoryRepository
+ */
+ public function getMailingListRepository(): InMemoryRepository
+ {
+ return $this->mailingListRepository;
+ }
+
+ /**
+ * @return InMemoryRepository
+ */
+ public function getRecipientRepository(): InMemoryRepository
+ {
+ return $this->recipientRepository;
+ }
+
+ /**
+ * @return InMemoryRepository
+ */
+ public function getMessageTemplateRepository(): InMemoryRepository
+ {
+ return $this->messageTemplateRepository;
+ }
+
+ /**
+ * @return InMemoryRepository
+ */
+ public function getContentTokenTypeRepository(): InMemoryRepository
+ {
+ return $this->contentTokenTypeRepository;
+ }
+
+ /**
+ * @return InMemoryRepository
+ */
+ public function getContentTokenRepository(): InMemoryRepository
+ {
+ return $this->contentTokenRepository;
+ }
+
+ /**
+ * @return array
+ */
+ public function getRawData(): array
+ {
+ return $this->rawData;
+ }
+}
diff --git a/src/Sil/Component/Emailing/Tests/Unit/MailingListTest.php b/src/Sil/Component/Emailing/Tests/Unit/MailingListTest.php
new file mode 100644
index 00000000..b92de679
--- /dev/null
+++ b/src/Sil/Component/Emailing/Tests/Unit/MailingListTest.php
@@ -0,0 +1,55 @@
+fixtures = new Fixtures();
+ }
+
+ public function test_mailing_list_creation()
+ {
+ $mailingList = new MailingList('Test list', 'a test mailing list');
+
+ $this->assertEquals('Test list', $mailingList->getName());
+ $this->assertEquals('a test mailing list', $mailingList->getDescription());
+ $this->assertFalse($mailingList->isEnabled());
+ }
+
+ public function test_mailing_list_enabling_without_recipients()
+ {
+ $mailingList = new MailingList('Test list', 'a test mailing list');
+
+ $this->expectException(\DomainException::class);
+
+ $mailingList->setEnabled(true);
+ }
+
+ public function test_mailing_list_enabling_with_recipients()
+ {
+ $mailingList = new MailingList('Test list', 'a test mailing list');
+
+ $mailingList->addRecipient(Recipient::createFromEmailAsString('recipient@sil.eu'));
+
+ $mailingList->setEnabled(true);
+ }
+}
diff --git a/src/Sil/Component/Emailing/Tests/Unit/MessageTemplateTest.php b/src/Sil/Component/Emailing/Tests/Unit/MessageTemplateTest.php
new file mode 100644
index 00000000..f3d216f2
--- /dev/null
+++ b/src/Sil/Component/Emailing/Tests/Unit/MessageTemplateTest.php
@@ -0,0 +1,99 @@
+fixtures = new Fixtures();
+ }
+
+ public function test_applying_template_with_template_handler_to_a_new_message()
+ {
+ $from = new EmailAddress('from@sil.eu');
+ $to = new EmailAddress('to@sil.eu');
+
+ $templateData = $this->fixtures->getRawData()['Templates']['rich'];
+ $template = $this->fixtures->getMessageTemplateRepository()->findOneBy(['name' => $templateData['name']]);
+
+ $message = new SimpleMessage('Simple message', 'a simple message test', $from, $to);
+
+ $templateHandler = new TemplateHandler();
+ $templateHandler->applyTemplateToMessage($template, $message);
+
+ $this->assertEquals(count($template->getTokenTypes()), count($message->getTokens()));
+
+ foreach ($message->getTokens() as $token) {
+ $this->assertContains($token->getTokenType(), $template->getTokenTypes());
+ }
+ }
+
+ public function test_clearing_message_tokens()
+ {
+ $messageData = $this->fixtures->getRawData()['Simple messages'][0];
+ $message = $this->fixtures->getSimpleMessageRepository()->findOneBy(['title' => $messageData['title']]);
+
+ $this->assertEquals(6, count($message->getTokens()));
+
+ $message->clearTokens();
+
+ $this->assertEquals(0, count($message->getTokens()));
+ }
+
+ public function test_replacing_tokens_on_message()
+ {
+ $messageData = $this->fixtures->getRawData()['Simple messages'][0];
+ $templateData = $this->fixtures->getRawData()['Templates'][$messageData['template']];
+ $message = $this->fixtures->getSimpleMessageRepository()->findOneBy(['title' => $messageData['title']]);
+
+ $tokenValues = [
+ ContentTokenDataType::TYPE_BOOLEAN => true,
+ ContentTokenDataType::TYPE_DATE => new DateTime('2020-05-01'),
+ ContentTokenDataType::TYPE_DATETIME => new DateTime('2020-05-01 12:12:42'),
+ ContentTokenDataType::TYPE_STRING => 'a replaced string',
+ ContentTokenDataType::TYPE_INTEGER => 42,
+ ContentTokenDataType::TYPE_FLOAT => 512.42,
+ ];
+
+ foreach ($message->getTokens() as $token) {
+ $token->setValue($tokenValues[$token->getTokenType()->getDataType()->getValue()]);
+ }
+
+ $this->assertEquals(trim($templateData['content']), $message->getContent());
+
+ $templateHandler = new TemplateHandler();
+ $templateHandler->replaceTokensOfMessage($message);
+
+ $expectedContent = trim('A template with token placeholders :
+ - Yes
+ - a replaced string
+ - 42
+ - 512.42
+ - 2020-05-01
+ - 2020-05-01 12:12:42
+ ');
+
+ $this->assertEquals($expectedContent, $message->getContent());
+ }
+}
diff --git a/src/Sil/Component/Emailing/Tests/Unit/MessageTest.php b/src/Sil/Component/Emailing/Tests/Unit/MessageTest.php
new file mode 100644
index 00000000..ae4cef63
--- /dev/null
+++ b/src/Sil/Component/Emailing/Tests/Unit/MessageTest.php
@@ -0,0 +1,49 @@
+fixtures = new Fixtures();
+ }
+
+ public function test_simple_email_creation()
+ {
+ $from = new EmailAddress('from@sil.eu');
+ $to = new EmailAddress('to@sil.eu');
+ $message = new SimpleMessage('Simple message', 'a simple message test', $from, $to);
+
+ $this->assertEquals('Simple message', $message->getTitle());
+ $this->assertEquals('a simple message test', $message->getContent());
+ $this->assertEquals($from->getValue(), $message->getFrom()->getValue());
+ $this->assertContains($to, $message->getTo());
+ }
+
+ public function test_grouped_message_creation()
+ {
+ $message = new GroupedMessage('Grouped message', 'a grouped message test');
+
+ $this->assertEquals('Grouped message', $message->getTitle());
+ $this->assertEquals('a grouped message test', $message->getContent());
+ }
+}
diff --git a/src/Sil/Component/Emailing/Tests/Unit/RecipientTest.php b/src/Sil/Component/Emailing/Tests/Unit/RecipientTest.php
new file mode 100644
index 00000000..8f1f3e73
--- /dev/null
+++ b/src/Sil/Component/Emailing/Tests/Unit/RecipientTest.php
@@ -0,0 +1,51 @@
+fixtures = new Fixtures();
+ }
+
+ public function test_recipient_creation()
+ {
+ $emailAddress = new EmailAddress('recipient@sil.eu');
+
+ $recipient = new Recipient($emailAddress);
+
+ $this->assertEquals($emailAddress, $recipient->getEmail());
+ }
+
+ public function test_recipient_creation_from_valid_string()
+ {
+ $recipient = Recipient::createFromEmailAsString('valid.recipient@sil.eu');
+
+ $this->assertEquals('valid.recipient@sil.eu', $recipient->getEmail()->getValue());
+ }
+
+ public function test_recipient_creation_from_invalid_string()
+ {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $recipient = Recipient::createFromEmailAsString('invalid.recipient');
+ }
+}
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/after_success_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/after_success_test.sh
new file mode 100644
index 00000000..87158518
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/after_success_test.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+set -ev
+
+coveralls -v
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/before_install_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_test.sh
new file mode 100644
index 00000000..e4fc977b
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_test.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env sh
+set -ev
+
+composer self-update --stable
+
+mkdir -p ${HOME}/bin
+
+# Coveralls client install
+wget https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar --output-document="${HOME}/bin/coveralls"
+chmod u+x "${HOME}/bin/coveralls"
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/before_install_travis_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_travis_test.sh
new file mode 100644
index 00000000..5e838f77
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/before_install_travis_test.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -ev
+
+mkdir --parents "${HOME}/bin"
+
+if [ "${WHORUN}" = travis ]
+then
+ # Ugly hack
+ echo "memory_limit=-1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
+
+fi
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/create_database_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/create_database_test.sh
new file mode 100644
index 00000000..4415f6af
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/create_database_test.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+set -v
+
+# TODO share this between script (in an include)
+if [ -f .env ]
+then
+ source .env
+else
+ echo "Please run this script from project root, and check .env file as it is mandatory"
+ echo "If it is missing a quick solution is :"
+ echo "ln -s .env.travis .env"
+ exit 42
+fi
+
+if [ -z "${DBHOST}" ]
+then
+ echo "Please add DBHOST in .env file as it is mandatory"
+ exit 42
+fi
+
+
+# Database creation
+
+###
+### mysql
+###
+
+# (mysql service is started by default by travis for each build instance
+# (mysql travis user is created by travis for each build instance
+# mysql -u travis -e 'CREATE DATABASE travis;' -v
+
+###
+### postgresql
+###
+
+#psql auto password
+#echo localhost:5432:*:postgres:postgres24 >> ~/.pgpass
+
+# needed in .travis.yml
+#services:
+# - postgresql
+# or here : sudo /etc/init.d/postgresql start
+
+
+psql -w -h ${DBHOST} -c "DROP DATABASE IF EXISTS ${DBAPPNAME};" -U ${DBROOTUSER}
+psql -w -h ${DBHOST} -c "DROP ROLE IF EXISTS ${DBAPPUSER};" -U ${DBROOTUSER}
+
+
+# (we try to create a travis user)
+psql -w -h ${DBHOST} -c "CREATE USER ${DBAPPUSER} WITH PASSWORD '${DBAPPPASSWORD}';" -U ${DBROOTUSER}
+psql -w -h ${DBHOST} -c "ALTER ROLE ${DBAPPUSER} WITH CREATEDB;" -U ${DBROOTUSER}
+
+psql -w -h ${DBHOST} -c "CREATE DATABASE ${DBAPPNAME};" -U ${DBROOTUSER}
+psql -w -h ${DBHOST} -c "ALTER DATABASE ${DBAPPNAME} OWNER TO ${DBAPPUSER};" -U ${DBROOTUSER}
+
+
+psql -w -h ${DBHOST} -c 'CREATE EXTENSION "uuid-ossp";' -U ${DBROOTUSER} -d ${DBAPPNAME}
+
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/do_run.sh b/src/Sil/Component/Emailing/bin/ci-scripts/do_run.sh
new file mode 100644
index 00000000..0411937d
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/do_run.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+set -e
+
+for name in $@
+do
+ script_name=$(dirname $0)/$name
+ if [ -x $script_name ]
+ then
+ $script_name
+ fi
+done
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/install_docs.sh b/src/Sil/Component/Emailing/bin/ci-scripts/install_docs.sh
new file mode 100644
index 00000000..362f6449
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/install_docs.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+set -ev
+
+pip install -r Resources/doc/requirements.txt --user
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/install_lint.sh b/src/Sil/Component/Emailing/bin/ci-scripts/install_lint.sh
new file mode 100644
index 00000000..42a58dca
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/install_lint.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env sh
+set -ev
+
+composer global require sllh/composer-lint:@stable --prefer-dist --no-interaction
+
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/install_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/install_test.sh
new file mode 100644
index 00000000..483ed38e
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/install_test.sh
@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+set -ev
+
+composer install --no-interaction --prefer-dist
+
+
+
+
+
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/run_docs.sh b/src/Sil/Component/Emailing/bin/ci-scripts/run_docs.sh
new file mode 100644
index 00000000..31fae91f
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/run_docs.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env sh
+
+cd Resources/doc
+sphinx-build -b html -d _build/doctrees . _build/html
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/run_lint.sh b/src/Sil/Component/Emailing/bin/ci-scripts/run_lint.sh
new file mode 100644
index 00000000..732ea402
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/run_lint.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env sh
+
+composer validate
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/run_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/run_test.sh
new file mode 100644
index 00000000..625ff2a5
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/run_test.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -ev
+
+# TODO share this between script (in an include)
+if [ -f .env ]
+then
+ source .env
+else
+ echo "Please run this script from project root, and check .env file as it is mandatory"
+ echo "If it is missing a quick solution is :"
+ echo "ln -s .env.travis .env"
+ exit 42
+fi
+
+export SILURL
+
+
+if [ -n "$PHPUNITCMD" ]
+then
+ $PHPUNITCMD
+fi
+
+#bin/ci-scripts/do_it_for_bundle.sh run test
diff --git a/src/Sil/Component/Emailing/bin/ci-scripts/set_db_host_test.sh b/src/Sil/Component/Emailing/bin/ci-scripts/set_db_host_test.sh
new file mode 100644
index 00000000..f9d82167
--- /dev/null
+++ b/src/Sil/Component/Emailing/bin/ci-scripts/set_db_host_test.sh
@@ -0,0 +1,44 @@
+#!/usr/bin/env bash
+set -ev
+
+# TODO share this between script (in an include)
+if [ -f .env ]
+then
+ source .env
+else
+ echo "Please run this script from project root, and check .env file as it is mandatory"
+ echo "If it is missing a quick solution is :"
+ echo "ln -s .env.travis .env"
+ exit 42
+fi
+
+
+
+
+##################
+#### POSTGRES ####
+
+
+# TODO
+# should remove db info from app/config/config_test.yml
+# and use sed or etcd or confd or both to update parameters.yml.dist or create parameters.yml
+# sed -e s/'database_host: 127.0.0.1'/'database_host: ${DBHOST}'/g -i app/config/parameters.yml.dist
+if [ -n "${DBHOST}" ]
+then
+
+ if [ -f Tests/Resources/App/config/parameters.yml ]
+ then
+ sed -e s/'127.0.0.1'/${DBHOST}/g -i Tests/Resources/App/config/parameters.yml
+ sed -e s/'blast_test_user'/${DBAPPUSER}/g -i Tests/Resources/App/config/parameters.yml
+ sed -e s/'blast_test_password'/${DBAPPPASSWORD}/g -i Tests/Resources/App/config/parameters.yml
+ sed -e s/'blast_test_db'/${DBAPPNAME}/g -i Tests/Resources/App/config/parameters.yml
+ cat Tests/Resources/App/config/parameters.yml
+ fi
+
+ #TODO
+ # should use env var from etcd (for password)
+ echo ${DBHOST}:5432:*:${DBROOTUSER}:${DBROOTPASSWORD} >> $HOME/.pgpass
+ echo ${DBHOST}:5432:*:${DBROOTUSER}:${DBROOTPASSWORD} >> $HOME/.pgpass
+ chmod 600 $HOME/.pgpass
+ cat $HOME/.pgpass
+fi
diff --git a/src/Sil/Component/Emailing/composer.json b/src/Sil/Component/Emailing/composer.json
new file mode 100644
index 00000000..eb711009
--- /dev/null
+++ b/src/Sil/Component/Emailing/composer.json
@@ -0,0 +1,40 @@
+{
+ "name": "sil-project/emailing",
+ "type": "library",
+ "description": "Emailing Management",
+ "require": {
+ "php": "^7.1",
+ "doctrine/collections": "^1.3",
+ "blast-project/resource": "self.version",
+ "winzou/state-machine": "^0.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.4"
+ },
+ "license": "LGPL-3.0",
+ "keywords": [
+ "emailing",
+ "emailing management"
+ ],
+ "homepage": "https://github.com/sil-project/Emailing",
+ "authors": [
+ {
+ "name": "Vivien FRASCA",
+ "email": "vivien.frasca@libre-informatique.fr"
+ },
+ {
+ "name": "Libre Informatique",
+ "homepage": "http://www.libre-informatique.fr/"
+ }
+ ],
+
+ "autoload": {
+ "psr-4": {
+ "Sil\\Component\\Emailing\\": "."
+ }
+ },
+ "config": {
+ "bin-dir": "bin/"
+ },
+ "version": "dev-wip-platform"
+}
diff --git a/src/Sil/Component/Emailing/etc/dockerfile.jenkins b/src/Sil/Component/Emailing/etc/dockerfile.jenkins
new file mode 100644
index 00000000..5acb9820
--- /dev/null
+++ b/src/Sil/Component/Emailing/etc/dockerfile.jenkins
@@ -0,0 +1,73 @@
+FROM debian:latest
+# https://issues.jenkins-ci.org/browse/JENKINS-44609
+# https://issues.jenkins-ci.org/browse/JENKINS-31507
+#as phpcli_for_platform
+
+RUN apt-get update
+
+RUN apt-get install -y apt-transport-https lsb-release ca-certificates git curl wget build-essential g++ unzip net-tools lsof dnsutils vim python-pip
+
+RUN echo "deb http://ftp.debian.org/debian $(lsb_release -sc)-backports main" >> /etc/apt/sources.list && apt-get update
+RUN wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg && echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list && apt-get update
+
+
+#RUN apt-get install -y libpq-dev libcurl4-openssl-dev libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng12-dev zlib1g-dev libicu-dev unixodbc-dev libxml2-dev libaio-dev libmemcached-dev freetds-dev
+
+RUN apt-get install -y nodejs python python3
+RUN apt-get install -y openjdk-8-jdk openjdk-8-jre && update-alternatives --config java
+RUN apt-get install -y xvfb chromium chromium-l10n firefox-esr postgresql-client
+
+
+ARG PHPVER=7.1
+RUN apt-get install -y php${PHPVER} php${PHPVER}-cli php${PHPVER}-pgsql php${PHPVER}-mysql php${PHPVER}-curl php${PHPVER}-json php${PHPVER}-gd php${PHPVER}-mcrypt php${PHPVER}-intl php${PHPVER}-sqlite3 php${PHPVER}-gmp php${PHPVER}-geoip php${PHPVER}-mbstring php${PHPVER}-redis php${PHPVER}-xml php${PHPVER}-zip php${PHPVER}-xdebug
+
+RUN echo "phar.readonly = Off" >> /etc/php/${PHPVER}/cli/conf.d/42-phar-readonly.ini
+RUN echo "memory_limit=-1" >> /etc/php/${PHPVER}/cli/conf.d/42-memory-limit.ini
+
+RUN curl -sS https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer && chmod 755 /usr/local/bin/composer
+
+ENV HOME=/home/jenkins
+ENV USER=jenkins
+ENV GROUP=users
+
+
+
+# -u $(id -u)
+# because jenkins docker pipeline force a -u option on run
+# https://github.com/jenkinsci/docker-workflow-plugin/pull/25/files
+# see whoAmI function here:
+# https://github.com/ndeloof/docker-workflow-plugin/blob/master/src/main/java/org/jenkinsci/plugins/docker/workflow/client/DockerClient.java
+# it should be fixed in next plugins version >= 1.14
+
+# jenkins user on sandboxrd has the 1004 id and it is forced by pipeline plugin on docker log on
+
+
+ARG UID=1004
+RUN useradd -d $HOME -g $GROUP -u ${UID} -m $USER
+
+
+# --no-create-home
+# if we don't want skeleton file so we create home by hand (or by RUN :) )
+# RUN mkdir -p $HOME && mkdir -p $HOME/bin && chown -R $USER:$GROUP $HOME
+
+RUN mkdir -p $HOME/bin && chown -R $USER:$GROUP $HOME
+
+USER $USER:$GROUP
+WORKDIR $HOME
+
+#should not be good, where does the current $PATH came from ?
+ENV PATH=$PATH:$HOME/bin:$HOME/.local/bin/
+
+# Add some before install step to run test faster
+RUN composer self-update --no-progress --stable && composer global config bin-dir ${HOME}/bin
+RUN pip install --upgrade pip
+
+RUN for i in "sllh/composer-lint" "pdepend/pdepend" "squizlabs/php_codesniffer" "phploc/phploc" "phpmd/phpmd" "sebastian/phpcpd" "theseer/phpdox" "phpmetrics/phpmetrics"; do composer global require $i:@stable --prefer-dist --no-interaction; done
+
+ENV NVM_DIR="$HOME/.nvm"
+RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash && [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" && nvm install 8.9 && npm config set color false
+
+#Try to cache various tools
+RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz && gunzip -f geckodriver-v0.19.1-linux64.tar.gz && tar -xvf geckodriver-v0.19.1-linux64.tar && mv geckodriver ${HOME}/bin/
+RUN wget -q https://selenium-release.storage.googleapis.com/3.7/selenium-server-standalone-3.7.0.jar && mv selenium-server-standalone-3.7.0.jar ${HOME}/bin/selenium-server-standalone.jar
+RUN wget -q https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar --output-document="${HOME}/bin/coveralls" && chmod u+x "${HOME}/bin/coveralls"
diff --git a/src/Sil/Component/Emailing/etc/pipeline.jenkins b/src/Sil/Component/Emailing/etc/pipeline.jenkins
new file mode 100644
index 00000000..6e39fe76
--- /dev/null
+++ b/src/Sil/Component/Emailing/etc/pipeline.jenkins
@@ -0,0 +1,153 @@
+pipeline {
+ agent {
+ dockerfile {
+ filename "etc/dockerfile.jenkins"
+ args '--network=ci.network --volume /home/jenkins/cache/composer:/home/jenkins/.composer/cache'
+ }
+ }
+
+ environment {
+ RND = "${BUILD_NUMBER}_${BRANCH_NAME}"
+ }
+
+ options {
+ timeout(time: 1, unit: 'HOURS')
+ timestamps()
+ disableConcurrentBuilds()
+ }
+
+ stages {
+ stage ('Where Am I') {
+ steps {
+ sh "uname -a"
+ sh "php -v"
+ sh "composer -V"
+ }
+ }
+ stage ('Set Env') {
+ steps {
+ sh "ln -fs ./.env.jenkins ./.env"
+ sh "cat ./.env"
+ sh "mkdir -p build"
+ }
+ }
+
+
+ stage ('Prepare') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh before_install_test.sh"
+ }
+ }
+
+ stage ('Create Database') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh set_db_host_test.sh # needed before create as it set .pgpass"
+ sh "./bin/ci-scripts/do_run.sh create_database_test.sh"
+ sh "./bin/ci-scripts/do_run.sh create_table_test.sh"
+
+ }
+ }
+
+ stage('Parallel Install') {
+ parallel {
+ stage ('Install Test') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh install_test.sh"
+ }
+ }
+
+ stage ('Install Doc') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh install_doc.sh"
+ }
+ }
+ }
+ }
+ stage('Parallel Start') {
+ parallel {
+ stage ('Start Project') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh before_script_test.sh"
+ }
+ }
+
+ stage ('Start Selenium') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh launch_selenium_test.sh"
+ }
+ }
+ }
+ }
+
+ stage('Parallel Report') {
+ parallel {
+ stage ('Run Test') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh run_test.sh"
+ step([
+ $class: 'XUnitBuilder',
+ thresholds: [[$class: 'FailedThreshold', unstableThreshold: '1']],
+ tools: [[$class: 'JUnitType', pattern: 'build/junit.xml']]
+ ])
+ publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'build/coverage', reportFiles: 'index.html', reportName: 'Coverage Report', reportTitles: ''])
+ }
+ }
+
+ stage ('Run Doc') {
+ steps {
+ sh "./bin/ci-scripts/do_run.sh run_doc.sh"
+ }
+ }
+
+
+ stage('Check Style') {
+ steps {
+ sh 'phpcs -q --report=checkstyle --report-file=build/checkstyle.xml --standard=PSR2 --extensions=php --ignore=vendor ./ || exit 0'
+ checkstyle pattern: 'build/checkstyle.xml'
+ }
+ }
+
+ stage('Copy Paste Detection') {
+ steps {
+ sh 'phpcpd -q --exclude=vendor --log-pmd build/pmd-cpd.xml ./ || exit 0'
+ dry canRunOnFailed: true, pattern: 'build/pmd-cpd.xml'
+ }
+ }
+
+ stage('Mess Detection') {
+ steps {
+ sh 'phpmd ./ xml phpmd.xml.dist --exclude vendor --reportfile build/pmd.xml || exit 0'
+ pmd canRunOnFailed: true, pattern: 'build/pmd.xml'
+ }
+ }
+
+ stage('Collect Metrics') {
+ steps {
+ sh "phpmetrics --quiet --excluded-dirs=vendor --report-html=build/metrics.html ./"
+ publishHTML([allowMissing: false, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'build/', reportFiles: 'metrics.html', reportName: 'Metrics Report', reportTitles: ''])
+ }
+ }
+ }
+ }
+ /*
+ stage ('Archive Gzip') {
+ steps {
+ sh 'tar -czf Platform_${BRANCH_NAME}.tgz ./*'
+ archiveArtifacts artifacts: "Platform_${BRANCH_NAME}.tgz", fingerprint: true
+ }
+ }
+ */
+
+
+ }
+
+
+
+ post {
+ always {
+ cleanWs()
+
+ }
+ }
+
+}
diff --git a/src/Sil/Component/Emailing/phpmd.xml.dist b/src/Sil/Component/Emailing/phpmd.xml.dist
new file mode 100644
index 00000000..766e122e
--- /dev/null
+++ b/src/Sil/Component/Emailing/phpmd.xml.dist
@@ -0,0 +1,15 @@
+
+ Standard Sil Coding Standard
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Sil/Component/Emailing/phpunit.xml.dist b/src/Sil/Component/Emailing/phpunit.xml.dist
new file mode 100644
index 00000000..231b362a
--- /dev/null
+++ b/src/Sil/Component/Emailing/phpunit.xml.dist
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ ./Tests/Unit
+
+
+
+
+
+
+ ./
+
+ ./Tests/
+ ./Resources/
+ ./vendor/
+ ./coverage/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+