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.
This commit is contained in:
Norman Maurer 2015-09-04 14:27:18 +02:00
parent 24254b159f
commit 562d8d2200
18 changed files with 1333 additions and 0 deletions

39
codec-smtp/pom.xml Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.netty</groupId>
<artifactId>netty-parent</artifactId>
<version>4.1.0.Final-SNAPSHOT</version>
</parent>
<artifactId>netty-codec-smtp</artifactId>
<packaging>jar</packaging>
<name>Netty/Codec/SMTP</name>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netty-codec</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<CharSequence> 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<CharSequence> parameters) {
this.command = ObjectUtil.checkNotNull(command, "command");
this.parameters = parameters != null ?
Collections.unmodifiableList(parameters) : Collections.<CharSequence>emptyList();
}
@Override
public SmtpCommand command() {
return command;
}
@Override
public List<CharSequence> 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 +
'}';
}
}

View File

@ -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<CharSequence> details;
/**
* Creates a new instance with the given smtp code and no details.
*/
public DefaultSmtpResponse(int code) {
this(code, (List<CharSequence>) 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<CharSequence> 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<CharSequence> 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 +
'}';
}
}

View File

@ -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 <a href="https://www.ietf.org/rfc/rfc2821.txt">RFC2821</a>.
*/
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);
}

View File

@ -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<CharSequence, SmtpCommand> COMMANDS = new HashMap<CharSequence, SmtpCommand>();
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 +
'}';
}
}

View File

@ -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 <a href="https://www.ietf.org/rfc/rfc2821.txt">RFC2821</a>.
*/
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);
}

View File

@ -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<CharSequence> parameters();
}

View File

@ -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<Object> {
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<Object> 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<CharSequence> 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<CharSequence> params = parameters.iterator();
for (;;) {
ByteBufUtil.writeAscii(out, params.next());
if (params.hasNext()) {
out.writeByte(SP);
} else {
break;
}
}
}
}
}

View File

@ -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<CharSequence> params = new ArrayList<CharSequence>(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<CharSequence> params = new ArrayList<CharSequence>(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() { }
}

View File

@ -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<CharSequence> details();
}

View File

@ -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<CharSequence> 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<CharSequence> 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<CharSequence>(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);
}
}

View File

@ -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<CharSequence> toUnmodifiableList(CharSequence... sequences) {
if (sequences == null || sequences.length == 0) {
return Collections.emptyList();
}
return Collections.unmodifiableList(Arrays.asList(sequences));
}
private SmtpUtils() { }
}

View File

@ -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.
*/
/**
* <a href="https://www.ietf.org/rfc/rfc2821.txt">SMTP</a> codec.
*/
package io.netty.handler.codec.smtp;

View File

@ -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:<me@netty.io>\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:<me@netty.io>\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());
}
}

View File

@ -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<CharSequence> 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<CharSequence> 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<CharSequence> 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<CharSequence> 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);
}
}

View File

@ -239,6 +239,7 @@
<module>codec-http2</module> <module>codec-http2</module>
<module>codec-memcache</module> <module>codec-memcache</module>
<module>codec-mqtt</module> <module>codec-mqtt</module>
<module>codec-smtp</module>
<module>codec-socks</module> <module>codec-socks</module>
<module>codec-stomp</module> <module>codec-stomp</module>
<module>codec-xml</module> <module>codec-xml</module>