RFC-2015 : further encode text quoted-printable to be sign safe #572 #576

This commit is contained in:
alexandre 2015-11-10 13:46:17 +01:00 committed by Vincent Breitmoser
parent 66520a2cb3
commit 90c6b666c8
3 changed files with 266 additions and 0 deletions

View file

@ -0,0 +1,36 @@
package com.fsck.k9.mail.filter;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
/**
* Created by alexandre on 11/10/15.
*/
public class SignSafeOutputStreamTest {
private static final String INPUT_STRING = "It's generally a good idea to encode lines that begin with\r\n"
+ "From because some mail transport agents will insert a greater-\r\n"
+ "than (>) sign, thus invalidating the signature.\r\n\r\n"
+ "Also, in some cases it might be desirable to encode any \r\n"
+ "trailing whitespace that occurs on lines in order to ensure \r\n"
+ "that the message signature is not invalidated when passing \r\n"
+ "a gateway that modifies such whitespace (like BITNET). \r\n\r\n";
private static final String EXPECTED = "It's generally a good idea to encode lines that begin with\r\n"
+ "From=20because some mail transport agents will insert a greater-\r\n"
+ "than (>) sign, thus invalidating the signature.\r\n\r\n"
+ "Also, in some cases it might be desirable to encode any =20\r\n"
+ "trailing whitespace that occurs on lines in order to ensure =20\r\n"
+ "that the message signature is not invalidated when passing =20\r\n"
+ "a gateway that modifies such whitespace (like BITNET). =20\r\n\r\n";
@Test
public void testWrite() throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
new SignSafeOutputStream(output).write(INPUT_STRING.getBytes("US-ASCII"));
assertEquals(EXPECTED, new String(output.toByteArray(), "US-ASCII"));
}
}

View file

@ -0,0 +1,219 @@
package com.fsck.k9.mail.filter;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* Further encode a quoted-printable stream into a safer format for signed email.
* @see <a href="http://tools.ietf.org/html/rfc2015">RFC-2015</a>
*/
public class SignSafeOutputStream extends FilterOutputStream {
private ByteBuffer buffer = ByteBuffer.allocate(1);
private State state = State.cr_FROM;
public SignSafeOutputStream(OutputStream out){
super(out);
}
public static FilterOutputStream newInstance(OutputStream out){
return new QuotedPrintableOutputStream(new SignSafeOutputStream(out), false);
}
private static final byte[] ESCAPE = new byte[]{'=','2','0'};
@Override
public void write(int oneByte) throws IOException {
State nextState = state.nextState(oneByte);
if (nextState == State.SPACE_FROM){
state = State.INIT;
out.write(ESCAPE);
} else if (nextState == State.lf_SPACE){
buffer.clear();
state = State.lf_FROM;
out.write(ESCAPE);
out.write(oneByte);
} else if (nextState == State.SPACE) {
writeBuffer();
buffer.put((byte)32);
state = nextState;
} else {
writeBuffer();
state = nextState;
out.write(oneByte);
}
}
private void writeBuffer() throws IOException {
buffer.flip();
if (buffer.hasRemaining()) {
out.write(buffer.get());
}
buffer.clear();
}
@Override
public void close() throws IOException {
writeBuffer();
out.close();
}
enum State {
INIT {
@Override
public State nextState(int b) {
switch (b) {
case '\r':
return lf_FROM;
case ' ':
return SPACE;
default:
return INIT;
}
}
},
lf_FROM {
@Override
public State nextState(int b) {
switch (b) {
case '\n':
return cr_FROM;
case ' ':
return SPACE;
case '\r':
return lf_FROM;
default:
return INIT;
}
}
},
cr_FROM {
@Override
public State nextState(int b) {
switch (b) {
case 'F':
return F_FROM;
case ' ':
return SPACE;
case '\r':
return lf_FROM;
default:
return INIT;
}
}
},
F_FROM {
@Override
public State nextState(int b) {
switch (b) {
case 'r':
return R_FROM;
case ' ':
return SPACE;
case '\r':
return lf_FROM;
default:
return INIT;
}
}
},
R_FROM {
@Override
public State nextState(int b) {
switch (b) {
case 'o':
return O_FROM;
case ' ':
return SPACE;
case '\r':
return lf_FROM;
default:
return INIT;
}
}
},
O_FROM {
@Override
public State nextState(int b) {
switch (b) {
case 'm':
return M_FROM;
case ' ':
return SPACE;
case '\r':
return lf_FROM;
default:
return INIT;
}
}
},
M_FROM {
@Override
public State nextState(int b) {
switch (b) {
case ' ':
return SPACE_FROM;
case '\r':
return lf_FROM;
default:
return INIT;
}
}
},
SPACE_FROM {
@Override
public State nextState(int b) {
switch (b) {
case '\r':
return lf_SPACE;
case 'F':
return F_FROM;
case ' ':
return SPACE;
default:
return INIT;
}
}
},
SPACE {
@Override
public State nextState(int b) {
switch (b) {
case '\r':
return lf_SPACE;
case 'F':
return F_FROM;
case ' ':
return SPACE;
default:
return INIT;
}
}
},
lf_SPACE {
@Override
public State nextState(int b) {
return INIT;
}
};
public abstract State nextState(int b);
}
}

View file

@ -11,6 +11,8 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import com.fsck.k9.mail.filter.CountingOutputStream;
import com.fsck.k9.mail.filter.SignSafeOutputStream;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil;
@ -34,6 +36,12 @@ public class TextBody implements Body, SizeAware {
this.mBody = body;
}
private static boolean signSafe = false;
public static void setSignSafe(boolean signable){
signSafe = signable;
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
if (mBody != null) {
@ -41,6 +49,9 @@ public class TextBody implements Body, SizeAware {
if (MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) {
out.write(bytes);
} else {
if (signSafe){
out = new SignSafeOutputStream(out);
}
QuotedPrintableOutputStream qp = new QuotedPrintableOutputStream(out, false);
qp.write(bytes);
qp.flush();