diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eeab65ea7..8f3ce51cb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Sun Nov 30 16:02:23 PST 2014 +#Sun Dec 07 14:12:42 GMT 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java index 45c85450f..a77e0c282 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java @@ -14,8 +14,8 @@ import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import static com.fsck.k9.mail.K9MailLib.LOG_TAG; - public abstract class Message implements Part, CompositeBody { + public enum RecipientType { TO, CC, BCC, } @@ -117,8 +117,32 @@ public abstract class Message implements Part, CompositeBody { public abstract void setReferences(String references) throws MessagingException; + @Override + public abstract Body getBody(); + + @Override + public abstract String getContentType() throws MessagingException; + + @Override + public abstract void addHeader(String name, String value) throws MessagingException; + + @Override + public abstract void addRawHeader(String name, String raw) throws MessagingException; + + @Override + public abstract void setHeader(String name, String value) throws MessagingException; + + @Override + public abstract String[] getHeader(String name) throws MessagingException; + public abstract Set getHeaderNames() throws MessagingException; + @Override + public abstract void removeHeader(String name) throws MessagingException; + + @Override + public abstract void setBody(Body body) throws MessagingException; + public abstract long getId(); public abstract String getPreview(); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java index 4c692b724..1d0274f32 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Part.java @@ -7,6 +7,8 @@ import java.io.OutputStream; public interface Part { void addHeader(String name, String value) throws MessagingException; + void addRawHeader(String name, String raw) throws MessagingException; + void removeHeader(String name) throws MessagingException; void setHeader(String name, String value) throws MessagingException; diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java index f0dd6f473..17870c140 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -15,7 +15,7 @@ import java.io.*; * and writeTo one time. After writeTo is called, or the InputStream returned from * getInputStream is closed the file is deleted and the Body should be considered disposed of. */ -public class BinaryTempFileBody implements Body { +public class BinaryTempFileBody implements RawDataBody { private static File mTempDirectory; private File mFile; @@ -26,15 +26,56 @@ public class BinaryTempFileBody implements Body { mTempDirectory = tempDirectory; } - public void setEncoding(String encoding) throws MessagingException { - mEncoding = encoding; + @Override + public String getEncoding() { + return mEncoding; } - public BinaryTempFileBody() { - if (mTempDirectory == null) { - throw new - RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!"); + public void setEncoding(String encoding) throws MessagingException { + if (mEncoding != null && mEncoding.equalsIgnoreCase(encoding)) { + return; } + + // The encoding changed, so we need to convert the message + if (!MimeUtil.ENC_8BIT.equalsIgnoreCase(mEncoding)) { + throw new RuntimeException("Can't convert from encoding: " + mEncoding); + } + + try { + File newFile = File.createTempFile("body", null, mTempDirectory); + OutputStream out = new FileOutputStream(newFile); + try { + if (MimeUtil.ENC_QUOTED_PRINTABLE.equals(encoding)) { + out = new QuotedPrintableOutputStream(out, false); + } else if (MimeUtil.ENC_BASE64.equals(encoding)) { + out = new Base64OutputStream(out); + } else { + throw new RuntimeException("Target encoding not supported: " + encoding); + } + + InputStream in = getInputStream(); + try { + IOUtils.copy(in, out); + } finally { + in.close(); + } + } finally { + out.close(); + } + + mFile = newFile; + mEncoding = encoding; + } catch (IOException e) { + throw new MessagingException("Unable to convert body", e); + } + } + + public BinaryTempFileBody(String encoding) { + if (mTempDirectory == null) { + throw new RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!"); + } + + mEncoding = encoding; } public OutputStream getOutputStream() throws IOException { @@ -54,22 +95,7 @@ public class BinaryTempFileBody implements Body { public void writeTo(OutputStream out) throws IOException, MessagingException { InputStream in = getInputStream(); try { - boolean closeStream = false; - if (MimeUtil.isBase64Encoding(mEncoding)) { - out = new Base64OutputStream(out); - closeStream = true; - } else if (MimeUtil.isQuotedPrintableEncoded(mEncoding)){ - out = new QuotedPrintableOutputStream(out, false); - closeStream = true; - } - - try { - IOUtils.copy(in, out); - } finally { - if (closeStream) { - out.close(); - } - } + IOUtils.copy(in, out); } finally { in.close(); } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java index c1503b342..e5fe7c370 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java @@ -18,6 +18,10 @@ import com.fsck.k9.mail.MessagingException; */ public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody { + public BinaryTempFileMessageBody(String encoding) { + super(encoding); + } + @Override public void setEncoding(String encoding) throws MessagingException { if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java index c4bca428a..32e3e2654 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -35,7 +35,7 @@ public class MimeBodyPart extends BodyPart { if (mimeType != null) { addHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType); } - setBody(body); + MimeMessageHelper.setBody(this, body); } private String getFirstHeader(String name) { @@ -47,6 +47,11 @@ public class MimeBodyPart extends BodyPart { mHeader.addHeader(name, value); } + @Override + public void addRawHeader(String name, String raw) { + mHeader.addRawHeader(name, raw); + } + @Override public void setHeader(String name, String value) { mHeader.setHeader(name, value); @@ -70,25 +75,6 @@ public class MimeBodyPart extends BodyPart { @Override public void setBody(Body body) throws MessagingException { this.mBody = body; - if (body instanceof Multipart) { - Multipart multipart = ((Multipart)body); - multipart.setParent(this); - String type = multipart.getContentType(); - setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); - if ("multipart/signed".equalsIgnoreCase(type)) { - setEncoding(MimeUtil.ENC_7BIT); - } else { - setEncoding(MimeUtil.ENC_8BIT); - } - } else if (body instanceof TextBody) { - String contentType = String.format("%s;\r\n charset=utf-8", getMimeType()); - String name = MimeUtility.getHeaderParameter(getContentType(), "name"); - if (name != null) { - contentType += String.format(";\r\n name=\"%s\"", name); - } - setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); - setEncoding(MimeUtil.ENC_8BIT); - } } @Override diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java index 7c9569df0..f3835d04a 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeHeader.java @@ -50,7 +50,13 @@ public class MimeHeader { } public void addHeader(String name, String value) { - mFields.add(new Field(name, MimeUtility.foldAndEncode(value))); + Field field = Field.newNameValueField(name, MimeUtility.foldAndEncode(value)); + mFields.add(field); + } + + void addRawHeader(String name, String raw) { + Field field = Field.newRawField(name, raw); + mFields.add(field); } public void setHeader(String name, String value) { @@ -64,7 +70,7 @@ public class MimeHeader { public Set getHeaderNames() { Set names = new LinkedHashSet(); for (Field field : mFields) { - names.add(field.name); + names.add(field.getName()); } return names; } @@ -72,8 +78,8 @@ public class MimeHeader { public String[] getHeader(String name) { List values = new ArrayList(); for (Field field : mFields) { - if (field.name.equalsIgnoreCase(name)) { - values.add(field.value); + if (field.getName().equalsIgnoreCase(name)) { + values.add(field.getValue()); } } if (values.isEmpty()) { @@ -85,7 +91,7 @@ public class MimeHeader { public void removeHeader(String name) { List removeFields = new ArrayList(); for (Field field : mFields) { - if (field.name.equalsIgnoreCase(name)) { + if (field.getName().equalsIgnoreCase(name)) { removeFields.add(field); } } @@ -96,26 +102,34 @@ public class MimeHeader { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); for (Field field : mFields) { if (!Arrays.asList(writeOmitFields).contains(field.name)) { - String v = field.value; - - if (hasToBeEncoded(v)) { - Charset charset = null; - - if (mCharset != null) { - charset = Charset.forName(mCharset); - } - v = EncoderUtil.encodeEncodedWord(field.value, charset); + if (field.hasRawData()) { + writer.write(field.getRaw()); + } else { + writeNameValueField(writer, field); } - - writer.write(field.name); - writer.write(": "); - writer.write(v); writer.write("\r\n"); } } writer.flush(); } + private void writeNameValueField(BufferedWriter writer, Field field) throws IOException { + String value = field.getValue(); + + if (hasToBeEncoded(value)) { + Charset charset = null; + + if (mCharset != null) { + charset = Charset.forName(mCharset); + } + value = EncoderUtil.encodeEncodedWord(field.getValue(), charset); + } + + writer.write(field.getName()); + writer.write(": "); + writer.write(value); + } + // encode non printable characters except LF/CR/TAB codes. private boolean hasToBeEncoded(String text) { for (int i = 0; i < text.length(); i++) { @@ -131,19 +145,67 @@ public class MimeHeader { private static class Field { private final String name; - private final String value; + private final String raw; + + public static Field newNameValueField(String name, String value) { + if (value == null) { + throw new NullPointerException("Argument 'value' cannot be null"); + } + + return new Field(name, value, null); + } + + public static Field newRawField(String name, String raw) { + if (raw == null) { + throw new NullPointerException("Argument 'raw' cannot be null"); + } + if (name != null && !raw.startsWith(name + ":")) { + throw new IllegalArgumentException("The value of 'raw' needs to start with the supplied field name " + + "followed by a colon"); + } + + return new Field(name, null, raw); + } + + private Field(String name, String value, String raw) { + if (name == null) { + throw new NullPointerException("Argument 'name' cannot be null"); + } - public Field(String name, String value) { this.name = name; this.value = value; + this.raw = raw; + } + + public String getName() { + return name; + } + + public String getValue() { + if (value != null) { + return value; + } + + int delimiterIndex = raw.indexOf(':'); + if (delimiterIndex == raw.length() - 1) { + return ""; + } + + return raw.substring(delimiterIndex + 1).trim(); + } + + public String getRaw() { + return raw; + } + + public boolean hasRawData() { + return raw != null; } @Override public String toString() { - StringBuilder sb = new StringBuilder("("); - sb.append(name).append('=').append(value).append(')'); - return sb.toString(); + return (hasRawData()) ? getRaw() : getName() + ": " + getValue(); } } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index eff4ffa2e..632018c29 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -2,6 +2,7 @@ package com.fsck.k9.mail.internet; import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -14,6 +15,7 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.dom.field.DateTimeField; import org.apache.james.mime4j.field.DefaultFieldParser; @@ -394,22 +396,6 @@ public class MimeMessage extends Message { @Override public void setBody(Body body) throws MessagingException { this.mBody = body; - setHeader("MIME-Version", "1.0"); - if (body instanceof Multipart) { - Multipart multipart = ((Multipart)body); - multipart.setParent(this); - String type = multipart.getContentType(); - setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); - if ("multipart/signed".equalsIgnoreCase(type)) { - setEncoding(MimeUtil.ENC_7BIT); - } else { - setEncoding(MimeUtil.ENC_8BIT); - } - } else if (body instanceof TextBody) { - setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\r\n charset=utf-8", - getMimeType())); - setEncoding(MimeUtil.ENC_8BIT); - } } private String getFirstHeader(String name) { @@ -421,6 +407,11 @@ public class MimeMessage extends Message { mHeader.addHeader(name, value); } + @Override + public void addRawHeader(String name, String raw) { + mHeader.addRawHeader(name, raw); + } + @Override public void setHeader(String name, String value) throws MessagingException { mHeader.setHeader(name, value); @@ -540,8 +531,7 @@ public class MimeMessage extends Message { public void body(BodyDescriptor bd, InputStream in) throws IOException { expect(Part.class); try { - Body body = MimeUtility.decodeBody(in, - bd.getTransferEncoding(), bd.getMimeType()); + Body body = MimeUtility.createBody(in, bd.getTransferEncoding(), bd.getMimeType()); ((Part)stack.peek()).setBody(body); } catch (MessagingException me) { throw new Error(me); @@ -575,16 +565,17 @@ public class MimeMessage extends Message { @Override public void preamble(InputStream is) throws IOException { expect(MimeMultipart.class); - StringBuilder sb = new StringBuilder(); - int b; - while ((b = is.read()) != -1) { - sb.append((char)b); - } - ((MimeMultipart)stack.peek()).setPreamble(sb.toString()); + ByteArrayOutputStream preamble = new ByteArrayOutputStream(); + IOUtils.copy(is, preamble); + ((MimeMultipart)stack.peek()).setPreamble(preamble.toByteArray()); } @Override public void epilogue(InputStream is) throws IOException { + expect(MimeMultipart.class); + ByteArrayOutputStream epilogue = new ByteArrayOutputStream(); + IOUtils.copy(is, epilogue); + ((MimeMultipart) stack.peek()).setEpilogue(epilogue.toByteArray()); } @Override @@ -596,7 +587,9 @@ public class MimeMessage extends Message { public void field(Field parsedField) throws MimeException { expect(Part.class); try { - ((Part)stack.peek()).addHeader(parsedField.getName(), parsedField.getBody().trim()); + String name = parsedField.getName(); + String raw = parsedField.getRaw().toString(); + ((Part) stack.peek()).addRawHeader(name, raw); } catch (MessagingException me) { throw new Error(me); } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java new file mode 100644 index 000000000..bc1695607 --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessageHelper.java @@ -0,0 +1,52 @@ +package com.fsck.k9.mail.internet; + + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; +import org.apache.james.mime4j.util.MimeUtil; + + +public class MimeMessageHelper { + private MimeMessageHelper() { + } + + public static void setBody(Part part, Body body) throws MessagingException { + part.setBody(body); + + if (part instanceof Message) { + part.setHeader("MIME-Version", "1.0"); + } + + if (body instanceof Multipart) { + Multipart multipart = ((Multipart) body); + multipart.setParent(part); + String type = multipart.getContentType(); + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); + if ("multipart/signed".equalsIgnoreCase(type)) { + setEncoding(part, MimeUtil.ENC_7BIT); + } else { + setEncoding(part, MimeUtil.ENC_8BIT); + } + } else if (body instanceof TextBody) { + String contentType = String.format("%s;\r\n charset=utf-8", part.getMimeType()); + String name = MimeUtility.getHeaderParameter(part.getContentType(), "name"); + if (name != null) { + contentType += String.format(";\r\n name=\"%s\"", name); + } + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); + + setEncoding(part, MimeUtil.ENC_8BIT); + } + } + + public static void setEncoding(Part part, String encoding) throws MessagingException { + Body body = part.getBody(); + if (body != null) { + body.setEncoding(encoding); + } + part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); + } +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java index 124ca5e4c..d6ce4377a 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMultipart.java @@ -10,7 +10,8 @@ import java.util.Locale; import java.util.Random; public class MimeMultipart extends Multipart { - private String mPreamble; + private byte[] mPreamble; + private byte[] mEpilogue; private String mContentType; @@ -45,12 +46,12 @@ public class MimeMultipart extends Multipart { return sb.toString().toUpperCase(Locale.US); } - public String getPreamble() { - return mPreamble; + public void setPreamble(byte[] preamble) { + this.mPreamble = preamble; } - public void setPreamble(String preamble) { - this.mPreamble = preamble; + public void setEpilogue(byte[] epilogue) { + mEpilogue = epilogue; } @Override @@ -67,7 +68,7 @@ public class MimeMultipart extends Multipart { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); if (mPreamble != null) { - writer.write(mPreamble); + out.write(mPreamble); writer.write("\r\n"); } @@ -90,6 +91,9 @@ public class MimeMultipart extends Multipart { writer.write(mBoundary); writer.write("--\r\n"); writer.flush(); + if (mEpilogue != null) { + out.write(mEpilogue); + } } @Override diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java index a5ac8efa7..bfde9e0b4 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeUtility.java @@ -982,42 +982,74 @@ public class MimeUtility { return DEFAULT_ATTACHMENT_MIME_TYPE.equalsIgnoreCase(mimeType); } - /** - * Removes any content transfer encoding from the stream and returns a Body. - * @throws MessagingException - */ - public static Body decodeBody(InputStream in, - String contentTransferEncoding, String contentType) + public static Body createBody(InputStream in, String contentTransferEncoding, String contentType) throws IOException, MessagingException { - /* - * We'll remove any transfer encoding by wrapping the stream. - */ + if (contentTransferEncoding != null) { - contentTransferEncoding = - getHeaderParameter(contentTransferEncoding, null); - if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(contentTransferEncoding)) { - in = new QuotedPrintableInputStream(in); - } else if (MimeUtil.ENC_BASE64.equalsIgnoreCase(contentTransferEncoding)) { - in = new Base64InputStream(in); - } + contentTransferEncoding = MimeUtility.getHeaderParameter(contentTransferEncoding, null); } BinaryTempFileBody tempBody; if (MimeUtil.isMessage(contentType)) { - tempBody = new BinaryTempFileMessageBody(); + tempBody = new BinaryTempFileMessageBody(contentTransferEncoding); } else { - tempBody = new BinaryTempFileBody(); + tempBody = new BinaryTempFileBody(contentTransferEncoding); } - tempBody.setEncoding(contentTransferEncoding); + OutputStream out = tempBody.getOutputStream(); try { IOUtils.copy(in, out); } finally { out.close(); } + return tempBody; } + /** + * Get decoded contents of a body. + *

