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 927cbe3d5f6f10298d7e9da510d6398925e05cc4 Author: Quan Tran <[email protected]> AuthorDate: Mon Mar 9 16:04:04 2026 +0700 JAMES-4187 MessageContentDeletionEvent: enrich Flags Allow listener could handle flags of deleted messages. --- .../apache/james/mailbox/events/MailboxEvents.java | 4 +- .../mailbox/cassandra/DeleteMessageListener.java | 16 ++++--- .../scala/org/apache/james/event/json/DTOs.scala | 1 + .../james/event/json/MailboxEventSerializer.scala | 4 +- .../MessageContentDeletionSerializationTest.java | 50 ++++++++++++++++++++++ .../mailbox/postgres/DeleteMessageListener.java | 22 +++++----- .../mail/dao/PostgresMailboxMessageDAO.java | 6 +-- .../james/mailbox/store/event/EventFactory.java | 18 ++++++-- 8 files changed, 96 insertions(+), 25 deletions(-) diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/events/MailboxEvents.java b/mailbox/api/src/main/java/org/apache/james/mailbox/events/MailboxEvents.java index a7bdcf9d41..e1ce43745c 100644 --- a/mailbox/api/src/main/java/org/apache/james/mailbox/events/MailboxEvents.java +++ b/mailbox/api/src/main/java/org/apache/james/mailbox/events/MailboxEvents.java @@ -27,6 +27,8 @@ import java.util.Objects; import java.util.Optional; import java.util.SortedMap; +import jakarta.mail.Flags; + import org.apache.james.core.Username; import org.apache.james.core.quota.QuotaCountLimit; import org.apache.james.core.quota.QuotaCountUsage; @@ -537,7 +539,7 @@ public interface MailboxEvents { } record MessageContentDeletionEvent(EventId eventId, Username username, MailboxId mailboxId, MessageId messageId, long size, - Instant internalDate, boolean hasAttachments, Optional<String> headerBlobId, Optional<String> headerContent, + Instant internalDate, Flags flags, boolean hasAttachments, Optional<String> headerBlobId, Optional<String> headerContent, String bodyBlobId) implements Event { @Override diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java index 7c18bbc696..3e57401ad3 100644 --- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java +++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java @@ -27,6 +27,7 @@ import java.util.Optional; import jakarta.inject.Inject; import jakarta.inject.Named; +import jakarta.mail.Flags; import org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration; import org.apache.james.backends.cassandra.init.configuration.JamesExecutionProfiles; @@ -169,7 +170,7 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi return Flux.mergeDelayError(prefetch, messageIdDAO.retrieveMessages(mailboxId, MessageRange.all(), Limit.unlimited()) .concatMap(metadata -> handleMessageDeletionAsPartOfMailboxDeletion((CassandraMessageId) metadata.getComposedMessageId().getComposedMessageId().getMessageId(), - metadata.getComposedMessageId().getThreadId(), mailboxId, path.getUser()) + metadata.getComposedMessageId().getThreadId(), metadata.getComposedMessageId().getFlags(), mailboxId, path.getUser()) .then(imapUidDAO.delete((CassandraMessageId) metadata.getComposedMessageId().getComposedMessageId().getMessageId(), mailboxId)) .then(messageIdDAO.delete(mailboxId, metadata.getComposedMessageId().getComposedMessageId().getUid()))), deleteAcl(mailboxId), @@ -184,7 +185,7 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi private Mono<Void> handleMessageDeletion(Expunged expunged) { return Flux.fromIterable(expunged.getExpunged().values()) .concatMap(metaData -> handleMessageDeletion((CassandraMessageId) metaData.getMessageId(), - expunged.getMailboxId(), metaData.getThreadId(), expunged.getMailboxPath().getUser())) + expunged.getMailboxId(), metaData.getThreadId(), metaData.getFlags(), expunged.getMailboxPath().getUser())) .then(); } @@ -194,11 +195,11 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi .then(aclMapper.delete(mailboxId))); } - private Mono<Void> handleMessageDeletion(CassandraMessageId messageId, MailboxId mailboxId, ThreadId threadId, Username owner) { + private Mono<Void> handleMessageDeletion(CassandraMessageId messageId, MailboxId mailboxId, ThreadId threadId, Flags flags, Username owner) { return Mono.just(messageId) .filterWhen(this::isReferenced) .flatMap(id -> readMessage(id) - .flatMap(message -> dispatchMessageContentDeletionEvent(mailboxId, owner, message) + .flatMap(message -> dispatchMessageContentDeletionEvent(mailboxId, owner, flags, message) .thenReturn(message)) .flatMap(message -> deleteUnreferencedAttachments(message).thenReturn(message)) .flatMap(this::deleteMessageBlobs) @@ -209,7 +210,7 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi .then(threadLookupDAO.deleteOneRow(threadId, messageId))); } - private Mono<Void> dispatchMessageContentDeletionEvent(MailboxId mailboxId, Username owner, MessageRepresentation message) { + private Mono<Void> dispatchMessageContentDeletionEvent(MailboxId mailboxId, Username owner, Flags flags, MessageRepresentation message) { AuditTrail.entry() .action("DELETION") .username(owner::asString) @@ -226,6 +227,7 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi .messageId(message.getMessageId()) .size(message.getSize()) .instant(message.getInternalDate().toInstant()) + .flags(flags) .hasAttachments(!message.getAttachments().isEmpty()) .bodyBlobId(message.getBodyId().asString()) .headerBlobId(message.getHeaderId().asString()) @@ -233,11 +235,11 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi ImmutableSet.of())); } - private Mono<Void> handleMessageDeletionAsPartOfMailboxDeletion(CassandraMessageId messageId, ThreadId threadId, CassandraId excludedId, Username owner) { + private Mono<Void> handleMessageDeletionAsPartOfMailboxDeletion(CassandraMessageId messageId, ThreadId threadId, Flags flags, CassandraId excludedId, Username owner) { return Mono.just(messageId) .filterWhen(id -> isReferenced(id, excludedId)) .flatMap(id -> readMessage(id) - .flatMap(message -> dispatchMessageContentDeletionEvent(excludedId, owner, message) + .flatMap(message -> dispatchMessageContentDeletionEvent(excludedId, owner, flags, message) .thenReturn(message))) .flatMap(message -> deleteUnreferencedAttachments(message).thenReturn(message)) .flatMap(this::deleteMessageBlobs) diff --git a/mailbox/event/json/src/main/scala/org/apache/james/event/json/DTOs.scala b/mailbox/event/json/src/main/scala/org/apache/james/event/json/DTOs.scala index 311ddafc1e..4000637ff7 100644 --- a/mailbox/event/json/src/main/scala/org/apache/james/event/json/DTOs.scala +++ b/mailbox/event/json/src/main/scala/org/apache/james/event/json/DTOs.scala @@ -114,6 +114,7 @@ object DTOs { case class Flags(systemFlags: Seq[SystemFlag], userFlags: Seq[UserFlag]) object Flags { + val empty: Flags = Flags(Seq.empty, Seq.empty) def toJavaFlags(scalaFlags: Flags): JavaMailFlags = { new FlagsBuilder { builder => diff --git a/mailbox/event/json/src/main/scala/org/apache/james/event/json/MailboxEventSerializer.scala b/mailbox/event/json/src/main/scala/org/apache/james/event/json/MailboxEventSerializer.scala index 9722b2dc6e..4e3f194141 100644 --- a/mailbox/event/json/src/main/scala/org/apache/james/event/json/MailboxEventSerializer.scala +++ b/mailbox/event/json/src/main/scala/org/apache/james/event/json/MailboxEventSerializer.scala @@ -140,11 +140,12 @@ private object DTO { messageId: MessageId, size: Long, internalDate: Instant, + flags: Option[DTOs.Flags], hasAttachments: Boolean, headerBlobId: Option[String], headerContent: Option[String], bodyBlobId: String) extends Event { - override def toJava: JavaEvent = new JavaMessageContentDeletionEvent(eventId, username, mailboxId, messageId, size, internalDate, hasAttachments, headerBlobId.toJava, headerContent.toJava, bodyBlobId) + override def toJava: JavaEvent = new JavaMessageContentDeletionEvent(eventId, username, mailboxId, messageId, size, internalDate, DTOs.Flags.toJavaFlags(flags.getOrElse(DTOs.Flags.empty)), hasAttachments, headerBlobId.toJava, headerContent.toJava, bodyBlobId) } } @@ -247,6 +248,7 @@ private object ScalaConverter { messageId = event.messageId(), size = event.size(), internalDate = event.internalDate(), + flags = Some(DTOs.Flags.fromJavaFlags(event.flags())), hasAttachments = event.hasAttachments, headerBlobId = event.headerBlobId().toScala, bodyBlobId = event.bodyBlobId(), diff --git a/mailbox/event/json/src/test/java/org/apache/james/event/json/MessageContentDeletionSerializationTest.java b/mailbox/event/json/src/test/java/org/apache/james/event/json/MessageContentDeletionSerializationTest.java index 4ac8929cc4..4f2425b381 100644 --- a/mailbox/event/json/src/test/java/org/apache/james/event/json/MessageContentDeletionSerializationTest.java +++ b/mailbox/event/json/src/test/java/org/apache/james/event/json/MessageContentDeletionSerializationTest.java @@ -27,7 +27,10 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.Instant; import java.util.Optional; +import jakarta.mail.Flags; + import org.apache.james.core.Username; +import org.apache.james.mailbox.FlagsBuilder; import org.apache.james.mailbox.events.MailboxEvents.MessageContentDeletionEvent; import org.apache.james.mailbox.model.MailboxId; import org.apache.james.mailbox.model.MessageId; @@ -41,6 +44,10 @@ class MessageContentDeletionSerializationTest { private static final MessageId MESSAGE_ID = TestMessageId.of(42); private static final long SIZE = 12345L; private static final Instant INTERNAL_DATE = Instant.parse("2024-12-15T08:23:45Z"); + private static final Flags FLAGS = new FlagsBuilder() + .add(Flags.Flag.FLAGGED) + .add("$Forwarded") + .build(); private static final boolean HAS_ATTACHMENTS = true; private static final Optional<String> HEADER_BLOB_ID = Optional.of("header-blob-id"); private static final Optional<String> HEADER_CONTENT = Optional.of("Header: value"); @@ -54,6 +61,7 @@ class MessageContentDeletionSerializationTest { MESSAGE_ID, SIZE, INTERNAL_DATE, + FLAGS, HAS_ATTACHMENTS, HEADER_BLOB_ID, HEADER_CONTENT, @@ -66,6 +74,7 @@ class MessageContentDeletionSerializationTest { MESSAGE_ID, SIZE, INTERNAL_DATE, + FLAGS, HAS_ATTACHMENTS, HEADER_BLOB_ID, EMPTY_HEADER_CONTENT, @@ -79,6 +88,10 @@ class MessageContentDeletionSerializationTest { "size": 12345, "hasAttachments": true, "internalDate": "2024-12-15T08:23:45Z", + "flags": { + "systemFlags": ["Flagged"], + "userFlags": ["$Forwarded"] + }, "mailboxId": "18", "headerBlobId": "header-blob-id", "messageId": "42", @@ -88,6 +101,22 @@ class MessageContentDeletionSerializationTest { } """; + private static final String JSON_WITHOUT_FLAGS = """ + { + "MessageContentDeletionEvent": { + "eventId": "6e0dd59d-660e-4d9b-b22f-0354479f47b4", + "username": "[email protected]", + "size": 12345, + "hasAttachments": true, + "internalDate": "2024-12-15T08:23:45Z", + "mailboxId": "18", + "headerBlobId": "header-blob-id", + "messageId": "42", + "bodyBlobId": "body-blob-id" + } + } + """; + private static final String JSON_WITHOUT_HEADER_CONTENT = """ { "MessageContentDeletionEvent": { @@ -96,6 +125,10 @@ class MessageContentDeletionSerializationTest { "size": 12345, "hasAttachments": true, "internalDate": "2024-12-15T08:23:45Z", + "flags": { + "systemFlags": ["Flagged"], + "userFlags": ["$Forwarded"] + }, "mailboxId": "18", "headerBlobId": "header-blob-id", "messageId": "42", @@ -116,4 +149,21 @@ class MessageContentDeletionSerializationTest { assertThat(EVENT_SERIALIZER.fromJson(JSON_WITHOUT_HEADER_CONTENT).get()).isEqualTo(EVENT_WITHOUT_HEADER_CONTENT); } + @Test + void messageContentDeletionEventWithoutFlagsShouldBeWellDeserialized() { + assertThat(EVENT_SERIALIZER.fromJson(JSON_WITHOUT_FLAGS).get()) + .isEqualTo(new MessageContentDeletionEvent( + EVENT_ID, + USERNAME, + MAILBOX_ID, + MESSAGE_ID, + SIZE, + INTERNAL_DATE, + new Flags(), + HAS_ATTACHMENTS, + HEADER_BLOB_ID, + EMPTY_HEADER_CONTENT, + BODY_BLOB_ID)); + } + } diff --git a/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/DeleteMessageListener.java b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/DeleteMessageListener.java index 3edd009044..27e76ceaa1 100644 --- a/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/DeleteMessageListener.java +++ b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/DeleteMessageListener.java @@ -23,6 +23,7 @@ import java.nio.charset.StandardCharsets; import jakarta.inject.Inject; import jakarta.inject.Named; +import jakarta.mail.Flags; import org.apache.commons.io.IOUtils; import org.apache.james.backends.postgres.PostgresConfiguration; @@ -36,7 +37,6 @@ import org.apache.james.mailbox.events.MailboxEvents; import org.apache.james.mailbox.events.MailboxEvents.Expunged; import org.apache.james.mailbox.events.MailboxEvents.MailboxDeletion; import org.apache.james.mailbox.model.MailboxId; -import org.apache.james.mailbox.model.MessageMetaData; import org.apache.james.mailbox.postgres.mail.MessageRepresentation; import org.apache.james.mailbox.postgres.mail.dao.PostgresAttachmentDAO; import org.apache.james.mailbox.postgres.mail.dao.PostgresMailboxMessageDAO; @@ -114,7 +114,8 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi PostgresThreadDAO threadDAO = threadDAOFactory.create(event.getUsername().getDomainPart()); return postgresMailboxMessageDAO.deleteByMailboxId((PostgresMailboxId) event.getMailboxId()) - .flatMap(msgId -> handleMessageDeletion(postgresMessageDAO, postgresMailboxMessageDAO, attachmentDAO, threadDAO, msgId, event.getMailboxId(), event.getMailboxPath().getUser()), + .flatMap(metaData -> handleMessageDeletion(postgresMessageDAO, postgresMailboxMessageDAO, attachmentDAO, threadDAO, + (PostgresMessageId) metaData.getMessageId(), event.getMailboxId(), event.getMailboxPath().getUser(), metaData.getFlags()), LOW_CONCURRENCY) .then(); } @@ -127,9 +128,8 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi return Flux.fromIterable(event.getExpunged() .values()) - .map(MessageMetaData::getMessageId) - .map(PostgresMessageId.class::cast) - .flatMap(msgId -> handleMessageDeletion(postgresMessageDAO, postgresMailboxMessageDAO, attachmentDAO, threadDAO, msgId, event.getMailboxId(), event.getMailboxPath().getUser()), LOW_CONCURRENCY) + .flatMap(metaData -> handleMessageDeletion(postgresMessageDAO, postgresMailboxMessageDAO, attachmentDAO, threadDAO, + (PostgresMessageId) metaData.getMessageId(), event.getMailboxId(), event.getMailboxPath().getUser(), metaData.getFlags()), LOW_CONCURRENCY) .then(); } @@ -139,25 +139,26 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi PostgresThreadDAO threadDAO, PostgresMessageId messageId, MailboxId mailboxId, - Username owner) { + Username owner, + Flags flags) { return Mono.just(messageId) .filterWhen(msgId -> isUnreferenced(msgId, postgresMailboxMessageDAO)) .flatMap(msgId -> postgresMessageDAO.retrieveMessage(messageId) - .flatMap(messageRepresentation -> dispatchMessageContentDeletionEvent(mailboxId, owner, messageRepresentation)) + .flatMap(messageRepresentation -> dispatchMessageContentDeletionEvent(mailboxId, owner, flags, messageRepresentation)) .then(deleteBodyBlob(msgId, postgresMessageDAO)) .then(deleteAttachmentIfEnabled(msgId, attachmentDAO)) .then(threadDAO.deleteSome(owner, msgId)) .then(postgresMessageDAO.deleteByMessageId(msgId))); } - private Mono<Void> dispatchMessageContentDeletionEvent(MailboxId mailboxId, Username owner, MessageRepresentation message) { + private Mono<Void> dispatchMessageContentDeletionEvent(MailboxId mailboxId, Username owner, Flags flags, MessageRepresentation message) { return Mono.fromCallable(() -> IOUtils.toString(message.getHeaderContent().getInputStream(), StandardCharsets.UTF_8)) .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER) - .flatMap(headerContent -> Mono.from(contentDeletionEventBus.dispatch(messageContentDeletionEvent(mailboxId, owner, message, headerContent), + .flatMap(headerContent -> Mono.from(contentDeletionEventBus.dispatch(messageContentDeletionEvent(mailboxId, owner, flags, message, headerContent), ImmutableSet.of()))); } - private MailboxEvents.MessageContentDeletionEvent messageContentDeletionEvent(MailboxId mailboxId, Username owner, MessageRepresentation message, String headerContent) { + private MailboxEvents.MessageContentDeletionEvent messageContentDeletionEvent(MailboxId mailboxId, Username owner, Flags flags, MessageRepresentation message, String headerContent) { return EventFactory.messageContentDeleted() .randomEventId() .user(owner) @@ -165,6 +166,7 @@ public class DeleteMessageListener implements EventListener.ReactiveGroupEventLi .messageId(message.getMessageId()) .size(message.getSize()) .instant(message.getInternalDate().toInstant()) + .flags(flags) .hasAttachments(!message.getAttachments().isEmpty()) .bodyBlobId(message.getBodyBlobId().asString()) .headerContent(headerContent) diff --git a/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java index 8ca50e936d..22197fe710 100644 --- a/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java +++ b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java @@ -236,11 +236,11 @@ public class PostgresMailboxMessageDAO { } } - public Flux<PostgresMessageId> deleteByMailboxId(PostgresMailboxId mailboxId) { + public Flux<MessageMetaData> deleteByMailboxId(PostgresMailboxId mailboxId) { return postgresExecutor.executeDeleteAndReturnList(dslContext -> dslContext.deleteFrom(TABLE_NAME) .where(MAILBOX_ID.eq(mailboxId.asUuid())) - .returning(MESSAGE_ID)) - .map(record -> PostgresMessageId.Factory.of(record.get(MESSAGE_ID))) + .returning(MESSAGE_METADATA_FIELDS_REQUIRE)) + .map(RECORD_TO_MESSAGE_METADATA_FUNCTION) .collectList() .flatMapMany(Flux::fromIterable); } diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/EventFactory.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/EventFactory.java index 19990c73ff..f280e33d0e 100644 --- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/EventFactory.java +++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/event/EventFactory.java @@ -29,6 +29,8 @@ import java.util.Optional; import java.util.SortedMap; import java.util.function.Function; +import jakarta.mail.Flags; + import org.apache.james.core.Username; import org.apache.james.core.quota.QuotaCountLimit; import org.apache.james.core.quota.QuotaCountUsage; @@ -208,6 +210,11 @@ public class EventFactory { T size(long size); } + @FunctionalInterface + public interface RequireFlags<T> { + T flags(Flags flags); + } + @FunctionalInterface public interface RequireHasAttachments<T> { T hasAttachments(boolean hasAttachments); @@ -548,6 +555,7 @@ public class EventFactory { private final MessageId messageId; private final long size; private final Instant internalDate; + private final Flags flags; private final boolean hasAttachments; private final String bodyBlobId; private Optional<String> headerBlobId; @@ -559,6 +567,7 @@ public class EventFactory { MessageId messageId, long size, Instant internalDate, + Flags flags, boolean hasAttachments, String bodyBlobId) { this.eventId = eventId; @@ -567,6 +576,7 @@ public class EventFactory { this.messageId = messageId; this.size = size; this.internalDate = internalDate; + this.flags = flags; this.hasAttachments = hasAttachments; this.bodyBlobId = bodyBlobId; this.headerBlobId = Optional.empty(); @@ -589,6 +599,7 @@ public class EventFactory { Preconditions.checkNotNull(mailboxId); Preconditions.checkNotNull(messageId); Preconditions.checkNotNull(internalDate); + Preconditions.checkNotNull(flags); Preconditions.checkNotNull(bodyBlobId); Preconditions.checkArgument(headerBlobId.isPresent() || headerContent.isPresent(), "Either headerBlobId or headerContent must be present"); @@ -599,6 +610,7 @@ public class EventFactory { messageId, size, internalDate, + flags, hasAttachments, headerBlobId, headerContent, @@ -692,9 +704,9 @@ public class EventFactory { return eventId -> user -> quotaRoot -> quotaCount -> quotaSize -> instant -> new QuotaUsageUpdatedFinalStage(eventId, user, quotaRoot, quotaCount, quotaSize, instant); } - public static RequireEventId<RequireUser<RequireMailboxId<RequireMessageId<RequireSize<RequireInstant<RequireHasAttachments<RequireBodyBlobId<MessageContentDeletionFinalStage>>>>>>>> messageContentDeleted() { - return eventId -> user -> mailboxId -> messageId -> size -> instant -> hasAttachments -> bodyBlobId -> - new MessageContentDeletionFinalStage(eventId, user, mailboxId, messageId, size, instant, hasAttachments, bodyBlobId); + public static RequireEventId<RequireUser<RequireMailboxId<RequireMessageId<RequireSize<RequireInstant<RequireFlags<RequireHasAttachments<RequireBodyBlobId<MessageContentDeletionFinalStage>>>>>>>>> messageContentDeleted() { + return eventId -> user -> mailboxId -> messageId -> size -> instant -> flags -> hasAttachments -> bodyBlobId -> + new MessageContentDeletionFinalStage(eventId, user, mailboxId, messageId, size, instant, flags, hasAttachments, bodyBlobId); } public static RequireMailboxEvent<MailboxSubscribedFinalStage> mailboxSubscribed() { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
