Merge remote-tracking branch 'k9mail_pgp_mime/master'

Fixed lots of conflicts
pgp_mime_preparations_view
cketti 9 years ago
commit 4f8fc5bc5b

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

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

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

@ -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;
}
@Override
public String getEncoding() {
return mEncoding;
}
public void setEncoding(String encoding) throws MessagingException {
mEncoding = encoding;
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() {
public BinaryTempFileBody(String encoding) {
if (mTempDirectory == null) {
throw new
RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!");
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();
}

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

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

@ -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<String> getHeaderNames() {
Set<String> names = new LinkedHashSet<String>();
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<String> values = new ArrayList<String>();
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<Field> removeFields = new ArrayList<Field>();
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();
}
}

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

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

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

@ -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.
* <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) {
String returnedType = null;
String extension = null;

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

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

@ -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 {

@ -5,6 +5,10 @@
android:versionCode="1"
android:versionName="1.0">
<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
this package needs to link against the android.test library,
which is needed when building test cases. -->

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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