diff --git a/config/pmd-suppressions.properties b/config/pmd-suppressions.properties
index 11439c609a026b20bde6d2eed37aa7a4777cabd7..a22f2263faba8628c510bb2b0f9cde2bb32e2e57 100644
--- a/config/pmd-suppressions.properties
+++ b/config/pmd-suppressions.properties
@@ -8,9 +8,17 @@ fr.agrometinfo.www.shared.dto.IndicatorDTOBeanJsonDeserializerImpl=UnnecessaryIm
 fr.agrometinfo.www.shared.dto.IndicatorDTOBeanJsonSerializerImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.IndicatorDTO_MapperImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.MessageDTOBeanJsonSerializerImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.dto.SurveyResponseDTOBeanJsonSerializerImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.PeriodDTOBeanJsonDeserializerImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.PeriodDTOBeanJsonSerializerImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.PeriodDTO_MapperImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.dto.SurveyQuestionDTOBeanJsonDeserializerImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.dto.SurveyQuestionDTOBeanJsonSerializerImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.dto.SurveyQuestionDTO_MapperImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.dto.SurveyOptionDTOBeanJsonSerializerImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.dto.SurveyOptionDTOBeanJsonDeserializerImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.dto.SurveyOptionDTO_MapperImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.service.SurveyOptionDTO_MapperImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.SimpleFeatureBeanJsonDeserializerImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.SimpleFeatureBeanJsonSerializerImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.SummaryDTOBeanJsonDeserializerImpl=UnnecessaryImport
@@ -18,6 +26,9 @@ fr.agrometinfo.www.shared.dto.SummaryDTOBeanJsonSerializerImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.dto.SummaryDTO_MapperImpl=UnnecessaryImport
 fr.agrometinfo.www.shared.service.ApplicationServiceFactory=UnnecessaryImport
 fr.agrometinfo.www.shared.service.IndicatorServiceFactory=UnnecessaryImport
+fr.agrometinfo.www.shared.service.SurveyFormServiceFactory=UnnecessaryImport
+fr.agrometinfo.www.shared.service.SurveyQuestionDTO_MapperImpl=UnnecessaryImport
+fr.agrometinfo.www.shared.service.SurveyOptionDTOBeanJsonDeserializerImpl=UnnecessaryImport
 org.geojson.FeatureBeanJsonDeserializerImpl=UnnecessaryImport
 org.geojson.FeatureBeanJsonSerializerImpl=UnnecessaryImport
 org.geojson.FeatureCollectionBeanJsonDeserializerImpl=UnnecessaryImport
diff --git a/pom.xml b/pom.xml
index 5cfaed5bbd2b7a80bb1833919e373070ab669038..ee1f2a43d0dcff775eec437cd37a1bdc575e5131 100644
--- a/pom.xml
+++ b/pom.xml
@@ -433,6 +433,7 @@
             <failOnViolation>true</failOnViolation>
             <propertyExpansion>basedir=${project.basedir}</propertyExpansion>
             <violationSeverity>warning</violationSeverity>
+            <consoleOutput>true</consoleOutput>
           </configuration>
         </plugin>
         <plugin>
diff --git a/sql/drop.sql b/sql/drop.sql
index ee674b8e63962ad585a1efb419d6870178047f00..24a594226e87e68eb646db987652eb0d95c29a7a 100644
--- a/sql/drop.sql
+++ b/sql/drop.sql
@@ -10,3 +10,7 @@ DROP TABLE period;
 DROP TABLE i18n;
 DROP TABLE i18nkey;
 DROP TABLE locale;
+DROP TABLE userresponse;
+DROP TABLE surveyoption;
+DROP TABLE surveyquestion;
+DROP TABLE useremail;
diff --git a/sql/init_data.h2.sql b/sql/init_data.h2.sql
index c89864532000e5c3071d864534c454efa80ad4bc..c40a7c014d33c74508b1bcb607abf1a24e87c520 100644
--- a/sql/init_data.h2.sql
+++ b/sql/init_data.h2.sql
@@ -127,3 +127,9 @@ INSERT INTO simulation (date, simulationid, started, ended) VALUES
 --
 REFRESH MATERIALIZED VIEW v_pra_dailyvalue;
 
+INSERT INTO surveyquestion(id, description)
+	SELECT * FROM CSVREAD('../sql/questions.csv');
+
+INSERT INTO surveyoption(surveyquestion, description)
+	SELECT * FROM CSVREAD('../sql/responses.csv', null, 'fieldSeparator=;');
+
diff --git a/sql/init_data.postgresql.sql b/sql/init_data.postgresql.sql
index 6648baa2a6c92daf1a8a1e7bfe656f6875315b56..4a607133135e996c0c7bd05f64b570afc9301475 100644
--- a/sql/init_data.postgresql.sql
+++ b/sql/init_data.postgresql.sql
@@ -1,6 +1,19 @@
 -- Initialization script for PostgreSQL database with sample data
 -- also do update.
 
