diff --git a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/ModelUtils.java b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/ModelUtils.java
index 06f2cdd8cfa1a15acf871e277564f4ba13f9c5b7..5b02435947e36f13f768df22eaa7795b18208e62 100644
--- a/duniter4j-core-client/src/main/java/org/duniter/core/client/model/ModelUtils.java
+++ b/duniter4j-core-client/src/main/java/org/duniter/core/client/model/ModelUtils.java
@@ -145,16 +145,20 @@ public class ModelUtils {
         return pubkey.substring(0, 8);
     }
 
-    public static String joinPubkeys(Collection<String> pubkeys, boolean minify, String separator) {
-        Preconditions.checkArgument(CollectionUtils.isNotEmpty(pubkeys));
-        Preconditions.checkNotNull(separator);
+    public static String joinPubkeys(Set<String> pubkeys, String separator, boolean minify) {
+        Preconditions.checkNotNull(pubkeys);
+        Preconditions.checkArgument(pubkeys.size()>0);
+        if (pubkeys.size() == 1) {
+            String pubkey = pubkeys.iterator().next();
+            return (minify ? ModelUtils.minifyPubkey(pubkey) : pubkey);
+        }
 
         StringBuilder sb = new StringBuilder();
-        for (String pubkey : pubkeys) {
-            sb.append(separator)
-                    .append(minify ? ModelUtils.minifyPubkey(pubkey) : pubkey);
-        }
+        pubkeys.stream().forEach((pubkey)-> {
+            sb.append(separator);
+            sb.append(minify ? ModelUtils.minifyPubkey(pubkey) : pubkey);
+        });
 
-        return sb.toString().substring(separator.length());
+        return sb.substring(separator.length());
     }
 }
