You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/07/06 01:37:04 UTC
[james-project] 02/05: JAMES-3095 Only retrieve requested mailboxes
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 672b9101c0b343b126c27ebd0f5dc0d8bf2ba746
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Thu Jun 25 11:08:28 2020 +0700
JAMES-3095 Only retrieve requested mailboxes
---
.../contract/MailboxGetMethodContract.scala | 2 +-
.../org/apache/james/jmap/json/Serializer.scala | 4 +-
.../org/apache/james/jmap/mail/MailboxGet.scala | 2 +-
.../james/jmap/method/MailboxGetMethod.scala | 41 +++---
.../apache/james/jmap/model/MailboxFactory.scala | 156 +++++++++++++++------
.../jmap/json/MailboxGetSerializationTest.scala | 2 +-
6 files changed, 136 insertions(+), 71 deletions(-)
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala
index 1c1a027..b079138 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxGetMethodContract.scala
@@ -621,7 +621,7 @@ trait MailboxGetMethodContract {
}
@Test
- def getMailboxesByIdsShouldReturnMailboxesInSorteredOrder(server: GuiceJamesServer): Unit = {
+ def getMailboxesByIdsShouldReturnMailboxesInSortedOrder(server: GuiceJamesServer): Unit = {
val mailboxId1: String = server.getProbe(classOf[MailboxProbeImpl])
.createMailbox(MailboxPath.forUser(BOB, DefaultMailboxes.TRASH))
.serialize
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
index 76c6863..e6d26e3 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
@@ -207,7 +207,9 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
private implicit val propertiesRead: Reads[Properties] = Json.valueReads[Properties]
private implicit val mailboxGetRequest: Reads[MailboxGetRequest] = Json.reads[MailboxGetRequest]
- private implicit val notFoundWrites: Writes[NotFound] = Json.valueWrites[NotFound]
+ private implicit def notFoundWrites(implicit mailboxIdWrites: Writes[MailboxId]): Writes[NotFound] =
+ notFound => JsArray(notFound.value.toList.map(mailboxIdWrites.writes))
+
private implicit val mailboxGetResponseWrites: Writes[MailboxGetResponse] = Json.writes[MailboxGetResponse]
private implicit val jsonValidationErrorWrites: Writes[JsonValidationError] = error => JsString(error.message)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
index a4400ee..5d9b971 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxGet.scala
@@ -32,7 +32,7 @@ case class MailboxGetRequest(accountId: AccountId,
ids: Option[Ids],
properties: Option[Properties])
-case class NotFound(value: List[MailboxId]) {
+case class NotFound(value: Set[MailboxId]) {
def merge(other: NotFound): NotFound = NotFound(this.value ++ other.value)
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index 6eaea11..b3c66c1 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -27,6 +27,7 @@ import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.model.State.INSTANCE
import org.apache.james.jmap.model.{Invocation, MailboxFactory}
import org.apache.james.jmap.utils.quotas.{QuotaLoader, QuotaLoaderWithPreloadedDefaultFactory}
+import org.apache.james.mailbox.exception.MailboxNotFoundException
import org.apache.james.mailbox.model.search.MailboxQuery
import org.apache.james.mailbox.model.{MailboxId, MailboxMetaData}
import org.apache.james.mailbox.{MailboxManager, MailboxSession}
@@ -44,11 +45,11 @@ class MailboxGetMethod @Inject() (serializer: Serializer,
override val methodName: MethodName = MethodName("Mailbox/get")
object MailboxGetResults {
- def found(mailbox: Mailbox): MailboxGetResults = MailboxGetResults(List(mailbox), NotFound(Nil))
- def notFound(mailboxId: MailboxId): MailboxGetResults = MailboxGetResults(Nil, NotFound(List(mailboxId)))
+ def found(mailbox: Mailbox): MailboxGetResults = MailboxGetResults(Set(mailbox), NotFound(Set.empty))
+ def notFound(mailboxId: MailboxId): MailboxGetResults = MailboxGetResults(Set.empty, NotFound(Set(mailboxId)))
}
- case class MailboxGetResults(mailboxes: List[Mailbox], notFound: NotFound) {
+ case class MailboxGetResults(mailboxes: Set[Mailbox], notFound: NotFound) {
def merge(other: MailboxGetResults): MailboxGetResults = MailboxGetResults(this.mailboxes ++ other.mailboxes, this.notFound.merge(other.notFound))
}
@@ -56,11 +57,11 @@ class MailboxGetMethod @Inject() (serializer: Serializer,
metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value,
asMailboxGetRequest(invocation.arguments)
.flatMap(mailboxGetRequest => getMailboxes(mailboxGetRequest, mailboxSession)
- .reduce(MailboxGetResults(Nil, NotFound(Nil)), (result1: MailboxGetResults, result2: MailboxGetResults) => result1.merge(result2))
+ .reduce(MailboxGetResults(Set.empty, NotFound(Set.empty)), (result1: MailboxGetResults, result2: MailboxGetResults) => result1.merge(result2))
.map(mailboxes => MailboxGetResponse(
accountId = mailboxGetRequest.accountId,
state = INSTANCE,
- list = mailboxes.mailboxes.sortBy(_.sortOrder),
+ list = mailboxes.mailboxes.toList.sortBy(_.sortOrder),
notFound = mailboxes.notFound))
.map(mailboxGetResponse => Invocation(
methodName = methodName,
@@ -77,18 +78,20 @@ class MailboxGetMethod @Inject() (serializer: Serializer,
private def getMailboxes(mailboxGetRequest: MailboxGetRequest, mailboxSession: MailboxSession): SFlux[MailboxGetResults] = mailboxGetRequest.ids match {
case None => getAllMailboxes(mailboxSession).map(MailboxGetResults.found)
- case Some(ids) =>
- getAllMailboxes(mailboxSession)
- .collectSeq()
- .flatMapMany(mailboxes => SFlux.merge(Seq(
- SFlux.fromIterable(mailboxes)
- .filter(mailbox => ids.value.contains(mailbox.id))
- .map(MailboxGetResults.found),
- SFlux.fromIterable(ids.value)
- .filter(id => !mailboxes.map(_.id).contains(id))
- .map(MailboxGetResults.notFound))))
+ case Some(ids) => SFlux.fromIterable(ids.value)
+ .flatMap(id => getMailboxResultById(id, mailboxSession))
}
+ private def getMailboxResultById(mailboxId: MailboxId, mailboxSession: MailboxSession): SMono[MailboxGetResults] =
+ quotaFactory.loadFor(mailboxSession)
+ .flatMap(quotaLoader => mailboxFactory.create(mailboxId, mailboxSession, quotaLoader)
+ .map(MailboxGetResults.found)
+ .onErrorResume {
+ case _: MailboxNotFoundException => SMono.just(MailboxGetResults.notFound(mailboxId))
+ case error => SMono.raiseError(error)
+ })
+ .subscribeOn(Schedulers.elastic)
+
private def getAllMailboxes(mailboxSession: MailboxSession): SFlux[Mailbox] = {
quotaFactory.loadFor(mailboxSession)
.subscribeOn(Schedulers.elastic)
@@ -96,7 +99,7 @@ class MailboxGetMethod @Inject() (serializer: Serializer,
getAllMailboxesMetaData(mailboxSession).flatMapMany(mailboxesMetaData =>
SFlux.fromIterable(mailboxesMetaData)
.flatMap(mailboxMetaData =>
- getMailboxOrThrow(mailboxMetaData = mailboxMetaData,
+ getMailboxResult(mailboxMetaData = mailboxMetaData,
mailboxSession = mailboxSession,
allMailboxesMetadata = mailboxesMetaData,
quotaLoader = quotaLoader))))
@@ -106,7 +109,7 @@ class MailboxGetMethod @Inject() (serializer: Serializer,
SFlux.fromPublisher(mailboxManager.search(MailboxQuery.builder.matchesAllMailboxNames.build, mailboxSession))
.collectSeq()
- private def getMailboxOrThrow(mailboxSession: MailboxSession,
+ private def getMailboxResult(mailboxSession: MailboxSession,
allMailboxesMetadata: Seq[MailboxMetaData],
mailboxMetaData: MailboxMetaData,
quotaLoader: QuotaLoader): SMono[Mailbox] =
@@ -114,8 +117,4 @@ class MailboxGetMethod @Inject() (serializer: Serializer,
mailboxSession = mailboxSession,
allMailboxesMetadata = allMailboxesMetadata,
quotaLoader = quotaLoader)
- .flatMap {
- case Left(error) => SMono.raiseError(error)
- case scala.Right(mailbox) => SMono.just(mailbox)
- }
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala
index 98c3eb7..738b0f9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/MailboxFactory.scala
@@ -24,9 +24,9 @@ import org.apache.james.jmap.mail.MailboxName.MailboxName
import org.apache.james.jmap.mail._
import org.apache.james.jmap.model.UnsignedInt.UnsignedInt
import org.apache.james.jmap.utils.quotas.QuotaLoader
+import org.apache.james.mailbox._
import org.apache.james.mailbox.model.MailboxACL.EntryKey
import org.apache.james.mailbox.model.{MailboxCounters, MailboxId, MailboxMetaData, MailboxPath, MailboxACL => JavaMailboxACL}
-import org.apache.james.mailbox.{MailboxSession, Role, SubscriptionManager}
import reactor.core.scala.publisher.SMono
import scala.jdk.CollectionConverters._
@@ -59,7 +59,7 @@ case class MailboxValidation(mailboxName: MailboxName,
totalEmails: TotalEmails,
totalThreads: TotalThreads)
-class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager) {
+class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager, mailboxManager: MailboxManager) {
private def retrieveMailboxName(mailboxPath: MailboxPath, mailboxSession: MailboxSession): Either[IllegalArgumentException, MailboxName] =
mailboxPath.getName
@@ -69,20 +69,63 @@ class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager) {
case None => Left(new IllegalArgumentException("No name for the mailbox found"))
}
+ private def getRole(mailboxPath: MailboxPath, mailboxSession: MailboxSession): Option[Role] = Role.from(mailboxPath.getName)
+ .filter(_ => mailboxPath.belongsTo(mailboxSession)).toScala
+
+ private def getSortOrder(role: Option[Role]): SortOrder = role.map(SortOrder.getSortOrder).getOrElse(SortOrder.defaultSortOrder)
+
+ private def getRights(resolveMailboxACL: JavaMailboxACL): Rights = Rights.fromACL(MailboxACL.fromJava(resolveMailboxACL))
+
+ private def getNamespace(mailboxPath: MailboxPath, mailboxSession: MailboxSession): MailboxNamespace = mailboxPath.belongsTo(mailboxSession) match {
+ case true => PersonalNamespace()
+ case false => DelegatedNamespace(mailboxPath.getUser)
+ }
+
+ private def getParentPath(mailboxPath: MailboxPath, mailboxSession: MailboxSession): Option[MailboxPath] = mailboxPath
+ .getHierarchyLevels(mailboxSession.getPathDelimiter)
+ .asScala
+ .reverse
+ .drop(1)
+ .headOption
+
+ private def aclEntryKey(mailboxSession: MailboxSession): EntryKey = EntryKey.createUserEntryKey(mailboxSession.getUser)
+
+ private def getMyRights(mailboxPath: MailboxPath, resolveMailboxACL: JavaMailboxACL, mailboxSession: MailboxSession): MailboxRights = mailboxPath.belongsTo(mailboxSession) match {
+ case true => MailboxRights.FULL
+ case false =>
+ val rights = Rfc4314Rights.fromJava(resolveMailboxACL
+ .getEntries
+ .getOrDefault(aclEntryKey(mailboxSession), JavaMailboxACL.NO_RIGHTS))
+ .toRights
+ MailboxRights(
+ mayReadItems = MayReadItems(rights.contains(Right.Read)),
+ mayAddItems = MayAddItems(rights.contains(Right.Insert)),
+ mayRemoveItems = MayRemoveItems(rights.contains(Right.DeleteMessages)),
+ maySetSeen = MaySetSeen(rights.contains(Right.Seen)),
+ maySetKeywords = MaySetKeywords(rights.contains(Right.Write)),
+ mayCreateChild = MayCreateChild(false),
+ mayRename = MayRename(false),
+ mayDelete = MayDelete(false),
+ maySubmit = MaySubmit(false))
+ }
+
+ private def retrieveIsSubscribed(path: MailboxPath, session: MailboxSession): IsSubscribed = IsSubscribed(subscriptionManager
+ .subscriptions(session)
+ .contains(path.getName))
+
def create(mailboxMetaData: MailboxMetaData,
mailboxSession: MailboxSession,
allMailboxesMetadata: Seq[MailboxMetaData],
- quotaLoader: QuotaLoader): SMono[Either[Exception, Mailbox]] = {
+ quotaLoader: QuotaLoader): SMono[Mailbox] = {
val id: MailboxId = mailboxMetaData.getId
val name: Either[IllegalArgumentException, MailboxName] = retrieveMailboxName(mailboxMetaData.getPath, mailboxSession)
- val role: Option[Role] = Role.from(mailboxMetaData.getPath.getName)
- .filter(_ => mailboxMetaData.getPath.belongsTo(mailboxSession)).toScala
- val sortOrder: SortOrder = role.map(SortOrder.getSortOrder).getOrElse(SortOrder.defaultSortOrder)
+ val role: Option[Role] = getRole(mailboxMetaData.getPath, mailboxSession)
+ val sortOrder: SortOrder = getSortOrder(role)
val quotas: SMono[Quotas] = quotaLoader.getQuotas(mailboxMetaData.getPath)
- val rights: Rights = Rights.fromACL(MailboxACL.fromJava(mailboxMetaData.getResolvedAcls))
+ val rights: Rights = getRights(mailboxMetaData.getResolvedAcls)
val sanitizedCounters: MailboxCounters = mailboxMetaData.getCounters.sanitize()
val unreadEmails: Either[NumberFormatException, UnsignedInt] = UnsignedInt.validate(sanitizedCounters.getUnseen)
@@ -90,54 +133,22 @@ class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager) {
val totalEmails: Either[NumberFormatException, UnsignedInt] = UnsignedInt.validate(sanitizedCounters.getCount)
val totalThreads: Either[NumberFormatException, UnsignedInt] = UnsignedInt.validate(sanitizedCounters.getCount)
- val isOwner = mailboxMetaData.getPath.belongsTo(mailboxSession)
- val aclEntryKey: EntryKey = EntryKey.createUserEntryKey(mailboxSession.getUser)
-
- val namespace: MailboxNamespace = if (isOwner) {
- PersonalNamespace()
- } else {
- DelegatedNamespace(mailboxMetaData.getPath.getUser)
- }
+ val namespace: MailboxNamespace = getNamespace(mailboxMetaData.getPath, mailboxSession)
- val parentPath: Option[MailboxPath] =
- mailboxMetaData.getPath
- .getHierarchyLevels(mailboxSession.getPathDelimiter)
- .asScala
- .reverse
- .drop(1)
- .headOption
+ val parentPath: Option[MailboxPath] = getParentPath(mailboxMetaData.getPath, mailboxSession)
val parentId: Option[MailboxId] = allMailboxesMetadata.filter(otherMetadata => parentPath.contains(otherMetadata.getPath))
.map(_.getId)
.headOption
- val myRights: MailboxRights = if (isOwner) {
- MailboxRights.FULL
- } else {
- val rights = Rfc4314Rights.fromJava(mailboxMetaData.getResolvedAcls
- .getEntries
- .getOrDefault(aclEntryKey, JavaMailboxACL.NO_RIGHTS))
- .toRights
- MailboxRights(
- mayReadItems = MayReadItems(rights.contains(Right.Read)),
- mayAddItems = MayAddItems(rights.contains(Right.Insert)),
- mayRemoveItems = MayRemoveItems(rights.contains(Right.DeleteMessages)),
- maySetSeen = MaySetSeen(rights.contains(Right.Seen)),
- maySetKeywords = MaySetKeywords(rights.contains(Right.Write)),
- mayCreateChild = MayCreateChild(false),
- mayRename = MayRename(false),
- mayDelete = MayDelete(false),
- maySubmit = MaySubmit(false))
- }
+ val myRights: MailboxRights = getMyRights(mailboxMetaData.getPath, mailboxMetaData.getResolvedAcls, mailboxSession)
- def retrieveIsSubscribed: IsSubscribed = IsSubscribed(subscriptionManager
- .subscriptions(mailboxSession)
- .contains(mailboxMetaData.getPath.getName))
+ val isSubscribed: IsSubscribed = retrieveIsSubscribed(mailboxMetaData.getPath, mailboxSession)
MailboxValidation.validate(name, unreadEmails, unreadThreads, totalEmails, totalThreads) match {
- case Left(error) => SMono.just(Left(error))
+ case Left(error) => SMono.raiseError(error)
case scala.Right(mailboxValidation) => SMono.fromPublisher(quotas)
- .map(quotas => scala.Right(
+ .map(quotas =>
Mailbox(
id = id,
name = mailboxValidation.mailboxName,
@@ -152,7 +163,60 @@ class MailboxFactory @Inject() (subscriptionManager: SubscriptionManager) {
namespace = namespace,
rights = rights,
quotas = quotas,
- isSubscribed = retrieveIsSubscribed)))
+ isSubscribed = isSubscribed))
+ }
+ }
+
+ def create(id: MailboxId, mailboxSession: MailboxSession, quotaLoader: QuotaLoader): SMono[Mailbox] = {
+ try {
+ val messageManager: MessageManager = mailboxManager.getMailbox(id, mailboxSession)
+ val resolvedACL = messageManager.getResolvedAcl(mailboxSession)
+
+ val name: Either[IllegalArgumentException, MailboxName] = retrieveMailboxName(messageManager.getMailboxPath, mailboxSession)
+
+ val role: Option[Role] = getRole(messageManager.getMailboxPath, mailboxSession)
+ val sortOrder: SortOrder = getSortOrder(role)
+ val quotas: SMono[Quotas] = quotaLoader.getQuotas(messageManager.getMailboxPath)
+ val rights: Rights = getRights(resolvedACL)
+
+ val sanitizedCounters: MailboxCounters = messageManager.getMailboxCounters(mailboxSession).sanitize()
+ val unreadEmails: Either[NumberFormatException, UnsignedInt] = UnsignedInt.validate(sanitizedCounters.getUnseen)
+ val unreadThreads: Either[NumberFormatException, UnsignedInt] = UnsignedInt.validate(sanitizedCounters.getUnseen)
+ val totalEmails: Either[NumberFormatException, UnsignedInt] = UnsignedInt.validate(sanitizedCounters.getCount)
+ val totalThreads: Either[NumberFormatException, UnsignedInt] = UnsignedInt.validate(sanitizedCounters.getCount)
+
+ val namespace: MailboxNamespace = getNamespace(messageManager.getMailboxPath, mailboxSession)
+
+ val parentId: Option[MailboxId] = getParentPath(messageManager.getMailboxPath, mailboxSession)
+ .map(parentPath => mailboxManager.getMailbox(parentPath, mailboxSession))
+ .map(_.getId)
+
+ val myRights: MailboxRights = getMyRights(messageManager.getMailboxPath, resolvedACL, mailboxSession)
+
+ val isSubscribed: IsSubscribed = retrieveIsSubscribed(messageManager.getMailboxPath, mailboxSession)
+
+ MailboxValidation.validate(name, unreadEmails, unreadThreads, totalEmails, totalThreads) match {
+ case Left(error) => SMono.raiseError(error)
+ case scala.Right(mailboxValidation) => SMono.fromPublisher(quotas)
+ .map(quotas =>
+ Mailbox(
+ id = id,
+ name = mailboxValidation.mailboxName,
+ parentId = parentId,
+ role = role,
+ sortOrder = sortOrder,
+ unreadEmails = mailboxValidation.unreadEmails,
+ totalEmails = mailboxValidation.totalEmails,
+ unreadThreads = mailboxValidation.unreadThreads,
+ totalThreads = mailboxValidation.totalThreads,
+ myRights = myRights,
+ namespace = namespace,
+ rights = rights,
+ quotas = quotas,
+ isSubscribed = isSubscribed))
+ }
+ } catch {
+ case error: Exception => SMono.raiseError(error)
}
}
}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
index 2449d6c..b27f06b 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/MailboxGetSerializationTest.scala
@@ -156,7 +156,7 @@ class MailboxGetSerializationTest extends AnyWordSpec with Matchers {
accountId = ACCOUNT_ID,
state = "75128aab4b1b",
list = List(MAILBOX),
- notFound = NotFound(List(MAILBOX_ID_1, MAILBOX_ID_2)))
+ notFound = NotFound(Set(MAILBOX_ID_1, MAILBOX_ID_2)))
val expectedJson: String =
"""
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org