blob: 6e6774acaa5f81c1d02e6db73d4907c30c48c4ff [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.protocols.smtp.core.esmtp;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apache.james.core.MaybeSender;
import org.apache.james.protocols.api.ProtocolSession;
import org.apache.james.protocols.api.ProtocolSession.State;
import org.apache.james.protocols.api.Response;
import org.apache.james.protocols.api.handler.LineHandler;
import org.apache.james.protocols.smtp.MailEnvelope;
import org.apache.james.protocols.smtp.SMTPResponse;
import org.apache.james.protocols.smtp.SMTPRetCode;
import org.apache.james.protocols.smtp.SMTPSession;
import org.apache.james.protocols.smtp.core.DataLineFilter;
import org.apache.james.protocols.smtp.dsn.DSNStatus;
import org.apache.james.protocols.smtp.hook.HookResult;
import org.apache.james.protocols.smtp.hook.HookReturnCode;
import org.apache.james.protocols.smtp.hook.MailParametersHook;
import org.apache.james.protocols.smtp.hook.MessageHook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handle the ESMTP SIZE extension.
*/
public class MailSizeEsmtpExtension implements MailParametersHook, EhloExtension, DataLineFilter, MessageHook {
private static final Logger LOGGER = LoggerFactory.getLogger(MailSizeEsmtpExtension.class);
private static final ProtocolSession.AttachmentKey<Integer> MESG_SIZE = ProtocolSession.AttachmentKey.of("MESG_SIZE", Integer.class); // The size of the
private static final String[] MAIL_PARAMS = { "SIZE" };
private static final HookResult SYNTAX_ERROR = HookResult.builder()
.hookReturnCode(HookReturnCode.deny())
.smtpReturnCode(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS)
.smtpDescription(DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.DELIVERY_INVALID_ARG) + " Syntactically incorrect value for SIZE parameter")
.build();
private static final HookResult QUOTA_EXCEEDED = HookResult.builder()
.hookReturnCode(HookReturnCode.deny())
.smtpReturnCode(SMTPRetCode.QUOTA_EXCEEDED)
.smtpDescription(DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SYSTEM_MSG_TOO_BIG) + " Message size exceeds fixed maximum message size")
.build();
public static final int SINGLE_CHARACTER_LINE = 3;
public static final int DOT_BYTE = 46;
@Override
public HookResult doMailParameter(SMTPSession session, String paramName,
String paramValue) {
MaybeSender tempSender = session.getAttachment(SMTPSession.SENDER, State.Transaction).orElse(MaybeSender.nullSender());
session.setMessageFailed(false);
return doMailSize(session, paramValue, tempSender);
}
@Override
public String[] getMailParamNames() {
return MAIL_PARAMS;
}
@Override
public List<String> getImplementedEsmtpFeatures(SMTPSession session) {
// Extension defined in RFC 1870
long maxMessageSize = session.getConfiguration().getMaxMessageSize();
if (maxMessageSize > 0) {
return Arrays.asList("SIZE " + maxMessageSize);
} else {
return Collections.emptyList();
}
}
/**
* Handles the SIZE MAIL option.
*
* @param session
* SMTP session object
* @param mailOptionValue
* the option string passed in with the SIZE option
* @param tempSender
* the sender specified in this mail command (for logging
* purpose)
* @return true if further options should be processed, false otherwise
*/
private HookResult doMailSize(SMTPSession session,
String mailOptionValue, MaybeSender tempSender) {
int size = 0;
try {
size = Integer.parseInt(mailOptionValue);
} catch (NumberFormatException pe) {
LOGGER.error("Rejected syntactically incorrect value for SIZE parameter.");
// This is a malformed option value. We return an error
return SYNTAX_ERROR;
}
LOGGER.debug("MAIL command option SIZE received with value {}.", size);
long maxMessageSize = session.getConfiguration().getMaxMessageSize();
if ((maxMessageSize > 0) && (size > maxMessageSize)) {
// Let the client know that the size limit has been hit.
LOGGER.info("Rejected message from {} to {} of size {} exceeding system maximum message size of {} based on SIZE option.",
tempSender.asPrettyString(),
session.getRemoteAddress().getAddress().getHostAddress(),
size,
maxMessageSize);
return QUOTA_EXCEEDED;
} else {
// put the message size in the message state so it can be used
// later to restrict messages for user quotas, etc.
session.setAttachment(MESG_SIZE, size, State.Transaction);
}
return null;
}
@Override
public Response onLine(SMTPSession session, byte[] line, LineHandler<SMTPSession> next) {
boolean failed = session.messageFailed();
// If we already defined we failed and sent a reply we should simply
// wait for a CRLF.CRLF to be sent by the client.
if (failed) {
if (isDataTerminated(line)) {
next.onLine(session, line);
return new SMTPResponse(SMTPRetCode.QUOTA_EXCEEDED, "Quota exceeded");
} else {
return null;
}
} else {
if (isDataTerminated(line)) {
return next.onLine(session, line);
} else {
Long newSize = Optional.ofNullable(session.currentMessageSize())
.map(currentSize -> Long.valueOf(currentSize.intValue() + line.length))
.orElseGet(() -> Long.valueOf(line.length));
session.setCurrentMessageSize(newSize);
if (session.getConfiguration().getMaxMessageSize() > 0 && newSize.intValue() > session.getConfiguration().getMaxMessageSize()) {
// Add an item to the state to suppress
// logging of extra lines of data
// that are sent after the size limit has
// been hit.
session.setMessageFailed(true);
return null;
} else {
return next.onLine(session, line);
}
}
}
}
private boolean isDataTerminated(byte[] line) {
return line.length == SINGLE_CHARACTER_LINE && line[0] == DOT_BYTE;
}
@Override
public HookResult onMessage(SMTPSession session, MailEnvelope mail) {
boolean failed = session.messageFailed();
if (failed) {
LOGGER.info("Rejected message from {} from {} exceeding system maximum message size of {}",
session.getAttachment(SMTPSession.SENDER, State.Transaction).orElse(MaybeSender.nullSender()).asPrettyString(),
session.getRemoteAddress().getAddress().getHostAddress(), session.getConfiguration().getMaxMessageSize());
return QUOTA_EXCEEDED;
} else {
return HookResult.DECLINED;
}
}
}