Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce the CalendarEventAttendance/get JMAP method #1447

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.linagora.tmail.james;

import static org.apache.james.data.UsersRepositoryModuleChooser.Implementation.DEFAULT;

import org.apache.james.JamesServerBuilder;
import org.apache.james.JamesServerExtension;
import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbeModule;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.linagora.tmail.james.app.MemoryConfiguration;
import com.linagora.tmail.james.app.MemoryServer;
import com.linagora.tmail.james.common.LinagoraCalendarEventAcceptMethodContract;
import com.linagora.tmail.james.common.LinagoraCalendarEventAttendanceGetMethodContract;
import com.linagora.tmail.james.jmap.firebase.FirebaseModuleChooserConfiguration;
import com.linagora.tmail.module.LinagoraTestJMAPServerModule;

import java.util.UUID;

public class MemoryLinagoraCalendarEventAttendanceGetMethodTest implements
LinagoraCalendarEventAttendanceGetMethodContract {

@RegisterExtension
static JamesServerExtension
jamesServerExtension = new JamesServerBuilder<MemoryConfiguration>(tmpDir ->
MemoryConfiguration.builder()
.workingDirectory(tmpDir)
.configurationFromClasspath()
.usersRepository(DEFAULT)
.firebaseModuleChooserConfiguration(FirebaseModuleChooserConfiguration.DISABLED)
.build())
.server(configuration -> MemoryServer.createServer(configuration)
.overrideWith(new LinagoraTestJMAPServerModule(), new DelegationProbeModule()))
.build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

import org.apache.james.core.Username;
import org.apache.james.jmap.mail.BlobIds;
import org.apache.james.mailbox.model.MessageId;
import org.reactivestreams.Publisher;

import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceResults;
import com.linagora.tmail.james.jmap.model.CalendarEventReplyResults;
import com.linagora.tmail.james.jmap.model.LanguageLocation;

public interface EventAttendanceRepository {
Publisher<AttendanceStatus> getAttendanceStatus(Username username, MessageId messageId);
Publisher<CalendarEventAttendanceResults> getAttendanceStatus(Username username, BlobIds calendarEventBlobIds);

Publisher<CalendarEventReplyResults> setAttendanceStatus(Username username, AttendanceStatus attendanceStatus,
BlobIds eventBlobIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import scala.util.Failure;
import scala.util.Success;
import scala.util.Try;

public final class MessagePartBlobId {
private static final Pattern MESSAGE_PART_BLOB_ID_PATTERN =
Expand Down Expand Up @@ -40,6 +43,14 @@ public MessagePartBlobId(String value) {
.toList();
}

public static Try<MessagePartBlobId> tryParse(String blobId) {
try {
return new Success<>(new MessagePartBlobId(blobId));
} catch (Exception e) {
return new Failure<>(e);
}
}

public String getMessageId() {
return messageId;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import org.apache.james.core.Username;
import org.apache.james.jmap.core.AccountId;
import org.apache.james.jmap.mail.BlobId;
import org.apache.james.jmap.mail.BlobIds;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageIdManager;
Expand All @@ -20,7 +21,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceResults;
import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceResults$;
import com.linagora.tmail.james.jmap.method.CalendarEventReplyPerformer;
import com.linagora.tmail.james.jmap.method.EventAttendanceStatusEntry;
import com.linagora.tmail.james.jmap.model.CalendarEventReplyRequest;
import com.linagora.tmail.james.jmap.model.CalendarEventReplyResults;
import com.linagora.tmail.james.jmap.model.LanguageLocation;
Expand Down Expand Up @@ -48,21 +52,44 @@ public StandaloneEventAttendanceRepository(MessageIdManager messageIdManager, Se
}

@Override
public Publisher<AttendanceStatus> getAttendanceStatus(Username username, MessageId messageId) {
LOGGER.trace("Getting attendance status for user '{}' and message '{}'", username, messageId);
public Publisher<CalendarEventAttendanceResults> getAttendanceStatus(Username username, BlobIds calendarEventBlobIds) {
LOGGER.trace("Getting attendance status for user '{}' and message '{}'", username,
calendarEventBlobIds);
MailboxSession systemMailboxSession = sessionProvider.createSystemSession(username);

return getFlags(messageId, systemMailboxSession)
.flatMap(userFlags -> Mono.justOrEmpty(AttendanceStatus.fromMessageFlags(userFlags)))
.switchIfEmpty(handleMissingEventAttendanceFlag(messageId));
return Flux.fromIterable(JavaConverters.seqAsJavaList(calendarEventBlobIds.value()))
.flatMap(blobId -> getAttendanceStatusFromEventBlob(blobId.value(), systemMailboxSession))
.reduce(CalendarEventAttendanceResults$.MODULE$.empty(), CalendarEventAttendanceResults$.MODULE$::merge);
}

private Mono<CalendarEventAttendanceResults> getAttendanceStatusFromEventBlob(String blobId, MailboxSession systemMailboxSession) {
return extractMessageId(blobId)
.flatMap(messageId ->
Mono.fromDirect(messageIdManager.getMessagesReactive(List.of(messageId), FetchGroup.MINIMAL, systemMailboxSession))
.flatMap(messageResult -> getAttendanceStatusFromMessage(messageResult, blobId))
.defaultIfEmpty(CalendarEventAttendanceResults$.MODULE$.notFound(BlobId.of(blobId).get())))
.onErrorResume(Exception.class, (error) ->
Mono.just(CalendarEventAttendanceResults$.MODULE$.notDone(
BlobId.of(blobId).get(), error, systemMailboxSession)));
}

private Mono<AttendanceStatus> handleMissingEventAttendanceFlag(MessageId messageId) {
private Mono<CalendarEventAttendanceResults> getAttendanceStatusFromMessage(MessageResult messageResult, String blobId) {
return Mono.just(messageResult.getFlags())
.flatMap(userFlags ->
Mono.justOrEmpty(AttendanceStatus.fromMessageFlags(userFlags))
.map(attendanceStatus ->
CalendarEventAttendanceResults$.MODULE$.done(
new EventAttendanceStatusEntry(blobId, attendanceStatus))))
.defaultIfEmpty(handleMissingEventAttendanceFlag(blobId));
}

private CalendarEventAttendanceResults handleMissingEventAttendanceFlag(String blobId) {
LOGGER.debug("""
No event attendance flag found for message {}.
No event attendance flag found for blob: {}.
Defaulting to NeedsAction
""", messageId);
return Mono.just(AttendanceStatus.NeedsAction);
""", blobId);
return CalendarEventAttendanceResults$.MODULE$.done(
new EventAttendanceStatusEntry(blobId, AttendanceStatus.NeedsAction));
}

@Override
Expand All @@ -72,7 +99,7 @@ public Publisher<CalendarEventReplyResults> setAttendanceStatus(Username usernam
MailboxSession systemMailboxSession = sessionProvider.createSystemSession(username);

return Flux.fromIterable(JavaConverters.seqAsJavaList(calendarEventBlobIds.value()))
.map(blobId -> extractMessageId(blobId.value()))
.flatMap(blobId -> extractMessageId(blobId.value()))
.onErrorContinue((throwable, o) -> LOGGER.debug("Failed to extract message id from blob id: {}", o, throwable))
.collectList()
.flatMap(messageIds ->
Expand All @@ -85,8 +112,12 @@ public Publisher<CalendarEventReplyResults> setAttendanceStatus(Username usernam
.then(tryToSendReplyEmail(username, calendarEventBlobIds, maybePreferredLanguage, systemMailboxSession, attendanceStatus));
}

private MessageId extractMessageId(String blobId) {
return messageIdFactory.fromString(new MessagePartBlobId(blobId).getMessageId());
private Mono<MessageId> extractMessageId(String blobId) {
return Mono.fromCallable(() ->
MessagePartBlobId.tryParse(blobId)
.map(MessagePartBlobId::getMessageId)
.map(messageIdFactory::fromString)
.get());
}

private Mono<CalendarEventReplyResults> tryToSendReplyEmail(Username username,
Expand Down Expand Up @@ -138,9 +169,4 @@ private Flux<Void> updateEventAttendanceFlags(MessageResult message, AttendanceS
session)
);
}

private Flux<Flags> getFlags(MessageId messageId, MailboxSession session) {
return Flux.from(messageIdManager.getMessagesReactive(List.of(messageId), FetchGroup.MINIMAL, session))
.map(MessageResult::getFlags);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.linagora.tmail.james.jmap.json

import com.linagora.tmail.james.jmap.method.{CalendarEventAttendanceGetRequest, CalendarEventAttendanceGetResponse, CalendarEventAttendanceResults}
import com.linagora.tmail.james.jmap.model.{CalendarEventNotDone, CalendarEventNotFound}
import org.apache.james.jmap.mail.{BlobId, BlobIds}
import play.api.libs.json.{JsResult, JsValue, Json, OWrites, Reads, Writes}

object CalendarEventAttendanceSerializer {
private implicit val blobIdReads: Reads[BlobId] = Json.valueReads[BlobId]
private implicit val blobIdsReads: Reads[BlobIds] = Json.valueReads[BlobIds]
private implicit val blobIdWrites: Writes[BlobId] = Json.valueWrites[BlobId]

private implicit val calendarEventNotFoundWrites: Writes[CalendarEventNotFound] = Json.valueWrites[CalendarEventNotFound]
private implicit val calendarEventNotDoneWrites: Writes[CalendarEventNotDone] = Json.valueWrites[CalendarEventNotDone]

private implicit val calendarEventAttendanceGetRequestReads: Reads[CalendarEventAttendanceGetRequest] = Json.reads[CalendarEventAttendanceGetRequest]

private implicit val calendarEventAttendanceGetResponseWrites: OWrites[CalendarEventAttendanceGetResponse] = Json.writes[CalendarEventAttendanceGetResponse]

def deserializeEventAttendanceGetRequest(input: JsValue): JsResult[CalendarEventAttendanceGetRequest] =
Json.fromJson[CalendarEventAttendanceGetRequest](input)

def serializeEventAttendanceGetResponse(response: CalendarEventAttendanceGetResponse): JsValue =
Json.toJson(response)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.linagora.tmail.james.jmap.method

import com.linagora.tmail.james.jmap.json.CalendarEventAttendanceSerializer
import com.linagora.tmail.james.jmap.method.CalendarEventAttendanceGetRequest.MAXIMUM_NUMBER_OF_BLOB_IDS
import com.linagora.tmail.james.jmap.method.CapabilityIdentifier.LINAGORA_CALENDAR
import com.linagora.tmail.james.jmap.model.{CalendarEventNotDone, CalendarEventNotFound, CalendarEventNotParsable, InvalidCalendarFileException}
import com.linagora.tmail.james.jmap.{AttendanceStatus, EventAttendanceRepository}
import eu.timepit.refined.auto._
import eu.timepit.refined.refineV
import jakarta.inject.Inject
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE}
import org.apache.james.jmap.core.Id.IdConstraint
import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.core.SetError.SetErrorDescription
import org.apache.james.jmap.core.{AccountId, Invocation, SessionTranslator, SetError}
import org.apache.james.jmap.json.ResponseSerializer
import org.apache.james.jmap.mail.{BlobId, BlobIds, RequestTooLargeException}
import org.apache.james.jmap.method.{InvocationWithContext, MethodRequiringAccountId, WithAccountId}
import org.apache.james.jmap.routes.SessionSupplier
import org.apache.james.mailbox.MailboxSession
import org.apache.james.metrics.api.MetricFactory
import org.reactivestreams.Publisher
import play.api.libs.json.JsObject
import reactor.core.scala.publisher.SMono

class CalendarEventAttendanceGetMethod @Inject()(val eventAttendanceRepository: EventAttendanceRepository,
val metricFactory: MetricFactory,
val sessionSupplier: SessionSupplier,
val sessionTranslator: SessionTranslator) extends MethodRequiringAccountId[CalendarEventAttendanceGetRequest] {
override val methodName: Invocation.MethodName = MethodName("CalendarEventAttendance/get")

override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, LINAGORA_CALENDAR)

override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: CalendarEventAttendanceGetRequest): Publisher[InvocationWithContext] =
SMono.fromDirect(eventAttendanceRepository.getAttendanceStatus(mailboxSession.getUser, request.blobIds))
.map(result => CalendarEventAttendanceGetResponse.from(request.accountId, result))
.map(response => Invocation(
methodName,
Arguments(CalendarEventAttendanceSerializer.serializeEventAttendanceGetResponse(response).as[JsObject]),
invocation.invocation.methodCallId))
.map(InvocationWithContext(_, invocation.processingContext))

override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[Exception, CalendarEventAttendanceGetRequest] =
CalendarEventAttendanceSerializer.deserializeEventAttendanceGetRequest(invocation.arguments.value)
.asEither.left.map(ResponseSerializer.asException)
.flatMap(_.validate())
}

object CalendarEventAttendanceGetRequest {
val MAXIMUM_NUMBER_OF_BLOB_IDS: Int = 16
}

case class CalendarEventAttendanceGetRequest(accountId: AccountId,
blobIds: BlobIds) extends WithAccountId {
def validate(): Either[Exception, CalendarEventAttendanceGetRequest] =
validateBlobIdsSize

private def validateBlobIdsSize: Either[RequestTooLargeException, CalendarEventAttendanceGetRequest] =
if (blobIds.value.length > MAXIMUM_NUMBER_OF_BLOB_IDS) {
Left(RequestTooLargeException("The number of ids requested by the client exceeds the maximum number the server is willing to process in a single method call"))
} else {
scala.Right(this)
}
}

object CalendarEventAttendanceGetResponse {
def from(accountId: AccountId, results: CalendarEventAttendanceResults): CalendarEventAttendanceGetResponse = {
val (accepted, rejected, tentativelyAccepted, needsAction) =
results.done.foldLeft((List.empty[BlobId], List.empty[BlobId], List.empty[BlobId], List.empty[BlobId])) {
case ((accepted, rejected, tentativelyAccepted, needsAction), entry) =>
val refinedBlobId = refineV[IdConstraint](entry.blobId).fold(
_ => BlobId("invalid"), // Fallback for invalid values
validBlobId => BlobId(validBlobId)
)
entry.eventAttendanceStatus match {
case AttendanceStatus.Accepted => (refinedBlobId :: accepted, rejected, tentativelyAccepted, needsAction)
case AttendanceStatus.Declined => (accepted, refinedBlobId :: rejected, tentativelyAccepted, needsAction)
case AttendanceStatus.Tentative => (accepted, rejected, refinedBlobId :: tentativelyAccepted, needsAction)
case AttendanceStatus.NeedsAction => (accepted, rejected, tentativelyAccepted, refinedBlobId :: needsAction)
}
}

CalendarEventAttendanceGetResponse(
accountId,
accepted,
rejected,
tentativelyAccepted,
needsAction,
results.notFound,
results.notDone)
}
}

case class CalendarEventAttendanceGetResponse(accountId: AccountId,
accepted: List[BlobId] = List(),
rejected: List[BlobId] = List(),
tentativelyAccepted: List[BlobId] = List(),
needsAction: List[BlobId] = List(),
notFound: Option[CalendarEventNotFound] = Option.empty,
notDone: Option[CalendarEventNotDone] = Option.empty) extends WithAccountId

object CalendarEventAttendanceResults {
def merge(r1: CalendarEventAttendanceResults, r2: CalendarEventAttendanceResults): CalendarEventAttendanceResults =
CalendarEventAttendanceResults(
done = r1.done ++ r2.done,
notFound = r1.notFound.orElse(r2.notFound),
notDone = r1.notDone.orElse(r2.notDone))

def notFound(blobId: BlobId): CalendarEventAttendanceResults = CalendarEventAttendanceResults(notFound = Some(CalendarEventNotFound(Set(blobId.value))))

def empty: CalendarEventAttendanceResults = CalendarEventAttendanceResults()

def done(eventAttendanceEntry: EventAttendanceStatusEntry): CalendarEventAttendanceResults =
CalendarEventAttendanceResults(List(eventAttendanceEntry))

def notDone(notParsable: CalendarEventNotParsable): CalendarEventAttendanceResults =
CalendarEventAttendanceResults(notDone = Some(CalendarEventNotDone(notParsable.asSetErrorMap)))

def notDone(blobId: BlobId, throwable: Throwable, mailboxSession: MailboxSession): CalendarEventAttendanceResults =
CalendarEventAttendanceResults(notDone = Some(CalendarEventNotDone(Map(blobId.value -> asSetError(throwable, mailboxSession)))), done = List(), notFound = None)

private def asSetError(throwable: Throwable, mailboxSession: MailboxSession): SetError = throwable match {
case _: InvalidCalendarFileException | _: IllegalArgumentException =>
// LOGGER.info("Error when generate reply mail for {}: {}", mailboxSession.getUser.asString(), throwable.getMessage)
SetError.invalidPatch(SetErrorDescription(throwable.getMessage))
case _ =>
// LOGGER.error("serverFail to generate reply mail for {}", mailboxSession.getUser.asString(), throwable)
SetError.serverFail(SetErrorDescription(throwable.getMessage))
}
}

case class CalendarEventAttendanceResults(done: List[EventAttendanceStatusEntry] = List(),
notFound: Option[CalendarEventNotFound] = Option.empty,
notDone: Option[CalendarEventNotDone] = Option.empty)

case class EventAttendanceStatusEntry(blobId: String, eventAttendanceStatus: AttendanceStatus)
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class CalendarEventMethodModule extends AbstractModule {
Multibinder.newSetBinder(binder(), classOf[Method])
.addBinding()
.to(classOf[CalendarEventMaybeMethod])
Multibinder.newSetBinder(binder(), classOf[Method])
.addBinding()
.to(classOf[CalendarEventAttendanceGetMethod])

bind(classOf[CalendarEventReplyPerformer]).in(Scopes.SINGLETON)
bind(classOf[CalendarEventReplySupportedLanguage]).in(Scopes.SINGLETON)
Expand Down
Loading