+DELETE FROM normalvalue;
+DELETE FROM dailyvalue;
+DELETE FROM cell;
+DELETE FROM department;
+DELETE FROM region;
+DELETE FROM indicator;
+DELETE FROM period;
+DELETE FROM i18n;
+DELETE FROM i18nkey;
+DELETE FROM locale;
+DELETE FROM surveyquestion;
+DELETE FROM surveyoption;
+
 -- translations
 CREATE TEMPORARY TABLE IF NOT EXISTS tmp_translation (
     key VARCHAR,
@@ -155,3 +168,11 @@ INSERT INTO normalvalue (indicator, cell, doy, medianvalue, q5, q95)
     JOIN period AS p ON p.id=i.period
     WHERE p.code=t.period
     ON CONFLICT ON CONSTRAINT "UK_normalvalue" DO NOTHING;
+
+-- questions
+\COPY surveyquestion(id, description) FROM questions.csv WITH DELIMITER ',' HEADER CSV;
+
+-- responses
+-- WARNING : responses CSV file use semicolon (« ; ») as separator !!
+\COPY surveyoption(surveyquestion, description) FROM responses.csv WITH DELIMITER ';' HEADER CSV;
+
diff --git a/sql/migration.sql b/sql/migration.sql
index cc5cb5de1cf70c003b4cf2e4aedf4b45b4c18836..423dc0df5163deeae232d83fcfb9e3f32835ce2f 100644
--- a/sql/migration.sql
+++ b/sql/migration.sql
@@ -85,65 +85,6 @@ END
 $$ LANGUAGE plpgsql;
 COMMENT ON FUNCTION drop_applied_migration_functions() IS 'Purge database from applied migration functions.';
 
----
---- #8
----
-CREATE OR REPLACE FUNCTION upgrade20231023() RETURNS boolean AS $BODY$
-BEGIN
-    ALTER TABLE indicator DROP COLUMN colorsequence;
-    DROP TABLE colorsequence;
-    ALTER TABLE indicator ADD COLUMN colorsequencename VARCHAR;
-    ALTER TABLE indicator ADD COLUMN nbofclasses INTEGER;
-    CREATE TYPE QUANTILETYPE AS ENUM('CENTILES_05', 'QUANTILES', 'DECILES', 'QUINTILES', 'QUARTILES');
-    ALTER TABLE indicator ADD COLUMN quantiletype QUANTILETYPE;
-    UPDATE indicator SET quantiletype='QUINTILES';
-    UPDATE indicator SET colorsequencename='Precipitation' WHERE code='rainsum';
-    UPDATE indicator SET colorsequencename='Blues' WHERE code='frostdaystmin';
-    UPDATE indicator SET colorsequencename='Temperature' WHERE code IN ('mint', 'meant', 'maxt');
-    UPDATE indicator SET colorsequencename='YlOrRd' WHERE code IN ('hdaystmax', 'hdaystmax1');
-    ALTER TABLE indicator ADD CONSTRAINT "CK_indicator_colorsequence" CHECK (nbofclasses IS NOT NULL OR quantiletype IS NOT NULL);
-    RETURN true;
-END
-$BODY$
-language plpgsql;
-
----
---- #8
----
-CREATE OR REPLACE FUNCTION upgrade20231030() RETURNS boolean AS $BODY$
-BEGIN
-    ALTER TABLE "indicator" ALTER COLUMN quantiletype TYPE VARCHAR USING QUANTILETYPE::VARCHAR;
-    DROP TYPE QUANTILETYPE;
-    CREATE TYPE QUANTILETYPE AS ENUM('CENTILES_05', 'QUANTILES', 'DECILES', 'QUINTILES', 'QUARTILES', 'SEXTILES');
-    ALTER TABLE "indicator" ALTER COLUMN quantiletype TYPE QUANTILETYPE USING quantiletype::quantiletype;
-    UPDATE indicator SET quantiletype='SEXTILES';
-    --- #12
-    CREATE TYPE AGGREGATIONTYPE AS ENUM('AVG', 'MAX');
-    ALTER TABLE "indicator" ADD COLUMN aggregationtype AGGREGATIONTYPE;
-    UPDATE indicator SET unit='mm' WHERE code IN ('rainsum');
-    UPDATE indicator SET unit='j' WHERE code IN ('frostdaystmin', 'hdaystmax', 'hdaystmax1');
-    UPDATE indicator SET unit='°C' WHERE code IN ('maxt', 'meant', 'mint');
-    UPDATE indicator SET aggregationtype='MAX' WHERE code IN ('frostdaystmin', 'hdaystmax', 'hdaystmax1', 'rainsum');
-    UPDATE indicator SET aggregationtype='AVG' WHERE code IN ('maxt', 'meant', 'mint');
-    ALTER TABLE "indicator" ALTER COLUMN aggregationtype SET NOT NULL;
-    --- #8
-    UPDATE indicator SET colorsequencename='TemperatureReversed' WHERE code IN ('mint', 'meant', 'maxt');
-    RETURN true;
-END
-$BODY$
-language plpgsql;
-
---
--- 31
---
-CREATE OR REPLACE FUNCTION upgrade20231215() RETURNS boolean AS $BODY$
-BEGIN
-    INSERT INTO department (id, "name", region) VALUES(75, '75 - Paris', 75);
-    RETURN true;
-END
-$BODY$
-language plpgsql;
-
 --
 -- 47
 --
@@ -232,8 +173,8 @@ language plpgsql;
 CREATE OR REPLACE FUNCTION upgrade20240513() RETURNS boolean AS $BODY$
 BEGIN
     UPDATE dailyvalue AS d
-        FROM normalvalue AS n ON n.indicator=d.indicator AND n.cell=d.cell AND n.doy=EXTRACT(DOY FROM d.date)
-        SET d.comparedvalue=COALESCE(d.computedvalue - n."medianvalue", 0);
+	SET comparedvalue=COALESCE(d.computedvalue - n."medianvalue", 0)
+        FROM normalvalue AS n WHERE n.indicator=d.indicator AND n.cell=d.cell AND n.doy=EXTRACT(DOY FROM d.date);
     RETURN true;
 END;
 $BODY$
diff --git a/sql/questions.csv b/sql/questions.csv
new file mode 100644
index 0000000000000000000000000000000000000000..f85176616d008b40ef3fa639cc9b17d949742a28
--- /dev/null
+++ b/sql/questions.csv
@@ -0,0 +1,4 @@
+id,description
+1,Quelle est votre profession ?
+2,Comment avez-vous connu AgroMetInfo ?
+3,Dans quel but voulez-vous utiliser cette application ?
diff --git a/sql/responses.csv b/sql/responses.csv
new file mode 100644
index 0000000000000000000000000000000000000000..173c16eeba41b7ae9e819305cac4d8d80c9f6452
--- /dev/null
+++ b/sql/responses.csv
@@ -0,0 +1,19 @@
+surveyquestion;description
+1;Agriculteur
+1;Conseiller agricole (Chambre d'agriculture, Entreprise de service)
+1;Scientifique
+1;Journaliste
+1;Administration
+1;Enseignant
+1;Citoyen curieux (c'est bien)
+2;Internet
+2;Via le site internet d'AgroClim
+2;Via le site internet INRAE
+2;Média (TV, journaux, radio)
+2;Réseaux sociaux (Linkedin, Facebook, X…)
+2;Bouche à oreilles
+3;S'informer
+3;Enseigner
+3;Conseiller
+3;Publier un article dans les médias
+3;Faire une présentation
diff --git a/sql/schema.tables.sql b/sql/schema.tables.sql
index 77eff33edcc7ade9a4851183ae2f56ac837bca37..47eab0f8cc02e07a1e7e79db315edd5f14833eeb 100644
--- a/sql/schema.tables.sql
+++ b/sql/schema.tables.sql
@@ -214,6 +214,43 @@ CREATE TABLE IF NOT EXISTS dailyvisit(
 );
 COMMENT ON TABLE dailyvisit IS 'Number of visits per day.';
 
+-- Tables for survey form - #5
+CREATE TABLE IF NOT EXISTS surveyquestion (
+	id SERIAL NOT NULL,
+	description VARCHAR(255) NULL,
+	CONSTRAINT PK_surveyquestion PRIMARY KEY (id)
+);
+COMMENT ON TABLE surveyquestion IS 'Questions for survey form';
+
+CREATE TABLE IF NOT EXISTS surveyoption (
+	id SERIAL NOT NULL,
+	surveyquestion INT4 NOT NULL,
+	description VARCHAR NULL,
+	CONSTRAINT PK_surveyoption PRIMARY KEY (id),
+	CONSTRAINT FK_surveyoption_surveyquestion FOREIGN KEY (surveyquestion) REFERENCES surveyquestion(id)
+);
+COMMENT ON TABLE surveyoption IS 'Options responses for questions.';
+
+CREATE TABLE IF NOT EXISTS userresponse (
+	id SERIAL NOT NULL,
+	datetime TIMESTAMP NOT NULL,
+	surveyquestion INT4 NOT NULL,
+	surveyoption INT4 NULL,
+	othertext VARCHAR NULL,
+	CONSTRAINT PK_userresponse PRIMARY KEY (id),
+	CONSTRAINT FK_userresponse_surveyquestion FOREIGN KEY (surveyquestion) REFERENCES surveyquestion(id),
+	CONSTRAINT FK_userresponse_surveyoption FOREIGN KEY (surveyoption) REFERENCES surveyoption(id)
+);
+COMMENT ON TABLE userresponse IS 'Responses of user to questions, and other text if present';
+
+CREATE TABLE IF NOT EXISTS useremail (
+	id SERIAL NOT NULL,
+	datetime TIMESTAMP NOT NULL,
+	email VARCHAR NOT NULL,
+	CONSTRAINT PK_useremail PRIMARY KEY (id)
+);
+COMMENT ON TABLE useremail IS 'Simple table for register email of user, when he fills out survey';
+
 CREATE OR REPLACE VIEW v_i18n
 AS SELECT l.languagetag,
     i.i18nkey,
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/App.java b/www-client/src/main/java/fr/agrometinfo/www/client/App.java
index 920e6230623c4ed6296263f11a65335d47a755fa..542e4c03fad3a1501ee0fdf566b8212e1a1b5302 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/App.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/App.java
@@ -22,6 +22,7 @@ import fr.agrometinfo.www.client.i18n.AppMessages;
 import fr.agrometinfo.www.client.presenter.LayoutPresenter;
 import fr.agrometinfo.www.client.util.ApplicationUtils;
 import fr.agrometinfo.www.shared.service.ApplicationServiceFactory;
+import fr.agrometinfo.www.client.presenter.SurveyPresenter;
 
 /**
  * Entry point classes define <code>onModuleLoad()</code>.
@@ -82,7 +83,6 @@ public class App implements EntryPoint {
     public static EventBus getEventBus() {
         return EVENT_BUS;
     }
-
     /**
      * This is the entry point method.
      */
@@ -100,5 +100,8 @@ public class App implements EntryPoint {
         Charba.enable();
 
         new LayoutPresenter().start();
+
+        // display survey form
+        new SurveyPresenter().start();
     }
 }
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java b/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java
index ec07adbce5689c43834798e29625b87e70cd0fe7..6a0dee6eb73b3c7fb1680f423366df9272444150 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java
@@ -183,6 +183,12 @@ public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLo
     @DefaultStringValue("HTTP status text:")
     String failureStatusText();
 
+    /**
+    * @return translation
+    */
+    @DefaultStringValue("Ignorer")
+    String ignore();
+
     /**
      * @return translation
      */
@@ -314,8 +320,8 @@ public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLo
     /**
      * @return translation
      */
-    @DefaultStringValue("Toggle right panel")
-    String toggleRightPanel();
+    @DefaultStringValue("Formulaire d'enquête")
+    String surveyFormTitle();
 
     /**
      * @return translation
@@ -327,16 +333,82 @@ public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLo
             + "or use the contact form.")
     String welcomeBody();
 
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Welcome to the new version of AgroMetInfo !<br/>"
+            + "To help us improve the application and make it as responsive as possible to your needs, "
+            + "<b>please fill in the short survey below</b> (it will take 2 minutes maximum).<br/>"
+            + "All information is anonymous, but will help us to better understand how you use the application."
+            + "<br/><b>Don't hesitate to give us your email address</b> "
+            + "so that we can keep you informed of all developments in the coming months and years."
+            + "<br/><br/>The AgroMetInfo development team")
+    String surveyFormDescription();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Your e-mail address (optional) :")
+    String surveyFromEmailDescription();
+
     /**
      * @return translation
      */
     @DefaultStringValue("Welcome to AgroMetInfo")
     String welcomeTitle();
 
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Other")
+    String surveyFormOtherTextCheckbox();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("You must click on checkbox above for activate this input field.")
+    String surveyFormOtherTextTooltip();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Your responses have been recorded.")
+    String surveyFormSuccess();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Your responses cannot been recorded, but you can use AgroMetInfo application.")
+    String surveyFormFail();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Toggle right panel")
+    String toggleRightPanel();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Profile and settings")
+    String userProfile();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("Valider")
+    String validate();
+
+    /**
+     * @return translation
+     */
+    @DefaultStringValue("You must be identified to access AgroMetInfo due to Meteo-France agreements related to "
+            + "SAFRAN data exchanges with AgroClim.")
+    String whyConnectionIsRequired();
+
     /**
      * @return translation
      */
     @DefaultStringValue("Yes")
     String yes();
-
 }
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/presenter/SurveyPresenter.java b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/SurveyPresenter.java
new file mode 100644
index 0000000000000000000000000000000000000000..00b81aa2a622d310136ad87c1b2d974bee6f502c
--- /dev/null
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/SurveyPresenter.java
@@ -0,0 +1,138 @@
+package fr.agrometinfo.www.client.presenter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.dominokit.rest.shared.request.FailedResponseBean;
+
+import com.google.gwt.core.client.GWT;
+
+import fr.agrometinfo.www.client.view.BaseView;
+import fr.agrometinfo.www.client.view.SurveyView;
+import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO;
+import fr.agrometinfo.www.shared.dto.SurveyOptionDTO;
+import fr.agrometinfo.www.shared.dto.SurveyResponseDTO;
+import fr.agrometinfo.www.shared.service.SurveyFormServiceFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Presenter for survey form.
+ *
+ * @author Olivier Maury
+ * @author Jérémie Décome
+ */
+public final class SurveyPresenter implements Presenter {
+
+    /**
+     * Interface for the related view.
+     */
+    public interface View extends BaseView<SurveyPresenter> {
+        /**
+         * Display notification on failure.
+         *
+         * @param failedResponse failure response
+         */
+        void failureNotification(FailedResponseBean failedResponse);
+        /**
+         * Close modal.
+         */
+        void close();
+        /**
+         * @param map list of availables responses.
+         */
+        void setResponses(Map<SurveyQuestionDTO, List<SurveyOptionDTO>> map);
+        /**
+         * Showing success login.
+         * @param msg returned message from webservice
+         */
+        void displaySuccessLogin(String msg);
+    }
+
+    /**
+     * Related view.
+     */
+    private final SurveyView view = new SurveyView();
+    /** List of responses from webservice. */
+    private List<SurveyOptionDTO> responses;
+    /** List of questions, extracted from responses list. */
+    private List<SurveyQuestionDTO> questions;
+    /**
+     * Set responses and extract questions from responses list.
+     * @param list list of responses
+     */
+    private void setResponses(final List<SurveyOptionDTO> list) {
+        GWT.log("SurveyPresenter.setResponses(" + list.size() + ") enter");
+        this.responses = list;
+        final Map<SurveyQuestionDTO, List<SurveyOptionDTO>> responsesMap = new HashMap<>();
+
+        // extract questions from responses via webservice
+        for (final SurveyOptionDTO dto : list) {
+            if (!responsesMap.containsKey(dto.getQuestion())) {
+                responsesMap.put(dto.getQuestion(), new ArrayList<SurveyOptionDTO>());
+            }
+            responsesMap.get(dto.getQuestion()).add(dto);
+        }
+        this.questions = new ArrayList<>(responsesMap.keySet());
+        view.setResponses(responsesMap);
+
+        view.init();
+        GWT.log("SurveyPresenter.setResponses() end");
+    }
+    @Override
+    public void start() {
+        view.setPresenter(this);
+
+        SurveyFormServiceFactory.INSTANCE.getResponses()
+        .onSuccess(this::setResponses)
+        .onFailed(view::failureNotification)
+        .send();
+    }
+    public void insertUserResponses(
+            final List<Long> responsesRef,
+            final Map<Long, String> otherTextMap,
+            final String email) {
+
+        // processing of predefined responses
+        final Set<SurveyQuestionDTO> questionsDto = new LinkedHashSet<>();
+        final Set<SurveyOptionDTO> responsesDto = new LinkedHashSet<>();
+
+        for (final Long ref : responsesRef) {
+            // getting response by reference
+            final SurveyOptionDTO rDto = this.responses.stream().filter(r -> r.getId() == ref).findFirst().get();
+            if (rDto != null) {
+                responsesDto.add(rDto);
+
+                // getting question corresponding of response
+                questionsDto.add(rDto.getQuestion());
+            }
+        }
+        // addition of the question, if it only has a free answer
+        this.questions.forEach((q) -> {
+            if (otherTextMap.containsKey(q.getId())) {
+                questionsDto.add(q);
+            }
+        });
+
+        // call to webservice for inserting data
+        final SurveyResponseDTO data = new SurveyResponseDTO(
+                new ArrayList<SurveyQuestionDTO>(questionsDto),
+                new ArrayList<SurveyOptionDTO>(responsesDto),
+                otherTextMap);
+
+        // if user has entered their email address, it is added to object
+        if (!email.isEmpty()) {
+            data.setEmail(email);
+        }
+
+        SurveyFormServiceFactory.INSTANCE.insertAllResponses(data)
+        .onSuccess(view::displaySuccessLogin)
+        .onFailed(view::failureNotification)
+        .send();
+    }
+
+}
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java
index 789fd1cddb005553ac903ffa01b41a9a0656db5e..5521df97505cc5131da12578a68a035002e8fbbd 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java
@@ -1,5 +1,16 @@
 package fr.agrometinfo.www.client.view;
 
+import java.util.StringJoiner;
+
+import org.dominokit.domino.ui.notifications.Notification;
+import org.dominokit.rest.shared.request.FailedResponseBean;
+
+import com.google.gwt.core.client.GWT;
+
+import elemental2.dom.DomGlobal;
+import fr.agrometinfo.www.client.i18n.AppConstants;
+import fr.agrometinfo.www.client.i18n.AppMessages;
+
 /**
  * Abstract class for all view implementations.
  *
@@ -12,6 +23,30 @@ public abstract class AbstractBaseView<T> implements BaseView<T> {
      * Related presenter.
      */
     private T presenter;
+    /**
+     * I18N constants.
+     */
+    private static final AppConstants CSTS = GWT.create(AppConstants.class);
+    /**
+     * I18N messages.
+     */
+    private static final AppMessages MSGS = GWT.create(AppMessages.class);
+    /**
+    *
+    * @param failure
+    * @return details of failure object
+    */
+   protected static String getDetails(final FailedResponseBean failure) {
+       final StringJoiner sj = new StringJoiner("<br/>");
+       sj.add(CSTS.failureBody());
+       sj.add(failure.getBody());
+       sj.add(CSTS.failureHeaders());
+       sj.add(failure.getHeaders().toString());
+       sj.add(MSGS.failureStatusCode(failure.getStatusCode()));
+       sj.add(CSTS.failureStatusText());
+       sj.add(failure.getStatusText());
+       return sj.toString();
+   }
 
     /**
      * @return related presenter
@@ -19,10 +54,18 @@ public abstract class AbstractBaseView<T> implements BaseView<T> {
     protected final T getPresenter() {
         return presenter;
     }
-
+    /** */
     @Override
     public final void setPresenter(final T value) {
         this.presenter = value;
     }
-
+    /**
+     * Show notification with message.
+     * @param msg message to display
+     * @return notification object
+     */
+    protected Notification notification(final String msg) {
+        DomGlobal.console.info("Notification!");
+        return Notification.create(msg).setPosition(Notification.TOP_LEFT).show();
+    }
 }
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java
index 5f7e8ba0cb5dc30ee7d10d08b4ea8cfd1d23a29e..7380db74dcb09afcfbe03e4ea272ce323798d80c 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java
@@ -6,7 +6,6 @@ import static org.jboss.elemento.Elements.img;
 import static org.jboss.elemento.Elements.li;
 
 import java.util.List;
-import java.util.StringJoiner;
 
 import org.dominokit.domino.ui.grid.flex.FlexItem;
 import org.dominokit.domino.ui.icons.Icons;
@@ -16,7 +15,6 @@ import org.dominokit.domino.ui.loaders.Loader;
 import org.dominokit.domino.ui.loaders.LoaderEffect;
 import org.dominokit.domino.ui.menu.Menu;
 import org.dominokit.domino.ui.menu.MenuItem;
-import org.dominokit.domino.ui.notifications.Notification;
 import org.dominokit.domino.ui.style.Styles;
 import org.dominokit.domino.ui.utils.DominoElement;
 import org.dominokit.domino.ui.utils.DominoUIConfig;
@@ -89,17 +87,6 @@ implements LayoutPresenter.View, LoadingHandler {
         addMenuItem(menu, text, icon, runnable);
     }
 
