Skip to content

Commit

Permalink
Move PublicAsset APIs to jmap-extensions-api
Browse files Browse the repository at this point in the history
  • Loading branch information
HoussemNasri committed Dec 3, 2024
1 parent 1c373ca commit 9de2c0d
Show file tree
Hide file tree
Showing 17 changed files with 167 additions and 148 deletions.
5 changes: 5 additions & 0 deletions tmail-backend/jmap/extensions-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
<groupId>${james.groupId}</groupId>
<artifactId>james-server-jmap-rfc-8621</artifactId>
</dependency>
<dependency>
<groupId>com.github.f4b6a3</groupId>
<artifactId>uuid-creator</artifactId>
<version>${uuid-creator.version}</version>
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
<artifactId>testing-base</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.linagora.tmail.james.jmap.publicAsset

import org.apache.james.blob.api.BlobId
import org.apache.james.core.Username
import org.apache.james.jmap.api.model.IdentityId
import org.reactivestreams.Publisher
import reactor.core.scala.publisher.SMono

trait PublicAssetRepository {
def create(username: Username, creationRequest: PublicAssetCreationRequest): Publisher[PublicAssetStorage]

def update(username: Username, id: PublicAssetId, identityIds: Set[IdentityId]): Publisher[Void]

def remove(username: Username, id: PublicAssetId): Publisher[Void]

def revoke(username: Username): Publisher[Void]

def get(username: Username, ids: Set[PublicAssetId]): Publisher[PublicAssetStorage]

def get(username: Username, id: PublicAssetId): Publisher[PublicAssetStorage] = get(username, Set(id))

def list(username: Username): Publisher[PublicAssetStorage]

def listPublicAssetMetaDataOrderByIdAsc(username: Username): Publisher[PublicAssetMetadata]

def listAllBlobIds(): Publisher[BlobId]

def updateIdentityIds(username: Username, id: PublicAssetId, identityIdsToAdd: Seq[IdentityId], identityIdsToRemove: Seq[IdentityId]): Publisher[Void] =
SMono(get(username, id))
.map(publicAsset => (publicAsset.identityIds.toSet ++ identityIdsToAdd.toSet) -- identityIdsToRemove.toSet)
.flatMap(identityIds => SMono(update(username, id, identityIds)))

def getTotalSize(username: Username): Publisher[Long]
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.linagora.tmail.james.jmap.publicAsset

import java.io.InputStream
import java.net.URI
import java.time.Instant
import java.util.UUID

import com.github.f4b6a3.uuid.UuidCreator
Expand All @@ -16,32 +15,21 @@ import org.apache.james.core.Username
import org.apache.james.jmap.api.model.IdentityId
import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.core.JmapRfc8621Configuration
import org.apache.james.jmap.mail.{BlobId => JmapBlobId}
import org.apache.james.mailbox.model.ContentType

import scala.util.Try

object PublicAssetIdFactory {
def generate(): PublicAssetId = PublicAssetId(UuidCreator.getTimeBased)

def from(value: String): Either[(String, IllegalArgumentException), PublicAssetId] =
Try(PublicAssetId(UUID.fromString(value)))
.toEither
.left.map(e => value -> new IllegalArgumentException(e))
}

object PublicAssetId {
def fromString(value: String): Try[PublicAssetId] =
Try(PublicAssetId(UUID.fromString(value)))
object PublicAssetSetCreationRequest {
val knownProperties: Set[String] = Set("blobId", "identityIds")
}

case class PublicAssetId(value: UUID) {
def asString(): String = value.toString
}
case class PublicAssetSetCreationRequest(blobId: JmapBlobId, identityIds: Option[Map[IdentityId, Boolean]] = None)

object PublicAssetURIPrefix {
def fromConfiguration(configuration: JmapRfc8621Configuration): Either[Throwable, URI] =
Try(new URI(configuration.urlPrefixString)).toEither
}
case class PublicAssetCreationRequest(size: Size,
contentType: ImageContentType,
identityIds: Seq[IdentityId] = Seq.empty,
content: () => InputStream)

object PublicURI {
def fromString(value: String): Either[Throwable, PublicURI] = Try(new URI(value))
Expand All @@ -62,6 +50,29 @@ object PublicURI {
}
}

object PublicAssetIdFactory {
def generate(): PublicAssetId = PublicAssetId(UuidCreator.getTimeBased)

def from(value: String): Either[(String, IllegalArgumentException), PublicAssetId] =
Try(PublicAssetId(UUID.fromString(value)))
.toEither
.left.map(e => value -> new IllegalArgumentException(e))
}

object PublicAssetId {
def fromString(value: String): Try[PublicAssetId] =
Try(PublicAssetId(UUID.fromString(value)))
}

case class PublicAssetId(value: UUID) {
def asString(): String = value.toString
}

object PublicAssetURIPrefix {
def fromConfiguration(configuration: JmapRfc8621Configuration): Either[Throwable, URI] =
Try(new URI(configuration.urlPrefixString)).toEither
}

case class PublicURI(value: URI) extends AnyVal

object ImageContentType {
Expand All @@ -87,24 +98,6 @@ object ImageContentType {
.map(e => PublicAssetInvalidContentTypeException(e))
}

trait PublicAssetException extends RuntimeException {
def message: String

override def getMessage: String = message
}

case class PublicAssetInvalidContentTypeException(contentType: String) extends PublicAssetException {
override val message: String = s"Invalid content type: $contentType"
}

case class PublicAssetNotFoundException(id: PublicAssetId) extends PublicAssetException {
override val message: String = s"Public asset not found: ${id.asString()}"
}

case class PublicAssetQuotaLimitExceededException(limitAsByte: Long) extends PublicAssetException {
override val message: String = s"Exceeding public asset quota limit of $limitAsByte bytes"
}

case class PublicAssetStorage(id: PublicAssetId,
publicURI: PublicURI,
size: Size,
Expand All @@ -117,11 +110,6 @@ case class PublicAssetStorage(id: PublicAssetId,
def contentTypeAsString(): String = contentType.value
}

case class PublicAssetCreationRequest(size: Size,
contentType: ImageContentType,
identityIds: Seq[IdentityId] = Seq.empty,
content: () => InputStream)

object PublicAssetMetadata {
def from(publicAsset: PublicAssetStorage): PublicAssetMetadata =
PublicAssetMetadata(
Expand Down Expand Up @@ -150,4 +138,22 @@ case class PublicAssetMetadata(id: PublicAssetId,
content = () => content)

def sizeAsLong(): java.lang.Long = size.value
}

trait PublicAssetException extends RuntimeException {
def message: String

override def getMessage: String = message
}

case class PublicAssetNotFoundException(id: PublicAssetId) extends PublicAssetException {
override val message: String = s"Public asset not found: ${id.asString()}"
}

case class PublicAssetQuotaLimitExceededException(limitAsByte: Long) extends PublicAssetException {
override val message: String = s"Exceeding public asset quota limit of $limitAsByte bytes"
}

case class PublicAssetInvalidContentTypeException(contentType: String) extends PublicAssetException {
override val message: String = s"Invalid content type: $contentType"
}
5 changes: 5 additions & 0 deletions tmail-backend/jmap/extensions-cassandra/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>jmap-extensions</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>${james.groupId}</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package com.linagora.tmail.james.jmap.publicAsset

import java.io.ByteArrayInputStream
import java.net.URI
import java.time.Clock

import com.google.inject.multibindings.Multibinder
import com.google.inject.{AbstractModule, Scopes}
import com.linagora.tmail.james.jmap.JMAPExtensionConfiguration
import com.linagora.tmail.james.jmap.PublicAssetTotalSizeLimit
import jakarta.inject.{Inject, Named}
import org.apache.james.backends.cassandra.components.CassandraModule
import org.apache.james.blob.api.{BlobId, BlobStore, BucketName}
Expand All @@ -18,15 +17,15 @@ import reactor.core.scala.publisher.SMono

class CassandraPublicAssetRepository @Inject()(val dao: CassandraPublicAssetDAO,
val blobStore: BlobStore,
val configuration: JMAPExtensionConfiguration,
val publicAssetTotalSizeLimit: PublicAssetTotalSizeLimit,
@Named("publicAssetUriPrefix") publicAssetUriPrefix: URI) extends PublicAssetRepository {
private val bucketName: BucketName = blobStore.getDefaultBucketName

override def create(username: Username, creationRequest: PublicAssetCreationRequest): Publisher[PublicAssetStorage] =
SMono(getTotalSize(username))
.filter(totalSize => (totalSize + creationRequest.size.value) <= configuration.publicAssetTotalSizeLimit.asLong())
.filter(totalSize => (totalSize + creationRequest.size.value) <= publicAssetTotalSizeLimit.asLong())
.flatMap(_ => SMono(createAsset(username, creationRequest)))
.switchIfEmpty(SMono.error(PublicAssetQuotaLimitExceededException(configuration.publicAssetTotalSizeLimit.asLong())))
.switchIfEmpty(SMono.error(PublicAssetQuotaLimitExceededException(publicAssetTotalSizeLimit.asLong())))

private def createAsset(username: Username, creationRequest: PublicAssetCreationRequest): Publisher[PublicAssetStorage] =
SMono.fromCallable(() => creationRequest.content.apply().readAllBytes())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import org.apache.james.blob.api.BucketName;
import org.apache.james.blob.memory.MemoryBlobStoreDAO;
import org.apache.james.server.blob.deduplication.DeDuplicationBlobStore;
import org.apache.james.util.Size;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.linagora.tmail.james.jmap.JMAPExtensionConfiguration;
import com.linagora.tmail.james.jmap.PublicAssetTotalSizeLimit;

class CassandraPublicAssetRepositoryTest implements PublicAssetRepositoryContract {
@RegisterExtension
Expand All @@ -18,10 +19,12 @@ class CassandraPublicAssetRepositoryTest implements PublicAssetRepositoryContrac

@BeforeEach
void setup(CassandraCluster cassandra) {
PublicAssetTotalSizeLimit publicAssetTotalSizeLimit =
PublicAssetTotalSizeLimit.of(Size.of(20L, Size.Unit.M)).get();
publicAssetRepository = new CassandraPublicAssetRepository(
new CassandraPublicAssetDAO(cassandra.getConf(), blobIdFactory()),
new DeDuplicationBlobStore(new MemoryBlobStoreDAO(), BucketName.DEFAULT, blobIdFactory()),
new JMAPExtensionConfiguration(JMAPExtensionConfiguration.PUBLIC_ASSET_TOTAL_SIZE_LIMIT_DEFAULT()),
publicAssetTotalSizeLimit,
PublicAssetRepositoryContract.PUBLIC_ASSET_URI_PREFIX());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
import org.apache.james.blob.api.BucketName;
import org.apache.james.blob.memory.MemoryBlobStoreDAO;
import org.apache.james.server.blob.deduplication.DeDuplicationBlobStore;
import org.apache.james.util.Size;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.linagora.tmail.james.jmap.JMAPExtensionConfiguration;
import com.linagora.tmail.james.jmap.PublicAssetTotalSizeLimit;

class CassandraPublicAssetServiceTest implements PublicAssetServiceContract {

Expand All @@ -21,14 +22,15 @@ class CassandraPublicAssetServiceTest implements PublicAssetServiceContract {

@BeforeEach
void setup(CassandraCluster cassandra) {
JMAPExtensionConfiguration jmapExtensionConfiguration = new JMAPExtensionConfiguration(JMAPExtensionConfiguration.PUBLIC_ASSET_TOTAL_SIZE_LIMIT_DEFAULT());
PublicAssetTotalSizeLimit publicAssetTotalSizeLimit =
PublicAssetTotalSizeLimit.of(Size.of(20L, Size.Unit.M)).get();
publicAssetRepository = new CassandraPublicAssetRepository(
new CassandraPublicAssetDAO(cassandra.getConf(), blobIdFactory()),
new DeDuplicationBlobStore(new MemoryBlobStoreDAO(), BucketName.DEFAULT, blobIdFactory()),
jmapExtensionConfiguration,
publicAssetTotalSizeLimit,
PublicAssetRepositoryContract.PUBLIC_ASSET_URI_PREFIX());

publicAssetSetService = new PublicAssetSetService(PublicAssetServiceContract.identityRepository(), publicAssetRepository, jmapExtensionConfiguration);
publicAssetSetService = new PublicAssetSetService(PublicAssetServiceContract.identityRepository(), publicAssetRepository, publicAssetTotalSizeLimit);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import java.util.Locale
import com.linagora.tmail.james.jmap.JMAPExtensionConfiguration.{CALENDAR_EVENT_REPLY_SUPPORTED_LANGUAGES_DEFAULT, PUBLIC_ASSET_TOTAL_SIZE_LIMIT_DEFAULT, TICKET_IP_VALIDATION_ENABLED}
import com.linagora.tmail.james.jmap.method.CalendarEventReplySupportedLanguage.LANGUAGE_DEFAULT
import com.linagora.tmail.james.jmap.model.LanguageLocation
import eu.timepit.refined
import org.apache.commons.configuration2.Configuration
import org.apache.james.core.MailAddress
import org.apache.james.jmap.core.UnsignedInt.{UnsignedInt, UnsignedIntConstraint}
import org.apache.james.server.core.MissingArgumentException
import org.apache.james.util.{DurationParser, Size}

Expand Down Expand Up @@ -58,13 +56,6 @@ object JMAPExtensionConfiguration {
}
}

object PublicAssetTotalSizeLimit {
def of(size: Size): Try[PublicAssetTotalSizeLimit] = refined.refineV[UnsignedIntConstraint](size.asBytes()) match {
case Right(value) => Success(PublicAssetTotalSizeLimit(value))
case Left(error) => Failure(new NumberFormatException(error))
}
}

case class JMAPExtensionConfiguration(publicAssetTotalSizeLimit: PublicAssetTotalSizeLimit = PUBLIC_ASSET_TOTAL_SIZE_LIMIT_DEFAULT,
supportMailAddress: Option[MailAddress] = Option.empty,
ticketIpValidationEnable: TicketIpValidationEnable = TICKET_IP_VALIDATION_ENABLED,
Expand All @@ -84,10 +75,6 @@ case class JMAPExtensionConfiguration(publicAssetTotalSizeLimit: PublicAssetTota
}
}

case class PublicAssetTotalSizeLimit(value: UnsignedInt) {
def asLong(): Long = value.value
}

case class TicketIpValidationEnable(value: Boolean)

case class CalendarEventReplySupportedLanguagesConfig(supportedLanguages: Set[Locale])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ class TMailJMAPModule extends AbstractModule {

@Provides
def provideJMAPExtensionConfiguration(@Named("jmap") configuration: Configuration): JMAPExtensionConfiguration = JMAPExtensionConfiguration.from(configuration)

@Provides
def providePublicAssetTotalSizeLimit(jmapExtensionConfiguration: JMAPExtensionConfiguration): PublicAssetTotalSizeLimit = jmapExtensionConfiguration.publicAssetTotalSizeLimit
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package com.linagora.tmail.james.jmap.json
import com.linagora.tmail.james.jmap.method.standardErrorMessage
import com.linagora.tmail.james.jmap.model.{PublicAssetDTO, PublicAssetGetRequest, PublicAssetGetResponse}
import com.linagora.tmail.james.jmap.publicAsset.ImageContentType.ImageContentType
import com.linagora.tmail.james.jmap.publicAsset.{PublicAssetCreationId, PublicAssetCreationResponse, PublicAssetId, PublicAssetPatchObject, PublicAssetSetCreationRequest, PublicAssetSetRequest, PublicAssetSetResponse, PublicAssetUpdateResponse, PublicURI, UnparsedPublicAssetId, ValidatedPublicAssetPatchObject}
import com.linagora.tmail.james.jmap.publicAsset.{PublicAssetCreationId, PublicAssetCreationParseException, PublicAssetCreationResponse, PublicAssetId, PublicAssetPatchObject, PublicAssetSetCreationRequest, PublicAssetSetRequest, PublicAssetSetResponse, PublicAssetUpdateResponse, PublicURI, UnparsedPublicAssetId, ValidatedPublicAssetPatchObject}
import org.apache.james.jmap.api.model.IdentityId
import org.apache.james.jmap.core.{SetError, UuidState}
import org.apache.james.jmap.core.SetError.SetErrorDescription
import org.apache.james.jmap.core.{Properties, SetError, UuidState}
import org.apache.james.jmap.json.{mapMarkerReads, mapWrites}
import org.apache.james.jmap.mail.{IdentityIds, UnparsedIdentityId, BlobId => JmapBlobId}
import play.api.libs.json.{JsBoolean, JsError, JsNull, JsObject, JsPath, JsResult, JsString, JsSuccess, JsValue, Json, JsonValidationError, Reads, Writes}
Expand Down Expand Up @@ -102,7 +103,23 @@ object PublicAssetSerializer {
private implicit val mapUnparsedPublicAssetIdPatchObject: Reads[Map[UnparsedPublicAssetId, PublicAssetPatchObject]] =
Reads.mapReads[UnparsedPublicAssetId, PublicAssetPatchObject] { string => unparsedPublicAssetIdReads.reads(JsString(string)) }

private implicit val publicAssetSetRequestReads: Reads[PublicAssetSetRequest] = Json.reads[PublicAssetSetRequest]
private implicit val publicAssetSetRequestStandardReads: Reads[PublicAssetSetRequest] = Json.reads[PublicAssetSetRequest]
implicit val publicAssetSetRequestReads: Reads[PublicAssetSetRequest] = new Reads[PublicAssetSetRequest] {
override def reads(json: JsValue): JsResult[PublicAssetSetRequest] =
publicAssetSetRequestStandardReads.reads(json)
.flatMap(request => {
validateProperties(json.as[JsObject])
.fold(_ => JsError("Failed to validate properties"), _ => JsSuccess(request))
})

def validateProperties(jsObject: JsObject): Either[PublicAssetCreationParseException, JsObject] = {
jsObject.fields.find(mapEntry => !PublicAssetSetCreationRequest.knownProperties.contains(mapEntry._1))
.map(e => Left(PublicAssetCreationParseException(SetError.invalidArguments(
SetErrorDescription("Some unknown properties were specified"),
Some(Properties.toProperties(Set(e._1)))))))
.getOrElse(Right(jsObject))
}
}

private implicit val publicAssetGetReads: Reads[PublicAssetGetRequest] = Json.reads[PublicAssetGetRequest]

Expand Down
Loading

0 comments on commit 9de2c0d

Please sign in to comment.