-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ISSUE-1163] Inject OpenPaaS user contacts into Tmail's auto-complete…
… database (#1215)
- Loading branch information
1 parent
8b403da
commit 40331b2
Showing
10 changed files
with
899 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 1 addition & 9 deletions
10
...tmail-third-party/openpaas/src/main/java/com/linagora/tmail/api/OpenPaasUserResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,8 @@ | ||
package com.linagora.tmail.api; | ||
|
||
import java.util.List; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
public record OpenPaasUserResponse(@JsonProperty("id") String id, | ||
@JsonProperty("firstname") String firstname, | ||
@JsonProperty("lastname") String lastname, | ||
@JsonProperty("preferredEmail") String preferredEmail, | ||
@JsonProperty("emails") List<String> emails, | ||
@JsonProperty("main_phone") String mainPhone, | ||
@JsonProperty("displayName") String displayName) { | ||
public record OpenPaasUserResponse(@JsonProperty("preferredEmail") String preferredEmail) { | ||
} |
19 changes: 19 additions & 0 deletions
19
...-party/openpaas/src/main/java/com/linagora/tmail/contact/ContactAddedRabbitMqMessage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.linagora.tmail.contact; | ||
|
||
import java.io.IOException; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
|
||
public record ContactAddedRabbitMqMessage(String bookId, String bookName, String contactId, | ||
String userId, JCardObject vcard) { | ||
|
||
private static final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
static ContactAddedRabbitMqMessage fromJSON(byte[] jsonBytes) { | ||
try { | ||
return objectMapper.readValue(jsonBytes, ContactAddedRabbitMqMessage.class); | ||
} catch (IOException e) { | ||
throw new RuntimeException("Failed to parse ContactAddedRabbitMqMessage", e); | ||
} | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
...kend/tmail-third-party/openpaas/src/main/java/com/linagora/tmail/contact/JCardObject.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package com.linagora.tmail.contact; | ||
|
||
import java.util.Optional; | ||
|
||
import org.apache.james.core.MailAddress; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
import com.google.common.base.Preconditions; | ||
import com.linagora.tmail.james.jmap.contact.ContactFields; | ||
|
||
@JsonDeserialize(using = JCardObjectDeserializer.class) | ||
public record JCardObject(Optional<String> fnOpt, Optional<MailAddress> emailOpt) { | ||
public static final Logger LOGGER = LoggerFactory.getLogger(JCardObject.class); | ||
|
||
public JCardObject { | ||
Preconditions.checkNotNull(fnOpt); | ||
Preconditions.checkNotNull(emailOpt); | ||
} | ||
|
||
/** | ||
* Purpose: To specify the formatted text corresponding to the name of | ||
* the object the vCard represents. | ||
* <p> | ||
* Example: Mr. John Q. Public\, Esq. | ||
*/ | ||
@Override | ||
public Optional<String> fnOpt() { | ||
return fnOpt; | ||
} | ||
|
||
/** | ||
* Purpose: To specify the electronic mail address for communication | ||
* with the object the vCard represents. | ||
* <p> | ||
* Example: [email protected] | ||
*/ | ||
@Override | ||
public Optional<MailAddress> emailOpt() { | ||
return emailOpt; | ||
} | ||
|
||
public Optional<ContactFields> asContactFields() { | ||
Optional<String> contactFullnameOpt = fnOpt(); | ||
Optional<MailAddress> contactMailAddressOpt = emailOpt(); | ||
|
||
if (contactMailAddressOpt.isEmpty()) { | ||
return Optional.empty(); | ||
} | ||
|
||
MailAddress contactMailAddress = contactMailAddressOpt.get(); | ||
String contactFullname = contactFullnameOpt.orElse(""); | ||
|
||
return Optional.of(new ContactFields(contactMailAddress, contactFullname, "")); | ||
} | ||
} |
93 changes: 93 additions & 0 deletions
93
...hird-party/openpaas/src/main/java/com/linagora/tmail/contact/JCardObjectDeserializer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package com.linagora.tmail.contact; | ||
|
||
import java.io.IOException; | ||
import java.util.Iterator; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.stream.Collectors; | ||
|
||
import jakarta.mail.internet.AddressException; | ||
|
||
import org.apache.commons.lang3.tuple.ImmutablePair; | ||
import org.apache.commons.lang3.tuple.Pair; | ||
import org.apache.james.core.MailAddress; | ||
import org.apache.james.util.streams.Iterators; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import com.fasterxml.jackson.core.JsonParser; | ||
import com.fasterxml.jackson.databind.DeserializationContext; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer; | ||
|
||
|
||
public class JCardObjectDeserializer extends StdDeserializer<JCardObject> { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(JCardObjectDeserializer.class); | ||
|
||
private static final String FN = "fn"; | ||
private static final String EMAIL = "email"; | ||
private static final Set<String> SUPPORTED_PROPERTY_NAMES = Set.of(FN, EMAIL); | ||
private static final int PROPERTY_NAME_INDEX = 0; | ||
private static final int PROPERTIES_ARRAY_INDEX = 1; | ||
private static final int TEXT_PROPERTY_VALUE_INDEX = 3; | ||
|
||
public JCardObjectDeserializer() { | ||
this(null); | ||
} | ||
|
||
protected JCardObjectDeserializer(Class<?> vc) { | ||
super(vc); | ||
} | ||
|
||
@Override | ||
public JCardObject deserialize(JsonParser p, DeserializationContext ctxt) | ||
throws IOException { | ||
JsonNode node = p.getCodec().readTree(p); | ||
|
||
JsonNode jCardPropertiesArray = node.get(PROPERTIES_ARRAY_INDEX); | ||
Map<String, String> jCardProperties = | ||
collectJCardProperties(jCardPropertiesArray.iterator()); | ||
|
||
if (!jCardProperties.containsKey(FN)) { | ||
String json = node.toString(); | ||
LOGGER.warn(""" | ||
Missing 'fn' property in the provided JCard object. 'fn' is required according to the specifications. | ||
Received data: {}. | ||
Ensure the 'fn' property is present and correctly formatted.""", json); | ||
} | ||
|
||
Optional<MailAddress> maybeMailAddress = getOptionalFromMap(jCardProperties, EMAIL) | ||
.flatMap(email -> { | ||
try { | ||
return Optional.of(new MailAddress(email)); | ||
} catch (AddressException e) { | ||
LOGGER.error("Invalid contact mail address '{}' found in JCard Object", email, e); | ||
return Optional.empty(); | ||
} | ||
}); | ||
|
||
return new JCardObject(getOptionalFromMap(jCardProperties, FN), maybeMailAddress); | ||
} | ||
|
||
private static Map<String, String> collectJCardProperties(Iterator<JsonNode> propertiesIterator) { | ||
return Iterators.toStream(propertiesIterator) | ||
.map(JCardObjectDeserializer::getPropertyKeyValuePair) | ||
.flatMap(Optional::stream) | ||
.collect(Collectors.toMap(Pair::getKey, Pair::getValue)); | ||
} | ||
|
||
private static Optional<ImmutablePair<String, String>> getPropertyKeyValuePair(JsonNode propertyNode) { | ||
String propertyName = propertyNode.get(PROPERTY_NAME_INDEX).asText(); | ||
if (SUPPORTED_PROPERTY_NAMES.contains(propertyName)) { | ||
String propertyValue = propertyNode.get(TEXT_PROPERTY_VALUE_INDEX).asText(); | ||
return Optional.of(ImmutablePair.of(propertyName, propertyValue)); | ||
} else { | ||
return Optional.empty(); | ||
} | ||
} | ||
|
||
private Optional<String> getOptionalFromMap(Map<String, String> map, String key) { | ||
return Optional.ofNullable(map.getOrDefault(key, null)); | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
...ird-party/openpaas/src/main/java/com/linagora/tmail/contact/OpenPaasContactsConsumer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package com.linagora.tmail.contact; | ||
|
||
import static org.apache.james.backends.rabbitmq.Constants.DURABLE; | ||
import static org.apache.james.backends.rabbitmq.Constants.EMPTY_ROUTING_KEY; | ||
|
||
import java.io.Closeable; | ||
import java.util.Optional; | ||
|
||
import jakarta.inject.Inject; | ||
|
||
import org.apache.james.backends.rabbitmq.RabbitMQConfiguration; | ||
import org.apache.james.backends.rabbitmq.ReactorRabbitMQChannelPool; | ||
import org.apache.james.backends.rabbitmq.ReceiverProvider; | ||
import org.apache.james.core.MailAddress; | ||
import org.apache.james.core.Username; | ||
import org.apache.james.jmap.api.model.AccountId; | ||
import org.apache.james.lifecycle.api.Startable; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.linagora.tmail.api.OpenPaasRestClient; | ||
import com.linagora.tmail.james.jmap.contact.ContactFields; | ||
import com.linagora.tmail.james.jmap.contact.ContactNotFoundException; | ||
import com.linagora.tmail.james.jmap.contact.EmailAddressContact; | ||
import com.linagora.tmail.james.jmap.contact.EmailAddressContactSearchEngine; | ||
import com.rabbitmq.client.BuiltinExchangeType; | ||
|
||
import reactor.core.Disposable; | ||
import reactor.core.publisher.Flux; | ||
import reactor.core.publisher.Mono; | ||
import reactor.rabbitmq.AcknowledgableDelivery; | ||
import reactor.rabbitmq.BindingSpecification; | ||
import reactor.rabbitmq.ExchangeSpecification; | ||
import reactor.rabbitmq.QueueSpecification; | ||
import reactor.rabbitmq.Receiver; | ||
import reactor.rabbitmq.Sender; | ||
|
||
|
||
public class OpenPaasContactsConsumer implements Startable, Closeable { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenPaasContactsConsumer.class); | ||
private static final boolean REQUEUE_ON_NACK = true; | ||
public static final String EXCHANGE_NAME = "contacts:contact:add"; | ||
public static final String QUEUE_NAME = "openpaas-contacts-queue"; | ||
public static final String DEAD_LETTER = QUEUE_NAME + "-dead-letter"; | ||
|
||
private final ReceiverProvider receiverProvider; | ||
private final Sender sender; | ||
private final RabbitMQConfiguration commonRabbitMQConfiguration; | ||
private final EmailAddressContactSearchEngine contactSearchEngine; | ||
private final ObjectMapper objectMapper = new ObjectMapper(); | ||
private final OpenPaasRestClient openPaasRestClient; | ||
private Disposable consumeContactsDisposable; | ||
|
||
@Inject | ||
public OpenPaasContactsConsumer(ReactorRabbitMQChannelPool channelPool, | ||
RabbitMQConfiguration commonRabbitMQConfiguration, | ||
EmailAddressContactSearchEngine contactSearchEngine, | ||
OpenPaasRestClient openPaasRestClient) { | ||
this.receiverProvider = channelPool::createReceiver; | ||
this.sender = channelPool.getSender(); | ||
this.commonRabbitMQConfiguration = commonRabbitMQConfiguration; | ||
this.contactSearchEngine = contactSearchEngine; | ||
this.openPaasRestClient = openPaasRestClient; | ||
} | ||
|
||
public void start() { | ||
Flux.concat( | ||
sender.declareExchange(ExchangeSpecification.exchange(EXCHANGE_NAME) | ||
.durable(DURABLE).type(BuiltinExchangeType.FANOUT.getType())), | ||
sender.declareQueue(QueueSpecification | ||
.queue(DEAD_LETTER) | ||
.durable(DURABLE) | ||
.arguments(commonRabbitMQConfiguration.workQueueArgumentsBuilder().build())), | ||
sender.declareQueue(QueueSpecification | ||
.queue(QUEUE_NAME) | ||
.durable(DURABLE) | ||
.arguments(commonRabbitMQConfiguration.workQueueArgumentsBuilder() | ||
.deadLetter(DEAD_LETTER) | ||
.build())), | ||
sender.bind(BindingSpecification.binding() | ||
.exchange(EXCHANGE_NAME) | ||
.queue(QUEUE_NAME) | ||
.routingKey(EMPTY_ROUTING_KEY))) | ||
.then() | ||
.block(); | ||
|
||
consumeContactsDisposable = doConsumeContactMessages(); | ||
} | ||
|
||
private Disposable doConsumeContactMessages() { | ||
return delivery() | ||
.flatMap(delivery -> messageConsume(delivery, delivery.getBody())) | ||
.subscribe(); | ||
} | ||
|
||
public Flux<AcknowledgableDelivery> delivery() { | ||
return Flux.using(receiverProvider::createReceiver, | ||
receiver -> receiver.consumeManualAck(QUEUE_NAME), | ||
Receiver::close); | ||
} | ||
|
||
private Mono<EmailAddressContact> messageConsume(AcknowledgableDelivery ackDelivery, byte[] messagePayload) { | ||
return Mono.just(messagePayload) | ||
.map(ContactAddedRabbitMqMessage::fromJSON) | ||
.flatMap(this::handleMessage) | ||
.doOnSuccess(result -> { | ||
LOGGER.debug("Consumed contact successfully '{}'", result); | ||
ackDelivery.ack(); | ||
}) | ||
.onErrorResume(error -> { | ||
LOGGER.error("Error when consume message", error); | ||
ackDelivery.nack(!REQUEUE_ON_NACK); | ||
return Mono.empty(); | ||
}); | ||
} | ||
|
||
private Mono<EmailAddressContact> handleMessage(ContactAddedRabbitMqMessage contactAddedMessage) { | ||
LOGGER.trace("Consumed jCard object message: {}", contactAddedMessage); | ||
Optional<ContactFields> maybeContactFields = contactAddedMessage.vcard().asContactFields(); | ||
return maybeContactFields.map( | ||
openPaasContact -> openPaasRestClient.retrieveMailAddress(contactAddedMessage.userId()) | ||
.map(this::getAccountIdFromMailAddress) | ||
.flatMap(ownerAccountId -> indexContactIfNeeded(ownerAccountId, openPaasContact))) | ||
.orElse(Mono.empty()); | ||
} | ||
|
||
private Mono<EmailAddressContact> indexContactIfNeeded(AccountId ownerAccountId, ContactFields openPaasContact) { | ||
return Mono.from(contactSearchEngine.get(ownerAccountId, openPaasContact.address())) | ||
.onErrorResume(ContactNotFoundException.class, e -> Mono.empty()) | ||
.switchIfEmpty( | ||
Mono.from(contactSearchEngine.index(ownerAccountId, openPaasContact))) | ||
.flatMap(existingContact -> { | ||
if (!openPaasContact.firstname().isBlank()) { | ||
return Mono.from(contactSearchEngine.index(ownerAccountId, openPaasContact)); | ||
} else { | ||
return Mono.empty(); | ||
} | ||
}); | ||
} | ||
|
||
private AccountId getAccountIdFromMailAddress(MailAddress mailAddress) { | ||
return AccountId.fromUsername(Username.fromMailAddress(mailAddress)); | ||
} | ||
|
||
@Override | ||
public void close() { | ||
Optional.ofNullable(consumeContactsDisposable) | ||
.ifPresent(Disposable::dispose); | ||
} | ||
} |
Oops, something went wrong.