/*
* Licensed 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 com.ok2c.lightmtp.message;
import java.nio.charset.CharacterCodingException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.apache.http.message.ParserCursor;
import org.apache.http.nio.reactor.SessionInputBuffer;
import org.apache.http.util.Args;
import org.apache.http.util.CharArrayBuffer;
import com.ok2c.lightmtp.SMTPCode;
import com.ok2c.lightmtp.SMTPConsts;
import com.ok2c.lightmtp.SMTPProtocolException;
import com.ok2c.lightmtp.SMTPReply;
public class SMTPReplyParser implements SMTPMessageParser<SMTPReply> {
private final CharArrayBuffer lineBuf;
private final LinkedList<ParsedLine> parsedLines;
private final int maxLineLen;
private final boolean useEnhancedCodes;
public SMTPReplyParser(final int maxLineLen, final boolean useEnhancedCodes) {
super();
this.lineBuf = new CharArrayBuffer(1024);
this.parsedLines = new LinkedList<ParsedLine>();
this.maxLineLen = maxLineLen;
this.useEnhancedCodes = useEnhancedCodes;
}
public SMTPReplyParser(final boolean useEnhancedCodes) {
this(SMTPConsts.MAX_REPLY_LEN, useEnhancedCodes);
}
public SMTPReplyParser() {
this(false);
}
@Override
public void reset() {
this.parsedLines.clear();
this.lineBuf.clear();
}
@Override
public SMTPReply parse(
final SessionInputBuffer buf, final boolean endOfStream) throws SMTPProtocolException {
Args.notNull(buf, "Session input buffer");
while (readLine(buf, endOfStream)) {
ParsedLine current = parseLine();
if (!this.parsedLines.isEmpty()) {
ParsedLine previous = this.parsedLines.getLast();
if (!sameCode(current, previous)) {
throw new SMTPProtocolException(
"Invalid multiline reply: status code mismatch");
}
}
this.parsedLines.add(current);
if (current.isTerminal()) {
List<String> lines = new ArrayList<String>(this.parsedLines.size());
for (ParsedLine parsedLine: this.parsedLines) {
lines.add(parsedLine.getText());
}
SMTPReply reply = new SMTPReply(
current.getCode(),
current.getEnhancedCode(),
lines);
reset();
return reply;
}
}
return null;
}
private boolean readLine(
final SessionInputBuffer buf, final boolean endOfStream) throws SMTPProtocolException {
try {
boolean lineComplete = buf.readLine(this.lineBuf, endOfStream);
if (this.maxLineLen > 0 &&
(this.lineBuf.length() > this.maxLineLen ||
(!lineComplete && buf.length() > this.maxLineLen))) {
throw new SMTPProtocolException("Maximum reply length limit exceeded");
}
return lineComplete;
} catch (CharacterCodingException ex) {
throw new SMTPProtocolException("Invalid character coding", ex);
}
}
private ParsedLine parseLine() throws SMTPProtocolException {
ParserCursor cursor = new ParserCursor(0, this.lineBuf.length());
int code = parseCode(cursor);
int codeClass = code / 100;
if (codeClass <= 0) {
throw new SMTPProtocolException("Malformed SMTP reply (invalid code): "
+ this.lineBuf.toString());
}
boolean terminal = parseCodeDelimiter(cursor);
SMTPCode enhancedCode;
if (this.useEnhancedCodes && (codeClass == 2 || codeClass == 4 || codeClass == 5)) {
enhancedCode = parseEnchancedCode(cursor);
if (enhancedCode.getCodeClass() != codeClass) {
throw new SMTPProtocolException("Malformed SMTP reply (code class mismatch): "
+ this.lineBuf.toString());
}
} else {
enhancedCode = null;
}
String text = parseText(cursor);
this.lineBuf.clear();
return new ParsedLine(code, terminal, enhancedCode, text);
}
private int parseCode(final ParserCursor cursor) throws SMTPProtocolException {
int c;
int i = cursor.getPos();
while (i < cursor.getUpperBound()) {
if (this.lineBuf.charAt(i) != ' ') {
break;
}
i++;
}
if (cursor.getUpperBound() - i < 4) {
throw new SMTPProtocolException("Malformed SMTP reply (no code): "
+ this.lineBuf.toString());
}
try {
c = Integer.parseInt(this.lineBuf.substring(i, i + 3));
} catch (NumberFormatException ex) {
throw new SMTPInvalidCodeException(this.lineBuf.toString());
}
cursor.updatePos(i + 3);
return c;
}
private boolean parseCodeDelimiter(final ParserCursor cursor) throws SMTPProtocolException {
boolean terminal;
int i = cursor.getPos();
int ch = this.lineBuf.charAt(i);
if (ch == ' ') {
terminal = true;
} else if (ch == '-') {
terminal = false;
} else {
throw new SMTPProtocolException("Malformed SMTP reply (invalid code separator): "
+ this.lineBuf.toString());
}
cursor.updatePos(i + 1);
return terminal;
}
private SMTPCode parseEnchancedCode(final ParserCursor cursor) throws SMTPProtocolException {
int codeClass;
int subject;
int detail;
int i1 = cursor.getPos();
int i2 = this.lineBuf.indexOf('.', i1, cursor.getUpperBound());
if (i2 == -1) {
throw new SMTPInvalidCodeException(this.lineBuf.toString());
}
try {
codeClass = Integer.parseInt(this.lineBuf.substring(i1, i2));
} catch (NumberFormatException ex) {
throw new SMTPInvalidCodeException(this.lineBuf.toString());
}
i1 = i2 + 1;
i2 = this.lineBuf.indexOf('.', i1, cursor.getUpperBound());
if (i2 == -1) {
throw new SMTPInvalidCodeException(this.lineBuf.toString());
}
try {
subject = Integer.parseInt(this.lineBuf.substring(i1, i2));
} catch (NumberFormatException ex) {
throw new SMTPInvalidCodeException(this.lineBuf.toString());
}
i1 = i2 + 1;
i2 = this.lineBuf.indexOf(' ', i1, cursor.getUpperBound());
if (i2 == -1) {
throw new SMTPInvalidCodeException(this.lineBuf.toString());
}
try {
detail = Integer.parseInt(this.lineBuf.substring(i1, i2));
} catch (NumberFormatException ex) {
throw new SMTPInvalidCodeException(this.lineBuf.toString());
}
cursor.updatePos(i2 + 1);
return new SMTPCode(codeClass, subject, detail);
}
private String parseText(final ParserCursor cursor) throws SMTPProtocolException {
String text = this.lineBuf.substringTrimmed(cursor.getPos(), cursor.getUpperBound());
cursor.updatePos(cursor.getUpperBound());
return text;
}
private static class ParsedLine {
private final int code;
private final boolean terminal;
private final SMTPCode enhancedCode;
private final String text;
ParsedLine(final int code, final boolean terminal, final SMTPCode enhancedCode, final String text) {
super();
this.code = code;
this.terminal = terminal;
this.enhancedCode = enhancedCode;
this.text = text;
}
public int getCode() {
return code;
}
public boolean isTerminal() {
return terminal;
}
public SMTPCode getEnhancedCode() {
return enhancedCode;
}
public String getText() {
return text;
}
}
private static boolean sameCode(final ParsedLine l1, final ParsedLine l2) {
int c1 = l1.getCode();
int c2 = l2.getCode();
SMTPCode e1 = l1.getEnhancedCode();
SMTPCode e2 = l2.getEnhancedCode();
return c1 == c2 && (e1 == null ? e2 == null : e1.equals(e2));
}
}