From 562d8d220028fbb3d62028bc5879a121dff2fdbd Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Fri, 4 Sep 2015 14:27:18 +0200 Subject: [PATCH] Add smtp codec (client side only). Motivation: When writing a SMTP client a provided SMTP codec that follows RFC2821 is useful. Modification: Add client side codec and test. Results: People who want to write a SMTP client can reuse the codec. --- codec-smtp/pom.xml | 39 +++++ .../codec/smtp/DefaultLastSmtpContent.java | 65 +++++++++ .../codec/smtp/DefaultSmtpContent.java | 66 +++++++++ .../codec/smtp/DefaultSmtpRequest.java | 98 +++++++++++++ .../codec/smtp/DefaultSmtpResponse.java | 93 ++++++++++++ .../handler/codec/smtp/LastSmtpContent.java | 101 +++++++++++++ .../netty/handler/codec/smtp/SmtpCommand.java | 121 ++++++++++++++++ .../netty/handler/codec/smtp/SmtpContent.java | 44 ++++++ .../netty/handler/codec/smtp/SmtpRequest.java | 34 +++++ .../codec/smtp/SmtpRequestEncoder.java | 107 ++++++++++++++ .../handler/codec/smtp/SmtpRequests.java | 134 ++++++++++++++++++ .../handler/codec/smtp/SmtpResponse.java | 34 +++++ .../codec/smtp/SmtpResponseDecoder.java | 111 +++++++++++++++ .../netty/handler/codec/smtp/SmtpUtils.java | 32 +++++ .../handler/codec/smtp/package-info.java | 20 +++ .../codec/smtp/SmtpRequestEncoderTest.java | 111 +++++++++++++++ .../codec/smtp/SmtpResponseDecoderTest.java | 122 ++++++++++++++++ pom.xml | 1 + 18 files changed, 1333 insertions(+) create mode 100644 codec-smtp/pom.xml create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultLastSmtpContent.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpContent.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpRequest.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpResponse.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/LastSmtpContent.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpContent.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequest.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponse.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponseDecoder.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpUtils.java create mode 100644 codec-smtp/src/main/java/io/netty/handler/codec/smtp/package-info.java create mode 100644 codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java create mode 100644 codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpResponseDecoderTest.java diff --git a/codec-smtp/pom.xml b/codec-smtp/pom.xml new file mode 100644 index 0000000000..f2d1c8cd01 --- /dev/null +++ b/codec-smtp/pom.xml @@ -0,0 +1,39 @@ + + + + + 4.0.0 + + io.netty + netty-parent + 4.1.0.Final-SNAPSHOT + + + netty-codec-smtp + jar + + Netty/Codec/SMTP + + + + ${project.groupId} + netty-codec + ${project.version} + + + + diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultLastSmtpContent.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultLastSmtpContent.java new file mode 100644 index 0000000000..76d36af841 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultLastSmtpContent.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; + +/** + * Default implementation of {@link LastSmtpContent} that does no validation of the raw data passed in. + */ +public final class DefaultLastSmtpContent extends DefaultSmtpContent implements LastSmtpContent { + + /** + * Creates a new instance using the given data. + */ + public DefaultLastSmtpContent(ByteBuf data) { + super(data); + } + + @Override + public LastSmtpContent copy() { + return new DefaultLastSmtpContent(content().copy()); + } + + @Override + public LastSmtpContent duplicate() { + return new DefaultLastSmtpContent(content().duplicate()); + } + + @Override + public LastSmtpContent retain() { + super.retain(); + return this; + } + + @Override + public LastSmtpContent retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public LastSmtpContent touch() { + super.touch(); + return this; + } + + @Override + public LastSmtpContent touch(Object hint) { + super.touch(hint); + return this; + } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpContent.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpContent.java new file mode 100644 index 0000000000..01418ad815 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpContent.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; + +/** + * Default implementation of {@link SmtpContent} that does no validation of the raw data passed in. + */ +public class DefaultSmtpContent extends DefaultByteBufHolder implements SmtpContent { + + /** + * Creates a new instance using the given data. + */ + public DefaultSmtpContent(ByteBuf data) { + super(data); + } + + @Override + public SmtpContent copy() { + return new DefaultSmtpContent(content().copy()); + } + + @Override + public SmtpContent duplicate() { + return new DefaultSmtpContent(content().duplicate()); + } + + @Override + public SmtpContent retain() { + super.retain(); + return this; + } + + @Override + public SmtpContent retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public SmtpContent touch() { + super.touch(); + return this; + } + + @Override + public SmtpContent touch(Object hint) { + super.touch(hint); + return this; + } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpRequest.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpRequest.java new file mode 100644 index 0000000000..0aacc93e85 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpRequest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.util.internal.ObjectUtil; + +import java.util.Collections; +import java.util.List; + +/** + * Default {@link SmtpRequest} implementation. + */ +public final class DefaultSmtpRequest implements SmtpRequest { + + private final SmtpCommand command; + private final List parameters; + + /** + * Creates a new instance with the given command and no parameters. + */ + public DefaultSmtpRequest(SmtpCommand command) { + this.command = ObjectUtil.checkNotNull(command, "command"); + parameters = Collections.emptyList(); + } + + /** + * Creates a new instance with the given command and parameters. + */ + public DefaultSmtpRequest(SmtpCommand command, CharSequence... parameters) { + this.command = ObjectUtil.checkNotNull(command, "command"); + this.parameters = SmtpUtils.toUnmodifiableList(parameters); + } + + /** + * Creates a new instance with the given command and parameters. + */ + public DefaultSmtpRequest(CharSequence command, CharSequence... parameters) { + this(SmtpCommand.valueOf(command), parameters); + } + + DefaultSmtpRequest(SmtpCommand command, List parameters) { + this.command = ObjectUtil.checkNotNull(command, "command"); + this.parameters = parameters != null ? + Collections.unmodifiableList(parameters) : Collections.emptyList(); + } + + @Override + public SmtpCommand command() { + return command; + } + + @Override + public List parameters() { + return parameters; + } + + @Override + public int hashCode() { + return command.hashCode() * 31 + parameters.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DefaultSmtpRequest)) { + return false; + } + + if (o == this) { + return true; + } + + DefaultSmtpRequest other = (DefaultSmtpRequest) o; + + return command().equals(other.command()) && + parameters().equals(other.parameters()); + } + + @Override + public String toString() { + return "DefaultSmtpRequest{" + + "command=" + command + + ", parameters=" + parameters + + '}'; + } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpResponse.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpResponse.java new file mode 100644 index 0000000000..04df671fd1 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/DefaultSmtpResponse.java @@ -0,0 +1,93 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import java.util.Collections; +import java.util.List; + +/** + * Default {@link SmtpResponse} implementation. + */ +public final class DefaultSmtpResponse implements SmtpResponse { + + private final int code; + private final List details; + + /** + * Creates a new instance with the given smtp code and no details. + */ + public DefaultSmtpResponse(int code) { + this(code, (List) null); + } + + /** + * Creates a new instance with the given smtp code and details. + */ + public DefaultSmtpResponse(int code, CharSequence... details) { + this(code, SmtpUtils.toUnmodifiableList(details)); + } + + DefaultSmtpResponse(int code, List details) { + if (code < 100 || code > 599) { + throw new IllegalArgumentException("code must be 100 <= code <= 599"); + } + this.code = code; + if (details == null) { + this.details = Collections.emptyList(); + } else { + this.details = Collections.unmodifiableList(details); + } + } + + @Override + public int code() { + return code; + } + + @Override + public List details() { + return details; + } + + @Override + public int hashCode() { + return code * 31 + details.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DefaultSmtpResponse)) { + return false; + } + + if (o == this) { + return true; + } + + DefaultSmtpResponse other = (DefaultSmtpResponse) o; + + return code() == other.code() && + details().equals(other.details()); + } + + @Override + public String toString() { + return "DefaultSmtpResponse{" + + "code=" + code + + ", details=" + details + + '}'; + } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/LastSmtpContent.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/LastSmtpContent.java new file mode 100644 index 0000000000..6849ed5e6a --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/LastSmtpContent.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +/** + * The last part of a sequence of {@link SmtpContent}s that are sent after a {@code DATA} request. + * Be aware that a {@link SmtpContent} / {@link LastSmtpContent} sequence must always use CRLF as line delimiter + * and the lines that start with a DOT must be escaped with an extra DOT as + * specified by RFC2821. + */ +public interface LastSmtpContent extends SmtpContent { + + /** + * Empty {@link LastSmtpContent}. + */ + LastSmtpContent EMPTY_LAST_CONTENT = new LastSmtpContent() { + @Override + public LastSmtpContent copy() { + return this; + } + + @Override + public LastSmtpContent duplicate() { + return this; + } + + @Override + public LastSmtpContent retain() { + return this; + } + + @Override + public LastSmtpContent retain(int increment) { + return this; + } + + @Override + public LastSmtpContent touch() { + return this; + } + + @Override + public LastSmtpContent touch(Object hint) { + return this; + } + + @Override + public ByteBuf content() { + return Unpooled.EMPTY_BUFFER; + } + + @Override + public int refCnt() { + return 1; + } + + @Override + public boolean release() { + return false; + } + + @Override + public boolean release(int decrement) { + return false; + } + }; + + @Override + LastSmtpContent copy(); + + @Override + LastSmtpContent duplicate(); + + @Override + LastSmtpContent retain(); + + @Override + LastSmtpContent retain(int increment); + + @Override + LastSmtpContent touch(); + + @Override + LastSmtpContent touch(Object hint); +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java new file mode 100644 index 0000000000..f1466cbc8b --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpCommand.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.util.AsciiString; +import io.netty.util.internal.ObjectUtil; + +import java.util.HashMap; +import java.util.Map; + +/** + * The command part of a {@link SmtpRequest}. + */ +public final class SmtpCommand { + public static final SmtpCommand EHLO = new SmtpCommand(new AsciiString("EHLO"), false); + public static final SmtpCommand HELO = new SmtpCommand(new AsciiString("HELO"), false); + public static final SmtpCommand MAIL = new SmtpCommand(new AsciiString("MAIL"), false); + public static final SmtpCommand RCPT = new SmtpCommand(new AsciiString("RCPT"), false); + public static final SmtpCommand DATA = new SmtpCommand(new AsciiString("DATA"), true); + public static final SmtpCommand NOOP = new SmtpCommand(new AsciiString("NOOP"), false); + public static final SmtpCommand RSET = new SmtpCommand(new AsciiString("RSET"), false); + public static final SmtpCommand EXPN = new SmtpCommand(new AsciiString("EXPN"), false); + public static final SmtpCommand VRFY = new SmtpCommand(new AsciiString("VRFY"), false); + public static final SmtpCommand HELP = new SmtpCommand(new AsciiString("HELP"), false); + public static final SmtpCommand QUIT = new SmtpCommand(new AsciiString("QUIT"), false); + + private static final CharSequence DATA_CMD = new AsciiString("DATA"); + private static final Map COMMANDS = new HashMap(); + static { + COMMANDS.put(EHLO.name(), EHLO); + COMMANDS.put(HELO.name(), HELO); + COMMANDS.put(MAIL.name(), MAIL); + COMMANDS.put(RCPT.name(), RCPT); + COMMANDS.put(DATA.name(), DATA); + COMMANDS.put(NOOP.name(), NOOP); + COMMANDS.put(RSET.name(), RSET); + COMMANDS.put(EXPN.name(), EXPN); + COMMANDS.put(VRFY.name(), VRFY); + COMMANDS.put(HELP.name(), HELP); + COMMANDS.put(QUIT.name(), QUIT); + } + + /** + * Returns the {@link SmtpCommand} for the given command name. + */ + public static SmtpCommand valueOf(CharSequence commandName) { + SmtpCommand command = COMMANDS.get(commandName); + if (command != null) { + return command; + } + return new SmtpCommand(AsciiString.of(ObjectUtil.checkNotNull(commandName, "commandName")), + AsciiString.contentEqualsIgnoreCase(commandName, DATA_CMD)); + } + + private final AsciiString name; + private final boolean contentExpected; + private int hashCode; + + private SmtpCommand(AsciiString name, boolean contentExpected) { + this.name = name; + this.contentExpected = contentExpected; + } + + /** + * Return the command name. + */ + public AsciiString name() { + return name; + } + + void encode(ByteBuf buffer) { + ByteBufUtil.writeAscii(buffer, name()); + } + + boolean isContentExpected() { + return contentExpected; + } + + @Override + public int hashCode() { + if (hashCode != -1) { + hashCode = AsciiString.hashCode(name); + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SmtpCommand)) { + return false; + } + return name.contentEqualsIgnoreCase(((SmtpCommand) obj).name()); + } + + @Override + public String toString() { + return "SmtpCommand{" + + "name=" + name + + ", contentExpected=" + contentExpected + + ", hashCode=" + hashCode + + '}'; + } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpContent.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpContent.java new file mode 100644 index 0000000000..b30a938395 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpContent.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBufHolder; + +/** + * Content that is sent after the {@code DATA} request. + * Be aware that a {@link SmtpContent} / {@link LastSmtpContent} sequence must always use CRLF as line delimiter + * and the lines that start with a DOT must be escaped with an extra DOT as + * specified by RFC2821. + */ +public interface SmtpContent extends ByteBufHolder { + @Override + SmtpContent copy(); + + @Override + SmtpContent duplicate(); + + @Override + SmtpContent retain(); + + @Override + SmtpContent retain(int increment); + + @Override + SmtpContent touch(); + + @Override + SmtpContent touch(Object hint); +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequest.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequest.java new file mode 100644 index 0000000000..73d4335678 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import java.util.List; + +/** + * An SMTP request. + */ +public interface SmtpRequest { + + /** + * Returns the {@link SmtpCommand} that belongs to the request. + */ + SmtpCommand command(); + + /** + * Returns a {@link List} which holds all the parameters of a request, which may be an empty list. + */ + List parameters(); +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java new file mode 100644 index 0000000000..12362550a3 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequestEncoder.java @@ -0,0 +1,107 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; + +import java.util.Iterator; +import java.util.List; +import java.util.RandomAccess; + +/** + * Encoder for SMTP requests. + */ +public final class SmtpRequestEncoder extends MessageToMessageEncoder { + private static final byte[] CRLF = {'\r', '\n'}; + private static final byte[] DOT_CRLF = {'.', '\r', '\n'}; + private static final byte SP = ' '; + private static final ByteBuf DOT_CRLF_BUFFER = Unpooled.unreleasableBuffer( + Unpooled.directBuffer(3).writeBytes(DOT_CRLF)); + + private boolean contentExpected; + + @Override + public boolean acceptOutboundMessage(Object msg) throws Exception { + return msg instanceof SmtpRequest || msg instanceof SmtpContent; + } + + @Override + protected void encode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { + if (msg instanceof SmtpRequest) { + if (contentExpected) { + throw new IllegalStateException("SmtpContent expected"); + } + boolean release = true; + final ByteBuf buffer = ctx.alloc().buffer(); + try { + final SmtpRequest req = (SmtpRequest) msg; + req.command().encode(buffer); + writeParameters(req.parameters(), buffer); + buffer.writeBytes(CRLF); + out.add(buffer); + release = false; + if (req.command().isContentExpected()) { + contentExpected = true; + } + } finally { + if (release) { + buffer.release(); + } + } + } + + if (msg instanceof SmtpContent) { + if (!contentExpected) { + throw new IllegalStateException("No SmtpContent expected"); + } + final ByteBuf content = ((SmtpContent) msg).content(); + out.add(content.retain()); + if (msg instanceof LastSmtpContent) { + out.add(DOT_CRLF_BUFFER.duplicate().retain()); + contentExpected = false; + } + } + } + + private static void writeParameters(List parameters, ByteBuf out) { + if (parameters.isEmpty()) { + return; + } + out.writeByte(SP); + if (parameters instanceof RandomAccess) { + final int sizeMinusOne = parameters.size() - 1; + for (int i = 0; i < sizeMinusOne; i++) { + ByteBufUtil.writeAscii(out, parameters.get(i)); + out.writeByte(SP); + } + ByteBufUtil.writeAscii(out, parameters.get(sizeMinusOne)); + } else { + final Iterator params = parameters.iterator(); + for (;;) { + ByteBufUtil.writeAscii(out, params.next()); + if (params.hasNext()) { + out.writeByte(SP); + } else { + break; + } + } + } + } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java new file mode 100644 index 0000000000..60b9cd1bb2 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpRequests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.util.AsciiString; +import io.netty.util.internal.ObjectUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides utility methods to create {@link SmtpRequest}s. + */ +public final class SmtpRequests { + + private static final SmtpRequest DATA = new DefaultSmtpRequest(SmtpCommand.DATA); + private static final SmtpRequest NOOP = new DefaultSmtpRequest(SmtpCommand.NOOP); + private static final SmtpRequest RSET = new DefaultSmtpRequest(SmtpCommand.RSET); + private static final SmtpRequest HELP_NO_ARG = new DefaultSmtpRequest(SmtpCommand.HELP); + private static final SmtpRequest QUIT = new DefaultSmtpRequest(SmtpCommand.QUIT); + private static final AsciiString FROM_NULL_SENDER = new AsciiString("FROM:<>"); + + /** + * Creates a {@code HELO} request. + */ + public static SmtpRequest helo(CharSequence hostname) { + return new DefaultSmtpRequest(SmtpCommand.HELO, hostname); + } + + /** + * Creates a {@code EHLO} request. + */ + public static SmtpRequest ehlo(CharSequence hostname) { + return new DefaultSmtpRequest(SmtpCommand.EHLO, hostname); + } + + /** + * Creates a {@code NOOP} request. + */ + public static SmtpRequest noop() { + return NOOP; + } + + /** + * Creates a {@code DATA} request. + */ + public static SmtpRequest data() { + return DATA; + } + + /** + * Creates a {@code RSET} request. + */ + public static SmtpRequest rset() { + return RSET; + } + + /** + * Creates a {@code HELP} request. + */ + public static SmtpRequest help(String cmd) { + return cmd == null ? HELP_NO_ARG : new DefaultSmtpRequest(SmtpCommand.HELP, cmd); + } + + /** + * Creates a {@code QUIT} request. + */ + public static SmtpRequest quit() { + return QUIT; + } + + /** + * Creates a {@code MAIL} request. + */ + public static SmtpRequest mail(CharSequence sender, CharSequence... mailParameters) { + if (mailParameters == null || mailParameters.length == 0) { + return new DefaultSmtpRequest(SmtpCommand.MAIL, + sender != null ? "FROM:<" + sender + '>' : FROM_NULL_SENDER); + } else { + List params = new ArrayList(mailParameters.length + 1); + params.add(sender != null? "FROM:<" + sender + '>' : FROM_NULL_SENDER); + for (CharSequence param : mailParameters) { + params.add(param); + } + return new DefaultSmtpRequest(SmtpCommand.MAIL, params); + } + } + + /** + * Creates a {@code RCPT} request. + */ + public static SmtpRequest rcpt(CharSequence recipient, CharSequence... rcptParameters) { + ObjectUtil.checkNotNull(recipient, "recipient"); + if (rcptParameters == null || rcptParameters.length == 0) { + return new DefaultSmtpRequest(SmtpCommand.RCPT, "TO:<" + recipient + '>'); + } else { + List params = new ArrayList(rcptParameters.length + 1); + params.add("TO:<" + recipient + '>'); + for (CharSequence param : rcptParameters) { + params.add(param); + } + return new DefaultSmtpRequest(SmtpCommand.RCPT, params); + } + } + + /** + * Creates a {@code EXPN} request. + */ + public static SmtpRequest expn(CharSequence mailingList) { + return new DefaultSmtpRequest(SmtpCommand.EXPN, ObjectUtil.checkNotNull(mailingList, "mailingList")); + } + + /** + * Creates a {@code VRFY} request. + */ + public static SmtpRequest vrfy(CharSequence user) { + return new DefaultSmtpRequest(SmtpCommand.VRFY, ObjectUtil.checkNotNull(user, "user")); + } + + private SmtpRequests() { } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponse.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponse.java new file mode 100644 index 0000000000..e5159481e8 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import java.util.List; + +/** + * A SMTP response + */ +public interface SmtpResponse { + + /** + * Returns the response code. + */ + int code(); + + /** + * Returns the details if any. + */ + List details(); +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponseDecoder.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponseDecoder.java new file mode 100644 index 0000000000..c8a3807953 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpResponseDecoder.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.util.CharsetUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Decoder for SMTP responses. + */ +public final class SmtpResponseDecoder extends LineBasedFrameDecoder { + + private List details; + + /** + * Creates a new instance that enforces the given {@code maxLineLength}. + */ + public SmtpResponseDecoder(int maxLineLength) { + super(maxLineLength); + } + + @Override + protected SmtpResponse decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, buffer); + if (frame == null) { + // No full line received yet. + return null; + } + try { + final int readable = frame.readableBytes(); + final int readerIndex = frame.readerIndex(); + if (readable < 3) { + throw newDecoderException(buffer, readerIndex, readable); + } + final int code = parseCode(frame); + final int separator = frame.readByte(); + final CharSequence detail = frame.isReadable() ? frame.toString(CharsetUtil.US_ASCII) : null; + + List details = this.details; + + switch (separator) { + case ' ': + // Marks the end of a response. + this.details = null; + if (details != null) { + if (detail != null) { + details.add(detail); + } + } else { + details = Collections.singletonList(detail); + } + return new DefaultSmtpResponse(code, details); + case '-': + // Multi-line response. + if (detail != null) { + if (details == null) { + // Using initial capacity as it is very unlikely that we will receive a multi-line response + // with more then 3 lines. + this.details = details = new ArrayList(4); + } + details.add(detail); + } + break; + default: + throw newDecoderException(buffer, readerIndex, readable); + } + } finally { + frame.release(); + } + return null; + } + + private static DecoderException newDecoderException(ByteBuf buffer, int readerIndex, int readable) { + return new DecoderException( + "Received invalid line: '" + buffer.toString(readerIndex, readable, CharsetUtil.US_ASCII) + '\''); + } + + /** + * Parses the io.netty.handler.codec.smtp code without any allocation, which is three digits. + */ + private static int parseCode(ByteBuf buffer) { + final int first = parseNumber(buffer.readByte()) * 100; + final int second = parseNumber(buffer.readByte()) * 10; + final int third = parseNumber(buffer.readByte()); + return first + second + third; + } + + private static int parseNumber(byte b) { + return Character.digit((char) b, 10); + } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpUtils.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpUtils.java new file mode 100644 index 0000000000..a2b84eac84 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/SmtpUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +final class SmtpUtils { + + static List toUnmodifiableList(CharSequence... sequences) { + if (sequences == null || sequences.length == 0) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(Arrays.asList(sequences)); + } + + private SmtpUtils() { } +} diff --git a/codec-smtp/src/main/java/io/netty/handler/codec/smtp/package-info.java b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/package-info.java new file mode 100644 index 0000000000..fc33e43cc5 --- /dev/null +++ b/codec-smtp/src/main/java/io/netty/handler/codec/smtp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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. + */ + +/** + * SMTP codec. + */ +package io.netty.handler.codec.smtp; diff --git a/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java new file mode 100644 index 0000000000..626bdfe671 --- /dev/null +++ b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpRequestEncoderTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.CharsetUtil; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SmtpRequestEncoderTest { + + @Test + public void testEncodeEhlo() { + testEncode(SmtpRequests.ehlo("localhost"), "EHLO localhost\r\n"); + } + + @Test + public void testEncodeHelo() { + testEncode(SmtpRequests.helo("localhost"), "HELO localhost\r\n"); + } + + @Test + public void testEncodeMail() { + testEncode(SmtpRequests.mail("me@netty.io"), "MAIL FROM:\r\n"); + } + + @Test + public void testEncodeMailNullSender() { + testEncode(SmtpRequests.mail(null), "MAIL FROM:<>\r\n"); + } + + @Test + public void testEncodeRcpt() { + testEncode(SmtpRequests.rcpt("me@netty.io"), "RCPT TO:\r\n"); + } + + @Test + public void testEncodeNoop() { + testEncode(SmtpRequests.noop(), "NOOP\r\n"); + } + + @Test + public void testEncodeRset() { + testEncode(SmtpRequests.rset(), "RSET\r\n"); + } + + @Test + public void testEncodeHelp() { + testEncode(SmtpRequests.help(null), "HELP\r\n"); + } + + @Test + public void testEncodeHelpWithArg() { + testEncode(SmtpRequests.help("MAIL"), "HELP MAIL\r\n"); + } + + @Test + public void testEncodeData() { + testEncode(SmtpRequests.data(), "DATA\r\n"); + } + + @Test + public void testEncodeDataAndContent() { + EmbeddedChannel channel = new EmbeddedChannel(new SmtpRequestEncoder()); + assertTrue(channel.writeOutbound(SmtpRequests.data())); + assertTrue(channel.writeOutbound( + new DefaultSmtpContent(Unpooled.copiedBuffer("Subject: Test\r\n\r\n", CharsetUtil.US_ASCII)))); + assertTrue(channel.writeOutbound( + new DefaultLastSmtpContent(Unpooled.copiedBuffer("Test\r\n", CharsetUtil.US_ASCII)))); + assertTrue(channel.finish()); + + ByteBuf written = Unpooled.buffer(); + + for (;;) { + ByteBuf buffer = channel.readOutbound(); + if (buffer == null) { + break; + } + written.writeBytes(buffer); + buffer.release(); + } + assertEquals("DATA\r\nSubject: Test\r\n\r\nTest\r\n.\r\n", written.toString(CharsetUtil.US_ASCII)); + written.release(); + } + + private static void testEncode(SmtpRequest request, String expected) { + EmbeddedChannel channel = new EmbeddedChannel(new SmtpRequestEncoder()); + assertTrue(channel.writeOutbound(request)); + assertTrue(channel.finish()); + ByteBuf buffer = channel.readOutbound(); + assertEquals(expected, buffer.toString(CharsetUtil.US_ASCII)); + buffer.release(); + assertNull(channel.readOutbound()); + } +} diff --git a/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpResponseDecoderTest.java b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpResponseDecoderTest.java new file mode 100644 index 0000000000..7ccc993acb --- /dev/null +++ b/codec-smtp/src/test/java/io/netty/handler/codec/smtp/SmtpResponseDecoderTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project 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 io.netty.handler.codec.smtp; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.DecoderException; +import io.netty.util.CharsetUtil; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.*; + +public class SmtpResponseDecoderTest { + + @Test + public void testDecodeOneLineResponse() { + EmbeddedChannel channel = newChannel(); + assertTrue(channel.writeInbound(newBuffer("200 Ok\r\n"))); + assertTrue(channel.finish()); + + SmtpResponse response = channel.readInbound(); + assertEquals(200, response.code()); + List sequences = response.details(); + assertEquals(1, sequences.size()); + + assertEquals("Ok", sequences.get(0).toString()); + assertNull(channel.readInbound()); + } + + @Test + public void testDecodeOneLineResponseChunked() { + EmbeddedChannel channel = newChannel(); + assertFalse(channel.writeInbound(newBuffer("200 Ok"))); + assertTrue(channel.writeInbound(newBuffer("\r\n"))); + assertTrue(channel.finish()); + + SmtpResponse response = channel.readInbound(); + assertEquals(200, response.code()); + List sequences = response.details(); + assertEquals(1, sequences.size()); + + assertEquals("Ok", sequences.get(0).toString()); + assertNull(channel.readInbound()); + } + + @Test + public void testDecodeTwoLineResponse() { + EmbeddedChannel channel = newChannel(); + assertTrue(channel.writeInbound(newBuffer("200-Hello\r\n200 Ok\r\n"))); + assertTrue(channel.finish()); + + SmtpResponse response = channel.readInbound(); + assertEquals(200, response.code()); + List sequences = response.details(); + assertEquals(2, sequences.size()); + + assertEquals("Hello", sequences.get(0).toString()); + assertEquals("Ok", sequences.get(1).toString()); + assertNull(channel.readInbound()); + } + + @Test + public void testDecodeTwoLineResponseChunked() { + EmbeddedChannel channel = newChannel(); + assertFalse(channel.writeInbound(newBuffer("200-"))); + assertFalse(channel.writeInbound(newBuffer("Hello\r\n2"))); + assertFalse(channel.writeInbound(newBuffer("00 Ok"))); + assertTrue(channel.writeInbound(newBuffer("\r\n"))); + assertTrue(channel.finish()); + + SmtpResponse response = channel.readInbound(); + assertEquals(200, response.code()); + List sequences = response.details(); + assertEquals(2, sequences.size()); + + assertEquals("Hello", sequences.get(0).toString()); + assertEquals("Ok", sequences.get(1).toString()); + assertNull(channel.readInbound()); + } + + @Test(expected = DecoderException.class) + public void testDecodeInvalidSeparator() { + EmbeddedChannel channel = newChannel(); + assertTrue(channel.writeInbound(newBuffer("200:Ok\r\n"))); + } + + @Test(expected = DecoderException.class) + public void testDecodeInvalidCode() { + EmbeddedChannel channel = newChannel(); + assertTrue(channel.writeInbound(newBuffer("xyz Ok\r\n"))); + } + + @Test(expected = DecoderException.class) + public void testDecodeInvalidLine() { + EmbeddedChannel channel = newChannel(); + assertTrue(channel.writeInbound(newBuffer("Ok\r\n"))); + } + + private static EmbeddedChannel newChannel() { + return new EmbeddedChannel(new SmtpResponseDecoder(Integer.MAX_VALUE)); + } + + private static ByteBuf newBuffer(CharSequence seq) { + return Unpooled.copiedBuffer(seq, CharsetUtil.US_ASCII); + } +} diff --git a/pom.xml b/pom.xml index b7e2c94552..55c474adb1 100644 --- a/pom.xml +++ b/pom.xml @@ -239,6 +239,7 @@ codec-http2 codec-memcache codec-mqtt + codec-smtp codec-socks codec-stomp codec-xml