package coco.smtp.message; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import org.scribble.net.ScribMessage; import org.scribble.net.ScribMessageFormatter; import coco.smtp.message.server._220; import coco.smtp.message.server._250; import coco.smtp.message.server._250d; // Currently supports only client-side reading, not server-side public class SmtpMessageFormatter implements ScribMessageFormatter { //.. fix formatting //.. try ssl wrapper //.. add connection (transport) actions public static Charset cs = Charset.forName("UTF8"); //private static CharsetDecoder cd = cs.newDecoder(); public SmtpMessageFormatter() { } @Override public byte[] toBytes(ScribMessage m) throws IOException { return ((SmtpMessage) m).toBytes(); } @Override public ScribMessage fromBytes(ByteBuffer bb) throws IOException, ClassNotFoundException { bb.flip(); //byte[] bs = new byte[2]; int rem = bb.remaining(); if (rem < 2) { bb.compact(); return null; } int pos = bb.position(); String front = new String(new byte[] { bb.get(pos), bb.get(pos + 1) }, SmtpMessageFormatter.cs); if (front.equals(SmtpMessage.CRLF)) { throw new RuntimeException("TODO: "); } if (rem < pos + 4) { bb.compact(); return null; } front += new String(new byte[] { bb.get(pos + 2), bb.get(pos + 3) }, SmtpMessageFormatter.cs); int code = isStatusCode(front); if (code > -1) { String body = readLine(bb, pos + 4).trim(); // Whitespace already built into the message classes bb.compact(); if (body == null) { return null; } switch (code) { case 220: return new _220(body); case 250: { if (front.charAt(3) == '-') { return new _250d(body); } return new _250(body); } /*case 235: return new _235(body); case 535: return new _535(body); case 501: return new _501(body); case 354: return new _354(body);*/ default: throw new RuntimeException("Unknown status code " + code + ": " + body); } } /*else if (front.startsWith(HttpMessage.GET)) // TODO: server-side (EHLO, STARTTLS, etc) { }*/ else { // TODO: server-side (MAIL FROM:, RCPT TO:, etc) String line = front + readLine(bb, pos + 4); bb.compact(); /*if (line == null) // deadcode { return null; }*/ throw new RuntimeException("Cannot parse message: " + line); } } private static String readLine(ByteBuffer bb, int i) throws IOException { StringBuilder sb = new StringBuilder(); for (int limit = bb.limit(); i <= limit; ) { char c = (char) bb.get(i++); sb.append(c); if (c == '\r') { c = (char) bb.get(i++); sb.append(c); if (c == '\n') { bb.position(i); return sb.substring(0, sb.length() - 2).toString(); } } } return null; } // Duplicated from HttpMessageFormatter // FIXME: factor out private int isStatusCode(String front) { String code = ""; for (int i = 0; i < 4; i++) { //char c = (char) bs[i]; char c = front.charAt(i); if (i < 3) { if (c < '0' || c > '9') { return -1; } code += c; } else { if (c != ' ' && c != '-') // HACK: hypen { return -1; } } } return Integer.parseInt(code); } /*private static String readWord(ByteBuffer bb, int i) throws IOException { StringBuilder sb = new StringBuilder(); for (int limit = bb.limit(); i <= limit; ) { char c = (char) bb.get(i); if (c == ' ') { bb.position(i); return sb.toString(); } sb.append(c); } return null; }*/ /* // Pre: flipped ready for reading @Override public SmtpMessage readMessage(DataInputStream dis) throws IOException { String header = getHeader(dis); switch (header) { // FIXME: factor out text constants with Session constants? case "220 ": { return new _220(getBody(dis)); } case "235 ": { return new _235(getBody(dis)); } case "250-": { return new _250_(getBody(dis)); } case "250 ": { return new _250(getBody(dis)); } case "354 ": { return new _354(getBody(dis)); } case "501 ": { return new _501(getBody(dis)); } case "535 ": { return new _535(getBody(dis)); } default: { throw new RuntimeException("Unknown header: " + header); } } } // *4* char headers (3 digits, dash or space) private String getHeader(DataInputStream dis) throws IOException { readMinimumBytes(dis, 4); int limit = bb.limit(); bb.limit(4); String header = cd.decode(bb).toString(); // updates bb position bb.limit(limit); bb.compact(); bb.flip(); return header; } // Reads up to \r\n inclusive; doesn't return the \r\n private String getBody(DataInputStream dis) throws IOException { int min = 2; readMinimumBytes(dis, min); // Min should be more? while (true) { String all = cd.decode(bb).toString(); // updates bb position if (all.contains("\n")) // FIXME: didn't check exactly \r\n { int i = all.indexOf("\n"); String body = all.substring(0, i - 1); bb.position(i + 1); // \r\n -- ASCII 1-to-1 byte-char index bb.compact(); bb.flip(); return body; } bb.rewind(); readMinimumBytes(dis, ++min); // FIXME: buffered readLine would be more efficient } } // FIXME: min should mean min fresh (i.e. not including bb cached -- there currently is no cache: is.available doesn't work for SSLSocket, always returns 0) private void readMinimumBytes(DataInputStream dis, int min) throws IOException { for (int remaining = bb.remaining(); remaining < min; remaining = bb.remaining()) { bb.compact(); bb.put((byte) dis.read()); // FIXME: check for buffer overflow bb.flip(); } }*/ @Override public void writeMessage(DataOutputStream dos, ScribMessage m) throws IOException { dos.write(((SmtpMessage) m).toBytes()); dos.flush(); } @Override public SmtpMessage readMessage(DataInputStream dis) throws IOException { byte[] bs = new byte[2]; dis.readFully(bs); String front = new String(bs, SmtpMessageFormatter.cs); if (front.equals(SmtpMessage.CRLF)) { throw new RuntimeException("TODO: "); } bs = new byte[2]; dis.readFully(bs); front += new String(bs, SmtpMessageFormatter.cs); int code = isStatusCode(front); if (code > -1) { String body = readLine(dis).trim(); // Whitespace already built into the message classes switch (code) { case 220: return new _220(body); case 250: { if (front.charAt(3) == '-') { return new _250d(body); } return new _250(body); } /*case 235: return new _235(body); case 535: return new _535(body); case 501: return new _501(body); case 354: return new _354(body);*/ default: throw new RuntimeException("Unknown status code: " + code); } } /*else if (front.startsWith(HttpMessage.GET)) { }*/ else { String line = front + readLine(dis); throw new RuntimeException("Cannot parse message: " + line); } } private static String readLine(DataInputStream dis) throws IOException { StringBuilder sb = new StringBuilder(); for (; true; ) { char c = (char) dis.read(); // readChar is not the same sb.append(c); if (c == '\r') { c = (char) dis.read(); sb.append(c); if (c == '\n') { return sb.substring(0, sb.length() - 2).toString(); } } } } /*private static String readWord(DataInputStream dis) throws IOException { StringBuilder sb = new StringBuilder(); for (; true; ) { char c = (char) dis.read(); if (c == ' ') { return sb.toString(); } sb.append(c); } }*/ /* // Pre: flipped ready for reading @Override public SmtpMessage readMessage(DataInputStream dis) throws IOException { String header = getHeader(dis); switch (header) { // FIXME: factor out text constants with Session constants? case "220 ": { return new _220(getBody(dis)); } case "235 ": { return new _235(getBody(dis)); } case "250-": { return new _250_(getBody(dis)); } case "250 ": { return new _250(getBody(dis)); } case "354 ": { return new _354(getBody(dis)); } case "501 ": { return new _501(getBody(dis)); } case "535 ": { return new _535(getBody(dis)); } default: { throw new RuntimeException("Unknown header: " + header); } } } // *4* char headers (3 digits, dash or space) private String getHeader(DataInputStream dis) throws IOException { readMinimumBytes(dis, 4); int limit = bb.limit(); bb.limit(4); String header = cd.decode(bb).toString(); // updates bb position bb.limit(limit); bb.compact(); bb.flip(); return header; } // Reads up to \r\n inclusive; doesn't return the \r\n private String getBody(DataInputStream dis) throws IOException { int min = 2; readMinimumBytes(dis, min); // Min should be more? while (true) { String all = cd.decode(bb).toString(); // updates bb position if (all.contains("\n")) // FIXME: didn't check exactly \r\n { int i = all.indexOf("\n"); String body = all.substring(0, i - 1); bb.position(i + 1); // \r\n -- ASCII 1-to-1 byte-char index bb.compact(); bb.flip(); return body; } bb.rewind(); readMinimumBytes(dis, ++min); // FIXME: buffered readLine would be more efficient } } // FIXME: min should mean min fresh (i.e. not including bb cached -- there currently is no cache: is.available doesn't work for SSLSocket, always returns 0) private void readMinimumBytes(DataInputStream dis, int min) throws IOException { for (int remaining = bb.remaining(); remaining < min; remaining = bb.remaining()) { bb.compact(); bb.put((byte) dis.read()); // FIXME: check for buffer overflow bb.flip(); } }*/ }