-    private static String getDetails(final FailedResponseBean failure) {
-        final StringJoiner sj = new StringJoiner("<br/>");
-        sj.add(CSTS.failureBody());
-        sj.add(failure.getBody());
-        sj.add(CSTS.failureHeaders());
-        sj.add(failure.getHeaders().toString());
-        sj.add(MSGS.failureStatusCode(failure.getStatusCode()));
-        sj.add(CSTS.failureStatusText());
-        sj.add(failure.getStatusText());
-        return sj.toString();
-    }
 
     /**
      * @param relativePath documentation path relative to documentation root
@@ -389,11 +376,6 @@ implements LayoutPresenter.View, LoadingHandler {
         return layout.isRightPanelVisible();
     }
 
-    private Notification notification(final String msg) {
-        DomGlobal.console.info("Notification! " + msg);
-        return Notification.create(msg).setPosition(Notification.TOP_LEFT).show();
-    }
-
     @Override
     public void onLoading(final LoadingEvent event) {
         GWT.log("LayoutView.onLoading() " + event);
diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java
new file mode 100644
index 0000000000000000000000000000000000000000..b98369a45295171993ca500bc06578430d0f00de
--- /dev/null
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java
@@ -0,0 +1,187 @@
+package fr.agrometinfo.www.client.view;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import org.dominokit.domino.ui.button.Button;
+import org.dominokit.domino.ui.forms.CheckBox;
+import org.dominokit.domino.ui.forms.TextArea;
+import org.dominokit.domino.ui.forms.TextBox;
+import org.dominokit.domino.ui.modals.ModalDialog;
+import org.dominokit.rest.shared.request.FailedResponseBean;
+import org.gwtproject.safehtml.shared.SafeHtmlBuilder;
+import org.jboss.elemento.Elements;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.storage.client.Storage;
+
+import fr.agrometinfo.www.client.i18n.AppConstants;
+import fr.agrometinfo.www.client.presenter.SurveyPresenter;
+import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO;
+import fr.agrometinfo.www.shared.dto.SurveyOptionDTO;
+
+/**
+ * View for survey form.
+ *
+ * @author Olivier Maury
+ * @author Jérémie Décome
+ */
+public final class SurveyView extends AbstractBaseView<SurveyPresenter> implements SurveyPresenter.View {
+    /**
+     * I18N constants.
+     */
+    private static final AppConstants CSTS = GWT.create(AppConstants.class);
+    /**
+     * Is survey was already validated by user ? Key for LocalStorage.
+     */
+    private static final String IS_VALIDATED_KEY = "isValidated";
+    /**
+     * Level for questions.
+     */
+    private static final int SECTION_TITLE_LEVEL = 5;
+    /**
+     * List of available responses.
+     */
+    private Map<SurveyQuestionDTO, List<SurveyOptionDTO>> availableResponses;
+    /**
+     * The modal used to display the login form.
+     */
+    private ModalDialog modal;
+    /** Validate button of modal. */
+    private Button validate;
+    /** List of all checkbox on modal. */
+    private List<CheckBox> checkBoxList = new ArrayList<>();
+    /**
+     * E-mail field.
+     */
+    private TextBox email;
+
+    @Override
+    public void close() {
+        modal.close();
+    }
+
+    @Override
+    public void init() {
+        GWT.log("LoginView.init()");
+        // check if survey form has not already been completed by user, via the browser's LocalStorage
+        final Storage ls = Storage.getLocalStorageIfSupported();
+        if (ls != null && ls.getItem(IS_VALIDATED_KEY) != null && ls.getItem(IS_VALIDATED_KEY).equals("true")) {
+            GWT.log("Survey was already validated for this user, don't showing again");
+            return;
+        }
+
+        final Map<Long, TextArea> otherTextArea = new HashMap<>();
+
+        this.modal = ModalDialog.create(CSTS.surveyFormTitle()).large().setAutoClose(true);
+        this.modal
+        .appendChild(Elements.p()
+                .innerHtml(new SafeHtmlBuilder().appendHtmlConstant(CSTS.surveyFormDescription()).toSafeHtml())
+                );
+
+        for (Map.Entry<SurveyQuestionDTO, List<SurveyOptionDTO>> entry : this.availableResponses.entrySet()) {
+            final SurveyQuestionDTO k = entry.getKey(); // For each question ...
+
+            // displaying label
+            this.modal.appendChild(Elements.h(SECTION_TITLE_LEVEL, "• " + k.getDescription()));
+
+            // displaying availables responses
+            entry.getValue().forEach((r) -> {
+                final CheckBox cb = CheckBox.create(r.getDescription()).id(Long.toString(r.getId()))
+                        .css("login-checkbox");
+                this.modal.appendChild(cb);
+                this.checkBoxList.add(cb);
+            });
+
+            // displaying free response field
+            final CheckBox otherCb = CheckBox.create(CSTS.surveyFormOtherTextCheckbox())
+                    .css("login-checkbox");
+            final TextArea otherText = TextArea.create()
+                    .setDisabled(true)
+                    .setTooltip(CSTS.surveyFormOtherTextTooltip())
+                    .setRows(2)
+                    .css("login-textarea");
+
+            otherCb.addChangeHandler((v) -> {
+                otherText.setDisabled(!otherCb.getValue());
+                if (!otherText.isDisabled()) {
+                    otherText.removeTooltip();
+                } else {
+                    otherText.setTooltip(CSTS.surveyFormOtherTextTooltip());
+                }
+            });
+            this.checkBoxList.add(otherCb);
+            this.modal.appendChild(otherCb).appendChild(otherText);
+            otherTextArea.put(k.getId(), otherText);
+        }
+        // Validate button is activ if if at least one answer is checked
+        this.checkBoxList.forEach((c) -> {
+            c.addChangeHandler((v) -> this.activateValidateButton());
+        });
+
+        // User's e-mail address
+        this.modal.appendChild(Elements.p().textContent(CSTS.surveyFromEmailDescription()));
+        this.email = TextBox.create().setType("email");
+        this.modal.appendChild(this.email);
+
+        // Window's button
+        this.validate = Button.create(CSTS.validate()).linkify();
+        this.validate.addClickListener((evt) -> {
+            final HashMap<Long, String> otherTextMap = new HashMap<>();
+
+            // process the predefined responses (retrieval of response references),
+            // excluding checkbox for free responses
+            final List<Long> selectedResponsesRef = this.checkBoxList.stream()
+                    .filter((v) -> v.getValue() && v.getId().matches("[0-9]+"))
+                    .map(v -> Long.parseLong(v.getId())).collect(Collectors.toList());
+
+            // process the free responses
+            otherTextArea.forEach((k, v) -> {
+                // if textarea is activ, there is (potentially) a response and if textarea is not empty
+                if (!v.isDisabled() && !v.getValue().equals("")) {
+                    otherTextMap.put(k, v.getValue());
+                }
+            });
+
+            this.getPresenter().insertUserResponses(selectedResponsesRef, otherTextMap, this.email.getValue());
+
+            ls.setItem(IS_VALIDATED_KEY, "true");
+            this.modal.close();
+        });
+        this.validate.setDisabled(true);  // Validate button is disable by default
+        this.modal.appendFooterChild(validate);
+
+        final Button ignore = Button.create(CSTS.ignore()).linkify();
+        ignore.addClickListener((evt) -> {
+            this.modal.close();
+        });
+        this.modal.appendFooterChild(ignore);
+
+        this.modal.open();
+        GWT.log("LoginView.init() end");
+    }
+
+    @Override
+    public void failureNotification(final FailedResponseBean failedResponse) {
+        this.notification(CSTS.surveyFormFail());
+    }
+
+    @Override
+    public void setResponses(final Map<SurveyQuestionDTO, List<SurveyOptionDTO>> map) {
+        this.availableResponses = map;
+    }
+
+    @Override
+    public void displaySuccessLogin(final String msg) {
+        this.notification(CSTS.surveyFormSuccess());
+    }
+    /**
+     * Enable or disable validate button depending of checkbox status.
+     */
+    private void activateValidateButton() {
+        this.validate.setDisabled(!this.checkBoxList.stream().anyMatch((c) -> c.getValue()));
+    }
+}
diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties b/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
index 3436fb93eb3c312d0265620558ded14dc70a0729..611ec51de775bf5a9963f65d6ffb2aad93726ed4 100644
--- a/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
+++ b/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
@@ -33,6 +33,10 @@ invalidEmailAddress = Adresse courriel invalide
 legalNotice = Mentions légales
 legalNoticePath = legal-notice.html
 messageSent = Votre message a bien été envoyé à l’équipe d’AgroMetInfo.
+ignore = Ignorer
+login = Se connecter
+loginOrSignIn = ou s’inscrire avec
+logout = Se déconnecter
 metropolitanFrance = France métropolitaine
 no = Non
 normalComparison= Comparaison à la normale
@@ -43,9 +47,21 @@ releaseNotes = Notes de version
 releaseNotesPath = release-notes.html
 reloadingApplication = Rechargement de l'application pour une nouvelle version\u2026
 seePrivacyPolicy = Consultez le paragraphe « Données personnelles » dans les mentions légales.
+selectPrompt = -- sélectionner --
+surveyFormTitle = Formulaire d'enquête
+surveyFormDescription = Bienvenu.e.s à la nouvelle version d'AgroMetInfo !<br/>Afin de nous aider à la faire évoluer et l'adapter le plus possible à vos besoins, <b>merci de renseigner</b> la petite enquête çi dessous (cela prendra 2 minutes maximum). L'ensemble des informations est anonyme, mais nous aidera à mieux comprendre l'utilisation de l'application. N'hésitez pas à <b>nous laisser votre adresse courriel</b> pour vous tenir informés de toutes les évolutions dans les mois et années à venir.<br/><br/>L'équipe AgroMetInfo.
+surveyFromEmailDescription = Votre adresse courriel (facultatif) :
+surveyFormOtherTextCheckbox = Autre, à préciser
+surveyFormOtherTextTooltip = Vous devez cliquer sur la case à cocher ci-dessus pour activer ce champ de saisie.
+surveyFormSuccess = Vos réponses au formulaire d'enquête ont bien été enregistrées.
+surveyFormFail = Vos réponses au formulaire n'ont pas pu être enregistrées, mais vous pouvez tout de même utiliser AgroMetInfo.
 toggleRightPanel = Afficher / masquer le panneau de droite
 welcomeBody = AgroMetInfo est en cours de refonte.<br/>\
 Pour rester au courant des évolutions, vous pouvez nous envoyer un message à \
 <a href="mailto:support-agrometinfo@inrae.fr?subject=AgroMetInfo:%20contact">support-agrometinfo@inrae.fr</a> ou utiliser le formulaire de contact.
 welcomeTitle = Bienvenue sur AgroMetInfo
 yes = Oui
+userProfile = Compte et paramètres
+validate = Valider
+whyConnectionIsRequired = Vous devez vous identifier pour accéder à AgroMetInfo en raison des accords avec Météo-France relatifs aux échanges de données SAFRAN avec AgroClim.
+yes= Oui
\ No newline at end of file
diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css
index bdbff6757d2439f49e3878c52edc502ef39cc5f0..4dac15b38a4b85d8db91be4b8998b54512979a16 100644
--- a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css
+++ b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css
@@ -348,3 +348,21 @@ body > .modal-backdrop {
         --rightsidebar-width: 600px;
     }
 }
+
+/* Formulaire d'enquête */
+.login-paragraph {
+	font-size: 15px;
+}
+.login-checkbox {
+	margin: 0 0 2px 5px;
+}
+.login-checkbox .field-cntr {
+	padding: 0 10px !important;
+}
+.login-checkbox label {
+	top: 0px !important;
+	margin-bottom: 0px !important;
+}
+.login-textarea .field-cntr {
+	padding-top: 5px;
+}
\ No newline at end of file
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java b/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java
index 9e95b3a7f50f990a88bb30955c8767f658199d38..5eb7981bee22fecd61a4aa6685ec498bab57187e 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java
@@ -67,7 +67,6 @@ public class AgroMetInfoConfiguration {
          * E-mail address for support.
          */
         SUPPORT_EMAIL("support.email");
-
         /**
          * Key in config.properties.
          */
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
index b9b9c20ad5c038bd46df971558c3d2c943b7fedc..48899a6cbea234de1ec8f0dbd2f080536b198000 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java
@@ -86,7 +86,21 @@ public abstract class DaoHibernate<T> {
             return 0L;
         }
     }
