1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-30 05:02:26 -05:00

Merge pull request #524 from k9mail/merge_pgp_mime_branch

Merge changes from PGP/MIME repository
This commit is contained in:
cketti 2015-01-02 22:04:28 +01:00
commit 82736f3a8b
42 changed files with 1398 additions and 157 deletions

View File

@ -1,4 +1,4 @@
#Sun Nov 30 16:02:23 PST 2014 #Sun Dec 07 14:12:42 GMT 2014
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -14,8 +14,8 @@ import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import static com.fsck.k9.mail.K9MailLib.LOG_TAG; import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
public abstract class Message implements Part, CompositeBody { public abstract class Message implements Part, CompositeBody {
public enum RecipientType { public enum RecipientType {
TO, CC, BCC, TO, CC, BCC,
} }
@ -117,8 +117,32 @@ public abstract class Message implements Part, CompositeBody {
public abstract void setReferences(String references) throws MessagingException; 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<String> getHeaderNames() throws MessagingException; public abstract Set<String> 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 long getId();
public abstract String getPreview(); public abstract String getPreview();

View File

@ -7,6 +7,8 @@ import java.io.OutputStream;
public interface Part { public interface Part {
void addHeader(String name, String value) throws MessagingException; void addHeader(String name, String value) throws MessagingException;
void addRawHeader(String name, String raw) throws MessagingException;
void removeHeader(String name) throws MessagingException; void removeHeader(String name) throws MessagingException;
void setHeader(String name, String value) throws MessagingException; void setHeader(String name, String value) throws MessagingException;

View File

@ -15,7 +15,7 @@ import java.io.*;
* and writeTo one time. After writeTo is called, or the InputStream returned from * 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. * 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 static File mTempDirectory;
private File mFile; private File mFile;
@ -26,15 +26,56 @@ public class BinaryTempFileBody implements Body {
mTempDirectory = tempDirectory; mTempDirectory = tempDirectory;
} }
public void setEncoding(String encoding) throws MessagingException { @Override
mEncoding = encoding; public String getEncoding() {
return mEncoding;
} }
public BinaryTempFileBody() { public void setEncoding(String encoding) throws MessagingException {
if (mTempDirectory == null) { if (mEncoding != null && mEncoding.equalsIgnoreCase(encoding)) {
throw new return;
RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!");
} }
// 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 { public OutputStream getOutputStream() throws IOException {
@ -54,22 +95,7 @@ public class BinaryTempFileBody implements Body {
public void writeTo(OutputStream out) throws IOException, MessagingException { public void writeTo(OutputStream out) throws IOException, MessagingException {
InputStream in = getInputStream(); InputStream in = getInputStream();
try { try {
boolean closeStream = false; IOUtils.copy(in, out);
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();
}
}
} finally { } finally {
in.close(); in.close();
} }

View File

@ -18,6 +18,10 @@ import com.fsck.k9.mail.MessagingException;
*/ */
public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody { public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody {
public BinaryTempFileMessageBody(String encoding) {
super(encoding);
}
@Override @Override
public void setEncoding(String encoding) throws MessagingException { public void setEncoding(String encoding) throws MessagingException {
if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding)

View File

@ -35,7 +35,7 @@ public class MimeBodyPart extends BodyPart {
if (mimeType != null) { if (mimeType != null) {
addHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType); addHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
} }
setBody(body); MimeMessageHelper.setBody(this, body);
} }
private String getFirstHeader(String name) { private String getFirstHeader(String name) {
@ -47,6 +47,11 @@ public class MimeBodyPart extends BodyPart {
mHeader.addHeader(name, value); mHeader.addHeader(name, value);
} }
@Override
public void addRawHeader(String name, String raw) {
mHeader.addRawHeader(name, raw);
}
@Override @Override
public void setHeader(String name, String value) { public void setHeader(String name, String value) {
mHeader.setHeader(name, value); mHeader.setHeader(name, value);
@ -70,25 +75,6 @@ public class MimeBodyPart extends BodyPart {
@Override @Override
public void setBody(Body body) throws MessagingException { public void setBody(Body body) throws MessagingException {
this.mBody = body; 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 @Override

View File

@ -50,7 +50,13 @@ public class MimeHeader {
} }
public void addHeader(String name, String value) { 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) { public void setHeader(String name, String value) {
@ -64,7 +70,7 @@ public class MimeHeader {
public Set<String> getHeaderNames() { public Set<String> getHeaderNames() {
Set<String> names = new LinkedHashSet<String>(); Set<String> names = new LinkedHashSet<String>();
for (Field field : mFields) { for (Field field : mFields) {
names.add(field.name); names.add(field.getName());
} }
return names; return names;
} }
@ -72,8 +78,8 @@ public class MimeHeader {
public String[] getHeader(String name) { public String[] getHeader(String name) {
List<String> values = new ArrayList<String>(); List<String> values = new ArrayList<String>();
for (Field field : mFields) { for (Field field : mFields) {
if (field.name.equalsIgnoreCase(name)) { if (field.getName().equalsIgnoreCase(name)) {
values.add(field.value); values.add(field.getValue());
} }
} }
if (values.isEmpty()) { if (values.isEmpty()) {
@ -85,7 +91,7 @@ public class MimeHeader {
public void removeHeader(String name) { public void removeHeader(String name) {
List<Field> removeFields = new ArrayList<Field>(); List<Field> removeFields = new ArrayList<Field>();
for (Field field : mFields) { for (Field field : mFields) {
if (field.name.equalsIgnoreCase(name)) { if (field.getName().equalsIgnoreCase(name)) {
removeFields.add(field); removeFields.add(field);
} }
} }
@ -96,26 +102,34 @@ public class MimeHeader {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
for (Field field : mFields) { for (Field field : mFields) {
if (!Arrays.asList(writeOmitFields).contains(field.name)) { if (!Arrays.asList(writeOmitFields).contains(field.name)) {
String v = field.value; if (field.hasRawData()) {
writer.write(field.getRaw());
if (hasToBeEncoded(v)) { } else {
Charset charset = null; writeNameValueField(writer, field);
if (mCharset != null) {
charset = Charset.forName(mCharset);
}
v = EncoderUtil.encodeEncodedWord(field.value, charset);
} }
writer.write(field.name);
writer.write(": ");
writer.write(v);
writer.write("\r\n"); writer.write("\r\n");
} }
} }
writer.flush(); 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. // encode non printable characters except LF/CR/TAB codes.
private boolean hasToBeEncoded(String text) { private boolean hasToBeEncoded(String text) {
for (int i = 0; i < text.length(); i++) { for (int i = 0; i < text.length(); i++) {
@ -131,19 +145,67 @@ public class MimeHeader {
private static class Field { private static class Field {
private final String name; private final String name;
private final String value; 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.name = name;
this.value = value; 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 @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder("("); return (hasRawData()) ? getRaw() : getName() + ": " + getValue();
sb.append(name).append('=').append(value).append(')');
return sb.toString();
} }
} }

View File

@ -2,6 +2,7 @@
package com.fsck.k9.mail.internet; package com.fsck.k9.mail.internet;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -14,6 +15,7 @@ import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.dom.field.DateTimeField; import org.apache.james.mime4j.dom.field.DateTimeField;
import org.apache.james.mime4j.field.DefaultFieldParser; import org.apache.james.mime4j.field.DefaultFieldParser;
@ -394,22 +396,6 @@ public class MimeMessage extends Message {
@Override @Override
public void setBody(Body body) throws MessagingException { public void setBody(Body body) throws MessagingException {
this.mBody = body; 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) { private String getFirstHeader(String name) {
@ -421,6 +407,11 @@ public class MimeMessage extends Message {
mHeader.addHeader(name, value); mHeader.addHeader(name, value);
} }
@Override
public void addRawHeader(String name, String raw) {
mHeader.addRawHeader(name, raw);
}
@Override @Override
public void setHeader(String name, String value) throws MessagingException { public void setHeader(String name, String value) throws MessagingException {
mHeader.setHeader(name, value); mHeader.setHeader(name, value);
@ -540,8 +531,7 @@ public class MimeMessage extends Message {
public void body(BodyDescriptor bd, InputStream in) throws IOException { public void body(BodyDescriptor bd, InputStream in) throws IOException {
expect(Part.class); expect(Part.class);
try { try {
Body body = MimeUtility.decodeBody(in, Body body = MimeUtility.createBody(in, bd.getTransferEncoding(), bd.getMimeType());
bd.getTransferEncoding(), bd.getMimeType());
((Part)stack.peek()).setBody(body); ((Part)stack.peek()).setBody(body);
} catch (MessagingException me) { } catch (MessagingException me) {
throw new Error(me); throw new Error(me);
@ -575,16 +565,17 @@ public class MimeMessage extends Message {
@Override @Override
public void preamble(InputStream is) throws IOException { public void preamble(InputStream is) throws IOException {
expect(MimeMultipart.class); expect(MimeMultipart.class);
StringBuilder sb = new StringBuilder(); ByteArrayOutputStream preamble = new ByteArrayOutputStream();
int b; IOUtils.copy(is, preamble);
while ((b = is.read()) != -1) { ((MimeMultipart)stack.peek()).setPreamble(preamble.toByteArray());
sb.append((char)b);
}
((MimeMultipart)stack.peek()).setPreamble(sb.toString());
} }
@Override @Override
public void epilogue(InputStream is) throws IOException { 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 @Override
@ -596,7 +587,9 @@ public class MimeMessage extends Message {
public void field(Field parsedField) throws MimeException { public void field(Field parsedField) throws MimeException {
expect(Part.class); expect(Part.class);
try { 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) { } catch (MessagingException me) {
throw new Error(me); throw new Error(me);
} }

View File

@ -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);
}
}

View File

@ -10,7 +10,8 @@ import java.util.Locale;
import java.util.Random; import java.util.Random;
public class MimeMultipart extends Multipart { public class MimeMultipart extends Multipart {
private String mPreamble; private byte[] mPreamble;
private byte[] mEpilogue;
private String mContentType; private String mContentType;
@ -45,12 +46,12 @@ public class MimeMultipart extends Multipart {
return sb.toString().toUpperCase(Locale.US); return sb.toString().toUpperCase(Locale.US);
} }
public String getPreamble() { public void setPreamble(byte[] preamble) {
return mPreamble; this.mPreamble = preamble;
} }
public void setPreamble(String preamble) { public void setEpilogue(byte[] epilogue) {
this.mPreamble = preamble; mEpilogue = epilogue;
} }
@Override @Override
@ -67,7 +68,7 @@ public class MimeMultipart extends Multipart {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
if (mPreamble != null) { if (mPreamble != null) {
writer.write(mPreamble); out.write(mPreamble);
writer.write("\r\n"); writer.write("\r\n");
} }
@ -90,6 +91,9 @@ public class MimeMultipart extends Multipart {
writer.write(mBoundary); writer.write(mBoundary);
writer.write("--\r\n"); writer.write("--\r\n");
writer.flush(); writer.flush();
if (mEpilogue != null) {
out.write(mEpilogue);
}
} }
@Override @Override

View File

@ -982,42 +982,74 @@ public class MimeUtility {
return DEFAULT_ATTACHMENT_MIME_TYPE.equalsIgnoreCase(mimeType); return DEFAULT_ATTACHMENT_MIME_TYPE.equalsIgnoreCase(mimeType);
} }
/** public static Body createBody(InputStream in, String contentTransferEncoding, String contentType)
* Removes any content transfer encoding from the stream and returns a Body.
* @throws MessagingException
*/
public static Body decodeBody(InputStream in,
String contentTransferEncoding, String contentType)
throws IOException, MessagingException { throws IOException, MessagingException {
/*
* We'll remove any transfer encoding by wrapping the stream.
*/
if (contentTransferEncoding != null) { if (contentTransferEncoding != null) {
contentTransferEncoding = contentTransferEncoding = MimeUtility.getHeaderParameter(contentTransferEncoding, null);
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);
}
} }
BinaryTempFileBody tempBody; BinaryTempFileBody tempBody;
if (MimeUtil.isMessage(contentType)) { if (MimeUtil.isMessage(contentType)) {
tempBody = new BinaryTempFileMessageBody(); tempBody = new BinaryTempFileMessageBody(contentTransferEncoding);
} else { } else {
tempBody = new BinaryTempFileBody(); tempBody = new BinaryTempFileBody(contentTransferEncoding);
} }
tempBody.setEncoding(contentTransferEncoding);
OutputStream out = tempBody.getOutputStream(); OutputStream out = tempBody.getOutputStream();
try { try {
IOUtils.copy(in, out); IOUtils.copy(in, out);
} finally { } finally {
out.close(); out.close();
} }
return tempBody; return tempBody;
} }
/**
* Get decoded contents of a body.
* <p/>
* 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()}.
* <p/>
* 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) { public static String getMimeTypeByExtension(String filename) {
String returnedType = null; String returnedType = null;
String extension = null; String extension = null;

View File

@ -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();
}

View File

@ -37,6 +37,7 @@ import android.os.PowerManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; 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;
import com.fsck.k9.mail.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.power.TracingPowerManager.TracingWakeLock;
import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.AuthType;
@ -1504,7 +1505,7 @@ public class ImapStore extends RemoteStore {
if (literal != null) { if (literal != null) {
if (literal instanceof Body) { if (literal instanceof Body) {
// Most of the work was done in FetchAttchmentCallback.foundLiteral() // Most of the work was done in FetchAttchmentCallback.foundLiteral()
part.setBody((Body)literal); MimeMessageHelper.setBody(part, (Body) literal);
} else if (literal instanceof String) { } else if (literal instanceof String) {
String bodyString = (String)literal; String bodyString = (String)literal;
InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes()); InputStream bodyStream = new ByteArrayInputStream(bodyString.getBytes());
@ -1513,7 +1514,7 @@ public class ImapStore extends RemoteStore {
.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; .getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
String contentType = part String contentType = part
.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0];
part.setBody(MimeUtility.decodeBody(bodyStream, MimeMessageHelper.setBody(part, MimeUtility.createBody(bodyStream,
contentTransferEncoding, contentType)); contentTransferEncoding, contentType));
} else { } else {
// This shouldn't happen // This shouldn't happen
@ -2881,7 +2882,7 @@ public class ImapStore extends RemoteStore {
String contentType = mPart String contentType = mPart
.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0];
return MimeUtility.decodeBody(literal, contentTransferEncoding, return MimeUtility.createBody(literal, contentTransferEncoding,
contentType); contentType);
} }
return null; return null;

View File

@ -17,6 +17,27 @@ dependencies {
compile 'com.android.support:support-v13:21.0.2' compile 'com.android.support:support-v13:21.0.2'
compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.10' compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.10'
compile 'de.cketti.library.changelog:ckchangelog:1.2.1' 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 { android {
@ -26,6 +47,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 17 targetSdkVersion 17
testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner"
} }
signingConfigs { signingConfigs {
@ -56,6 +79,7 @@ android {
exclude 'META-INF/LICENSE.txt' exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/NOTICE' exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt' exclude 'META-INF/NOTICE.txt'
exclude 'LICENSE.txt'
} }
compileOptions { compileOptions {

View File

@ -5,6 +5,10 @@
android:versionCode="1" android:versionCode="1"
android:versionName="1.0"> android:versionName="1.0">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
<!-- Allow debug trace to be written -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- We add an application tag here just so that we can indicate that <!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library, this package needs to link against the android.test library,
which is needed when building test cases. --> which is needed when building test cases. -->

View File

@ -0,0 +1,28 @@
package com.fsck.k9.activity.setup;
import com.fsck.k9.mail.ConnectionSecurity;
import com.google.android.apps.common.testing.ui.espresso.matcher.BoundedMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import static com.google.android.apps.common.testing.testrunner.util.Checks.checkNotNull;
public class ConnectionSecurityHolderMatcher {
public static Matcher<Object> is(final ConnectionSecurity connectionSecurity) {
checkNotNull(connectionSecurity);
return new BoundedMatcher<Object, ConnectionSecurityHolder>(ConnectionSecurityHolder.class) {
@Override
public boolean matchesSafely(ConnectionSecurityHolder connectionSecurityHolder) {
return connectionSecurityHolder.connectionSecurity == connectionSecurity;
}
@Override
public void describeTo(Description description) {
description.appendText("connection security is: ");
description.appendText(connectionSecurity.name());
}
};
}
}

View File

@ -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<WelcomeMessage> {
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());
}
}

View File

@ -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<Accounts>{
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);
}
}

View File

@ -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<T extends Activity> extends ActivityInstrumentationTestCase2<T> {
private ApplicationState state = ApplicationState.getInstance();
private final boolean bypassWelcome;
public AbstractEndToEndTest(Class<T> activityClass) {
this(activityClass, true);
}
public AbstractEndToEndTest(Class<T> 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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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<AccountForTest> accounts = new ArrayList<AccountForTest>();
public StubMailServer stubMailServer;
public static ApplicationState getInstance() {
return state;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
package com.fsck.k9.endtoend.pages;
public class AbstractPage {
// used to have some content. Now a placeholder class
}

View File

@ -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();
}
}

View File

@ -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.
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -0,0 +1,64 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import com.fsck.k9.mail.ConnectionSecurity;
import static com.fsck.k9.activity.setup.ConnectionSecurityHolderMatcher.is;
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;
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(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;
}
}

View File

@ -0,0 +1,67 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import com.fsck.k9.mail.ConnectionSecurity;
import static com.fsck.k9.activity.setup.ConnectionSecurityHolderMatcher.is;
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;
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(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<View> 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();
}
}

View File

@ -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();
}
}

View File

@ -8,6 +8,7 @@ import java.io.OutputStream;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
import com.fsck.k9.mail.internet.MimeMessageHelper;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.Base64InputStream;
import org.apache.james.mime4j.util.MimeUtil; import org.apache.james.mime4j.util.MimeUtil;
@ -199,7 +200,8 @@ public class MessageTest extends AndroidTestCase {
+ "Content-Transfer-Encoding: 7bit\r\n" + "Content-Transfer-Encoding: 7bit\r\n"
+ "\r\n" + "\r\n"
+ "------Boundary102\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" + "Content-Transfer-Encoding: quoted-printable\r\n"
+ "\r\n" + "\r\n"
+ "Testing=2E\r\n" + "Testing=2E\r\n"
@ -208,7 +210,8 @@ public class MessageTest extends AndroidTestCase {
+ "End of test=2E\r\n" + "End of test=2E\r\n"
+ "\r\n" + "\r\n"
+ "------Boundary102\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" + "Content-Transfer-Encoding: quoted-printable\r\n"
+ "\r\n" + "\r\n"
+ "Testing=2E\r\n" + "Testing=2E\r\n"
@ -236,7 +239,8 @@ public class MessageTest extends AndroidTestCase {
+ "Content-Transfer-Encoding: 7bit\r\n" + "Content-Transfer-Encoding: 7bit\r\n"
+ "\r\n" + "\r\n"
+ "------Boundary101\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" + "Content-Transfer-Encoding: quoted-printable\r\n"
+ "\r\n" + "\r\n"
+ "Testing=2E\r\n" + "Testing=2E\r\n"
@ -245,7 +249,8 @@ public class MessageTest extends AndroidTestCase {
+ "End of test=2E\r\n" + "End of test=2E\r\n"
+ "\r\n" + "\r\n"
+ "------Boundary101\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" + "Content-Transfer-Encoding: quoted-printable\r\n"
+ "\r\n" + "\r\n"
+ "Testing=2E\r\n" + "Testing=2E\r\n"
@ -315,8 +320,7 @@ public class MessageTest extends AndroidTestCase {
private MimeMessage nestedMessage(MimeMessage subMessage) private MimeMessage nestedMessage(MimeMessage subMessage)
throws MessagingException, IOException { throws MessagingException, IOException {
BinaryTempFileMessageBody tempMessageBody = new BinaryTempFileMessageBody(); BinaryTempFileMessageBody tempMessageBody = new BinaryTempFileMessageBody(MimeUtil.ENC_8BIT);
tempMessageBody.setEncoding(MimeUtil.ENC_8BIT);
OutputStream out = tempMessageBody.getOutputStream(); OutputStream out = tempMessageBody.getOutputStream();
try { try {
@ -348,7 +352,7 @@ public class MessageTest extends AndroidTestCase {
multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_8BIT)); multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_8BIT));
multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_QUOTED_PRINTABLE)); multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_QUOTED_PRINTABLE));
multipartBody.addBodyPart(binaryBodyPart()); multipartBody.addBodyPart(binaryBodyPart());
message.setBody(multipartBody); MimeMessageHelper.setBody(message, multipartBody);
return message; return message;
} }
@ -356,12 +360,12 @@ public class MessageTest extends AndroidTestCase {
private MimeBodyPart binaryBodyPart() throws IOException, private MimeBodyPart binaryBodyPart() throws IOException,
MessagingException { MessagingException {
String encodedTestString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" String encodedTestString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz0123456789+/"; + "abcdefghijklmnopqrstuvwxyz0123456789+/\r\n";
BinaryTempFileBody tempFileBody = new BinaryTempFileBody(); BinaryTempFileBody tempFileBody = new BinaryTempFileBody(MimeUtil.ENC_BASE64);
InputStream in = new Base64InputStream(new ByteArrayInputStream( InputStream in = new ByteArrayInputStream(
encodedTestString.getBytes("UTF-8"))); encodedTestString.getBytes("UTF-8"));
OutputStream out = tempFileBody.getOutputStream(); OutputStream out = tempFileBody.getOutputStream();
try { try {

View File

@ -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 <cketti@googlemail.com>\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();
}
}

View File

@ -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);
}
}

View File

@ -11,6 +11,7 @@ import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeMessage; 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.MimeMultipart;
import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.internet.TextBody;
@ -26,7 +27,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase {
// Create message // Create message
MimeMessage message = new MimeMessage(); MimeMessage message = new MimeMessage();
message.setBody(body); MimeMessageHelper.setBody(message, body);
// Extract text // Extract text
ViewableContainer container = extractTextAndAttachments(getContext(), message); ViewableContainer container = extractTextAndAttachments(getContext(), message);
@ -50,7 +51,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase {
// Create message // Create message
MimeMessage message = new MimeMessage(); MimeMessage message = new MimeMessage();
message.setHeader("Content-Type", "text/html"); message.setHeader("Content-Type", "text/html");
message.setBody(body); MimeMessageHelper.setBody(message, body);
// Extract text // Extract text
ViewableContainer container = extractTextAndAttachments(getContext(), message); ViewableContainer container = extractTextAndAttachments(getContext(), message);
@ -80,7 +81,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase {
// Create message // Create message
MimeMessage message = new MimeMessage(); MimeMessage message = new MimeMessage();
message.setBody(multipart); MimeMessageHelper.setBody(message, multipart);
// Extract text // Extract text
ViewableContainer container = extractTextAndAttachments(getContext(), message); 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.setRecipients(RecipientType.TO, new Address[] { new Address("to@example.com") });
innerMessage.setSubject("Subject"); innerMessage.setSubject("Subject");
innerMessage.setFrom(new Address("from@example.com")); innerMessage.setFrom(new Address("from@example.com"));
innerMessage.setBody(innerBody); MimeMessageHelper.setBody(innerMessage, innerBody);
// Create multipart/mixed part // Create multipart/mixed part
MimeMultipart multipart = new MimeMultipart(); MimeMultipart multipart = new MimeMultipart();
@ -136,7 +137,7 @@ public class LocalMessageExtractorTest extends AndroidTestCase {
// Create message // Create message
MimeMessage message = new MimeMessage(); MimeMessage message = new MimeMessage();
message.setBody(multipart); MimeMessageHelper.setBody(message, multipart);
// Extract text // Extract text
ViewableContainer container = extractTextAndAttachments(getContext(), message); ViewableContainer container = extractTextAndAttachments(getContext(), message);

View File

@ -105,6 +105,7 @@ import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeBodyPart;
import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeMessage; 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.MimeMultipart;
import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.internet.TextBody;
@ -1400,10 +1401,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
MimeMultipart mp = new MimeMultipart(); MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(new MimeBodyPart(composedMimeMessage)); mp.addBodyPart(new MimeBodyPart(composedMimeMessage));
addAttachmentsToMessage(mp); addAttachmentsToMessage(mp);
message.setBody(mp); MimeMessageHelper.setBody(message, mp);
} else { } else {
// If no attachments, our multipart/alternative part is the only one we need. // 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) { } else if (mMessageFormat == SimpleMessageFormat.TEXT) {
// Text-only message. // Text-only message.
@ -1411,10 +1412,10 @@ public class MessageCompose extends K9Activity implements OnClickListener,
MimeMultipart mp = new MimeMultipart(); MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(new MimeBodyPart(body, "text/plain")); mp.addBodyPart(new MimeBodyPart(body, "text/plain"));
addAttachmentsToMessage(mp); addAttachmentsToMessage(mp);
message.setBody(mp); MimeMessageHelper.setBody(message, mp);
} else { } else {
// No attachments to include, just stick the text body in the message and call it good. // No attachments to include, just stick the text body in the message and call it good.
message.setBody(body); MimeMessageHelper.setBody(message, body);
} }
} }

View File

@ -80,6 +80,7 @@ import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeMessage; 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.MimeUtility;
import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.internet.TextBody;
import com.fsck.k9.mailstore.MessageRemovalListener; import com.fsck.k9.mailstore.MessageRemovalListener;
@ -2697,7 +2698,7 @@ public class MessagingController implements Runnable {
LocalFolder localFolder = (LocalFolder)localStore.getFolder(account.getErrorFolderName()); LocalFolder localFolder = (LocalFolder)localStore.getFolder(account.getErrorFolderName());
MimeMessage message = new MimeMessage(); MimeMessage message = new MimeMessage();
message.setBody(new TextBody(body)); MimeMessageHelper.setBody(message, new TextBody(body));
message.setFlag(Flag.X_DOWNLOADED_FULL, true); message.setFlag(Flag.X_DOWNLOADED_FULL, true);
message.setSubject(subject); 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. //FIXME: This is an ugly hack that won't be needed once the Message objects have been united.
Message remoteMessage = remoteFolder.getMessage(message.getUid()); Message remoteMessage = remoteFolder.getMessage(message.getUid());
remoteMessage.setBody(message.getBody()); MimeMessageHelper.setBody(remoteMessage, message.getBody());
remoteFolder.fetchPart(remoteMessage, part, null); remoteFolder.fetchPart(remoteMessage, part, null);
localFolder.updateMessage((LocalMessage)message); localFolder.updateMessage((LocalMessage)message);

View File

@ -17,6 +17,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import com.fsck.k9.mail.internet.MimeMessageHelper;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.util.MimeUtil; import org.apache.james.mime4j.util.MimeUtil;
@ -784,17 +785,16 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
// triggering T_MIME_NO_TEXT and T_TVD_MIME_NO_HEADERS // triggering T_MIME_NO_TEXT and T_TVD_MIME_NO_HEADERS
// SpamAssassin rules. // SpamAssassin rules.
localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain"); localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain");
localMessage.setBody(new TextBody("")); MimeMessageHelper.setBody(localMessage, new TextBody(""));
} else if (mp.getCount() == 1 && } 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. // If we have only one part, drop the MimeMultipart container.
BodyPart part = mp.getBodyPart(0); BodyPart part = mp.getBodyPart(0);
localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType()); localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType());
localMessage.setBody(part.getBody()); MimeMessageHelper.setBody(localMessage, part.getBody());
} else { } else {
// Otherwise, attach the MimeMultipart to the message. // Otherwise, attach the MimeMultipart to the message.
localMessage.setBody(mp); MimeMessageHelper.setBody(localMessage, mp);
} }
} }
} }
@ -1566,7 +1566,7 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
* If the attachment has a body we're expected to save it into the local store * 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. * so we copy the data into a cached attachment file.
*/ */
InputStream in = attachment.getBody().getInputStream(); InputStream in = MimeUtility.decodeBody(attachment.getBody());
try { try {
tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory); tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory);
FileOutputStream out = new FileOutputStream(tempAttachmentFile); FileOutputStream out = new FileOutputStream(tempAttachmentFile);
@ -1648,11 +1648,13 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
getAccount(), getAccount(),
attachmentId); attachmentId);
if (MimeUtil.isMessage(attachment.getMimeType())) { if (MimeUtil.isMessage(attachment.getMimeType())) {
attachment.setBody(new LocalAttachmentMessageBody( LocalAttachmentMessageBody body = new LocalAttachmentMessageBody(
contentUri, LocalFolder.this.localStore.context)); contentUri, LocalFolder.this.localStore.context);
MimeMessageHelper.setBody(attachment, body);
} else { } else {
attachment.setBody(new LocalAttachmentBody( LocalAttachmentBody body = new LocalAttachmentBody(
contentUri, LocalFolder.this.localStore.context)); contentUri, LocalFolder.this.localStore.context);
MimeMessageHelper.setBody(attachment, body);
} }
ContentValues cv = new ContentValues(); ContentValues cv = new ContentValues();
cv.put("content_uri", contentUri != null ? contentUri.toString() : null); cv.put("content_uri", contentUri != null ? contentUri.toString() : null);

View File

@ -514,6 +514,11 @@ public class LocalMessage extends MimeMessage {
super.addHeader(name, value); super.addHeader(name, value);
} }
@Override
public void addRawHeader(String name, String raw) {
throw new RuntimeException("Not supported");
}
@Override @Override
public void setHeader(String name, String value) throws MessagingException { public void setHeader(String name, String value) throws MessagingException {
if (!mHeadersLoaded) if (!mHeadersLoaded)

View File

@ -44,7 +44,7 @@ public class MimeMessageParseTest {
checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org");
assertEquals("Testmail", msg.getSubject()); assertEquals("Testmail", msg.getSubject());
assertEquals("text/plain", msg.getContentType()); 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 @Test
@ -63,7 +63,7 @@ public class MimeMessageParseTest {
checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org");
assertEquals("Testmail", msg.getSubject()); assertEquals("Testmail", msg.getSubject());
assertEquals("text/plain; encoding=ISO-8859-1", msg.getContentType()); 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 @Test
@ -82,7 +82,7 @@ public class MimeMessageParseTest {
checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org");
assertEquals("Testmail", msg.getSubject()); assertEquals("Testmail", msg.getSubject());
assertEquals("text/plain", msg.getContentType()); 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 @Test
@ -238,7 +238,7 @@ public class MimeMessageParseTest {
private static void checkLeafParts(MimeMessage msg, String... expectedParts) throws Exception { private static void checkLeafParts(MimeMessage msg, String... expectedParts) throws Exception {
List<String> actual = new ArrayList<String>(); List<String> actual = new ArrayList<String>();
for (Body leaf : getLeafParts(msg.getBody())) { for (Body leaf : getLeafParts(msg.getBody())) {
actual.add(streamToString(leaf.getInputStream())); actual.add(streamToString(MimeUtility.decodeBody(leaf)));
} }
assertEquals(Arrays.asList(expectedParts), actual); assertEquals(Arrays.asList(expectedParts), actual);
} }