diff --git a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java
index dce12461e98206e121c5d4ea4f05ffe09b341be7..852643dd1e453ee1a986fdee64d583bb5c4ab981 100644
--- a/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java
+++ b/duniter4j-es-core/src/main/java/org/duniter/elasticsearch/service/AbstractService.java
@@ -233,7 +233,7 @@ public abstract class AbstractService implements Bean {
 
 
     protected String getIssuer(JsonNode actualObj) {
-        return  actualObj.get(Record.PROPERTY_ISSUER).asText();
+        return  getMandatoryField(actualObj, Record.PROPERTY_ISSUER).asText();
     }
 
     protected JsonNode getMandatoryField(JsonNode actualObj, String fieldName) {
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
index e1a5053e4e7c490f9c89d71b62a9dd5aa39157b0..7f7ac356cc002e5824eb1a3f6a9a0c60c10dc24c 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/model/UserEventCodes.java
@@ -38,6 +38,9 @@ public enum UserEventCodes {
 
     // TX
     TX_SENT,
-    TX_RECEIVED
+    TX_RECEIVED,
+
+    // Message
+    MESSAGE_RECEIVED
 
 }
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java
index 690a76bce948fb867562e2ffa1af324b45a6273d..dec3f83554d0fbb80b3a5f2c71139a60801299be 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/BlockchainUserEventService.java
@@ -204,8 +204,8 @@ public class BlockchainUserEventService extends AbstractService implements Chang
         Set<String> senders = ImmutableSet.copyOf(tx.getIssuers());
 
         // Received
-        String senderNames = getNamesFromPubkeys(senders, true);
-        String sendersPubkeys = joinPubkeys(senders, false);
+        String senderNames = userService.joinNamesFromPubkeys(senders, DEFAULT_PUBKEYS_SEPARATOR, true);
+        String sendersPubkeys = ModelUtils.joinPubkeys(senders, DEFAULT_PUBKEYS_SEPARATOR, false);
         Set<String> receivers = new HashSet<>();
         for (String output : tx.getOutputs()) {
             String[] parts = output.split(":");
@@ -220,8 +220,8 @@ public class BlockchainUserEventService extends AbstractService implements Chang
 
         // Sent
         if (CollectionUtils.isNotEmpty(receivers)) {
-            String receiverNames = getNamesFromPubkeys(receivers, true);
-            String receiverPubkeys = joinPubkeys(receivers, false);
+            String receiverNames = userService.joinNamesFromPubkeys(receivers, DEFAULT_PUBKEYS_SEPARATOR, true);
+            String receiverPubkeys = ModelUtils.joinPubkeys(receivers, DEFAULT_PUBKEYS_SEPARATOR, false);
             for (String sender : senders) {
                 notifyUserEvent(block, sender, UserEventCodes.TX_SENT, I18n.n("duniter.user.event.tx.sent"), receiverPubkeys, receiverNames);
             }
@@ -249,42 +249,5 @@ public class BlockchainUserEventService extends AbstractService implements Chang
         userEventService.deleteEventsByReference(new UserEvent.Reference(change.getIndex(), change.getType(), change.getId()));
     }
 
-    private String getNamesFromPubkeys(Set<String> pubkeys, boolean minify) {
-        Preconditions.checkNotNull(pubkeys);
-        Preconditions.checkArgument(pubkeys.size()>0);
-        if (pubkeys.size() == 1) {
-            String pubkey = pubkeys.iterator().next();
-            String title = userService.getProfileTitle(pubkey);
-            return title != null ? title :
-                    (minify ? ModelUtils.minifyPubkey(pubkey) : pubkey);
-        }
-
-        Map<String, String> profileTitles = userService.getProfileTitles(pubkeys);
-        StringBuilder sb = new StringBuilder();
-        pubkeys.stream().forEach((pubkey)-> {
-            String title = profileTitles != null ? profileTitles.get(pubkey) : null;
-            sb.append(DEFAULT_PUBKEYS_SEPARATOR);
-            sb.append(title != null ? title :
-                    (minify ? ModelUtils.minifyPubkey(pubkey) : pubkey));
-        });
-
-        return sb.substring(DEFAULT_PUBKEYS_SEPARATOR.length());
-    }
 
-    private String joinPubkeys(Set<String> pubkeys, boolean minify) {
-        Preconditions.checkNotNull(pubkeys);
-        Preconditions.checkArgument(pubkeys.size()>0);
-        if (pubkeys.size() == 1) {
-            String pubkey = pubkeys.iterator().next();
-            return (minify ? ModelUtils.minifyPubkey(pubkey) : pubkey);
-        }
-
-        StringBuilder sb = new StringBuilder();
-        pubkeys.stream().forEach((pubkey)-> {
-            sb.append(DEFAULT_PUBKEYS_SEPARATOR);
-            sb.append(minify ? ModelUtils.minifyPubkey(pubkey) : pubkey);
-        });
-
-        return sb.substring(DEFAULT_PUBKEYS_SEPARATOR.length());
-    }
 }
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java
index 78053edf2bbeeffc15ab8fc79899c00e657e9925..e6cd1aa07b5508576f2b4c88efbe155804292366 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/MessageService.java
@@ -25,14 +25,17 @@ package org.duniter.elasticsearch.user.service;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
+import org.duniter.core.client.model.ModelUtils;
 import org.duniter.core.client.model.elasticsearch.Record;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.service.CryptoService;
 import org.duniter.elasticsearch.PluginSettings;
 import org.duniter.elasticsearch.exception.InvalidSignatureException;
 import org.duniter.elasticsearch.service.AbstractService;
+import org.duniter.elasticsearch.service.BlockchainService;
 import org.duniter.elasticsearch.user.model.Message;
 import org.duniter.elasticsearch.user.model.UserEvent;
+import org.duniter.elasticsearch.user.model.UserEventCodes;
 import org.elasticsearch.action.admin.indices.create.CreateIndexRequestBuilder;
 import org.elasticsearch.action.index.IndexResponse;
 import org.elasticsearch.action.update.UpdateRequestBuilder;
@@ -41,6 +44,7 @@ import org.elasticsearch.common.inject.Inject;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
+import org.nuiton.i18n.I18n;
 
 import java.io.IOException;
 import java.util.Map;
@@ -54,10 +58,13 @@ public class MessageService extends AbstractService {
     public static final String RECORD_TYPE = "record";
     public static final String OUTBOX_TYPE = "outbox";
 
+    private final UserEventService userEventService;
 
     @Inject
-    public MessageService(Client client, PluginSettings settings, CryptoService cryptoService, UserService userService) {
+    public MessageService(Client client, PluginSettings settings,
+                          CryptoService cryptoService, UserEventService userEventService) {
         super("duniter." + INDEX, client, settings, cryptoService);
+        this.userEventService = userEventService;
     }
 
     /**
@@ -114,6 +121,8 @@ public class MessageService extends AbstractService {
 
         JsonNode actualObj = readAndVerifyIssuerSignature(recordJson);
         String issuer = getIssuer(actualObj);
+        String recipient = getMandatoryField(actualObj, Message.PROPERTY_RECIPIENT).asText();
+        Long time = getMandatoryField(actualObj, Message.PROPERTY_TIME).asLong();
 
         if (logger.isDebugEnabled()) {
             logger.debug(String.format("Indexing a message from issuer [%s]", issuer.substring(0, 8)));
@@ -124,7 +133,17 @@ public class MessageService extends AbstractService {
                 .setRefresh(false)
                 .execute().actionGet();
 
-        return response.getId();
+        String messageId = response.getId();
+
+        // Notify recipient
+        userEventService.notifyUser(UserEvent.newBuilder(UserEvent.EventType.INFO, UserEventCodes.MESSAGE_RECEIVED.name())
+                .setRecipient(recipient)
+                .setMessage(I18n.n("duniter.user.event.message.received"), issuer, ModelUtils.minifyPubkey(issuer))
+                .setTime(time)
+                .setReference(INDEX, RECORD_TYPE, messageId)
+                .build());
+
+        return messageId;
     }
 
     public String indexOuboxFromJson(String recordJson) {
diff --git a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
index 003d7089cf60b677850f2eb97cffeb94272aa248..96b162ccce719b308f733a0925b23e24140191f9 100644
--- a/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
+++ b/duniter4j-es-user/src/main/java/org/duniter/elasticsearch/user/service/UserService.java
@@ -25,7 +25,9 @@ package org.duniter.elasticsearch.user.service;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.base.Preconditions;
 import org.apache.commons.collections4.MapUtils;
+import org.duniter.core.client.model.ModelUtils;
 import org.duniter.core.client.model.elasticsearch.UserProfile;
 import org.duniter.core.exception.TechnicalException;
 import org.duniter.core.service.CryptoService;
@@ -214,6 +216,29 @@ public class UserService extends AbstractService {
         return result;
     }
 
+    public String joinNamesFromPubkeys(Set<String> pubkeys, String separator, boolean minify) {
+        Preconditions.checkNotNull(pubkeys);
+        Preconditions.checkNotNull(separator);
+        Preconditions.checkArgument(pubkeys.size()>0);
+        if (pubkeys.size() == 1) {
+            String pubkey = pubkeys.iterator().next();
+            String title = getProfileTitle(pubkey);
+            return title != null ? title :
+                    (minify ? ModelUtils.minifyPubkey(pubkey) : pubkey);
+        }
+
+        Map<String, String> profileTitles = getProfileTitles(pubkeys);
+        StringBuilder sb = new StringBuilder();
+        pubkeys.stream().forEach((pubkey)-> {
+            String title = profileTitles != null ? profileTitles.get(pubkey) : null;
+            sb.append(separator);
+            sb.append(title != null ? title :
+                    (minify ? ModelUtils.minifyPubkey(pubkey) : pubkey));
+        });
+
+        return sb.substring(separator.length());
+    }
+
     /* -- Internal methods -- */
 
 
diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties
index 4917ad91be8c20d95f6a655b90487d15f1721ef0..d5b454c0ffd555e237b519485c09d4f238ba9418 100644
--- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties
+++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_en_GB.properties
@@ -4,6 +4,7 @@ duniter.event.NODE_STARTED=Node started on cluster Duniter4j ES [%s]
 duniter.user.event.active=
 duniter.user.event.join=
 duniter.user.event.leave=
+duniter.user.event.message.received=
 duniter.user.event.ms.active=
 duniter.user.event.ms.join=
 duniter.user.event.ms.leave=
diff --git a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties
index 03e86fc3ccb28cef89307ea245e4c383f9dbdd68..8e74a2b7152d1c12ceee321cf20dfd7ceea6501e 100644
--- a/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties
+++ b/duniter4j-es-user/src/main/resources/i18n/duniter4j-es-user_fr_FR.properties
@@ -1,6 +1,7 @@
 duniter.event.NODE_BMA_DOWN=Noeud Duniter [%1$s\:%2$s] non joignable, depuis le noeud ES API [%3$s]. Dernière connexion à %4$d. Indexation de blockchain en attente.
 duniter.event.NODE_BMA_UP=Noeud Duniter [%1$s\:%2$s] à nouveau accessible.
 duniter.event.NODE_STARTED=Noeud ES API démarré sur le cluster Duniter [%1$s]
+duniter.user.event.message.received=Vous avez reçu un message de %2$s
 duniter.user.event.ms.active=Votre adhésion comme membre a bien été renouvellée
 duniter.user.event.ms.join=Vous êtes maintenant membre de la monnaie
 duniter.user.event.ms.leave=Votre adhésion comme membre à expirée