-
+    /**
+     * Delete all row in table.
+     */
+    public void deleteAll() {
+        LOGGER.traceEntry();
+        try (ScopedEntityManager em = getScopedEntityManager()) {
+            final Query q = em.createQuery("DELETE FROM " + clazz.getName() + " t");
+            em.getTransaction().begin();
+            q.executeUpdate();
+            em.getTransaction().commit();
+        } catch (final Exception e) {
+            LOGGER.catching(e);
+        }
+        LOGGER.traceExit();
+    }
     /**
      * Use a consumer to execute JPA operations in a transaction.
      *
@@ -304,4 +318,16 @@ public abstract class DaoHibernate<T> {
     protected final ScopedEntityManager getScopedEntityManager() {
         return PersistenceManager.getInstance().createScopedEntityManager();
     }
+    /**
+     * Save a new entity in database.
+     * @param object
+     *            new entity to save
+     */
+    protected final void save(final Object object) {
+        try (ScopedEntityManager em = getScopedEntityManager()) {
+            em.executeTransaction(() -> em.persist(object));
+        } catch (final Exception e) {
+            LOGGER.catching(e);
+        }
+    }
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDao.java
new file mode 100644
index 0000000000000000000000000000000000000000..214e608a6f384cf55ba92e2fb0b315fc832f8c17
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDao.java
@@ -0,0 +1,29 @@
+package fr.agrometinfo.www.server.dao;
+
+import fr.agrometinfo.www.server.model.SurveyOption;
+
+import java.util.List;
+
+/**
+ * DAO for {@link SurveyOption}.
+ * @author jdecome
+ */
+public interface SurveyOptionDao {
+    /**
+     * Find all responses in database.
+     * @return all responses
+     */
+    List<SurveyOption> findAll();
+    /**
+     * Find all responses by question.
+     * @param questionRef
+     * @return list of responses
+     */
+    List<SurveyOption> findAllByQuestion(long questionRef);
+    /**
+     * Get response with reference.
+     * @param responseRef
+     * @return response
+     */
+    SurveyOption findByRef(long responseRef);
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDaoHibernate.java
new file mode 100644
index 0000000000000000000000000000000000000000..6b98c993dfed5058a1aa473f79c322d8a625ab55
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDaoHibernate.java
@@ -0,0 +1,31 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.util.List;
+import java.util.Map;
+
+import fr.agrometinfo.www.server.model.SurveyOption;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Implementation of {@link SurveyOptionDao}.
+ * @author jdecome
+ *
+ */
+@ApplicationScoped
+public final class SurveyOptionDaoHibernate extends DaoHibernate<SurveyOption> implements SurveyOptionDao {
+    /**
+     * Constructor.
+     */
+    public SurveyOptionDaoHibernate() {
+        super(SurveyOption.class);
+    }
+    @Override
+    public List<SurveyOption> findAllByQuestion(final long questionRef) {
+        final String jpql = "SELECT r FROM SurveyOption r WHERE r.question.id = :ref";
+        return super.findAllByJPQL(jpql, Map.of("ref", questionRef), SurveyOption.class);
+    }
+    @Override
+    public SurveyOption findByRef(final long responseRef) {
+        return super.find(responseRef);
+    }
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDao.java
new file mode 100644
index 0000000000000000000000000000000000000000..6a73dc530011898fdf5f1f82daf0692406540bee
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDao.java
@@ -0,0 +1,23 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.util.List;
+
+import fr.agrometinfo.www.server.model.SurveyQuestion;
+
+/**
+ * DAO for {@link SurveyQuestion}.
+ * @author jdecome
+ *
+ */
+public interface SurveyQuestionDao {
+    /**
+     * @return all of Questions
+     */
+    List<SurveyQuestion> findAll();
+    /**
+     * Find question object by reference.
+     * @param questionRef question_ref
+     * @return SurveyQuestion object
+     */
+    SurveyQuestion findByRef(long questionRef);
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDaoHibernate.java
new file mode 100644
index 0000000000000000000000000000000000000000..0ef2956db04bdcb3c81040397ec2393e3f56d1c1
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDaoHibernate.java
@@ -0,0 +1,23 @@
+package fr.agrometinfo.www.server.dao;
+
+import fr.agrometinfo.www.server.model.SurveyQuestion;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Implementation of {@link SurveyQuestionDao}.
+ * @author jdecome
+ *
+ */
+@ApplicationScoped
+public final class SurveyQuestionDaoHibernate extends DaoHibernate<SurveyQuestion> implements SurveyQuestionDao {
+    /**
+     * Constructor.
+     */
+    public SurveyQuestionDaoHibernate() {
+        super(SurveyQuestion.class);
+    }
+    @Override
+    public SurveyQuestion findByRef(final long ref) {
+        return super.find(ref);
+    }
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDao.java
new file mode 100644
index 0000000000000000000000000000000000000000..e38e5b7f02ae81ec346987e36b3f012c16f82dc3
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDao.java
@@ -0,0 +1,29 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import fr.agrometinfo.www.server.model.UserEmail;
+
+/**
+ * DAO for {@link UserEmail}.
+ * @author jdecome
+ *
+ */
+public interface UserEmailDao {
+    /**
+     * Insert e-mail address in table.
+     * @param email
+     * @param datetime provided localdatetime
+     */
+    void insertEmailUserAddress(String email, LocalDateTime datetime);
+    /**
+     * Getting all email address in database.
+     * @return list of email address
+     */
+    List<UserEmail> getEmailAddressList();
+    /**
+     * Delete all rows in table.
+     */
+    void deleteAll();
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDaoHibernate.java
new file mode 100644
index 0000000000000000000000000000000000000000..e45b99c28e61a9daff90964f9e87610535fbe890
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDaoHibernate.java
@@ -0,0 +1,32 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import fr.agrometinfo.www.server.model.UserEmail;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Implementation of {@link UserEmailDao}.
+ * @author jdecome
+ *
+ */
+@ApplicationScoped
+public final class UserEmailDaoHibernate extends DaoHibernate<UserEmail> implements UserEmailDao {
+    /** Default constructor. */
+    public UserEmailDaoHibernate() {
+        super(UserEmail.class);
+    }
+    @Override
+    public void insertEmailUserAddress(final String email, final LocalDateTime datetime) {
+        final UserEmail um = new UserEmail();
+        um.setEmail(email);
+        um.setDatetime(datetime);
+        this.save(um);
+    }
+    @Override
+    public List<UserEmail> getEmailAddressList() {
+        return super.findAll();
+    }
+
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDao.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a364db7cc2685cf9378f24998a5fd7f06e5bfca
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDao.java
@@ -0,0 +1,49 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import fr.agrometinfo.www.server.model.SurveyQuestion;
+import fr.agrometinfo.www.server.model.SurveyOption;
+import fr.agrometinfo.www.server.model.UserResponse;
+/**
+ * DAO for {@link UserResponse}.
+ * @author jdecome
+ */
+public interface UserResponsesDao {
+    /**
+     * Get all user responses by question.
+     * @param questionRef
+     * @return list of user's responses
+     */
+    List<UserResponse> findAllByQuestion(long questionRef);
+    /**
+     * Insert response or text for specified question.<br>
+     * If r is provided, {@code otherText} must be null.<br>
+     * If otherText is provided, {@code r} must be null.<br>
+     * datetime is defined by {@link LocalDateTime#now} in this method.
+     * @param q question to answer
+     * @param r response
+     * @param otherText text if response is other
+     */
+    void insertResponse(SurveyQuestion q, SurveyOption r, String otherText);
+    /**
+     * Insert response or text for specified question.<br>
+     * If r is provided, {@code otherText} must be null.<br>
+     * If otherText is provided, {@code r} must be null.
+     * @param q question to answer
+     * @param r response
+     * @param otherText text if response is other
+     * @param datetime provided localdatetime
+     */
+    void insertResponse(SurveyQuestion q, SurveyOption r, String otherText, LocalDateTime datetime);
+    /**
+     * Get all user responses.
+     * @return number of responses
+     */
+    int getNbUserResponses();
+    /**
+     * Delete all rows in table.
+     */
+    void deleteAll();
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDaoHibernate.java
new file mode 100644
index 0000000000000000000000000000000000000000..01cd1ff53ac68a5214d47a10d015a2acd7e6a00b
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDaoHibernate.java
@@ -0,0 +1,48 @@
+package fr.agrometinfo.www.server.dao;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+import fr.agrometinfo.www.server.model.SurveyQuestion;
+import fr.agrometinfo.www.server.model.SurveyOption;
+import fr.agrometinfo.www.server.model.UserResponse;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Implementation of {@link UserResponsesDao}.
+ * @author jdecome
+ *
+ */
+@ApplicationScoped
+public final class UserResponsesDaoHibernate extends DaoHibernate<UserResponse> implements UserResponsesDao {
+    /** Default constructor. */
+    public UserResponsesDaoHibernate() {
+        super(UserResponse.class);
+    }
+    @Override
+    public List<UserResponse> findAllByQuestion(final long questionRef) {
+        final String jpql = "SELECT u FROM UserResponse u WHERE u.question.id = :ref";
+        return super.findAllByJPQL(jpql, Map.of("ref", questionRef), UserResponse.class);
+    }
+    @Override
+    public void insertResponse(
+            final SurveyQuestion q, final SurveyOption r,
+            final String otherText, final LocalDateTime datetime) {
+        final UserResponse ur = new UserResponse();
+        ur.setDateTime(datetime);
+        ur.setQuestion(q);
+        ur.setOption(r);
+        ur.setOtherText(otherText);
+
+        this.save(ur);
+    }
+    @Override
+    public void insertResponse(final SurveyQuestion q, final SurveyOption r, final String otherText) {
+       this.insertResponse(q, r, otherText, LocalDateTime.now());
+    }
+    @Override
+    public int getNbUserResponses() {
+        return super.findAll().size();
+    }
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyOption.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyOption.java
new file mode 100644
index 0000000000000000000000000000000000000000..ddc5fe7a42c1ef20c8b5e0e5055e78e3f06f1a94
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyOption.java
@@ -0,0 +1,40 @@
+package fr.agrometinfo.www.server.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+/**
+ * Option (response) for questions of survey form.
+ * @author jdecome
+ */
+@Data
+@Entity
+@Table(name = "surveyoption")
+public class SurveyOption {
+    /**
+     * Reference of the option.<br>
+     * Primary key.
+     */
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id")
+    private long id;
+    /**
+     * Question related to the option.
+     */
+    @OneToOne
+    @JoinColumn(name = "surveyquestion")
+    private SurveyQuestion question;
+    /**
+     * Label of the option.
+     */
+    @Column(name = "description")
+    private String description;
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyQuestion.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyQuestion.java
new file mode 100644
index 0000000000000000000000000000000000000000..6596c6d0966067a9e32cc2f252a14a05a80b672c
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyQuestion.java
@@ -0,0 +1,32 @@
+package fr.agrometinfo.www.server.model;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+/**
+ * Questions for survey form.
+ * @author jdecome
+ */
+@Data
+@Entity
+@Table(name = "surveyquestion")
+public class SurveyQuestion {
+    /**
+     * Reference of the question.<br>
+     * Primary key.
+     */
+    @Id
+    @GeneratedValue(strategy = GenerationType.AUTO)
+    @Column(name = "id")
+    private long id;
+    /**
+     * Label of the question.
+     */
+    @Column(name = "description")
+    private String description;
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/UserEmail.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserEmail.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c54efc95ebf9fed393b0440dcd0b14ee08a1f82
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserEmail.java
@@ -0,0 +1,38 @@
+package fr.agrometinfo.www.server.model;
+
+import java.time.LocalDateTime;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+/**
+ * Table for storage email adress.<br>
+ * @author jdecome
+ */
+@Data
+@Entity
+@Table(name = "useremail")
+public class UserEmail {
+    /**
+     * Primary key.
+     */
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private long id;
+    /**
+     * Date time of user response (creation date).
+     */
+    @Column(name = "datetime")
+    private LocalDateTime datetime;
+    /**
+     * E-mail address.
+     */
+    @Column(name = "email")
+    private String email;
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/UserResponse.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..b6e61475ec38667cdd806f13a00e55bdb7870dd8
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserResponse.java
@@ -0,0 +1,54 @@
+package fr.agrometinfo.www.server.model;
+
+import java.time.LocalDateTime;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import lombok.Data;
+
+/**
+ * User's responses values of survey form.
+ * @author jdecome
+ */
+@Data
+@Entity
+@Table(name = "userresponse")
+public class UserResponse {
+    /**
+     * Primary key.
+     */
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private long id;
+    /**
+     * Date time of user responses (creation date).
+     */
+    @Column(name = "datetime")
+    private LocalDateTime dateTime;
+    /**
+     * Related question.
+     */
+    @OneToOne
+    @JoinColumn(name = "surveyquestion")
+    private SurveyQuestion question;
+    /**
+     * Related option.<br>
+     * Can be null if it's a other response.
+     */
+    @OneToOne
+    @JoinColumn(name = "surveyoption")
+    private SurveyOption option;
+    /**
+     * Other text response.<br>
+     * Can be null if it's a related response.
+     */
+    @Column(name = "othertext")
+    private String otherText;
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java
index 26fef07458cd2a65255a88a869f67a0263d17ced..b6ff6d4573d4c0cd97b92ea948f682291470f661 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java
@@ -39,11 +39,12 @@ public class ApplicationConfig extends Application {
     public final Set<Class<?>> getClasses() {
         return Set.of(
                 // OpenAPI / Swagger
-                OpenApiResource.class, AcceptHeaderOpenApiResource.class,
+                AcceptHeaderOpenApiResource.class, OpenApiResource.class,
                 // Jackson configuration
                 JacksonConfig.class,
                 // JAX-RS resources
-                ApplicationResource.class, GeometryResource.class, IndicatorResource.class, UserResource.class,
+                ApplicationResource.class, GeometryResource.class, IndicatorResource.class,
+                SurveyFormResource.class, UserResource.class,
                 // POJO
                 // Dependencies of resources
                 LogFilter.class
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java
index c843f96655d8eb0b84909a67b9703560495d4678..6b9e2e35b14856119fe0c242d6da2264ad85f184 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java
@@ -11,7 +11,7 @@ import fr.agrometinfo.www.server.exception.AgroMetInfoException;
 import fr.agrometinfo.www.server.model.DailyVisit;
 import fr.agrometinfo.www.server.service.CacheService;
 import fr.agrometinfo.www.server.service.MailService;
-import fr.agrometinfo.www.server.service.MailService.Mail;
+import fr.agrometinfo.www.server.service.MailServiceImpl.Mail;
 import fr.agrometinfo.www.server.util.AppVersion;
 import fr.agrometinfo.www.server.util.ST;
 import fr.agrometinfo.www.server.util.ST.Key;
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/SurveyFormResource.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/SurveyFormResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..9ac3ddcd563035ab96d1cafbf114a44a62a3128d
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/SurveyFormResource.java
@@ -0,0 +1,180 @@
+package fr.agrometinfo.www.server.rs;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import fr.agrometinfo.www.server.dao.SurveyQuestionDao;
+import fr.agrometinfo.www.server.dao.SurveyOptionDao;
+import fr.agrometinfo.www.server.dao.UserEmailDao;
+import fr.agrometinfo.www.server.dao.UserResponsesDao;
+import fr.agrometinfo.www.server.model.SurveyQuestion;
+import fr.agrometinfo.www.server.model.SurveyOption;
+import fr.agrometinfo.www.server.service.MailService;
+import fr.agrometinfo.www.server.util.EmailUtils;
+import fr.agrometinfo.www.shared.dto.ErrorResponseDTO;
+import fr.agrometinfo.www.shared.dto.SurveyResponseDTO;
+import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO;
+import fr.agrometinfo.www.shared.dto.SurveyOptionDTO;
+import fr.agrometinfo.www.shared.service.SurveyFormService;
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+
+/**
+ * Endpoint for WS login form.
+ * @author jdecome
+ *
+ */
+@Path(SurveyFormService.PATH)
+@RequestScoped
+public class SurveyFormResource implements SurveyFormService {
+    /**
+     * Convert {@link SurveyQuestion} entity to dto.
+     * @param question entity
+     * @return dto
+     */
+    static SurveyQuestionDTO toDto(final SurveyQuestion question) {
+        final SurveyQuestionDTO dto = new SurveyQuestionDTO();
+        dto.setId(question.getId());
+        dto.setDescription(question.getDescription());
+        return dto;
+    }
+    /**
+     * Convert {@link SurveyOption} entity to dto.
+     * @param response entity
+     * @return dto
+     */
+    static SurveyOptionDTO toDto(final SurveyOption response) {
+        final SurveyOptionDTO dto = new SurveyOptionDTO();
+        dto.setId(response.getId());
+        dto.setQuestion(toDto(response.getQuestion()));
+        dto.setDescription(response.getDescription());
+        return dto;
+    }
+    /**
+     * Convert {@link SurveyQuestionDTO} dto to entity.
+     * @param dto question
+     * @return entity
+     */
+    private static SurveyQuestion toEntity(final SurveyQuestionDTO dto) {
+        final SurveyQuestion question = new SurveyQuestion();
+        question.setId(dto.getId());
+        question.setDescription(dto.getDescription());
+        return question;
+    }
+    /**
+     * Convert {@link SurveyOptionDTO} dto to entity.
+     * @param dto response
+     * @return entity
+     */
+    private static SurveyOption toEntity(final SurveyOptionDTO dto) {
+        final SurveyOption response = new SurveyOption();
+        response.setId(dto.getId());
+        response.setQuestion(toEntity(dto.getQuestion()));
+        response.setDescription(dto.getDescription());
+        return response;
+    }
+    /**
+     * DAO for questions ({@link SurveyQuestionDao}).
+     */
+    @Inject
+    private SurveyQuestionDao questionsDao;
+    /**
+     * DAO for options ({@link SurveyOptionDao}).
+     */
+    @Inject
+    private SurveyOptionDao responsesDao;
+    /**
+     * DAO for user responses ({@link UserResponsesDao}.
+     */
+    @Inject
+    private UserResponsesDao userResponsesDao;
+    /**
+     * DAO for user email {@link UserEmailDao}.
+     */
+    @Inject
+    private UserEmailDao userEmailDao;
+    /**
+     * Mail service.
+     */
+    @Inject
+    private MailService mailService;
+    /**
+     * Ensure the value of query parameter is not null and not blanck.
+     * @param value value of query parameter to check
+     * @param queryParamName name of query parameter
+     * @throws WebApplicationException if validation failed
+     */
+    private void checkRequired(final Object value, final String queryParamName) {
+        if (value instanceof final String str && str.isBlank() || value == null) {
+            final jakarta.ws.rs.core.Response.Status badRequest = jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
+            throw new WebApplicationException(
+                    jakarta.ws.rs.core.Response.status(badRequest).entity(ErrorResponseDTO.of(
+                            badRequest.getStatusCode(),
+                            badRequest.getReasonPhrase(),
+                            queryParamName + " parameter is mandatory")
+                        ).build());
+        }
+    }
+    @GET
+    @Path(SurveyFormService.PATH_RESPONSES_LIST)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Override
+    public List<SurveyOptionDTO> getResponses() {
+        return responsesDao.findAll().stream()
+                .map(SurveyFormResource::toDto).collect(Collectors.toList());
+    }
+    @POST
+    @Path(SurveyFormService.PATH_INSERT_RESPONSE)
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Override
+    public String insertAllResponses(final SurveyResponseDTO data) {
+        this.checkRequired(data, "data");
+        // common datetime to all records (user responses and user email)
+        final LocalDateTime datetime = LocalDateTime.now();
+
+        final List<SurveyQuestion> questions = questionsDao.findAll();
+
+        // proccessing of predefined responses
+        for (final SurveyQuestionDTO dto : data.getQuestions()) {
+            final SurveyQuestion q = toEntity(dto);
+            // if question from survey doesn't exist, don't inserting response
+            if (!questions.contains(q)) {
+                continue;
+            }
+
+            final Long qRef = dto.getId();
+            List<SurveyOptionDTO> associatedResponses = data.getResponses()
+                    .stream().filter(f -> f.getQuestion().getId() == qRef)
+                    .collect(Collectors.toList());
+
+            for (final SurveyOptionDTO rDto : associatedResponses) {
+                final SurveyOption r = toEntity(rDto);
+                this.userResponsesDao.insertResponse(q, r, null, datetime);
+            }
+
+            // if free response present, inserted on database
+            if (data.getOtherTextMap().containsKey(dto.getId())
+                    && data.getOtherTextMap().get(dto.getId()) != null) {
+                this.userResponsesDao.insertResponse(q, null, data.getOtherTextMap().get(q.getId()), datetime);
+            }
+        }
+
+        // if the user provided their email address and is vaild
+        if (EmailUtils.isEmailAddressValid(data.getEmail())) {
+            this.userEmailDao.insertEmailUserAddress(data.getEmail(), datetime);
+        }
+
+        // once everything is inserted, send an email to support
+        this.mailService.sendSurveyFilled(data);
+        return "0";
+    }
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java
index f3f025a93096a0b1717f5f0c1562a41731f4b4cf..e85fe40f830569a09178e1974aaf3624126b0a51 100644
--- a/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java
@@ -1,319 +1,23 @@
 package fr.agrometinfo.www.server.service;
 
-import java.io.Serializable;
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Properties;
-import java.util.StringJoiner;
-
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.core.Appender;
-import org.apache.logging.log4j.core.Filter;
-import org.apache.logging.log4j.core.Layout;
-import org.apache.logging.log4j.core.LogEvent;
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.appender.AbstractAppender;
-import org.apache.logging.log4j.core.config.Configuration;
-import org.apache.logging.log4j.core.config.LoggerConfig;
-import org.apache.logging.log4j.core.layout.PatternLayout;
-
-import fr.agrometinfo.www.server.AgroMetInfoConfiguration;
-import fr.agrometinfo.www.server.AgroMetInfoConfiguration.ConfigurationKey;
-import fr.agrometinfo.www.server.exception.AgroMetInfoErrorCategory;
 import fr.agrometinfo.www.server.exception.AgroMetInfoException;
-import fr.agrometinfo.www.server.exception.ErrorCategory;
-import fr.agrometinfo.www.server.exception.ErrorType;
-import jakarta.annotation.PostConstruct;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.mail.Address;
-import jakarta.mail.Authenticator;
-import jakarta.mail.Message;
-import jakarta.mail.MessagingException;
-import jakarta.mail.PasswordAuthentication;
-import jakarta.mail.SendFailedException;
-import jakarta.mail.Session;
-import jakarta.mail.Transport;
-import jakarta.mail.internet.InternetAddress;
-import jakarta.mail.internet.MimeMessage;
-import lombok.Data;
-import lombok.Getter;
-import lombok.extern.log4j.Log4j2;
-
-/**
- * Service to send e-mails.
- *
- * @author Olivier Maury
- */
-@ApplicationScoped
-@Log4j2
-public class MailService {
-
-    /**
-     * Text message.
-     */
-    @Data
-    public static class Mail {
-        /**
-         * Body.
-         */
-        private String content;
-        /**
-         * Sender's e-mail address.
-         */
-        private String fromAddress;
-        /**
-         * Subject.
-         */
-        private String subject;
-        /**
-         * Recipients' e-mail addresses.
-         */
-        private List<String> toAddresses;
-    }
+import fr.agrometinfo.www.server.service.MailServiceImpl.Mail;
+import fr.agrometinfo.www.shared.dto.SurveyResponseDTO;
 
+public interface MailService {
     /**
-     * Log4j2 appender to send error logs by e-mail.
+     * Sending e-mail.
+     * @param mail object to send
+     * @throws AgroMetInfoException
      */
-    private class MailAppender extends AbstractAppender {
-        /**
-         * Logger name.
-         */
-        private static final String NAME = "MailAppender";
-
-        /**
-         * Constructor.
-         *
-         * @param filter           The Filter to associate with the Appender.
-         * @param layout           The layout to use to format the event.
-         * @param ignoreExceptions If true, exceptions will be logged and suppressed. If
-         *                         false errors will be logged and then passed to the
-         *                         application.
-         */
-        MailAppender(final Filter filter, final Layout<? extends Serializable> layout, final boolean ignoreExceptions) {
-            super(NAME, filter, layout, ignoreExceptions, null);
-
-        }
-
-        @Override
-        public void append(final LogEvent event) {
-            final Mail mail = new Mail();
-            mail.setSubject("error log");
-            mail.setContent(super.toSerializable(event).toString());
-            mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL)));
-            try {
-                send(mail);
-            } catch (final AgroMetInfoException e) {
-                // do nothing
-                LOGGER.info("Cannot send email: {}", e.getMessage());
-            }
-        }
-    }
-
+    void send(Mail mail) throws AgroMetInfoException;
     /**
-     * Keys from messages.properties used to warn about errors.
+     * Send mail when application started.
      */
-    public enum MailErrorType implements ErrorType {
-        /**
-         * Cannot get message details.
-         */
-        DETAILS("01"),
-        /**
-         * Generic exception.
-         */
-        SEND_FAILED("02"),
-        /**
-         * Some e-mail addresses are not valid.
-         */
-        SEND_FAILED_INVALID("03"),
-        /**
-         * Some messages failed to be sent to valid e-mail addresses.
-         */
-        SEND_FAILED_VALID("04");
-
-        /**
-         * Subcode for the error.
-         */
-        @Getter
-        private final String subCode;
-
-        /**
-         * Constructor.
-         *
-         * @param c Subcode for the error.
-         */
-        MailErrorType(final String c) {
-            this.subCode = c;
-        }
-
-        @Override
-        public ErrorCategory getCategory() {
-            return AgroMetInfoErrorCategory.MAIL;
-        }
-
-        @Override
-        public String getName() {
-            return name();
-        }
-
-    }
-
-    /**
-     * Application configuration.
-     */
-    @Inject
-    private AgroMetInfoConfiguration configuration;
-
-    /**
-     * Session created from parameters in context.xml on Tomcat.
-     */
-    private Session session;
-
-    /**
-     * Initialization of session and log appender.
-     */
-    @PostConstruct
-    public void init() {
-        final Properties props = new Properties();
-        props.put("mail.smtp.auth", "true");
-        props.put("mail.smtp.host", configuration.get(ConfigurationKey.SMTP_HOST));
-        props.put("mail.smtp.port", configuration.get(ConfigurationKey.SMTP_PORT));
-        final Authenticator authenticator = new Authenticator() {
-            @Override
-            protected PasswordAuthentication getPasswordAuthentication() {
-                return new PasswordAuthentication(configuration.get(ConfigurationKey.SMTP_USER),
-                        configuration.get(ConfigurationKey.SMTP_PASSWORD));
-            }
-        };
-        session = Session.getInstance(props, authenticator);
-        // Send e-mails for ERROR level messages
-        final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
-        final Configuration config = ctx.getConfiguration();
-        final String pattern = """
-                date : %d
-                level : %-5p
-                class : %c#%M()
-                line : %L
-                msg : %m
-                throwable : %throwable
-                """;
-        final PatternLayout layout = PatternLayout.newBuilder() //
-                .withConfiguration(config) //
-                .withPattern(pattern).build();
-        final Appender appender = new MailAppender(null, layout, true);
-        appender.start();
-        config.addAppender(appender);
-        final LoggerConfig loggerConfig = LoggerConfig.newBuilder() //
-                .withAdditivity(false) //
-                .withConfig(config) //
-                .withLevel(Level.ERROR) //
-                .withLoggerName(MailAppender.NAME) //
-                .build();
-        loggerConfig.addAppender(appender, null, null);
-        config.addLogger("fr.agrometinfo", loggerConfig);
-        ctx.updateLoggers();
-    }
-
-    /**
-     * @param mail text message to send
-     * @throws AgroMetInfoException exception
-     */
-    public void send(final Mail mail) throws AgroMetInfoException {
-        send(toMessage(mail));
-    }
-
-    /**
-     * @param message jakarta message to send
-     * @throws AgroMetInfoException exception
-     */
-    private void send(final Message message) throws AgroMetInfoException {
-        final String subject;
-        final String recipients;
-        try {
-            subject = message.getSubject();
-            recipients = toString(message.getAllRecipients());
-        } catch (final MessagingException e) {
-            throw new AgroMetInfoException(MailErrorType.DETAILS, e.getMessage());
-        }
-        try {
-            Transport.send(message);
-        } catch (final MessagingException e) {
-            if (e instanceof final SendFailedException sfe) {
-                final Address[] invalid = sfe.getInvalidAddresses();
-                if (invalid != null) {
-                    throw new AgroMetInfoException(MailErrorType.SEND_FAILED_INVALID, subject, toString(invalid));
-                }
-                final Address[] validUnsent = sfe.getValidUnsentAddresses();
-                if (validUnsent != null) {
-                    throw new AgroMetInfoException(MailErrorType.SEND_FAILED_VALID, subject, toString(validUnsent));
-                }
-            }
-            throw new AgroMetInfoException(MailErrorType.SEND_FAILED, subject, recipients, e.getMessage());
-        }
-    }
-
-    /**
-     * Send an e-mail to log e-mail address at application start.
-     */
-    public void sendApplicationStarted() {
-        final Mail mail = new Mail();
-        mail.setContent("Démarrage de AgroMetInfo-www : " + LocalDateTime.now());
-        mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL));
-        mail.setSubject("Démarrage");
-        mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL)));
-        try {
-            send(mail);
-        } catch (final AgroMetInfoException e) {
-            LOGGER.info("failed to send e-mail : {}", e);
-        }
-    }
-
-    /**
-     * @param mail text message to convert
-     * @return jakarta converted message
-     * @throws AgroMetInfoException exception
-     */
-    private Message toMessage(final Mail mail) throws AgroMetInfoException {
-        final String environment = configuration.get(ConfigurationKey.ENVIRONMENT);
-        try {
-            final Message message = new MimeMessage(session);
-            if (mail.getFromAddress() == null) {
-                mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL));
-            }
-            message.setFrom(new InternetAddress(mail.getFromAddress()));
-            final List<InternetAddress> to = new ArrayList<>();
-            if (mail.getToAddresses() == null || mail.getToAddresses().isEmpty()) {
-                mail.setToAddresses(List.of(configuration.get(ConfigurationKey.SUPPORT_EMAIL)));
-            }
-            for (final String addr : mail.getToAddresses()) {
-                to.add(new InternetAddress(addr));
-            }
-            message.setRecipients(Message.RecipientType.TO, to.toArray(InternetAddress[]::new));
-            if ("prod".equals(environment)) {
-                message.setSubject("AgroMetInfo : " + mail.getSubject());
-            } else {
-                message.setSubject("AgroMetInfo " + environment + " : " + mail.getSubject());
-            }
-            final String content = mail.getContent() + "\n-- \n" + configuration.get(ConfigurationKey.APP_URL);
-            message.setContent(content, "text/plain; charset=UTF-8");
-            message.setHeader("X-Environment", environment);
-            return message;
-        } catch (final MessagingException e) {
-            throw new AgroMetInfoException(MailErrorType.DETAILS, e.getLocalizedMessage());
-        }
-    }
-
+    void sendApplicationStarted();
     /**
-     * @param addresses e-mail addresses
-     * @return string representation of addresses
+     * Send mail when user fill login form, with questions and responses.
+     * @param data
      */
-    private String toString(final Address[] addresses) {
-        final StringJoiner sj = new StringJoiner(", ");
-        for (final Address addr : addresses) {
-            sj.add(addr.toString());
-        }
-        return sj.toString();
-    }
+    void sendSurveyFilled(SurveyResponseDTO data);
 }
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/service/MailServiceImpl.java b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailServiceImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..a6c2c4210ba266959c02c8298003a12343757358
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailServiceImpl.java
@@ -0,0 +1,372 @@
+package fr.agrometinfo.www.server.service;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.LoggerConfig;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+
+import fr.agrometinfo.www.server.AgroMetInfoConfiguration;
+import fr.agrometinfo.www.server.AgroMetInfoConfiguration.ConfigurationKey;
+import fr.agrometinfo.www.server.exception.AgroMetInfoErrorCategory;
+import fr.agrometinfo.www.server.exception.AgroMetInfoException;
+import fr.agrometinfo.www.server.exception.ErrorCategory;
+import fr.agrometinfo.www.server.exception.ErrorType;
+import fr.agrometinfo.www.shared.dto.SurveyResponseDTO;
+import jakarta.annotation.PostConstruct;
+import jakarta.mail.Address;
+import jakarta.mail.Authenticator;
+import jakarta.mail.Message;
+import jakarta.mail.MessagingException;
+import jakarta.mail.PasswordAuthentication;
+import jakarta.mail.SendFailedException;
+import jakarta.mail.Session;
+import jakarta.mail.Transport;
+import jakarta.mail.internet.InternetAddress;
+import jakarta.mail.internet.MimeMessage;
+import lombok.Data;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j2;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+/**
+ * Service to send e-mails.
+ *
+ * @author Olivier Maury
+ */
+@ApplicationScoped
+@Log4j2
+public class MailServiceImpl implements MailService {
+
+    /**
+     * Text message.
+     */
+    @Data
+    public static class Mail {
+        /**
+         * Body.
+         */
+        private String content;
+        /**
+         * Sender's e-mail address.
+         */
+        private String fromAddress;
+        /**
+         * Subject.
+         */
+        private String subject;
+        /**
+         * Recipients' e-mail addresses.
+         */
+        private List<String> toAddresses;
+    }
+
+    /**
+     * Log4j2 appender to send error logs by e-mail.
+     */
+    private class MailAppender extends AbstractAppender {
+        /**
+         * Logger name.
+         */
+        private static final String NAME = "MailAppender";
+
+        /**
+         * Constructor.
+         *
+         * @param filter           The Filter to associate with the Appender.
+         * @param layout           The layout to use to format the event.
+         * @param ignoreExceptions If true, exceptions will be logged and suppressed. If
+         *                         false errors will be logged and then passed to the
+         *                         application.
+         */
+        MailAppender(final Filter filter, final Layout<? extends Serializable> layout, final boolean ignoreExceptions) {
+            super(NAME, filter, layout, ignoreExceptions, null);
+
+        }
+
+        @Override
+        public void append(final LogEvent event) {
+            Mail mail = new Mail();
+            mail.setFromAddress("agrometinfo@inrae.fr");
+            mail.setSubject("AgroMetInfo - error log");
+            mail.setContent(super.toSerializable(event).toString());
+            mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL)));
+            try {
+                send(mail);
+            } catch (AgroMetInfoException e) {
+                // do nothing
+                LOGGER.info("Cannot send email: {}", e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Keys from messages.properties used to warn about errors.
+     */
+    public enum MailErrorType implements ErrorType {
+        /**
+         * Cannot get message details.
+         */
+        DETAILS("01"),
+        /**
+         * Generic exception.
+         */
+        SEND_FAILED("02"),
+        /**
+         * Some e-mail addresses are not valid.
+         */
+        SEND_FAILED_INVALID("03"),
+        /**
+         * Some messages failed to be sent to valid e-mail addresses.
+         */
+        SEND_FAILED_VALID("04");
+
+        /**
+         * Subcode for the error.
+         */
+        @Getter
+        private final String subCode;
+
+        /**
+         * Constructor.
+         *
+         * @param c Subcode for the error.
+         */
+        MailErrorType(final String c) {
+            this.subCode = c;
+        }
+
+        @Override
+        public ErrorCategory getCategory() {
+            return AgroMetInfoErrorCategory.MAIL;
+        }
+
+        @Override
+        public String getName() {
+            return name();
+        }
+
+    }
+
+    /**
+     * Application configuration.
+     */
+    @Inject
+    private AgroMetInfoConfiguration configuration;
+
+    /**
+     * Session created from parameters in context.xml on Tomcat.
+     */
+    private Session session;
+
+    /**
+     * Initialization of session and log appender.
+     */
+    @PostConstruct
+    public void init() {
+        Properties props = new Properties();
+        props.put("mail.smtp.auth", "true");
+        props.put("mail.smtp.host", configuration.get(ConfigurationKey.SMTP_HOST));
+        props.put("mail.smtp.port", configuration.get(ConfigurationKey.SMTP_PORT));
+        final Authenticator authenticator = new Authenticator() {
+            @Override
+            protected PasswordAuthentication getPasswordAuthentication() {
+                return new PasswordAuthentication(configuration.get(ConfigurationKey.SMTP_USER),
+                        configuration.get(ConfigurationKey.SMTP_PASSWORD));
+            }
+        };
+        session = Session.getInstance(props, authenticator);
+        // Send e-mails for ERROR level messages
+        final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+        final Configuration config = ctx.getConfiguration();
+        final String pattern = """
+                date : %d
+                level : %-5p
+                class : %c#%M()
+                line : %L
+                msg : %m
+                throwable : %throwable
+                """;
+        final PatternLayout layout = PatternLayout.newBuilder() //
+                .withConfiguration(config) //
+                .withPattern(pattern).build();
+        final Appender appender = new MailAppender(null, layout, true);
+        appender.start();
+        config.addAppender(appender);
+        LoggerConfig loggerConfig = LoggerConfig.newBuilder() //
+                .withAdditivity(false) //
+                .withConfig(config) //
+                .withLevel(Level.ERROR) //
+                .withLoggerName(MailAppender.NAME) //
+                .build();
+        loggerConfig.addAppender(appender, null, null);
+        config.addLogger("fr.agrometinfo", loggerConfig);
+        ctx.updateLoggers();
+    }
+
+    /**
+     * @param mail text message to send
+     * @throws AgroMetInfoException exception
+     */
+    public void send(final Mail mail) throws AgroMetInfoException {
+        send(toMessage(mail));
+    }
+
+    /**
+     * @param message jakarta message to send
+     * @throws AgroMetInfoException exception
+     */
+    private void send(final Message message) throws AgroMetInfoException {
+        final String subject;
+        final String recipients;
+        try {
+            subject = message.getSubject();
+            recipients = toString(message.getAllRecipients());
+        } catch (MessagingException e) {
+            throw new AgroMetInfoException(MailErrorType.DETAILS, e.getMessage());
+        }
+        try {
+            Transport.send(message);
+        } catch (MessagingException e) {
+            if (e instanceof SendFailedException sfe) {
+                final Address[] invalid = sfe.getInvalidAddresses();
+                if (invalid != null) {
+                    throw new AgroMetInfoException(MailErrorType.SEND_FAILED_INVALID, subject, toString(invalid));
+                }
+                final Address[] validUnsent = sfe.getValidUnsentAddresses();
+                if (validUnsent != null) {
+                    throw new AgroMetInfoException(MailErrorType.SEND_FAILED_VALID, subject, toString(validUnsent));
+                }
+            }
+            throw new AgroMetInfoException(MailErrorType.SEND_FAILED, subject, recipients, e.getMessage());
+        }
+    }
+
+    /**
+     * @param mail text message to convert
+     * @return jakarta converted message
+     * @throws AgroMetInfoException exception
+     */
+    private Message toMessage(final Mail mail) throws AgroMetInfoException {
+        final String environment = configuration.get(ConfigurationKey.ENVIRONMENT);
+        try {
+            final Message message = new MimeMessage(session);
+            if (mail.getFromAddress() == null) {
+                mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL));
+            }
+            message.setFrom(new InternetAddress(mail.getFromAddress()));
+            List<InternetAddress> to = new ArrayList<>();
+            if (mail.getToAddresses() == null || mail.getToAddresses().isEmpty()) {
+                mail.setToAddresses(List.of(configuration.get(ConfigurationKey.SUPPORT_EMAIL)));
+            }
+            for (String addr : mail.getToAddresses()) {
+                to.add(new InternetAddress(addr));
+            }
+            message.setRecipients(Message.RecipientType.TO, to.toArray(InternetAddress[]::new));
+            if ("prod".equals(environment)) {
+                message.setSubject("AgroMetInfo : " + mail.getSubject());
+            } else {
+                message.setSubject("AgroMetInfo " + environment + " : " + mail.getSubject());
+            }
+            final String content = mail.getContent() + "\n-- \n" + configuration.get(ConfigurationKey.APP_URL);
+            message.setContent(content, "text/plain; charset=UTF-8");
+            message.setHeader("X-Environment", environment);
+            return message;
+        } catch (MessagingException e) {
+            throw new AgroMetInfoException(MailErrorType.DETAILS, e.getLocalizedMessage());
+        }
+    }
+
+    /**
+     * @param addresses e-mail addresses
+     * @return string representation of addresses
+     */
+    private String toString(final Address[] addresses) {
+        final StringJoiner sj = new StringJoiner(", ");
+        for (Address addr : addresses) {
+            sj.add(addr.toString());
+        }
+        return sj.toString();
+    }
+
+    /**
+     * Send an e-mail to log e-mail address at application start.
+     */
+    public void sendApplicationStarted() {
+        if (configuration.get(ConfigurationKey.ENVIRONMENT).equals("dev")) {
+            LOGGER.trace("Environment is dev, e-mail isn't send");
+            return;
+        }
+        Mail mail = new Mail();
+        mail.setContent("Démarrage de AgroMetInfo-www : " + LocalDateTime.now() + "\n-- \n"
+                + configuration.get(ConfigurationKey.APP_URL));
+        mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL));
+        mail.setSubject("Démarrage");
+        mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL)));
+        try {
+            send(mail);
+        } catch (AgroMetInfoException e) {
+            LOGGER.info("failed to send e-mail : {}", e);
+        }
+    }
+    /**
+     * Send an e-mail when user filled survey form.
+     * @param data data of survey
+     */
+    public void sendSurveyFilled(final SurveyResponseDTO data) {
+        final Mail mail = createContentFromData(data);
+        mail.setSubject("Un utilisateur a rempli le formulaire d'enquête");
+        mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL));
+        mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL)));
+        try {
+            send(mail);
+        } catch (AgroMetInfoException e) {
+            LOGGER.info("failed to send e-mail : {}", e);
+        }
+    }
+    /**
+     * Generate body e-mail with survey data.<br>
+     * Mail object returned is just a mail with content.
+     * @param data data of survey
+     * @return mail object
+     */
+    public static Mail createContentFromData(final SurveyResponseDTO data) {
+        final Mail mail = new Mail();
+
+        final StringBuilder builder = new StringBuilder();
+        builder.append("Bonjour,\n");
+        builder.append("Un utilisateur vient de remplir le formulaire d'enquête d'AgroMetInfo à l'instant.\n\n");
+        builder.append("Il a répondu à " + data.getQuestions().size() + " question(s) :\n");
+
+        data.getQuestions().forEach((q) -> {
+            builder.append("\t• « " + q.getDescription() + " » :\n");
+            data.getResponses().stream().filter(f -> f.getQuestion().getId() == q.getId())
+                .collect(Collectors.toList())
+                .forEach((r) -> builder.append("\t\t- " + r.getDescription() + "\n"));
+
+            if (data.getOtherTextMap().containsKey(q.getId())
+                    && data.getOtherTextMap().get(q.getId()) != null) {
+                builder.append("\t\t- Réponse libre : « " + data.getOtherTextMap().get(q.getId()) + " »\n");
+            }
+            builder.append("\n");
+        });
+        mail.setContent(builder.toString());
+        return mail;
+    }
+}
diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/util/EmailUtils.java b/www-server/src/main/java/fr/agrometinfo/www/server/util/EmailUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..1006b22714800523db69ff85cbbcdb91901da04a
--- /dev/null
+++ b/www-server/src/main/java/fr/agrometinfo/www/server/util/EmailUtils.java
@@ -0,0 +1,29 @@
+package fr.agrometinfo.www.server.util;
+
+import java.util.regex.Pattern;
+
+/**
+ * @author jdecome
+ *
+ */
+public final class EmailUtils {
+    /**
+     * Private default constructor.
+     */
+    private EmailUtils() {
+
+    }
+    /**
+     * Check if email address provided is a valid address.<br>
+     * Verification by not null and regex
+     * @param email address
+     * @return {@code true} or {@code false}
+     */
+    public static boolean isEmailAddressValid(final String email) {
+        if (email == null) {
+            return false;
+        }
+        final String emailPattern = "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}";
+        return Pattern.matches(emailPattern, email);
+    }
+}
diff --git a/www-server/src/main/webapp/index.html b/www-server/src/main/webapp/index.html
index 5498ba698e09956810c74697e20bbccb2c1cc1cc..bbf735acbba3dac6369e98dcd86cf0a46b946e9a 100644
--- a/www-server/src/main/webapp/index.html
+++ b/www-server/src/main/webapp/index.html
@@ -24,11 +24,11 @@
         <!--                                                               -->
         <!-- Consider inlining CSS to reduce the number of requested files -->
         <!--                                                               -->