+ * Right now only some classes retain the original encoding of the body contents. Those classes have to implement + * the {@link RawDataBody} interface in order for this method to decode the data delivered by + * {@link Body#getInputStream()}. + *

+ * The ultimate goal is to get to a point where all classes retain the original data and {@code RawDataBody} can be + * merged into {@link Body}. + */ + public static InputStream decodeBody(Body body) throws MessagingException { + InputStream inputStream; + if (body instanceof RawDataBody) { + RawDataBody rawDataBody = (RawDataBody) body; + String encoding = rawDataBody.getEncoding(); + final InputStream rawInputStream = rawDataBody.getInputStream(); + if (MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) || MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { + inputStream = rawInputStream; + } else if (MimeUtil.ENC_BASE64.equalsIgnoreCase(encoding)) { + inputStream = new Base64InputStream(rawInputStream, false) { + @Override + public void close() throws IOException { + super.close(); + rawInputStream.close(); + } + }; + } else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) { + inputStream = new QuotedPrintableInputStream(rawInputStream) { + @Override + public void close() throws IOException { + super.close(); + rawInputStream.close(); + } + }; + } else { + throw new RuntimeException("Encoding for RawDataBody not supported: " + encoding); + } + } else { + inputStream = body.getInputStream(); + } + + return inputStream; + } + public static String getMimeTypeByExtension(String filename) { String returnedType = null; String extension = null; diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/RawDataBody.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/RawDataBody.java new file mode 100644 index 000000000..e3dee616e --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/RawDataBody.java @@ -0,0 +1,12 @@ +package com.fsck.k9.mail.internet; + + +import com.fsck.k9.mail.Body; + + +/** + * See {@link MimeUtility#decodeBody(Body)} + */ +public interface RawDataBody extends Body { + String getEncoding(); +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java index f0959b4b0..13561b38a 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/store/imap/ImapStore.java @@ -37,6 +37,7 @@ import android.os.PowerManager; import android.text.TextUtils; import android.util.Log; +import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.power.TracingPowerManager; import com.fsck.k9.mail.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.AuthType; @@ -1501,7 +1502,7 @@ public class ImapStore extends RemoteStore { if (literal != null) { if (literal instanceof Body) { // Most of the work was done in FetchAttchmentCallback.foundLiteral() - part.setBody((Body)literal); + MimeMessageHelper.setBody(part, (Body) literal); } else if (literal instanceof String) { String bodyString = (String)literal; InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes()); @@ -1510,7 +1511,7 @@ public class ImapStore extends RemoteStore { .getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; String contentType = part .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; - part.setBody(MimeUtility.decodeBody(bodyStream, + MimeMessageHelper.setBody(part, MimeUtility.createBody(bodyStream, contentTransferEncoding, contentType)); } else { // This shouldn't happen @@ -2878,7 +2879,7 @@ public class ImapStore extends RemoteStore { String contentType = mPart .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; - return MimeUtility.decodeBody(literal, contentTransferEncoding, + return MimeUtility.createBody(literal, contentTransferEncoding, contentType); } return null; diff --git a/k9mail/build.gradle b/k9mail/build.gradle index 8fc4877a1..7981d0f75 100644 --- a/k9mail/build.gradle +++ b/k9mail/build.gradle @@ -17,6 +17,27 @@ dependencies { compile 'com.android.support:support-v13:21.0.2' compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.2' compile 'de.cketti.library.changelog:ckchangelog:1.2.1' + + androidTestCompile ('com.jakewharton.espresso:espresso:1.1-r3' ) { + // Note: some of these exclusions may become necessary. See the + // github site https://github.com/JakeWharton/double-espresso + //exclude group: 'com.squareup.dagger' + //exclude group: 'javax.inject' + //exclude group: 'javax.annotation' + //exclude group: 'com.google.guava' + //exclude group: 'org.hamcrest' + exclude group: 'com.google.code.findbugs' + } + + androidTestCompile("com.icegreen:greenmail:1.3.1b") { + // Use a better, later version + exclude group: "javax.mail" + } + + // this version avoids some "Ignoring InnerClasses attribute for an anonymous inner class" warnings + androidTestCompile "javax.mail:javax.mail-api:1.5.2" + + androidTestCompile "com.madgag.spongycastle:pg:1.51.0.0" } android { @@ -26,6 +47,8 @@ android { defaultConfig { minSdkVersion 15 targetSdkVersion 17 + + testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner" } signingConfigs { @@ -56,6 +79,7 @@ android { exclude 'META-INF/LICENSE.txt' exclude 'META-INF/NOTICE' exclude 'META-INF/NOTICE.txt' + exclude 'LICENSE.txt' } compileOptions { diff --git a/k9mail/src/androidTest/AndroidManifest.xml b/k9mail/src/androidTest/AndroidManifest.xml index 30770a319..a16d3c89b 100644 --- a/k9mail/src/androidTest/AndroidManifest.xml +++ b/k9mail/src/androidTest/AndroidManifest.xml @@ -5,6 +5,10 @@ android:versionCode="1" android:versionName="1.0"> + + + + diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/A000_WelcomeAndSetupAccountIntegrationTest.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/A000_WelcomeAndSetupAccountIntegrationTest.java new file mode 100644 index 000000000..c6a9187dc --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/A000_WelcomeAndSetupAccountIntegrationTest.java @@ -0,0 +1,24 @@ +package com.fsck.k9.endtoend; + +import com.fsck.k9.activity.setup.WelcomeMessage; +import com.fsck.k9.endtoend.pages.WelcomeMessagePage; + +/** + * Creates a new IMAP account via the getting started flow. + */ +public class A000_WelcomeAndSetupAccountIntegrationTest extends AbstractEndToEndTest { + + public A000_WelcomeAndSetupAccountIntegrationTest() { + super(WelcomeMessage.class, false); + } + + public void testCreateAccount() throws Exception { + new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage()); + } + + public void testCreateSecondAccount() throws Exception { + new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage()); + } + +} + diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/A010_AccountIntegrationTest.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/A010_AccountIntegrationTest.java new file mode 100644 index 000000000..d3fc8701c --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/A010_AccountIntegrationTest.java @@ -0,0 +1,40 @@ +package com.fsck.k9.endtoend; + +import com.fsck.k9.activity.Accounts; +import com.fsck.k9.endtoend.framework.AccountForTest; +import com.fsck.k9.endtoend.framework.ApplicationState; +import com.fsck.k9.endtoend.pages.AccountsPage; + +/** + * Creates and removes accounts. + * + * Because of the way K-9 shows the start page, there must already be two accounts + * in existence for this test to work. + */ +public class A010_AccountIntegrationTest extends AbstractEndToEndTest{ + + public A010_AccountIntegrationTest() { + super(Accounts.class); + } + + public void testCreateAccountDirectly() throws Exception { + new AccountSetupFlow(this).setupAccountFromAccountsPage(new AccountsPage()); + } + + public void testDeleteAccount() { + + AccountsPage accountsPage = new AccountsPage(); + + AccountForTest accountForTest = ApplicationState.getInstance().accounts.get(0); + accountsPage.assertAccountExists(accountForTest.description); + + accountsPage.clickLongOnAccount(accountForTest); + + accountsPage.clickRemoveInAccountMenu(); + + accountsPage.clickOK(); + + accountsPage.assertAccountDoesNotExist(accountForTest.description); + + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/AbstractEndToEndTest.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/AbstractEndToEndTest.java new file mode 100644 index 000000000..f1ed25a8c --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/AbstractEndToEndTest.java @@ -0,0 +1,60 @@ +package com.fsck.k9.endtoend; + +import android.app.Activity; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; + +import com.fsck.k9.R; +import com.fsck.k9.endtoend.framework.ApplicationState; +import com.fsck.k9.endtoend.framework.StubMailServer; +import com.fsck.k9.endtoend.pages.WelcomeMessagePage; +import com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions; + +import junit.framework.AssertionFailedError; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + +public abstract class AbstractEndToEndTest extends ActivityInstrumentationTestCase2 { + + private ApplicationState state = ApplicationState.getInstance(); + private final boolean bypassWelcome; + + public AbstractEndToEndTest(Class activityClass) { + this(activityClass, true); + } + + public AbstractEndToEndTest(Class activityClass, boolean bypassWelcome) { + super(activityClass); + this.bypassWelcome = bypassWelcome; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + getActivity(); + + if (bypassWelcome) { + bypassWelcomeScreen(); + } + } + + private void bypassWelcomeScreen() { + try { + onView(withId(R.id.welcome_message)).check(ViewAssertions.doesNotExist()); + } catch (AssertionFailedError ex) { + /* + * The view doesn't NOT exist == the view exists, and needs to be bypassed! + */ + Log.d(getClass().getName(), "Bypassing welcome"); + new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage()); + } + } + + protected StubMailServer setupMailServer() { + if (null == state.stubMailServer) { + state.stubMailServer = new StubMailServer(); + } + return state.stubMailServer; + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/AccountSetupFlow.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/AccountSetupFlow.java new file mode 100644 index 000000000..978ffd332 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/AccountSetupFlow.java @@ -0,0 +1,98 @@ +package com.fsck.k9.endtoend; + +import com.fsck.k9.endtoend.framework.AccountForTest; +import com.fsck.k9.endtoend.framework.ApplicationState; +import com.fsck.k9.endtoend.framework.StubMailServer; +import com.fsck.k9.endtoend.framework.UserForImap; +import com.fsck.k9.endtoend.pages.AccountOptionsPage; +import com.fsck.k9.endtoend.pages.AccountSetupNamesPage; +import com.fsck.k9.endtoend.pages.AccountSetupPage; +import com.fsck.k9.endtoend.pages.AccountTypePage; +import com.fsck.k9.endtoend.pages.AccountsPage; +import com.fsck.k9.endtoend.pages.IncomingServerSettingsPage; +import com.fsck.k9.endtoend.pages.OutgoingServerSettingsPage; +import com.fsck.k9.endtoend.pages.WelcomeMessagePage; +import com.fsck.k9.mail.ConnectionSecurity; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Encapsulated the steps required to set up a new mail account. + */ +public class AccountSetupFlow { + + static final String ACCOUNT_NAME = "sendAndReceiveTestName"; + + private final AbstractEndToEndTest test; + + public AccountSetupFlow(AbstractEndToEndTest test) { + this.test = test; + } + + public AccountsPage setupAccountFromWelcomePage(WelcomeMessagePage welcomeMessagePage) { + AccountSetupPage accountSetupPage = welcomeMessagePage.clickNext(); + return setupAccountFromSetupNewAccountActivity(accountSetupPage); + } + + public AccountsPage setupAccountFromAccountsPage(AccountsPage accountPage) { + AccountSetupPage accountSetupPage = accountPage.clickAddNewAccount(); + return setupAccountFromSetupNewAccountActivity(accountSetupPage); + } + + public AccountsPage setupAccountFromSetupNewAccountActivity(AccountSetupPage accountSetupPage) { + AccountTypePage accountTypePage = fillInCredentialsAndClickManualSetup(accountSetupPage); + + IncomingServerSettingsPage incoming = accountTypePage.clickImap(); + + StubMailServer stubMailServer = test.setupMailServer(); + + OutgoingServerSettingsPage outgoing = setupIncomingServerAndClickNext(incoming, stubMailServer); + + AccountOptionsPage accountOptionsPage = setupOutgoingServerAndClickNext(outgoing, stubMailServer); + + AccountSetupNamesPage accountSetupNamesPage = accountOptionsPage.clickNext(); + + String accountDescription = tempAccountName(); + accountSetupNamesPage.inputAccountDescription(accountDescription); + accountSetupNamesPage.inputAccountName(ACCOUNT_NAME); + + AccountsPage accountsPage = accountSetupNamesPage.clickDone(); + + accountsPage.assertAccountExists(accountDescription); + + ApplicationState.getInstance().accounts.add(new AccountForTest(ACCOUNT_NAME, accountDescription, stubMailServer)); + + return accountsPage; + } + + + private String tempAccountName() { + return "sendAndReceiveTest-" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date()); + } + + private AccountTypePage fillInCredentialsAndClickManualSetup(AccountSetupPage page) { + return page + .inputEmailAddress(UserForImap.TEST_USER.emailAddress) + .inputPassword(UserForImap.TEST_USER.password) + .clickManualSetup(); + } + + private AccountOptionsPage setupOutgoingServerAndClickNext(OutgoingServerSettingsPage page, StubMailServer stubMailServer) { + return page + .inputSmtpServer(stubMailServer.getSmtpBindAddress()) + .inputSmtpSecurity(ConnectionSecurity.NONE) + .inputPort(stubMailServer.getSmtpPort()) + .inputRequireSignIn(false) + .clickNext(); + } + + private OutgoingServerSettingsPage setupIncomingServerAndClickNext(IncomingServerSettingsPage page, StubMailServer stubMailServer) { + return page + .inputImapServer(stubMailServer.getImapBindAddress()) + .inputImapSecurity(ConnectionSecurity.NONE) + .inputPort(stubMailServer.getImapPort()) + .inputUsername(UserForImap.TEST_USER.loginUsername) + .clickNext(); + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/AccountForTest.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/AccountForTest.java new file mode 100644 index 000000000..6845efdbe --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/AccountForTest.java @@ -0,0 +1,17 @@ +package com.fsck.k9.endtoend.framework; + +/** + * An account that was added by a test. + */ +public class AccountForTest { + + public final String name; + public final String description; + public final StubMailServer stubMailServer; + + public AccountForTest(String name, String description, StubMailServer stubMailServer) { + this.name = name; + this.description = description; + this.stubMailServer = stubMailServer; + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/ApplicationState.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/ApplicationState.java new file mode 100644 index 000000000..149a2a977 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/ApplicationState.java @@ -0,0 +1,22 @@ +package com.fsck.k9.endtoend.framework; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stores the state of the application from the point of view of end-to-end tests. + */ +public class ApplicationState { + + private static final ApplicationState state = new ApplicationState(); + + public final List accounts = new ArrayList(); + + public StubMailServer stubMailServer; + + public static ApplicationState getInstance() { + return state; + } + + +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/StubMailServer.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/StubMailServer.java new file mode 100644 index 000000000..d8d1df2a3 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/StubMailServer.java @@ -0,0 +1,41 @@ +package com.fsck.k9.endtoend.framework; + +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.ServerSetup; + +/** + * Configuration and management of a pair of stub servers for use by an account. + */ +public class StubMailServer { + private static final ServerSetup IMAP_SERVER_SETUP = new ServerSetup(10143, "127.0.0.2", ServerSetup.PROTOCOL_IMAP); + private static final ServerSetup SMTP_SERVER_SETUP = new ServerSetup(10587, "127.0.0.2", ServerSetup.PROTOCOL_SMTP); + + /** + * Stub server that speaks SMTP, IMAP etc., that K-9 can talk to. + */ + private GreenMail greenmail; + + public StubMailServer() { + + greenmail = new GreenMail(new ServerSetup[]{IMAP_SERVER_SETUP, SMTP_SERVER_SETUP}); + greenmail.setUser(UserForImap.TEST_USER.emailAddress, UserForImap.TEST_USER.loginUsername, UserForImap.TEST_USER.password); + greenmail.start(); + } + + public String getSmtpBindAddress() { + return SMTP_SERVER_SETUP.getBindAddress(); + } + + public int getSmtpPort() { + return SMTP_SERVER_SETUP.getPort(); + } + + public String getImapBindAddress() { + return IMAP_SERVER_SETUP.getBindAddress(); + } + + public int getImapPort() { + return IMAP_SERVER_SETUP.getPort(); + } +} + diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/UserForImap.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/UserForImap.java new file mode 100644 index 000000000..80c1c08f4 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/framework/UserForImap.java @@ -0,0 +1,19 @@ +package com.fsck.k9.endtoend.framework; + +/** + * Credentials for the stub IMAP/SMTP server + */ +public class UserForImap { + + public static final UserForImap TEST_USER = new UserForImap("test-username", "test-password", "test-email@example.com"); + + public final String loginUsername; + public final String password; + public final String emailAddress; + + private UserForImap(String loginUsername, String password, String emailAddress) { + this.loginUsername = loginUsername; + this.password = password; + this.emailAddress = emailAddress; + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AbstractPage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AbstractPage.java new file mode 100644 index 000000000..bc308210e --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AbstractPage.java @@ -0,0 +1,6 @@ +package com.fsck.k9.endtoend.pages; + +public class AbstractPage { + + // used to have some content. Now a placeholder class +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountOptionsPage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountOptionsPage.java new file mode 100644 index 000000000..b2f400121 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountOptionsPage.java @@ -0,0 +1,17 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + + +public class AccountOptionsPage extends AbstractPage { + + public AccountSetupNamesPage clickNext() { + onView(withId(R.id.next)).perform(click()); + return new AccountSetupNamesPage(); + } + +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountSetupNamesPage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountSetupNamesPage.java new file mode 100644 index 000000000..058f613b5 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountSetupNamesPage.java @@ -0,0 +1,48 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException; +import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + + +public class AccountSetupNamesPage extends AbstractPage { + + public AccountSetupNamesPage inputAccountName(String name) { + onView(withId(R.id.account_name)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(name)); + return this; + } + + public AccountSetupNamesPage inputAccountDescription(String name) { + onView(withId(R.id.account_description)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(name)); + return this; + } + + public AccountsPage clickDone() { + onView(withId(R.id.done)) + .perform(click()); + dismissChangelog(); + return new AccountsPage(); + } + + private void dismissChangelog() { + try { + onView(ViewMatchers.withText("OK")).perform(click()); + } catch (NoMatchingViewException ex) { + // Ignored. Not the best way of doing this, but Espresso rightly makes + // conditional flow difficult. + } + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountSetupPage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountSetupPage.java new file mode 100644 index 000000000..2fa659e90 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountSetupPage.java @@ -0,0 +1,27 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + +public class AccountSetupPage extends AbstractPage { + + public AccountSetupPage inputEmailAddress(String emailAddress) { + onView(withId(R.id.account_email)).perform(typeText(emailAddress)); + return this; + } + + public AccountSetupPage inputPassword(String password) { + onView(withId(R.id.account_password)).perform(typeText(password)); + return this; + } + + public AccountTypePage clickManualSetup() { + onView(withId(R.id.manual_setup)).perform(click()); + return new AccountTypePage(); + } + +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountTypePage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountTypePage.java new file mode 100644 index 000000000..386184161 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountTypePage.java @@ -0,0 +1,14 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +public class AccountTypePage extends AbstractPage { + + public IncomingServerSettingsPage clickImap() { + onView(withId(R.id.imap)).perform(click()); + return new IncomingServerSettingsPage(); + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountsPage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountsPage.java new file mode 100644 index 000000000..653281523 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/AccountsPage.java @@ -0,0 +1,56 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.fsck.k9.endtoend.framework.AccountForTest; +import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException; +import com.google.android.apps.common.testing.ui.espresso.ViewAssertion; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.longClick; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.doesNotExist; +import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText; + +public class AccountsPage extends AbstractPage { + + private void assertAccount(String accountDisplayName, boolean exists) { + ViewAssertion assertion = exists ? matches(isDisplayed()) : doesNotExist(); + onView(withText(accountDisplayName)).check(assertion); + } + + public AccountSetupPage clickAddNewAccount() { + // need to click twice for some reason? + onView(withId(R.id.add_new_account)).perform(click()); + try { + onView(withId(R.id.add_new_account)).perform(click()); + } catch (NoMatchingViewException ex) { + // Ignore + } + onView(withId(R.id.account_email)).perform(scrollTo()); + return new AccountSetupPage(); + } + + public void assertAccountExists(String accountDisplayName) { + assertAccount(accountDisplayName, true); + } + + public void assertAccountDoesNotExist(String accountDisplayName) { + assertAccount(accountDisplayName, false); + } + + public void clickLongOnAccount(AccountForTest accountForTest) { + onView(withText(accountForTest.description)).perform(longClick()); + } + + public void clickRemoveInAccountMenu() { + onView(withText("Remove account")).perform(click()); + } + + public void clickOK() { + onView(withText("OK")).perform(click()); + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/IncomingServerSettingsPage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/IncomingServerSettingsPage.java new file mode 100644 index 000000000..f0b07579c --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/IncomingServerSettingsPage.java @@ -0,0 +1,66 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.fsck.k9.mail.ConnectionSecurity; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData; +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isClickable; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +public class IncomingServerSettingsPage extends AbstractPage { + + public IncomingServerSettingsPage inputImapServer(String imapServer) { + onView(withId(R.id.account_server)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(imapServer)); + return this; + } + + public IncomingServerSettingsPage inputImapSecurity(ConnectionSecurity security) { + onView(withId(R.id.account_security_type)) + .perform(scrollTo()) + .perform(click()); + onData(allOf(is(instanceOf(ConnectionSecurity.class)), is(security))).perform(click()); + return this; + } + + public IncomingServerSettingsPage inputPort(int port) { + onView(withId(R.id.account_port)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(String.valueOf(port))); + return this; + } + + + public OutgoingServerSettingsPage clickNext() { + onView(withId(R.id.next)) +// .perform(scrollTo()) + .check(matches(isClickable())) + .perform(click()); + + // We know this view is on the next page, this functions as a wait. + onView(withText("SMTP server")).perform(scrollTo()); + return new OutgoingServerSettingsPage(); + } + + public IncomingServerSettingsPage inputUsername(String loginUsername) { + onView(withId(R.id.account_username)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(loginUsername)); + return this; + } + +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/OutgoingServerSettingsPage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/OutgoingServerSettingsPage.java new file mode 100644 index 000000000..87ec9723f --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/OutgoingServerSettingsPage.java @@ -0,0 +1,69 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.fsck.k9.mail.ConnectionSecurity; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData; +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + + +public class OutgoingServerSettingsPage extends AbstractPage { + + public OutgoingServerSettingsPage inputSmtpServer(String serverAddress) { + onView(withId(R.id.account_server)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(serverAddress)); + return this; + + } + + public OutgoingServerSettingsPage inputSmtpSecurity(ConnectionSecurity security) { + onView(withId(R.id.account_security_type)) + .perform(scrollTo()) + .perform(click()); + onData(allOf(is(instanceOf(ConnectionSecurity.class)), is(security))).perform(click()); + return this; + } + + public OutgoingServerSettingsPage inputPort(int port) { + onView(withId(R.id.account_port)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(String.valueOf(port))); + return this; + } + + public OutgoingServerSettingsPage inputRequireSignIn(boolean requireSignInInput) { + onView(withId(R.id.account_require_login)) + .perform(scrollTo()); + /* + * Make this smarter; click if necessary. + */ + if (!requireSignInInput) { + onView(withId(R.id.account_require_login)) + .perform(click()); + } +// Matcher checkedOrNot = requireSignInInput ? isChecked(): isNotChecked(); +// try { +// onView(withId(R.id.account_require_login)).check((matches(checkedOrNot))); +// } catch (AssertionFailedWithCauseError ex) { +// onView(withId(R.id.account_require_login)).perform(click()); +// } + return this; + } + + public AccountOptionsPage clickNext() { + onView(withId(R.id.next)).perform(click()); + return new AccountOptionsPage(); + } + +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/WelcomeMessagePage.java b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/WelcomeMessagePage.java new file mode 100644 index 000000000..723fb3645 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/endtoend/pages/WelcomeMessagePage.java @@ -0,0 +1,15 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + +public class WelcomeMessagePage extends AbstractPage { + + public final AccountSetupPage clickNext() { + onView(withId(R.id.next)).perform(click()); + return new AccountSetupPage(); + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mail/MessageTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mail/MessageTest.java index 70d771df4..a9ec466e8 100644 --- a/k9mail/src/androidTest/java/com/fsck/k9/mail/MessageTest.java +++ b/k9mail/src/androidTest/java/com/fsck/k9/mail/MessageTest.java @@ -8,6 +8,7 @@ import java.io.OutputStream; import java.util.Date; import java.util.TimeZone; +import com.fsck.k9.mail.internet.MimeMessageHelper; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.util.MimeUtil; @@ -199,7 +200,8 @@ public class MessageTest extends AndroidTestCase { + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "------Boundary102\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" @@ -208,7 +210,8 @@ public class MessageTest extends AndroidTestCase { + "End of test=2E\r\n" + "\r\n" + "------Boundary102\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" @@ -236,7 +239,8 @@ public class MessageTest extends AndroidTestCase { + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "------Boundary101\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" @@ -245,7 +249,8 @@ public class MessageTest extends AndroidTestCase { + "End of test=2E\r\n" + "\r\n" + "------Boundary101\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" @@ -315,8 +320,7 @@ public class MessageTest extends AndroidTestCase { private MimeMessage nestedMessage(MimeMessage subMessage) throws MessagingException, IOException { - BinaryTempFileMessageBody tempMessageBody = new BinaryTempFileMessageBody(); - tempMessageBody.setEncoding(MimeUtil.ENC_8BIT); + BinaryTempFileMessageBody tempMessageBody = new BinaryTempFileMessageBody(MimeUtil.ENC_8BIT); OutputStream out = tempMessageBody.getOutputStream(); try { @@ -348,7 +352,7 @@ public class MessageTest extends AndroidTestCase { multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_8BIT)); multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_QUOTED_PRINTABLE)); multipartBody.addBodyPart(binaryBodyPart()); - message.setBody(multipartBody); + MimeMessageHelper.setBody(message, multipartBody); return message; } @@ -356,12 +360,12 @@ public class MessageTest extends AndroidTestCase { private MimeBodyPart binaryBodyPart() throws IOException, MessagingException { String encodedTestString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - + "abcdefghijklmnopqrstuvwxyz0123456789+/"; + + "abcdefghijklmnopqrstuvwxyz0123456789+/\r\n"; - BinaryTempFileBody tempFileBody = new BinaryTempFileBody(); + BinaryTempFileBody tempFileBody = new BinaryTempFileBody(MimeUtil.ENC_BASE64); - InputStream in = new Base64InputStream(new ByteArrayInputStream( - encodedTestString.getBytes("UTF-8"))); + InputStream in = new ByteArrayInputStream( + encodedTestString.getBytes("UTF-8")); OutputStream out = tempFileBody.getOutputStream(); try { diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mail/PgpMimeMessageTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mail/PgpMimeMessageTest.java new file mode 100644 index 000000000..75cee3ba2 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/mail/PgpMimeMessageTest.java @@ -0,0 +1,269 @@ +package com.fsck.k9.mail; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import android.test.AndroidTestCase; + +import com.fsck.k9.mail.internet.BinaryTempFileBody; +import com.fsck.k9.mail.internet.MimeMessage; +import org.spongycastle.openpgp.PGPCompressedData; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRingCollection; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureList; +import org.spongycastle.openpgp.PGPUtil; +import org.spongycastle.openpgp.bc.BcPGPObjectFactory; +import org.spongycastle.openpgp.bc.BcPGPPublicKeyRingCollection; +import org.spongycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; + + +public class PgpMimeMessageTest extends AndroidTestCase { + private static final String PUBLIC_KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: GnuPG v1\n" + + "\n" + + "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\n" + + "HzpeeqkwzVU=\n" + + "=3yEX\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + + public void testSignedMessage() throws IOException, MessagingException, PGPException { + String messageSource = "Date: Mon, 08 Dec 2014 17:44:18 +0100\r\n" + + "From: cketti \r\n" + + "MIME-Version: 1.0\r\n" + + "To: test@example.com\r\n" + + "Subject: OpenPGP signature test\r\n" + + "Content-Type: multipart/signed; micalg=pgp-sha1;\r\n" + + " protocol=\"application/pgp-signature\";\r\n" + + " boundary=\"24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM\"\r\n" + + "\r\n" + + "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)\r\n" + + "--24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=\"------------030308060900040601010501\"\r\n" + + "\r\n" + + "This is a multi-part message in MIME format.\r\n" + + "--------------030308060900040601010501\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "Message body\r\n" + + "goes here\r\n" + + "\r\n" + + "\r\n" + + "--------------030308060900040601010501\r\n" + + "Content-Type: text/plain; charset=UTF-8;\r\n" + + " name=\"attachment.txt\"\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "Content-Disposition: attachment;\r\n" + + " filename=\"attachment.txt\"\r\n" + + "\r\n" + + "VGV4dCBhdHRhY2htZW50Cg==\r\n" + + "--------------030308060900040601010501--\r\n" + + "\r\n" + + "--24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM\r\n" + + "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n" + + "Content-Description: OpenPGP digital signature\r\n" + + "Content-Disposition: attachment; filename=\"signature.asc\"\r\n" + + "\r\n" + + "-----BEGIN PGP SIGNATURE-----\r\n" + + "Version: GnuPG v1\r\n" + + "\r\n" + + "iQIcBAEBAgAGBQJUhdVqAAoJEO4v7zp9qOKJ8DQP/1+JE8UF7UmirnN1ZO+25hFC\r\n" + + "jAfFMxRWMWXN0gGB+6ySy6ah0bCwmRwHpRBsW/tNcsmOPKb2XBf9zwF06uk/lLp4\r\n" + + "ZmGXxSdQ9XJrlaHk8Sitn9Gi/1L+MNWgrsrLROAZv2jfc9wqN3FOrhN9NC1QXQvO\r\n" + + "+D7sMorSr3l94majoIDrzvxEnfJVfrZWNTUaulJofOJ55GBZ3UJNob1WKjrnculL\r\n" + + "IwmSERmVUoFBUfe/MBqqZH0WDJq9nt//NZFHLunj6nGsrpush1dQRcbR3zzQfXkk\r\n" + + "s7zDLDa8VUv6OxcefjsVN/O7EenoWWgNg6GfW6tY2+oUsLSP2OS3JXvYsylQP4hR\r\n" + + "iU1V9vvsu2Ax6bVb0+uTqw3jNiqVFy3o4mBigVUqp1EFIwBYmyNbe5wj4ACs9Avj\r\n" + + "9t2reFSfXobWQFUS4s71JeMefNAHHJWZI63wNTxE6LOw01YxdJiDaPWGTOyM75MK\r\n" + + "yqn7r5uIfeSv8NypGJaUv4firxKbrcZKk7Wpeh/rZuUSgoPcf3I1IzXfGKKIBHjU\r\n" + + "WUMhTF5SoC5kIZyeXvHrhTM8HszcS8EoG2XcmcYArwgCUlOunFwZNqLPsfdMTRL6\r\n" + + "9rcioaohEtroqoJiGAToJtIz8kqCaamnP/ASBkp9qqJizRd6fqt+tE8BsmJbuPLS\r\n" + + "6lBpS8j0TqmaZMYfB9u4\r\n" + + "=QvET\r\n" + + "-----END PGP SIGNATURE-----\r\n" + + "\r\n" + + "--24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM--\r\n"; + + BinaryTempFileBody.setTempDirectory(getContext().getCacheDir()); + + InputStream messageInputStream = new ByteArrayInputStream(messageSource.getBytes()); + MimeMessage message; + try { + message = new MimeMessage(messageInputStream, true); + } finally { + messageInputStream.close(); + } + + Multipart multipartSigned = (Multipart) message.getBody(); + + BodyPart signedPart = multipartSigned.getBodyPart(0); + ByteArrayOutputStream signedPartOutputStream = new ByteArrayOutputStream(); + signedPart.writeTo(signedPartOutputStream); + byte[] signedData = signedPartOutputStream.toByteArray(); + + Body signatureBody = multipartSigned.getBodyPart(1).getBody(); + ByteArrayOutputStream signatureBodyOutputStream = new ByteArrayOutputStream(); + signatureBody.writeTo(signatureBodyOutputStream); + byte[] signatureData = signatureBodyOutputStream.toByteArray(); + + assertTrue(verifySignature(signedData, signatureData)); + } + + private boolean verifySignature(byte[] signedData, byte[] signatureData) throws IOException, PGPException { + InputStream signatureInputStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(signatureData)); + PGPObjectFactory pgpObjectFactory = new BcPGPObjectFactory(signatureInputStream); + Object pgpObject = pgpObjectFactory.nextObject(); + + PGPSignatureList pgpSignatureList; + if (pgpObject instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) pgpObject; + pgpObjectFactory = new BcPGPObjectFactory(compressedData.getDataStream()); + pgpSignatureList = (PGPSignatureList) pgpObjectFactory.nextObject(); + } else { + pgpSignatureList = (PGPSignatureList) pgpObject; + } + PGPSignature signature = pgpSignatureList.get(0); + + InputStream keyInputStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(PUBLIC_KEY.getBytes())); + PGPPublicKeyRingCollection pgpPublicKeyRingCollection = new BcPGPPublicKeyRingCollection(keyInputStream); + PGPPublicKey publicKey = pgpPublicKeyRingCollection.getPublicKey(signature.getKeyID()); + + signature.init(new BcPGPContentVerifierBuilderProvider(), publicKey); + InputStream signedDataInputStream = new ByteArrayInputStream(signedData); + int ch; + while ((ch = signedDataInputStream.read()) >= 0) { + signature.update((byte) ch); + } + + signedDataInputStream.close(); + keyInputStream.close(); + signatureInputStream.close(); + + return signature.verify(); + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mail/ReconstructMessageTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mail/ReconstructMessageTest.java new file mode 100644 index 000000000..e36854354 --- /dev/null +++ b/k9mail/src/androidTest/java/com/fsck/k9/mail/ReconstructMessageTest.java @@ -0,0 +1,69 @@ +package com.fsck.k9.mail; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import android.test.AndroidTestCase; + +import com.fsck.k9.mail.internet.BinaryTempFileBody; +import com.fsck.k9.mail.internet.MimeMessage; + + +public class ReconstructMessageTest extends AndroidTestCase { + + public void testMessage() throws IOException, MessagingException { + String messageSource = + "From: from@example.com\r\n" + + "To: to@example.com\r\n" + + "Subject: Test Message \r\n" + + "Date: Thu, 13 Nov 2014 17:09:38 +0100\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=\"----Boundary\"\r\n" + + "Content-Transfer-Encoding: 8bit\r\n" + + "MIME-Version: 1.0\r\n" + + "\r\n" + + "This is a multipart MIME message.\r\n" + + "------Boundary\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Transfer-Encoding: 8bit\r\n" + + "\r\n" + + "Testing.\r\n" + + "This is a text body with some greek characters.\r\n" + + "αβγδεζηθ\r\n" + + "End of test.\r\n" + + "\r\n" + + "------Boundary\r\n" + + "Content-Type: text/plain\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "\r\n" + + "VGhpcyBpcyBhIHRl\r\n" + + "c3QgbWVzc2FnZQ==\r\n" + + "\r\n" + + "------Boundary--\r\n" + + "Hi, I'm the epilogue"; + + BinaryTempFileBody.setTempDirectory(getContext().getCacheDir()); + + InputStream messageInputStream = new ByteArrayInputStream(messageSource.getBytes()); + MimeMessage message; + try { + message = new MimeMessage(messageInputStream, true); + } finally { + messageInputStream.close(); + } + + ByteArrayOutputStream messageOutputStream = new ByteArrayOutputStream(); + try { + message.writeTo(messageOutputStream); + } finally { + messageOutputStream.close(); + } + + String reconstructedMessage = new String(messageOutputStream.toByteArray()); + + assertEquals(messageSource, reconstructedMessage); + } +} diff --git a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java index a6b417fa5..179f72053 100644 --- a/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java +++ b/k9mail/src/androidTest/java/com/fsck/k9/mailstore/LocalMessageExtractorTest.java @@ -11,6 +11,7 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.TextBody; @@ -26,7 +27,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); - message.setBody(body); + MimeMessageHelper.setBody(message, body); // Extract text ViewableContainer container = extractTextAndAttachments(getContext(), message); @@ -50,7 +51,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); message.setHeader("Content-Type", "text/html"); - message.setBody(body); + MimeMessageHelper.setBody(message, body); // Extract text ViewableContainer container = extractTextAndAttachments(getContext(), message); @@ -80,7 +81,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); - message.setBody(multipart); + MimeMessageHelper.setBody(message, multipart); // Extract text ViewableContainer container = extractTextAndAttachments(getContext(), message); @@ -124,7 +125,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase { innerMessage.setRecipients(RecipientType.TO, new Address[] { new Address("to@example.com") }); innerMessage.setSubject("Subject"); innerMessage.setFrom(new Address("from@example.com")); - innerMessage.setBody(innerBody); + MimeMessageHelper.setBody(innerMessage, innerBody); // Create multipart/mixed part MimeMultipart multipart = new MimeMultipart(); @@ -136,7 +137,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); - message.setBody(multipart); + MimeMessageHelper.setBody(message, multipart); // Extract text ViewableContainer container = extractTextAndAttachments(getContext(), message); diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java index ff8cbc28d..bfebe1bba 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -105,6 +105,7 @@ import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; @@ -1400,10 +1401,10 @@ public class MessageCompose extends K9Activity implements OnClickListener, MimeMultipart mp = new MimeMultipart(); mp.addBodyPart(new MimeBodyPart(composedMimeMessage)); addAttachmentsToMessage(mp); - message.setBody(mp); + MimeMessageHelper.setBody(message, mp); } else { // If no attachments, our multipart/alternative part is the only one we need. - message.setBody(composedMimeMessage); + MimeMessageHelper.setBody(message, composedMimeMessage); } } else if (mMessageFormat == SimpleMessageFormat.TEXT) { // Text-only message. @@ -1411,10 +1412,10 @@ public class MessageCompose extends K9Activity implements OnClickListener, MimeMultipart mp = new MimeMultipart(); mp.addBodyPart(new MimeBodyPart(body, "text/plain")); addAttachmentsToMessage(mp); - message.setBody(mp); + MimeMessageHelper.setBody(message, mp); } else { // No attachments to include, just stick the text body in the message and call it good. - message.setBody(body); + MimeMessageHelper.setBody(message, body); } } diff --git a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java index a0d4ef2e3..fe80a607e 100644 --- a/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java +++ b/k9mail/src/main/java/com/fsck/k9/controller/MessagingController.java @@ -80,6 +80,7 @@ import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mailstore.MessageRemovalListener; @@ -2697,7 +2698,7 @@ public class MessagingController implements Runnable { LocalFolder localFolder = (LocalFolder)localStore.getFolder(account.getErrorFolderName()); MimeMessage message = new MimeMessage(); - message.setBody(new TextBody(body)); + MimeMessageHelper.setBody(message, new TextBody(body)); message.setFlag(Flag.X_DOWNLOADED_FULL, true); message.setSubject(subject); @@ -3190,7 +3191,7 @@ public class MessagingController implements Runnable { //FIXME: This is an ugly hack that won't be needed once the Message objects have been united. Message remoteMessage = remoteFolder.getMessage(message.getUid()); - remoteMessage.setBody(message.getBody()); + MimeMessageHelper.setBody(remoteMessage, message.getBody()); remoteFolder.fetchPart(remoteMessage, part, null); localFolder.updateMessage((LocalMessage)message); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index 6677fcd05..cc353a06e 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -17,6 +17,7 @@ import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; +import com.fsck.k9.mail.internet.MimeMessageHelper; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.util.MimeUtil; @@ -784,17 +785,16 @@ public class LocalFolder extends Folder implements Serializable { // triggering T_MIME_NO_TEXT and T_TVD_MIME_NO_HEADERS // SpamAssassin rules. localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain"); - localMessage.setBody(new TextBody("")); + MimeMessageHelper.setBody(localMessage, new TextBody("")); } else if (mp.getCount() == 1 && - !(mp.getBodyPart(0) instanceof LocalAttachmentBodyPart)) - { + !(mp.getBodyPart(0) instanceof LocalAttachmentBodyPart)) { // If we have only one part, drop the MimeMultipart container. BodyPart part = mp.getBodyPart(0); localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType()); - localMessage.setBody(part.getBody()); + MimeMessageHelper.setBody(localMessage, part.getBody()); } else { // Otherwise, attach the MimeMultipart to the message. - localMessage.setBody(mp); + MimeMessageHelper.setBody(localMessage, mp); } } } @@ -1566,7 +1566,7 @@ public class LocalFolder extends Folder implements Serializable { * If the attachment has a body we're expected to save it into the local store * so we copy the data into a cached attachment file. */ - InputStream in = attachment.getBody().getInputStream(); + InputStream in = MimeUtility.decodeBody(attachment.getBody()); try { tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory); FileOutputStream out = new FileOutputStream(tempAttachmentFile); @@ -1648,11 +1648,13 @@ public class LocalFolder extends Folder implements Serializable { getAccount(), attachmentId); if (MimeUtil.isMessage(attachment.getMimeType())) { - attachment.setBody(new LocalAttachmentMessageBody( - contentUri, LocalFolder.this.localStore.context)); + LocalAttachmentMessageBody body = new LocalAttachmentMessageBody( + contentUri, LocalFolder.this.localStore.context); + MimeMessageHelper.setBody(attachment, body); } else { - attachment.setBody(new LocalAttachmentBody( - contentUri, LocalFolder.this.localStore.context)); + LocalAttachmentBody body = new LocalAttachmentBody( + contentUri, LocalFolder.this.localStore.context); + MimeMessageHelper.setBody(attachment, body); } ContentValues cv = new ContentValues(); cv.put("content_uri", contentUri != null ? contentUri.toString() : null); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index ca28ce812..e54a9b93f 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -514,6 +514,11 @@ public class LocalMessage extends MimeMessage { super.addHeader(name, value); } + @Override + public void addRawHeader(String name, String raw) { + throw new RuntimeException("Not supported"); + } + @Override public void setHeader(String name, String value) throws MessagingException { if (!mHeadersLoaded) diff --git a/tests-on-jvm/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java b/tests-on-jvm/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java index 028912f25..38c5f95c8 100644 --- a/tests-on-jvm/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java +++ b/tests-on-jvm/src/test/java/com/fsck/k9/mail/internet/MimeMessageParseTest.java @@ -42,7 +42,7 @@ public class MimeMessageParseTest { checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); assertEquals("Testmail", msg.getSubject()); assertEquals("text/plain", msg.getContentType()); - assertEquals("this is some test text.", streamToString(msg.getBody().getInputStream())); + assertEquals("this is some test text.", streamToString(MimeUtility.decodeBody(msg.getBody()))); } @Test public void testSinglePart8BitRecurse() throws Exception { @@ -60,7 +60,7 @@ public class MimeMessageParseTest { checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); assertEquals("Testmail", msg.getSubject()); assertEquals("text/plain; encoding=ISO-8859-1", msg.getContentType()); - assertEquals("gefährliche Umlaute", streamToString(msg.getBody().getInputStream())); + assertEquals("gefährliche Umlaute", streamToString(MimeUtility.decodeBody(msg.getBody()))); } @Test public void testSinglePartBase64NoRecurse() throws Exception { @@ -78,7 +78,7 @@ public class MimeMessageParseTest { checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); assertEquals("Testmail", msg.getSubject()); assertEquals("text/plain", msg.getContentType()); - assertEquals("this is some more test text.", streamToString(msg.getBody().getInputStream())); + assertEquals("this is some more test text.", streamToString(MimeUtility.decodeBody(msg.getBody()))); } @Test public void testMultipartSingleLayerNoRecurse() throws Exception { @@ -231,7 +231,7 @@ public class MimeMessageParseTest { private static void checkLeafParts(MimeMessage msg, String... expectedParts) throws Exception { List actual = new ArrayList(); for (Body leaf : getLeafParts(msg.getBody())) { - actual.add(streamToString(leaf.getInputStream())); + actual.add(streamToString(MimeUtility.decodeBody(leaf))); } assertEquals(Arrays.asList(expectedParts), actual); }