This is an automated email from the ASF dual-hosted git repository. hqtran pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push: new 2582ff5eec JAMES-4132 HeaderLimitations (#2721) 2582ff5eec is described below commit 2582ff5eec2d63fbe6ee15d3a5771d522c583f05 Author: AlaeMghirbi <amghi...@linagora.com> AuthorDate: Mon May 19 10:59:48 2025 +0200 JAMES-4132 HeaderLimitations (#2721) --- .../servers/partials/configure/smtp-hooks.adoc | 4 +- .../partials/configure/smtp-limitation-hoot.adoc | 30 +++++ .../EnforceHeaderLimitationsMessageHook.java | 107 ++++++++++++++++ ...nforcedHeaderLimitationHookIntegrationTest.java | 135 +++++++++++++++++++++ .../test/resources/smtpserver-EnforceHeader.xml | 54 +++++++++ 5 files changed, 329 insertions(+), 1 deletion(-) diff --git a/docs/modules/servers/partials/configure/smtp-hooks.adoc b/docs/modules/servers/partials/configure/smtp-hooks.adoc index e99848a089..f7d8fa7c18 100644 --- a/docs/modules/servers/partials/configure/smtp-hooks.adoc +++ b/docs/modules/servers/partials/configure/smtp-hooks.adoc @@ -403,4 +403,6 @@ Example handlerchain configuration for `smtpserver.xml`: .... Would allow emails using `apache.org` as a MAIL FROM or from header domain if, and only if they contain a -valid DKIM signature for the `apache.org` domain. \ No newline at end of file +valid DKIM signature for the `apache.org` domain. + +include::partial$EnforceHeaderLimitationsMessageHook.adoc[] \ No newline at end of file diff --git a/docs/modules/servers/partials/configure/smtp-limitation-hoot.adoc b/docs/modules/servers/partials/configure/smtp-limitation-hoot.adoc new file mode 100644 index 0000000000..6dbd1e9768 --- /dev/null +++ b/docs/modules/servers/partials/configure/smtp-limitation-hoot.adoc @@ -0,0 +1,30 @@ +=== EnforceHeaderLimitationsMessageHook + +The `EnforceHeaderLimitationsMessageHook` is used to enforce limitations on the headers of incoming emails. It ensures that emails comply with configurable restrictions on the number of header lines and the total size of headers. + +To configure this hook, add it to the `<handlerchain>` section of your SMTP server configuration: + +* `maxLines`: The maximum number of header lines allowed (default: 500). +* `maxSize`: The maximum total size of headers in kilobytes (default: 64 KB). + +- If the number of header lines exceeds the `maxLines` limit, the email is rejected with the SMTP error code `552 Too many header lines`. +- If the total size of headers exceeds the `maxSize` limit, the email is rejected with the SMTP error code `552 Header size too large`. +- If both limits are respected, the hook declines further processing, allowing the email to proceed. + +==== Example + +Here is an example of a SetUp using this hook: +[source,xml] +.... +<smtpserver enabled="true"> + <handlerchain> + <handler class="org.apache.james.smtpserver.EnforceHeaderLimitationsMessageHook"> + <maxLines>500</maxLines> + <maxSize>64</maxSize> + </handler> + <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> + </handlerchain> + <gracefulShutdown>false</gracefulShutdown> +</smtpserver> +.... +---- \ No newline at end of file diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/EnforceHeaderLimitationsMessageHook.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/EnforceHeaderLimitationsMessageHook.java new file mode 100644 index 0000000000..d57bc83e0f --- /dev/null +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/EnforceHeaderLimitationsMessageHook.java @@ -0,0 +1,107 @@ +/**************************************************************** + * 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.smtpserver; + +import java.util.Enumeration; + +import jakarta.mail.Header; + +import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.james.protocols.smtp.SMTPRetCode; +import org.apache.james.protocols.smtp.SMTPSession; +import org.apache.james.protocols.smtp.hook.HookResult; +import org.apache.james.protocols.smtp.hook.HookReturnCode; +import org.apache.james.util.Size; +import org.apache.mailet.Mail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class implements an SMTP hook to enforce limitations on the headers of incoming emails. + * + * It allows configuring and enforcing two types of restrictions: + * - A maximum number of header lines (default: 500). + * - A maximum total size of headers in bytes (default: 64 KB). + * If any of these thresholds are exceeded, the message is rejected with an SMTP error code: + * <code>552 Too many header lines</code> if the number of lines exceeds the limit. + * <code>552 Header size too large</code> if the total size exceeds the limit. + * + * Example XML configuration: + * <pre>{ + * <handler class="org.apache.james.smtpserver.EnforceHeaderLimitationsMessageHook"> + * <maxLines>500</maxLines> + * <maxSize>64KB</maxSize> + * </handler> + * }</pre> + * + */ + +public class EnforceHeaderLimitationsMessageHook implements JamesMessageHook { + + private static final Logger LOGGER = LoggerFactory.getLogger(EnforceHeaderLimitationsMessageHook.class); + private static final int DEFAULT_MAX_LINES = 500; + private static final int DEFAULT_MAX_SIZE = 1024 * 64; + + private int maxLines; + private long maxSize; + + @Override + public HookResult onMessage(SMTPSession session, Mail mail) { + try { + int actualLines = 0; + int actualSize = 0; + Enumeration<Header> headers = mail.getMessage().getAllHeaders(); + while (headers.hasMoreElements()) { + Header header = headers.nextElement(); + actualLines += 1; + actualSize += header.getName().length() + header.getValue().length() + 4; + if (actualLines > maxLines) { + LOGGER.warn("Email rejected: too many header lines"); + return HookResult.builder() + .hookReturnCode(HookReturnCode.denySoft()) + .smtpReturnCode(SMTPRetCode.QUOTA_EXCEEDED) + .smtpDescription("Header Lines are too many") + .build(); + } + if (actualSize > maxSize) { + LOGGER.warn("Email rejected: header size too large"); + return HookResult.builder() + .hookReturnCode(HookReturnCode.denySoft()) + .smtpReturnCode(SMTPRetCode.QUOTA_EXCEEDED) + .smtpDescription("Header size is too large") + .build(); + } + } + return HookResult.DECLINED; + } catch (Exception e) { + LOGGER.warn("Error while checking header size", e); + return HookResult.DENY; + } + } + + @Override + public void init(Configuration config) throws ConfigurationException { + this.maxLines = config.getInt("maxLines") <= 0 ? DEFAULT_MAX_LINES : config.getInt("maxLines", DEFAULT_MAX_LINES); + long size = Size.parse(config.getString("maxSize")).asBytes(); + this.maxSize = size > 0 ? size : DEFAULT_MAX_SIZE; + } +} + diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/EnforcedHeaderLimitationHookIntegrationTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/EnforcedHeaderLimitationHookIntegrationTest.java new file mode 100644 index 0000000000..6c406bdd9a --- /dev/null +++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/EnforcedHeaderLimitationHookIntegrationTest.java @@ -0,0 +1,135 @@ +/**************************************************************** + * 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.smtpserver; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.james.smtpserver.SMTPServerTestSystem.BOB; +import static org.apache.james.smtpserver.SMTPServerTestSystem.PASSWORD; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Base64; + +import org.apache.commons.net.smtp.SMTPClient; +import org.apache.james.server.core.configuration.Configuration; +import org.apache.james.server.core.configuration.FileConfigurationProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class EnforcedHeaderLimitationHookIntegrationTest { + protected Configuration configuration; + + private final SMTPServerTestSystem testSystem = new SMTPServerTestSystem(); + + @BeforeEach + void setUp() throws Exception { + testSystem.preSetUp(); + } + + @AfterEach + void tearDown() { + testSystem.smtpServer.destroy(); + } + + @Test + void shouldRejectWhenTooManyHeaderLines() throws Exception { + testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( + ClassLoader.getSystemResourceAsStream("smtpserver-EnforceHeader.xml"))); + testSystem.smtpServer.init(); + + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = testSystem.getBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> RET=HDRS"); + smtpProtocol.sendCommand("RCPT TO:<rcpt@localhost>"); + + StringBuilder headers = new StringBuilder(); + headers.append("From: bob@localhost\r\n"); + headers.append("To: rcpt@localhost\r\n"); + headers.append("HEADER1: value1\r\n"); + headers.append("HEADER2: value2\r\n"); + headers.append("HEADER3: value3\r\n"); + smtpProtocol.sendShortMessageData(headers.toString() + "Subject: test mail\r\n\r\nTest body testSimpleMailSendWithDSN\r\n.\r\n\""); + + assertThat(smtpProtocol.getReplyString()) + .as("expected 552 error") + .isEqualTo("552 Header Lines are too many\r\n"); + } + + @Test + void shouldRejectWhenSizeTooLarge() throws Exception { + testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( + ClassLoader.getSystemResourceAsStream("smtpserver-EnforceHeader.xml"))); + testSystem.smtpServer.init(); + + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = testSystem.getBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> RET=HDRS"); + smtpProtocol.sendCommand("RCPT TO:<rcpt@localhost>"); + + StringBuilder headers = new StringBuilder(); + headers.append("From: bob@localhost\r\n"); + headers.append("HEADER1: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazertyuytgyuetgfrugreyryutgryuryfguyrtgfruyrftguyyurgryegyugfreyurgiopqsdfghjklwxfyhgyuguygftfytfytfytftfteffsrzfztzzftztdtfcvbnzertyuiosdfgh [...] + headers.append("HEADER2: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazertyuighgghgguygjygyugyuguyyfuftyfytfyfythfytfryefopqsdfghjklwxfyhgyuguygftfytfytfytftfteffsrzfztzzftztdtfcvbnzertyuiosdfghjkjnnzaesfrvfdt\r\n"); + + smtpProtocol.sendShortMessageData(headers.toString() + "Subject: test mail for the smtp sever to check if the hook that controls the limits of lines and size allowed is correctly stopping the server from excepting ecxeeded quota iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii \r\n\r\nTest body testSimpleMailSendWithDSN\r\n.\r\n\""); + + assertThat(smtpProtocol.getReplyString()) + .as("expected 552 error") + .isEqualTo("552 Header size is too large\r\n"); + } + + @Test + void shouldSendMessageWhenWithinLimits() throws Exception { + testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( + ClassLoader.getSystemResourceAsStream("smtpserver-EnforceHeader.xml"))); + testSystem.smtpServer.init(); + + SMTPClient smtpProtocol = new SMTPClient(); + InetSocketAddress bindedAddress = testSystem.getBindedAddress(); + smtpProtocol.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + authenticate(smtpProtocol); + + smtpProtocol.sendCommand("EHLO localhost"); + smtpProtocol.sendCommand("MAIL FROM: <bob@localhost> RET=HDRS"); + smtpProtocol.sendCommand("RCPT TO:<rcpt@localhost>"); + smtpProtocol.sendShortMessageData("From: bob@localhost\r\nFrom: bob@localhost\r\nto: aziz@localhost\r\n\r\nTest body testSimpleMailSendWithDSN\r\n.\r\n"); + + assertThat(smtpProtocol.getReplyCode()) + .as("expected message to be sent") + .isEqualTo(250); + } + + private void authenticate(SMTPClient smtpProtocol) throws IOException { + smtpProtocol.sendCommand("AUTH PLAIN"); + smtpProtocol.sendCommand(Base64.getEncoder().encodeToString(("\0" + BOB.asString() + "\0" + PASSWORD + "\0").getBytes(UTF_8))); + assertThat(smtpProtocol.getReplyCode()) + .as("authenticated") + .isEqualTo(235); + } +} diff --git a/server/protocols/protocols-smtp/src/test/resources/smtpserver-EnforceHeader.xml b/server/protocols/protocols-smtp/src/test/resources/smtpserver-EnforceHeader.xml new file mode 100644 index 0000000000..f81c6f7e63 --- /dev/null +++ b/server/protocols/protocols-smtp/src/test/resources/smtpserver-EnforceHeader.xml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> + +<!-- + 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. + --> + +<!-- Read https://james.apache.org/server/config-smtp-lmtp.html#SMTP_Configuration for further details --> + +<smtpserver enabled="true"> + <bind>0.0.0.0:0</bind> + <connectionBacklog>200</connectionBacklog> + <tls socketTLS="false" startTLS="false"> + <keystore>file://conf/keystore</keystore> + <secret>james72laBalle</secret> + <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider> + <algorithm>SunX509</algorithm> + </tls> + <connectiontimeout>360</connectiontimeout> + <connectionLimit>0</connectionLimit> + <connectionLimitPerIP>0</connectionLimitPerIP> + <auth> + <announce>forUnauthorizedAddresses</announce> + <requireSSL>false</requireSSL> + </auth> + <verifyIdentity>true</verifyIdentity> + <maxmessagesize>0</maxmessagesize> + <addressBracketsEnforcement>true</addressBracketsEnforcement> + <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting> + <handlerchain> + <handler class="org.apache.james.smtpserver.EnforceHeaderLimitationsMessageHook"> + <maxLines>5</maxLines> + <maxSize>1K</maxSize> + </handler> + <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> + </handlerchain> + <gracefulShutdown>false</gracefulShutdown> +</smtpserver> + + --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org