-        <link type="text/css" rel="stylesheet" href="app/style.css">
         <link type="text/css" rel="stylesheet" href="app/vendors/ol/ol.css">
         <link type="text/css" rel="stylesheet" href="app/vendors/ol/ol-layerswitcher.css">
         <link type="text/css" rel="stylesheet" href="app/css/domino-ui.css">
         <link type="text/css" rel="stylesheet" href="app/css/themes/all-themes.min.css">
+        <link type="text/css" rel="stylesheet" href="app/style.css">
 
         <!--                                           -->
         <!-- Any title is fine                         -->
diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6e5f606754ddc6886879bd4f93440385c864a483
--- /dev/null
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java
@@ -0,0 +1,204 @@
+package fr.agrometinfo.www.server;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import org.junit.jupiter.api.Test;
+
+import fr.agrometinfo.www.server.dao.SurveyQuestionDao;
+import fr.agrometinfo.www.server.dao.SurveyQuestionDaoHibernate;
+import fr.agrometinfo.www.server.dao.SurveyOptionDao;
+import fr.agrometinfo.www.server.dao.SurveyOptionDaoHibernate;
+import fr.agrometinfo.www.server.dao.UserEmailDao;
+import fr.agrometinfo.www.server.dao.UserEmailDaoHibernate;
+import fr.agrometinfo.www.server.dao.UserResponsesDao;
+import fr.agrometinfo.www.server.dao.UserResponsesDaoHibernate;
+import fr.agrometinfo.www.server.model.SurveyQuestion;
+import fr.agrometinfo.www.server.model.SurveyOption;
+import fr.agrometinfo.www.server.model.UserEmail;
+import fr.agrometinfo.www.server.model.UserResponse;
+
+/**
+ * Test of UserResponsesDaoHibernate.
+ * @author jdecome
+ *
+ */
+public class SurveyFormHibernateTest {
+    /** DAO for user's responses. */
+    private UserResponsesDao userResponsesDao = new UserResponsesDaoHibernate();
+    /** DAO for questions. */
+    private SurveyQuestionDao questionsDao = new SurveyQuestionDaoHibernate();
+    /** DAO for responses. */
+    private SurveyOptionDao responsesDao = new SurveyOptionDaoHibernate();
+    /** DAO for email address of user. */
+    private UserEmailDao userEmailDao = new UserEmailDaoHibernate();
+    
+    @Test
+    public void getQuestion() {
+        final List<SurveyQuestion> list = questionsDao.findAll();
+        assertFalse(list.isEmpty(), "Questions list is empty");
+        for (final SurveyQuestion q : list) {
+            // search question by reference.
+            final SurveyQuestion foundedByRef = questionsDao.findByRef(q.getId());
+            assertNotNull(foundedByRef, "Object for question « " + q.getId() + " » is null");
+            assertEquals(foundedByRef, q, "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
+        }
+    }
+    
+    @Test
+    public void getResponses() {
+        final List<SurveyQuestion> questions = questionsDao.findAll();
+        final List<SurveyOption> responses = responsesDao.findAll();
+        assertFalse(responses.isEmpty(), "Options list is empty");
+        for (final SurveyOption r : responses) {
+            assertTrue(questions.contains(r.getQuestion()), "Questions list doesn't contains question " + r.getQuestion().getId());
+            
+            // Test from question corresponding to response
+            final SurveyQuestion q = r.getQuestion();
+            assertNotNull(q, "Question corresponding to response « " + r.getId() + " » is null");
+            
+            final SurveyQuestion foundedByRef = questionsDao.findByRef(q.getId());
+            assertNotNull(foundedByRef, "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
+            assertEquals(foundedByRef, r.getQuestion(), "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
+            
+            // Search response by reference
+            final SurveyOption responseByRef = responsesDao.findByRef(r.getId());
+            assertNotNull(responseByRef, "Object corresponding to response " + r.getId() + " is null");
+            assertEquals(responseByRef, r, "Object corresponding to response " + r.getId() + " is empty");
+        }
+    }
+    
+    @Test
+    public void setUserResponses() {
+        this.resetUserResponses();
+        final List<SurveyQuestion> questions = questionsDao.findAll();
+        final SurveyQuestion profession = questions.stream().filter((q) -> q.getDescription().contains("profession")).findFirst().get();
+        assertNotNull(profession, "No question object matching to question of question about profession");
+        List<SurveyOption> responses = responsesDao.findAllByQuestion(profession.getId());
+        assertNotNull(responses, "No response object matching to question of question about profession");
+        assertFalse(responses.isEmpty(), "List of responses about profession question is empty");
+        for (final SurveyOption r : responses) {
+            assertEquals(r.getQuestion(), profession, "Question of responses doesn't corresponding to original question");
+        }
+        
+        // Inserting a single response
+        final List<SurveyOption> listOfResponses = new ArrayList<>();   // list of inserted answers
+        SurveyOption r = responses.get(ThreadLocalRandom.current().nextInt(0, responses.size() - 1));
+        userResponsesDao.insertResponse(profession, r, null);
+        listOfResponses.add(r);
+        
+        List<UserResponse> list = userResponsesDao.findAllByQuestion(profession.getId());
+        assertEquals(list.size(), 1, "The user's response list must contain an item only");
+        assertEquals(list.get(0).getQuestion(), profession, "The question in the user's response does not match the original question");
+        assertEquals(list.get(0).getOption(), r, "The answer contained in the user's response does not match the inserted answer");
+        assertNull(list.get(0).getOtherText(), "Free response must be null");
+        
+        // Inserting of multiple responses
+        final int nbToInsert = 3;
+        for (final SurveyOption res : pickNRandom(responses, nbToInsert)) {
+            userResponsesDao.insertResponse(profession, res, null);
+            listOfResponses.add(res);
+        }
+        list = userResponsesDao.findAllByQuestion(profession.getId());
+        assertEquals(list.size(), (1 + nbToInsert), "The user's answer list must contain the previous answer + " + nbToInsert + " additional answers");
+        for (final UserResponse ur : list) {
+            assertEquals(ur.getQuestion(), profession, "The original question doesn't match with the Question object");
+            assertTrue(listOfResponses.contains(ur.getOption()), "The answer contained in the user's answer is not in the list of inserted answers");
+            assertEquals(ur.getOption().getQuestion(), profession, "The original question does not match with the Question object present in the Option object");
+            assertNull(ur.getOtherText(), "The text must be null if it is an answer from a choice");
+        }
+
+        final int newNbResponses = 1 + nbToInsert;
+        
+        // Insertion of a free choice answer
+        String other = "This is a free answer";
+        userResponsesDao.insertResponse(profession, null, other);
+        
+        list = userResponsesDao.findAllByQuestion(profession.getId());
+        assertEquals(list.size(), (newNbResponses + 1), "The user's response list must contain the " + newNbResponses + "+ the free answer");
+        for (final UserResponse ur : list) {
+            assertEquals(ur.getQuestion(), profession, "The original question does not match with the Question object in the user's response");
+            if (ur.getOtherText() != null) {    // free response
+                assertNull(ur.getOption(), "The Option object must be null if it is a free response");
+                assertEquals(ur.getOtherText(), other, "The text does not correspond to what was inserted in the base");
+            } else {    // predefined response
+                assertEquals(ur.getOption().getQuestion(), profession, "The original question does not match with the Question object present in the Option object");
+                assertNull(ur.getOtherText(), "The text must be null if it is an answer from a choice");
+            }
+        }
+        
+        final SurveyQuestion useCase = questions.stream().filter((q) -> q.getId() == 3).findFirst().get();
+        assertNotNull(useCase, "No object match the use cases question");
+        responses = responsesDao.findAllByQuestion(useCase.getId());
+        assertNotNull(responses, "No answer matches the use case question object");
+        assertFalse(responses.isEmpty(), "The answer list for the use case question object is empty");
+        
+        r = responses.get(ThreadLocalRandom.current().nextInt(0, responses.size() - 1));
+        userResponsesDao.insertResponse(useCase, r, null);
+        
+        list = userResponsesDao.findAllByQuestion(useCase.getId());
+        assertEquals(list.size(), 1, "The user answer list for the use case question must contain an item only");
+        assertEquals(list.get(0).getQuestion(), useCase, "The question in the user's response does not match the original question");
+        assertEquals(list.get(0).getOption(), r, "The answer contained in the user's response does not match the inserted answer");
+        assertNull(list.get(0).getOtherText(), "Free text must be null");
+        
+        other = other.concat(" for the question of use cases");
+        userResponsesDao.insertResponse(useCase, null, other);
+        
+        list = userResponsesDao.findAllByQuestion(useCase.getId());
+        assertEquals(list.size(), 2, "The user answer list for the use case question must contain two items");
+        for (final UserResponse ur : list) {
+            assertEquals(ur.getQuestion(), useCase, "The original question does not match with the Question object in the user's response");
+            if (ur.getOtherText() != null) {    // free response
+                assertNull(ur.getOption(), "The response object must be null if it is a free response");
+                assertEquals(ur.getOtherText(), other, "The text does not correspond to what was inserted in the base");
+            } else {    // predefined response
+                assertEquals(ur.getOption().getQuestion(), useCase, "The original question does not match with the Question object present in the Option object");
+                assertNull(ur.getOtherText(), "The text must be null if it is an answer from a choice");
+            }
+        }
+    }
+    @Test
+    public void setUserResponsesWithEmail() {
+        this.resetUserResponses();
+        // just testing insertion of email user, without insertion of responses (already tested)
+        final String email = "jane.doe@inrae.fr";
+        List<UserEmail> list = this.userEmailDao.getEmailAddressList();
+        assertNotNull(list, "The email list is not initialized");
+        assertTrue(list.isEmpty(), "The email list must be empty");
+        this.userEmailDao.insertEmailUserAddress(email, LocalDateTime.now());
+        
+        list = this.userEmailDao.getEmailAddressList();
+        assertEquals(list.size(), 1, "The email list must contain an item");
+        assertEquals(list.get(0).getEmail(), email, "The saved item does not match the inserted email « " + email + " »");
+    }
+    /**
+     * Return randomly responses list
+     * @param list original list
+     * @param nbElements to pick
+     * @return
+     */
+    private List<SurveyOption> pickNRandom(final List<SurveyOption> list, final int nbElements) {
+        final List<SurveyOption> copy = new ArrayList<SurveyOption>(list);
+        Collections.shuffle(copy);
+        if (nbElements > copy.size()) {
+            return copy.subList(0, copy.size());
+        } else {
+            return copy.subList(0, nbElements);
+        }
+    }
+    private void resetUserResponses() {
+        this.userResponsesDao.deleteAll();
+        this.userEmailDao.deleteAll();
+    }
+}
diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java
index abbdc2a8d396a34af4345cb37204c7789db8df6e..678149fe5e01e6cdc5f3e6d8d719f86f8c35a026 100644
--- a/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java
@@ -15,7 +15,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
 import fr.agrometinfo.www.server.I18n;
-import fr.agrometinfo.www.server.service.MailService;
+import fr.agrometinfo.www.server.service.MailServiceImpl;
 
 /**
  * Ensure all implementations of {@link ErrorType} are well defined.
@@ -33,7 +33,7 @@ public class ErrorTypeTest {
      * @return classes to test.
      */
     public static List<Class<? extends Enum<?>>> data() {
-        return Arrays.asList(MailService.MailErrorType.class);
+        return Arrays.asList(MailServiceImpl.MailErrorType.class);
     }
 
     /**
diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/rs/SurveyFormResourceTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/rs/SurveyFormResourceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c3a7f41a6ffec54a6e64f2bdcee36d410b11e98
--- /dev/null
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/rs/SurveyFormResourceTest.java
@@ -0,0 +1,247 @@
+package fr.agrometinfo.www.server.rs;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.jupiter.api.Test;
+
+
+import fr.agrometinfo.www.server.dao.SurveyQuestionDao;
+import fr.agrometinfo.www.server.dao.SurveyQuestionDaoHibernate;
+import fr.agrometinfo.www.server.dao.SurveyOptionDao;
+import fr.agrometinfo.www.server.dao.SurveyOptionDaoHibernate;
+import fr.agrometinfo.www.server.dao.UserEmailDao;
+import fr.agrometinfo.www.server.dao.UserEmailDaoHibernate;
+import fr.agrometinfo.www.server.dao.UserResponsesDao;
+import fr.agrometinfo.www.server.dao.UserResponsesDaoHibernate;
+import fr.agrometinfo.www.server.exception.AgroMetInfoException;
+import fr.agrometinfo.www.server.model.SurveyQuestion;
+import fr.agrometinfo.www.server.model.SurveyOption;
+import fr.agrometinfo.www.server.model.UserEmail;
+import fr.agrometinfo.www.server.model.UserResponse;
+import fr.agrometinfo.www.server.service.MailService;
+import fr.agrometinfo.www.server.service.MailServiceImpl;
+import fr.agrometinfo.www.server.service.MailServiceImpl.Mail;
+import fr.agrometinfo.www.shared.dto.SurveyResponseDTO;
+import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO;
+import fr.agrometinfo.www.shared.dto.SurveyOptionDTO;
+
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Form;
+import lombok.Getter;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Test survey form webservice.
+ * @author jdecome
+ *
+ */
+@Log4j2
+public class SurveyFormResourceTest extends JerseyTest {
+    /**
+     * Mock class for mail service.
+     * @author jdecome
+     *
+     */
+    public class MailServiceTest implements MailService {
+        /** Mail object. */
+        @Getter
+        private Mail mail = null;
+        @Override
+        public void sendSurveyFilled(SurveyResponseDTO data) {
+            this.mail = MailServiceImpl.createContentFromData(data);
+        }
+        
+        @Override
+        public void sendApplicationStarted() {
+            // do nothing
+        }
+        
+        @Override
+        public void send(Mail mail) throws AgroMetInfoException {
+            // do nothing
+        }
+        
+    }
+    /** Path separator. */
+    private static final String SEP = "/";
+    /** Parsing JSON to object. */
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    /** DAO for Questions. */
+    private final SurveyQuestionDao questionsDao = new SurveyQuestionDaoHibernate();
+    /** DAO for Responses. */
+    private final SurveyOptionDao responsesDao = new SurveyOptionDaoHibernate();
+    /** DAO for UserResponses. */
+    private final UserResponsesDao userResponsesDao = new UserResponsesDaoHibernate();
+    /** DAO for UserMail */
+    private final UserEmailDao userEmailDao = new UserEmailDaoHibernate();
+    /** Mail service. */
+    private final MailService mailService = new MailServiceTest();
+    
+    @Override
+    protected final Application configure() {
+        return new ResourceConfig(SurveyFormResource.class).register(new AbstractBinder() {
+            @Override
+            protected void configure() {
+                bind(questionsDao).to(SurveyQuestionDao.class);
+                bind(responsesDao).to(SurveyOptionDao.class);
+                bind(userResponsesDao).to(UserResponsesDao.class);
+                bind(userEmailDao).to(UserEmailDao.class);
+                bind(mailService).to(MailService.class);
+            }
+        });
+    }
+    /** Testing response retrieval from webservice. */
+    @Test
+    public void getResponses() {
+        try {
+            // get all questions from DAO for verification.
+            final List<SurveyQuestion> questions = questionsDao.findAll();
+            assertFalse(questions.isEmpty(), "Question list is empty");
+            
+            // Get all responses (options) by webservice
+            final String jsonAllResponses = target(SurveyFormResource.PATH + SEP + SurveyFormResource.PATH_RESPONSES_LIST).request().get(String.class);
+            assertNotNull(jsonAllResponses, "Unable to retrieve resource (null object) from responses");
+            assertFalse(jsonAllResponses.isEmpty(), "The JSON character string of the responses is empty");
+            
+            final List<SurveyOption> allResponses = objectMapper.readValue(jsonAllResponses, new TypeReference<List<SurveyOption>>() {});
+            assertNotNull(allResponses, "Conversion of JSON from responses to list failed");
+            assertFalse(allResponses.isEmpty(), "The answer list is empty");
+            
+            for (final SurveyOption r : allResponses) {
+                assertTrue(questions.contains(r.getQuestion()), "Question of the answer « " + r.getDescription() + " » is not contained in the list of questions");
+            }
+        } catch (final JsonProcessingException e) {
+            log.info(e.getMessage());
+        }
+    }
+    /** Test by setting answers for questions. */
+    @Test
+    public void testWithJsonResponses() {
+        this.resetUserResponses();
+        final List<SurveyQuestion> questionsList = questionsDao.findAll();
+        final List<SurveyQuestionDTO> questionsDTO = questionsList.stream().map(SurveyFormResource::toDto).toList();
+        
+        final List<SurveyOption> responsesList = new ArrayList<>();
+        final HashMap<Long, String> otherTextMap = new HashMap<>();
+        
+        for (final SurveyQuestion q : questionsList) {
+            final List<SurveyOption> responsesListForQuestion = responsesDao.findAllByQuestion(q.getId());
+            
+            // Getting a randomly list of response
+            final List<SurveyOption> randomList = this.pickNRandom(responsesListForQuestion, ThreadLocalRandom.current().nextInt(0, responsesListForQuestion.size() - 1));
+           
+            if (randomList.size() > 0) {
+                responsesList.addAll(randomList);
+            }
+            
+            otherTextMap.put(q.getId(), "Free answer for the question « " + q.getDescription() + " »");
+        }
+        final List<SurveyOptionDTO> responsesDTO = responsesList.stream().map(SurveyFormResource::toDto).toList();
+        
+        final SurveyResponseDTO data = new SurveyResponseDTO(questionsDTO, responsesDTO, otherTextMap);
+        data.setEmail("john.doe@inrae.fr");
+        assertEquals(data.getQuestions(), questionsDTO, "List of questions in LoginFormData object does not match with original list");
+        assertEquals(data.getResponses(), responsesDTO, "List of responses in LoginFormData object does not match with the original list");
+        assertEquals(data.getOtherTextMap(), otherTextMap, "List of free responses in LoginFormData object does not match with the original list");
+        
+        jakarta.ws.rs.core.Response ret = target(SurveyFormResource.PATH + SEP + SurveyFormResource.PATH_INSERT_RESPONSE)
+                .request(MediaType.APPLICATION_JSON).post(Entity.entity(data, MediaType.APPLICATION_JSON));
+        
+        assertEquals(ret.getStatus(), jakarta.ws.rs.core.Response.Status.OK.getStatusCode(), "The insert entry point should return status code 200 (OK)");
+        
+        // Retrieves all answers for all questions
+        List<UserResponse> list = new ArrayList<>();
+        for (final SurveyQuestion q : questionsDao.findAll()) {
+            final List<UserResponse> responsesByQuestion = userResponsesDao.findAllByQuestion(q.getId());
+            assertNotNull(responsesByQuestion, "The user's answer list for the question « " + q.getId() + " » must not be null");
+            responsesByQuestion.forEach((ur) -> {
+                assertEquals(ur.getQuestion(), q, "The question in the answer does not match the original question « " + q.getDescription() + " »");
+            });
+            list.addAll(responsesByQuestion);
+        }
+        
+        // Test on all answers
+        list.forEach((ur) -> {
+            assertNotNull(ur.getDateTime(), "The response timestamp must not be null");
+            assertTrue(ur.getId() > 0, "Reference of the user response must be greater than 0");
+            if (ur.getOption() != null) {
+                assertNotNull(ur.getOption(), "The user response, for a predefined response, must not be null");
+                assertNull(ur.getOtherText(), "The user's free response, for a predefined response, must be null");
+            } else {
+                assertNull(ur.getOption(), "The user's response, for a free response, must be null");
+                assertNotNull(ur.getOtherText(), "The user's free response, for a predefined response, must not be null");
+            }
+        });
+
+        // Check if all records has the same datetime
+        assertEquals(list.stream().map((u) -> u.getDateTime()).distinct().count(), 1, "Not all answers have the same datetime");
+        final LocalDateTime datetimeResponse = list.stream().map((u) -> u.getDateTime()).distinct().findFirst().get();
+        assertNotNull(datetimeResponse, "The datetime read in the responses is null");
+        
+        // in this case, email address has inserted
+        final List<UserEmail> emails = this.userEmailDao.getEmailAddressList();
+        assertNotNull(emails, "The email list is not initialized (= null)");
+        assertFalse(emails.isEmpty(), "Email list is empty");
+        
+        emails.forEach((e) -> {
+            assertTrue((e.getId() > 0), "The object id must be greater than 0");
+            assertNotNull(e.getDatetime(), "The timestamp of the question must not be null");
+            assertNotNull(e.getEmail(), "The email address contained in the subject must not be null");
+        });
+
+        assertEquals(emails.stream().map((e) -> e.getDatetime()).distinct().count(), 1, "Not all email addresses have the same datetime");
+        final LocalDateTime datetimeEmail = emails.stream().map((e) -> e.getDatetime()).distinct().findFirst().get();
+        assertNotNull(datetimeEmail, "The datetime read in emails is null");
+        
+        assertEquals(datetimeResponse, datetimeEmail, "The two datetimes are not identical");
+        
+        // Check mail sended to support, when user filled survey form
+        final Mail mail = ((MailServiceTest) this.mailService).getMail();
+        assertNotNull(mail, "The mail object must not be null");
+        assertNotNull(mail.getContent(), "Message body is not defined (= null)");
+        assertFalse(mail.getContent().isEmpty(), "The message body is empty");
+    }
+    /**
+     * 
+     * @param list origin list
+     * @param nbElements to pick in list
+     * @return randomized list with nbElements
+     */
+    private List<SurveyOption> pickNRandom(final List<SurveyOption> list, final int nbElements) {
+        final List<SurveyOption> copy = new ArrayList<SurveyOption>(list);
+        Collections.shuffle(copy);
+        if (nbElements > copy.size()) {
+            return copy.subList(0, copy.size());
+        } else {
+            return copy.subList(0, nbElements);
+        }
+    }
+    /**
+     * Reset tables.
+     */
+    private void resetUserResponses() {
+        this.userResponsesDao.deleteAll();
+        this.userEmailDao.deleteAll();
+    }
+}
diff --git a/www-server/src/test/resources/META-INF/persistence.xml b/www-server/src/test/resources/META-INF/persistence.xml
index 55e45d5f4a3201beda796c044bc38de54791015a..3a55dd0ce0dc6a93a4abb9fe34fb083dfb5a989d 100644
--- a/www-server/src/test/resources/META-INF/persistence.xml
+++ b/www-server/src/test/resources/META-INF/persistence.xml
@@ -18,6 +18,10 @@
     <class>fr.agrometinfo.www.server.model.PraDailyValue</class>
     <class>fr.agrometinfo.www.server.model.Region</class>
     <class>fr.agrometinfo.www.server.model.Simulation</class>
+    <class>fr.agrometinfo.www.server.model.SurveyQuestion</class>
+    <class>fr.agrometinfo.www.server.model.SurveyOption</class>
+    <class>fr.agrometinfo.www.server.model.UserResponse</class>
+    <class>fr.agrometinfo.www.server.model.UserEmail</class>
     <properties>
       <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:agrometinfo;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM '../sql/schema.types.h2.sql'\;RUNSCRIPT FROM '../sql/schema.tables.sql'\;RUNSCRIPT FROM '../sql/init_data.h2.sql';" />
       <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver" />
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyOptionDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyOptionDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..029b6dee2e6003c6a3c3ab0c23e358c03a521793
--- /dev/null
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyOptionDTO.java
@@ -0,0 +1,60 @@
+package fr.agrometinfo.www.shared.dto;
+
+import org.dominokit.jackson.annotation.JSONMapper;
+
+/**
+ *  DTO for options for survey form.
+ * @author jdecome
+ *
+ */
+@JSONMapper
+public class SurveyOptionDTO {
+    /**
+     * Reference of this response.
+     */
+    private long id;
+    /**
+     * DTO of question associated to response.
+     */
+    private SurveyQuestionDTO question;
+    /**
+     * Label of response.
+     */
+    private String description;
+    /**
+     * @return the responseRef
+     */
+    public long getId() {
+        return id;
+    }
+    /**
+     * @return the question
+     */
+    public SurveyQuestionDTO getQuestion() {
+        return question;
+    }
+    /**
+     * @return the fullname
+     */
+    public String getDescription() {
+        return description;
+    }
+    /**
+     * @param value the responseRef to set
+     */
+    public void setId(final long value) {
+        this.id = value;
+    }
+    /**
+     * @param dto the question to set
+     */
+    public void setQuestion(final SurveyQuestionDTO dto) {
+        this.question = dto;
+    }
+    /**
+     * @param value the fullname to set
+     */
+    public void setDescription(final String value) {
+        this.description = value;
+    }
+}
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyQuestionDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyQuestionDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..265f2c50b1cd2f56d7a92be27bf0f2985eb8a8d6
--- /dev/null
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyQuestionDTO.java
@@ -0,0 +1,67 @@
+package fr.agrometinfo.www.shared.dto;
+
+import java.util.Objects;
+
+import org.dominokit.jackson.annotation.JSONMapper;
+
+/**
+ * DTO for questions of survey form.
+ * @author jdecome
+ *
+ */
+@JSONMapper
+public class SurveyQuestionDTO {
+    /**
+     * Reference of question.
+     */
+    private long id;
+    /**
+     * Label of question.
+     */
+    private String description;
+    /**
+     * @return the questionRef
+     */
+    public long getId() {
+        return id;
+    }
+    /**
+     * @return the fullName
+     */
+    public String getDescription() {
+        return description;
+    }
+    /**
+     * @param value the questionRef to set
+     */
+    public void setId(final long value) {
+        this.id = value;
+    }
+    /**
+     * @param value the fullName to set
+     */
+    public void setDescription(final String value) {
+        this.description = value;
+    }
+    /** */
+    @Override
+    public int hashCode() {
+        return Objects.hash(description, id);
+    }
+    /** */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        SurveyQuestionDTO other = (SurveyQuestionDTO) obj;
+        return Objects.equals(description, other.description)
+                && id == other.id;
+    }
+}
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyResponseDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyResponseDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..67054b1849cda3224f499d0b9e833a2439244af7
--- /dev/null
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyResponseDTO.java
@@ -0,0 +1,97 @@
+package fr.agrometinfo.www.shared.dto;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class contains all data of survey form.
+ * @author jdecome
+ *
+ */
+public class SurveyResponseDTO {
+    /**
+     * List of login form questions.<br>
+     * This list questions contains available questions, obtained by {@link QuestionsDao}
+     */
+    private List<SurveyQuestionDTO> questions;
+    /**
+     * List of login form responses.<br>
+     * This list responses contains available responses, obtained by {@link ResponsesDao}
+     */
+    private List<SurveyOptionDTO> responses;
+    /**
+     * Storage, for each response, the other text, if specified.
+     */
+    private Map<Long, String> otherTextMap = new HashMap<>();
+    /**
+     * Email user.<br>
+     * Can be null if user don't provide it (by called {@link SurveyResponseDTO#setEmail(String)} method only).
+     */
+    private String email = null;
+    /**
+     * Default constructor.
+     */
+    public SurveyResponseDTO() {
+    }
+    /**
+     * Constructor.
+     * @param qList questions list
+     * @param rList responses list
+     * @param otherText otherText map
+     */
+    public SurveyResponseDTO(final List<SurveyQuestionDTO> qList,
+            final List<SurveyOptionDTO> rList, final Map<Long, String> otherText) {
+        this.questions = qList;
+        this.responses = rList;
+        this.otherTextMap = otherText;
+    }
+    /**
+     * @return the questions
+     */
+    public List<SurveyQuestionDTO> getQuestions() {
+        return questions;
+    }
+    /**
+     * @return the responses
+     */
+    public List<SurveyOptionDTO> getResponses() {
+        return responses;
+    }
+    /**
+     * @return the otherTextMap
+     */
+    public Map<Long, String> getOtherTextMap() {
+        return otherTextMap;
+    }
+    /**
+     * @param questionsList the questions to set
+     */
+    public void setQuestions(final List<SurveyQuestionDTO> questionsList) {
+        this.questions = questionsList;
+    }
+    /**
+     * @param responsesList the responses to set
+     */
+    public void setResponses(final List<SurveyOptionDTO> responsesList) {
+        this.responses = responsesList;
+    }
+    /**
+     * @param map the otherTextMap to set
+     */
+    public void setOtherTextMap(final HashMap<Long, String> map) {
+        this.otherTextMap = map;
+    }
+    /**
+     * @return the email
+     */
+    public String getEmail() {
+        return email;
+    }
+    /**
+     * @param value the email to set
+     */
+    public void setEmail(final String value) {
+        this.email = value;
+    }
+}
diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/service/SurveyFormService.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/service/SurveyFormService.java
new file mode 100644
index 0000000000000000000000000000000000000000..b41ab7070ac2253b662fb44ae89cf5dec0d8ac47
--- /dev/null
+++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/service/SurveyFormService.java
@@ -0,0 +1,43 @@
+package fr.agrometinfo.www.shared.service;
+
+import java.util.List;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+
+import org.dominokit.rest.shared.request.service.annotations.RequestFactory;
+import org.dominokit.rest.shared.request.service.annotations.RequestBody;
+
+import fr.agrometinfo.www.shared.dto.SurveyResponseDTO;
+import fr.agrometinfo.www.shared.dto.SurveyOptionDTO;
+
+/**
+ * Interface for survey form client server resource.
+ * @author jdecome
+ *
+ */
+@RequestFactory
+@Path(SurveyFormService.PATH)
+public interface SurveyFormService {
+    /** Service base path. */
+    String PATH = "survey";
+    /** Path for {@link SurveyFormService#getResponses}. */
+    String PATH_RESPONSES_LIST = "responses";
+    /** Path for {@link SurveyFormService#insertAllResponses(SurveyResponseDTO)}. */
+    String PATH_INSERT_RESPONSE = "insertResponses";
+    /**
+     * @return list of availables options for questions.
+     */
+    @GET
+    @Path(PATH_RESPONSES_LIST)
+    List<SurveyOptionDTO> getResponses();
+    /**
+     * Insert all responses of survey form.
+     * @param data object to insert
+     * @return return code
+     */
+    @POST
+    @Path(PATH_INSERT_RESPONSE)
+    String insertAllResponses(@RequestBody SurveyResponseDTO data);
+}