diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 55089edb56..fa1215750a 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -3835,7 +3835,8 @@ public class FragmentCompose extends FragmentBase { }; bpContent.setContent(imessage.getContent(), imessage.getContentType()); - try (OutputStream out = new FileOutputStream(input)) { + try (OutputStream out = new MessageHelper.CanonicalizingStream( + new BufferedOutputStream(new FileOutputStream(input)))) { bpContent.writeTo(out); } } else { @@ -4248,8 +4249,9 @@ public class FragmentCompose extends FragmentBase { // Build content File sinput = new File(tmp, draft.id + ".smime_sign"); - try (FileOutputStream fos = new FileOutputStream(sinput)) { - bpContent.writeTo(fos); + try (OutputStream os = new MessageHelper.CanonicalizingStream( + new BufferedOutputStream(new FileOutputStream(sinput)))) { + bpContent.writeTo(os); } if (EntityMessage.SMIME_SIGNONLY.equals(type)) { diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 8773d504d7..fe8ecde783 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -77,6 +77,7 @@ import org.simplejavamail.outlookmessageparser.model.OutlookFileAttachment; import org.simplejavamail.outlookmessageparser.model.OutlookMessage; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -84,6 +85,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FileReader; +import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -4098,23 +4100,9 @@ public class MessageHelper { if (TextUtils.isEmpty(boundary)) throw new ParseException("Signed boundary missing"); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - apart.part.writeTo(bos); - String raw = new String(bos.toByteArray(), StandardCharsets.ISO_8859_1); - String[] parts = raw.split("\\r?\\n" + Pattern.quote("--" + boundary) + "\\r?\\n"); - if (parts.length < 2) - throw new ParseException("Signed part missing"); - - // PGP: https://datatracker.ietf.org/doc/html/rfc3156#section-5 - // S/MIME: https://datatracker.ietf.org/doc/html/rfc8551#section-3.1.1 - String c = parts[1] - .replaceAll("\\r?\\n", "\r\n"); // normalize new lines - if (EntityAttachment.PGP_CONTENT.equals(apart.encrypt)) - c = c.replaceAll(" +$", ""); // trim trailing spaces - File file = local.getFile(context); - try (OutputStream os = new FileOutputStream(file)) { - os.write(c.getBytes(StandardCharsets.ISO_8859_1)); + try (OutputStream os = new BufferedOutputStream(new CanonicalizingStream(new FileOutputStream(file), boundary))) { + apart.part.writeTo(os); } DB db = DB.getInstance(context); @@ -5771,4 +5759,104 @@ public class MessageHelper { return "message/feedback-report".equalsIgnoreCase(type); } } + + static class CanonicalizingStream extends FilterOutputStream { + private OutputStream os; + private String boundary; + + private int boundaries = 0; + private boolean carriage = false; + private ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.ISO_8859_1); + + // PGP: https://datatracker.ietf.org/doc/html/rfc3156#section-5 + // S/MIME: https://datatracker.ietf.org/doc/html/rfc8551#section-3.1.1 + + public CanonicalizingStream(OutputStream out) { + super(out); + this.os = out; + this.boundary = null; + } + + public CanonicalizingStream(OutputStream out, String boundary) { + super(out); + this.os = out; + this.boundary = "--" + boundary; + } + + @Override + public void write(int b) throws IOException { + this.write(new byte[]{(byte) b}, 0, 1); + } + + @Override + public void write(byte[] b) throws IOException { + this.write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = off; i < off + len; i++) { + byte k = b[i]; + if (k == '\r') + carriage = true; + else { + if (k == '\n') { + if (writeBuffer()) + buffer.write(CRLF); + } else { + if (carriage) { + if (writeBuffer()) + buffer.write(CRLF); + } + buffer.write(k); + } + carriage = false; + } + } + } + + @Override + public void flush() throws IOException { + flushBuffer(); + super.flush(); + } + + @Override + public void close() throws IOException { + flushBuffer(); + super.close(); + } + + private boolean writeBuffer() throws IOException { + try { + String line = new String(buffer.toByteArray(), StandardCharsets.ISO_8859_1); + + if (boundary != null) { + if (boundary.equals(line.trim())) { + boundaries++; + return false; + } + if (boundaries != 1) + return false; + } + + line = line.replaceAll(" +$", ""); + + os.write(line.getBytes(StandardCharsets.ISO_8859_1)); + + return true; + } finally { + buffer.reset(); + } + } + + private void flushBuffer() throws IOException { + if (boundary != null && boundaries != 2) + throw new IOException("Content boundaries=" + boundaries); + if (buffer.size() > 0) + writeBuffer(); + } + } }