blob: 59314fcd68264f9bebfefc4958909665c61d3ed4 [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
package org.apache.james.imap.processor;
import static org.apache.james.mailbox.MessageManager.MailboxMetaData.RecentMode.IGNORE;
import static org.apache.james.mailbox.MessageManager.MailboxMetaData.RecentMode.RETRIEVE;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import jakarta.inject.Inject;
import jakarta.mail.Flags;
import org.apache.james.imap.api.ImapConfiguration;
import org.apache.james.imap.api.display.HumanReadableText;
import org.apache.james.imap.api.message.Capability;
import org.apache.james.imap.api.message.StatusDataItems;
import org.apache.james.imap.api.message.response.StatusResponseFactory;
import org.apache.james.imap.api.process.ImapSession;
import org.apache.james.imap.main.PathConverter;
import org.apache.james.imap.message.request.StatusRequest;
import org.apache.james.imap.message.response.MailboxStatusResponse;
import org.apache.james.mailbox.MailboxManager;
import org.apache.james.mailbox.MailboxSession;
import org.apache.james.mailbox.MessageManager;
import org.apache.james.mailbox.MessageManager.MailboxMetaData.RecentMode;
import org.apache.james.mailbox.MessageUid;
import org.apache.james.mailbox.ModSeq;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.model.ComposedMessageIdWithMetaData;
import org.apache.james.mailbox.model.FetchGroup;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.model.MessageRange;
import org.apache.james.mailbox.model.MessageResult;
import org.apache.james.mailbox.model.UidValidity;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.util.MDCBuilder;
import org.apache.james.util.ReactorUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableList;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* STATUS command initial definition: https://www.rfc-editor.org/rfc/rfc3501#section-6.3.10
*
* STATUS=SIZE extension: https://www.rfc-editor.org/rfc/rfc8438.html
* DELETED extension: https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.11
* and https://datatracker.ietf.org/doc/html/rfc9208#section-4.1.4
* DELETED-STORAGE extension: https://datatracker.ietf.org/doc/html/rfc9208#section-4.1.4
*/
public class StatusProcessor extends AbstractMailboxProcessor<StatusRequest> implements CapabilityImplementingProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(StatusProcessor.class);
private ImapConfiguration imapConfiguration;
@Inject
public StatusProcessor(MailboxManager mailboxManager, StatusResponseFactory factory,
MetricFactory metricFactory) {
super(StatusRequest.class, mailboxManager, factory, metricFactory);
}
@Override
public void configure(ImapConfiguration imapConfiguration) {
super.configure(imapConfiguration);
this.imapConfiguration = imapConfiguration;
}
@Override
public List<Capability> getImplementedCapabilities(ImapSession session) {
return ImmutableList.of(Capability.of("STATUS=SIZE"));
}
@Override
protected Mono<Void> processRequestReactive(StatusRequest request, ImapSession session, Responder responder) {
MailboxPath mailboxPath = PathConverter.forSession(session).buildFullPath(request.getMailboxName());
MailboxSession mailboxSession = session.getMailboxSession();
StatusDataItems statusDataItems = request.getStatusDataItems();
return logInitialRequest(mailboxPath)
.then(sendStatus(mailboxPath, statusDataItems, responder, session, mailboxSession))
.then(unsolicitedResponses(session, responder, false))
.then(Mono.fromRunnable(() -> okComplete(request, responder)))
.onErrorResume(MailboxNotFoundException.class, e -> {
no(request, responder, HumanReadableText.MAILBOX_NOT_FOUND);
return ReactorUtils.logAsMono(() -> LOGGER.info("Status failed for mailbox '{}' as it does not exist.", mailboxPath));
})
.onErrorResume(MailboxException.class, e -> {
no(request, responder, HumanReadableText.STATUS_FAILED);
return ReactorUtils.logAsMono(() -> LOGGER.error("Status failed for mailbox {}", mailboxPath, e));
})
.then();
}
private Mono<MailboxStatusResponse> sendStatus(MailboxPath mailboxPath, StatusDataItems statusDataItems, Responder responder, ImapSession session, MailboxSession mailboxSession) {
return Mono.from(getMailboxManager().getMailboxReactive(mailboxPath, mailboxSession))
.flatMap(mailbox -> sendStatus(mailbox, statusDataItems, responder, session, mailboxSession));
}
Mono<MailboxStatusResponse> sendStatus(MessageManager mailbox, StatusDataItems statusDataItems, Responder responder, ImapSession session, MailboxSession mailboxSession) {
return retrieveMetadata(mailbox, statusDataItems, mailboxSession)
.flatMap(metaData -> computeStatusResponse(mailbox, statusDataItems, metaData, mailboxSession)
.doOnNext(response -> {
// Enable CONDSTORE as this is a CONDSTORE enabling command
if (response.getHighestModSeq() != null) {
condstoreEnablingCommand(session, responder, metaData, false);
}
responder.respond(response);
}));
}
private Mono<Void> logInitialRequest(MailboxPath mailboxPath) {
if (LOGGER.isDebugEnabled()) {
return ReactorUtils.logAsMono(() -> LOGGER.debug("Status called on mailbox named {}", mailboxPath));
} else {
return Mono.empty();
}
}
private Mono<MessageManager.MailboxMetaData> retrieveMetadata(MessageManager mailbox, StatusDataItems statusDataItems, MailboxSession mailboxSession) {
EnumSet<MessageManager.MailboxMetaData.Item> fetchGroup = computeFetchGroup(statusDataItems);
RecentMode recentMode = computeRecentMode(statusDataItems);
try {
return mailbox.getMetaDataReactive(recentMode, mailboxSession, fetchGroup);
} catch (MailboxException e) {
throw new RuntimeException(e);
}
}
private RecentMode computeRecentMode(StatusDataItems statusDataItems) {
if (statusDataItems.isRecent()) {
return RETRIEVE;
}
return IGNORE;
}
private Mono<MailboxStatusResponse> computeStatusResponse(MessageManager mailbox,
StatusDataItems statusDataItems,
MessageManager.MailboxMetaData metaData,
MailboxSession session) {
return iterateMailbox(statusDataItems, mailbox, session)
.map(maybeIterationResult -> {
Optional<Long> appendLimit = appendLimit(statusDataItems);
Long messages = messages(statusDataItems, metaData);
Long recent = recent(statusDataItems, metaData);
MessageUid uidNext = uidNext(statusDataItems, metaData);
UidValidity uidValidity = uidValidity(statusDataItems, metaData);
Long unseen = unseen(statusDataItems, metaData);
ModSeq highestModSeq = highestModSeq(statusDataItems, metaData);
MailboxId mailboxId = mailboxId(statusDataItems, mailbox);
return new MailboxStatusResponse(
appendLimit,
maybeIterationResult.flatMap(result -> result.getSize(statusDataItems)).orElse(null),
maybeIterationResult.flatMap(result -> result.getDeleted(statusDataItems)).orElse(null),
maybeIterationResult.flatMap(result -> result.getDeletedStorage(statusDataItems)).orElse(null),
messages, recent, uidNext, highestModSeq, uidValidity, unseen, mailbox.getMailboxPath().getName(), mailboxId);
});
}
private EnumSet<MessageManager.MailboxMetaData.Item> computeFetchGroup(StatusDataItems statusDataItems) {
EnumSet<MessageManager.MailboxMetaData.Item> result = EnumSet.noneOf(MessageManager.MailboxMetaData.Item.class);
if (statusDataItems.isUnseen() || statusDataItems.isMessages()) {
result.add(MessageManager.MailboxMetaData.Item.MailboxCounters);
}
if (statusDataItems.isHighestModSeq()) {
result.add(MessageManager.MailboxMetaData.Item.HighestModSeq);
}
if (statusDataItems.isUidNext()) {
result.add(MessageManager.MailboxMetaData.Item.NextUid);
}
return result;
}
private Long unseen(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
if (statusDataItems.isUnseen()) {
return metaData.getUnseenCount();
} else {
return null;
}
}
private MailboxId mailboxId(StatusDataItems statusDataItems, MessageManager mailbox) {
if (statusDataItems.isMailboxId()) {
return mailbox.getId();
} else {
return null;
}
}
private Optional<Long> appendLimit(StatusDataItems statusDataItems) {
if (statusDataItems.isAppendLimit()) {
return imapConfiguration.getAppendLimit();
} else {
return null;
}
}
private UidValidity uidValidity(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
if (statusDataItems.isUidValidity()) {
return metaData.getUidValidity();
} else {
return null;
}
}
private ModSeq highestModSeq(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
if (statusDataItems.isHighestModSeq()) {
return metaData.getHighestModSeq();
} else {
return null;
}
}
private MessageUid uidNext(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
if (statusDataItems.isUidNext()) {
return metaData.getUidNext();
} else {
return null;
}
}
private Long recent(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
if (statusDataItems.isRecent()) {
return metaData.countRecent();
} else {
return null;
}
}
private Long messages(StatusDataItems statusDataItems, MessageManager.MailboxMetaData metaData) {
if (statusDataItems.isMessages()) {
return metaData.getMessageCount();
} else {
return null;
}
}
private Mono<Optional<MailboxIterationResult>> iterateMailbox(StatusDataItems statusDataItems, MessageManager messageManager, MailboxSession session) {
if (statusDataItems.isSize() || statusDataItems.isDeletedStorage()) {
return Flux.from(messageManager.getMessagesReactive(MessageRange.all(), FetchGroup.MINIMAL, session))
.reduce(new MailboxIterationResult(), MailboxIterationResult::accumulate)
.map(Optional::of);
}
if (statusDataItems.isDeleted()) {
return Flux.from(messageManager.listMessagesMetadata(MessageRange.all(), session))
.reduce(new MailboxIterationResult(), MailboxIterationResult::accumulateDeleted)
.map(Optional::of);
}
return Mono.just(Optional.empty());
}
public static class MailboxIterationResult {
private long size = 0;
private long deleted = 0;
private long deletedStorage = 0;
public MailboxIterationResult accumulate(MessageResult messageResult) {
if (messageResult.getFlags().contains(Flags.Flag.DELETED)) {
deleted++;
deletedStorage += messageResult.getSize();
}
size += messageResult.getSize();
return this;
}
public MailboxIterationResult accumulateDeleted(ComposedMessageIdWithMetaData metaData) {
if (metaData.getFlags().contains(Flags.Flag.DELETED)) {
deleted++;
}
return this;
}
public Optional<Long> getSize(StatusDataItems items) {
if (items.isSize()) {
return Optional.of(size);
}
return Optional.empty();
}
public Optional<Long> getDeleted(StatusDataItems items) {
if (items.isDeleted()) {
return Optional.of(deleted);
}
return Optional.empty();
}
public Optional<Long> getDeletedStorage(StatusDataItems items) {
if (items.isDeletedStorage()) {
return Optional.of(deletedStorage);
}
return Optional.empty();
}
}
@Override
protected MDCBuilder mdc(StatusRequest request) {
return MDCBuilder.create()
.addToContext(MDCBuilder.ACTION, "STATUS")
.addToContext("mailbox", request.getMailboxName())
.addToContext("parameters", request.getStatusDataItems().toString());
}
}