From 854b1b3ffc57ae2978e5b629100e91c88cd8b6a6 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Sun, 9 Nov 2014 21:14:34 +0000 Subject: [PATCH 01/13] Add end-to-end tests using Espresso --- build.gradle | 21 ++++ tests/AndroidManifest.xml | 13 +-- ...WelcomeAndSetupAccountIntegrationTest.java | 24 +++++ .../endtoend/A010_AccountIntegrationTest.java | 40 ++++++++ .../k9/endtoend/AbstractEndToEndTest.java | 60 ++++++++++++ .../fsck/k9/endtoend/AccountSetupFlow.java | 98 +++++++++++++++++++ .../k9/endtoend/framework/AccountForTest.java | 17 ++++ .../endtoend/framework/ApplicationState.java | 22 +++++ .../k9/endtoend/framework/StubMailServer.java | 41 ++++++++ .../k9/endtoend/framework/UserForImap.java | 19 ++++ .../fsck/k9/endtoend/pages/AbstractPage.java | 6 ++ .../k9/endtoend/pages/AccountOptionsPage.java | 17 ++++ .../endtoend/pages/AccountSetupNamesPage.java | 48 +++++++++ .../k9/endtoend/pages/AccountSetupPage.java | 27 +++++ .../k9/endtoend/pages/AccountTypePage.java | 14 +++ .../fsck/k9/endtoend/pages/AccountsPage.java | 56 +++++++++++ .../pages/IncomingServerSettingsPage.java | 66 +++++++++++++ .../pages/OutgoingServerSettingsPage.java | 69 +++++++++++++ .../k9/endtoend/pages/WelcomeMessagePage.java | 15 +++ 19 files changed, 665 insertions(+), 8 deletions(-) create mode 100644 tests/src/com/fsck/k9/endtoend/A000_WelcomeAndSetupAccountIntegrationTest.java create mode 100644 tests/src/com/fsck/k9/endtoend/A010_AccountIntegrationTest.java create mode 100644 tests/src/com/fsck/k9/endtoend/AbstractEndToEndTest.java create mode 100644 tests/src/com/fsck/k9/endtoend/AccountSetupFlow.java create mode 100644 tests/src/com/fsck/k9/endtoend/framework/AccountForTest.java create mode 100644 tests/src/com/fsck/k9/endtoend/framework/ApplicationState.java create mode 100644 tests/src/com/fsck/k9/endtoend/framework/StubMailServer.java create mode 100644 tests/src/com/fsck/k9/endtoend/framework/UserForImap.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/AbstractPage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/AccountOptionsPage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/AccountSetupNamesPage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/AccountSetupPage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/AccountTypePage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/AccountsPage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/IncomingServerSettingsPage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/OutgoingServerSettingsPage.java create mode 100644 tests/src/com/fsck/k9/endtoend/pages/WelcomeMessagePage.java diff --git a/build.gradle b/build.gradle index 1aeda0139..73835b378 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,25 @@ dependencies { compile 'com.beetstra.jutf7:jutf7:1.0.0' compile 'com.android.support:support-v13:19.1.0' compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.2' + + 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: 'com.google.code.findbugs' +// exclude group: 'org.hamcrest' + } + + 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" } project.ext.preDexLibs = !project.hasProperty('disablePreDex') @@ -49,6 +68,7 @@ android { defaultConfig { minSdkVersion 15 targetSdkVersion 17 + testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner" } dexOptions { @@ -89,6 +109,7 @@ android { exclude 'META-INF/DEPENDENCIES' exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE.txt' + exclude 'LICENSE.txt' exclude 'META-INF/NOTICE' exclude 'META-INF/NOTICE.txt' } diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index 12121f1f8..05a7f8a90 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -5,18 +5,15 @@ android:versionCode="1" android:versionName="1.0"> + + + + - - + diff --git a/tests/src/com/fsck/k9/endtoend/A000_WelcomeAndSetupAccountIntegrationTest.java b/tests/src/com/fsck/k9/endtoend/A000_WelcomeAndSetupAccountIntegrationTest.java new file mode 100644 index 000000000..c6a9187dc --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/A000_WelcomeAndSetupAccountIntegrationTest.java @@ -0,0 +1,24 @@ +package com.fsck.k9.endtoend; + +import com.fsck.k9.activity.setup.WelcomeMessage; +import com.fsck.k9.endtoend.pages.WelcomeMessagePage; + +/** + * Creates a new IMAP account via the getting started flow. + */ +public class A000_WelcomeAndSetupAccountIntegrationTest extends AbstractEndToEndTest { + + public A000_WelcomeAndSetupAccountIntegrationTest() { + super(WelcomeMessage.class, false); + } + + public void testCreateAccount() throws Exception { + new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage()); + } + + public void testCreateSecondAccount() throws Exception { + new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage()); + } + +} + diff --git a/tests/src/com/fsck/k9/endtoend/A010_AccountIntegrationTest.java b/tests/src/com/fsck/k9/endtoend/A010_AccountIntegrationTest.java new file mode 100644 index 000000000..d3fc8701c --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/A010_AccountIntegrationTest.java @@ -0,0 +1,40 @@ +package com.fsck.k9.endtoend; + +import com.fsck.k9.activity.Accounts; +import com.fsck.k9.endtoend.framework.AccountForTest; +import com.fsck.k9.endtoend.framework.ApplicationState; +import com.fsck.k9.endtoend.pages.AccountsPage; + +/** + * Creates and removes accounts. + * + * Because of the way K-9 shows the start page, there must already be two accounts + * in existence for this test to work. + */ +public class A010_AccountIntegrationTest extends AbstractEndToEndTest{ + + public A010_AccountIntegrationTest() { + super(Accounts.class); + } + + public void testCreateAccountDirectly() throws Exception { + new AccountSetupFlow(this).setupAccountFromAccountsPage(new AccountsPage()); + } + + public void testDeleteAccount() { + + AccountsPage accountsPage = new AccountsPage(); + + AccountForTest accountForTest = ApplicationState.getInstance().accounts.get(0); + accountsPage.assertAccountExists(accountForTest.description); + + accountsPage.clickLongOnAccount(accountForTest); + + accountsPage.clickRemoveInAccountMenu(); + + accountsPage.clickOK(); + + accountsPage.assertAccountDoesNotExist(accountForTest.description); + + } +} diff --git a/tests/src/com/fsck/k9/endtoend/AbstractEndToEndTest.java b/tests/src/com/fsck/k9/endtoend/AbstractEndToEndTest.java new file mode 100644 index 000000000..f1ed25a8c --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/AbstractEndToEndTest.java @@ -0,0 +1,60 @@ +package com.fsck.k9.endtoend; + +import android.app.Activity; +import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; + +import com.fsck.k9.R; +import com.fsck.k9.endtoend.framework.ApplicationState; +import com.fsck.k9.endtoend.framework.StubMailServer; +import com.fsck.k9.endtoend.pages.WelcomeMessagePage; +import com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions; + +import junit.framework.AssertionFailedError; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + +public abstract class AbstractEndToEndTest extends ActivityInstrumentationTestCase2 { + + private ApplicationState state = ApplicationState.getInstance(); + private final boolean bypassWelcome; + + public AbstractEndToEndTest(Class activityClass) { + this(activityClass, true); + } + + public AbstractEndToEndTest(Class activityClass, boolean bypassWelcome) { + super(activityClass); + this.bypassWelcome = bypassWelcome; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + getActivity(); + + if (bypassWelcome) { + bypassWelcomeScreen(); + } + } + + private void bypassWelcomeScreen() { + try { + onView(withId(R.id.welcome_message)).check(ViewAssertions.doesNotExist()); + } catch (AssertionFailedError ex) { + /* + * The view doesn't NOT exist == the view exists, and needs to be bypassed! + */ + Log.d(getClass().getName(), "Bypassing welcome"); + new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage()); + } + } + + protected StubMailServer setupMailServer() { + if (null == state.stubMailServer) { + state.stubMailServer = new StubMailServer(); + } + return state.stubMailServer; + } +} diff --git a/tests/src/com/fsck/k9/endtoend/AccountSetupFlow.java b/tests/src/com/fsck/k9/endtoend/AccountSetupFlow.java new file mode 100644 index 000000000..978ffd332 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/AccountSetupFlow.java @@ -0,0 +1,98 @@ +package com.fsck.k9.endtoend; + +import com.fsck.k9.endtoend.framework.AccountForTest; +import com.fsck.k9.endtoend.framework.ApplicationState; +import com.fsck.k9.endtoend.framework.StubMailServer; +import com.fsck.k9.endtoend.framework.UserForImap; +import com.fsck.k9.endtoend.pages.AccountOptionsPage; +import com.fsck.k9.endtoend.pages.AccountSetupNamesPage; +import com.fsck.k9.endtoend.pages.AccountSetupPage; +import com.fsck.k9.endtoend.pages.AccountTypePage; +import com.fsck.k9.endtoend.pages.AccountsPage; +import com.fsck.k9.endtoend.pages.IncomingServerSettingsPage; +import com.fsck.k9.endtoend.pages.OutgoingServerSettingsPage; +import com.fsck.k9.endtoend.pages.WelcomeMessagePage; +import com.fsck.k9.mail.ConnectionSecurity; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Encapsulated the steps required to set up a new mail account. + */ +public class AccountSetupFlow { + + static final String ACCOUNT_NAME = "sendAndReceiveTestName"; + + private final AbstractEndToEndTest test; + + public AccountSetupFlow(AbstractEndToEndTest test) { + this.test = test; + } + + public AccountsPage setupAccountFromWelcomePage(WelcomeMessagePage welcomeMessagePage) { + AccountSetupPage accountSetupPage = welcomeMessagePage.clickNext(); + return setupAccountFromSetupNewAccountActivity(accountSetupPage); + } + + public AccountsPage setupAccountFromAccountsPage(AccountsPage accountPage) { + AccountSetupPage accountSetupPage = accountPage.clickAddNewAccount(); + return setupAccountFromSetupNewAccountActivity(accountSetupPage); + } + + public AccountsPage setupAccountFromSetupNewAccountActivity(AccountSetupPage accountSetupPage) { + AccountTypePage accountTypePage = fillInCredentialsAndClickManualSetup(accountSetupPage); + + IncomingServerSettingsPage incoming = accountTypePage.clickImap(); + + StubMailServer stubMailServer = test.setupMailServer(); + + OutgoingServerSettingsPage outgoing = setupIncomingServerAndClickNext(incoming, stubMailServer); + + AccountOptionsPage accountOptionsPage = setupOutgoingServerAndClickNext(outgoing, stubMailServer); + + AccountSetupNamesPage accountSetupNamesPage = accountOptionsPage.clickNext(); + + String accountDescription = tempAccountName(); + accountSetupNamesPage.inputAccountDescription(accountDescription); + accountSetupNamesPage.inputAccountName(ACCOUNT_NAME); + + AccountsPage accountsPage = accountSetupNamesPage.clickDone(); + + accountsPage.assertAccountExists(accountDescription); + + ApplicationState.getInstance().accounts.add(new AccountForTest(ACCOUNT_NAME, accountDescription, stubMailServer)); + + return accountsPage; + } + + + private String tempAccountName() { + return "sendAndReceiveTest-" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date()); + } + + private AccountTypePage fillInCredentialsAndClickManualSetup(AccountSetupPage page) { + return page + .inputEmailAddress(UserForImap.TEST_USER.emailAddress) + .inputPassword(UserForImap.TEST_USER.password) + .clickManualSetup(); + } + + private AccountOptionsPage setupOutgoingServerAndClickNext(OutgoingServerSettingsPage page, StubMailServer stubMailServer) { + return page + .inputSmtpServer(stubMailServer.getSmtpBindAddress()) + .inputSmtpSecurity(ConnectionSecurity.NONE) + .inputPort(stubMailServer.getSmtpPort()) + .inputRequireSignIn(false) + .clickNext(); + } + + private OutgoingServerSettingsPage setupIncomingServerAndClickNext(IncomingServerSettingsPage page, StubMailServer stubMailServer) { + return page + .inputImapServer(stubMailServer.getImapBindAddress()) + .inputImapSecurity(ConnectionSecurity.NONE) + .inputPort(stubMailServer.getImapPort()) + .inputUsername(UserForImap.TEST_USER.loginUsername) + .clickNext(); + } +} diff --git a/tests/src/com/fsck/k9/endtoend/framework/AccountForTest.java b/tests/src/com/fsck/k9/endtoend/framework/AccountForTest.java new file mode 100644 index 000000000..6845efdbe --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/framework/AccountForTest.java @@ -0,0 +1,17 @@ +package com.fsck.k9.endtoend.framework; + +/** + * An account that was added by a test. + */ +public class AccountForTest { + + public final String name; + public final String description; + public final StubMailServer stubMailServer; + + public AccountForTest(String name, String description, StubMailServer stubMailServer) { + this.name = name; + this.description = description; + this.stubMailServer = stubMailServer; + } +} diff --git a/tests/src/com/fsck/k9/endtoend/framework/ApplicationState.java b/tests/src/com/fsck/k9/endtoend/framework/ApplicationState.java new file mode 100644 index 000000000..149a2a977 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/framework/ApplicationState.java @@ -0,0 +1,22 @@ +package com.fsck.k9.endtoend.framework; + +import java.util.ArrayList; +import java.util.List; + +/** + * Stores the state of the application from the point of view of end-to-end tests. + */ +public class ApplicationState { + + private static final ApplicationState state = new ApplicationState(); + + public final List accounts = new ArrayList(); + + public StubMailServer stubMailServer; + + public static ApplicationState getInstance() { + return state; + } + + +} diff --git a/tests/src/com/fsck/k9/endtoend/framework/StubMailServer.java b/tests/src/com/fsck/k9/endtoend/framework/StubMailServer.java new file mode 100644 index 000000000..d8d1df2a3 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/framework/StubMailServer.java @@ -0,0 +1,41 @@ +package com.fsck.k9.endtoend.framework; + +import com.icegreen.greenmail.util.GreenMail; +import com.icegreen.greenmail.util.ServerSetup; + +/** + * Configuration and management of a pair of stub servers for use by an account. + */ +public class StubMailServer { + private static final ServerSetup IMAP_SERVER_SETUP = new ServerSetup(10143, "127.0.0.2", ServerSetup.PROTOCOL_IMAP); + private static final ServerSetup SMTP_SERVER_SETUP = new ServerSetup(10587, "127.0.0.2", ServerSetup.PROTOCOL_SMTP); + + /** + * Stub server that speaks SMTP, IMAP etc., that K-9 can talk to. + */ + private GreenMail greenmail; + + public StubMailServer() { + + greenmail = new GreenMail(new ServerSetup[]{IMAP_SERVER_SETUP, SMTP_SERVER_SETUP}); + greenmail.setUser(UserForImap.TEST_USER.emailAddress, UserForImap.TEST_USER.loginUsername, UserForImap.TEST_USER.password); + greenmail.start(); + } + + public String getSmtpBindAddress() { + return SMTP_SERVER_SETUP.getBindAddress(); + } + + public int getSmtpPort() { + return SMTP_SERVER_SETUP.getPort(); + } + + public String getImapBindAddress() { + return IMAP_SERVER_SETUP.getBindAddress(); + } + + public int getImapPort() { + return IMAP_SERVER_SETUP.getPort(); + } +} + diff --git a/tests/src/com/fsck/k9/endtoend/framework/UserForImap.java b/tests/src/com/fsck/k9/endtoend/framework/UserForImap.java new file mode 100644 index 000000000..80c1c08f4 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/framework/UserForImap.java @@ -0,0 +1,19 @@ +package com.fsck.k9.endtoend.framework; + +/** + * Credentials for the stub IMAP/SMTP server + */ +public class UserForImap { + + public static final UserForImap TEST_USER = new UserForImap("test-username", "test-password", "test-email@example.com"); + + public final String loginUsername; + public final String password; + public final String emailAddress; + + private UserForImap(String loginUsername, String password, String emailAddress) { + this.loginUsername = loginUsername; + this.password = password; + this.emailAddress = emailAddress; + } +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/AbstractPage.java b/tests/src/com/fsck/k9/endtoend/pages/AbstractPage.java new file mode 100644 index 000000000..bc308210e --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/AbstractPage.java @@ -0,0 +1,6 @@ +package com.fsck.k9.endtoend.pages; + +public class AbstractPage { + + // used to have some content. Now a placeholder class +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/AccountOptionsPage.java b/tests/src/com/fsck/k9/endtoend/pages/AccountOptionsPage.java new file mode 100644 index 000000000..b2f400121 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/AccountOptionsPage.java @@ -0,0 +1,17 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + + +public class AccountOptionsPage extends AbstractPage { + + public AccountSetupNamesPage clickNext() { + onView(withId(R.id.next)).perform(click()); + return new AccountSetupNamesPage(); + } + +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/AccountSetupNamesPage.java b/tests/src/com/fsck/k9/endtoend/pages/AccountSetupNamesPage.java new file mode 100644 index 000000000..058f613b5 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/AccountSetupNamesPage.java @@ -0,0 +1,48 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException; +import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + + +public class AccountSetupNamesPage extends AbstractPage { + + public AccountSetupNamesPage inputAccountName(String name) { + onView(withId(R.id.account_name)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(name)); + return this; + } + + public AccountSetupNamesPage inputAccountDescription(String name) { + onView(withId(R.id.account_description)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(name)); + return this; + } + + public AccountsPage clickDone() { + onView(withId(R.id.done)) + .perform(click()); + dismissChangelog(); + return new AccountsPage(); + } + + private void dismissChangelog() { + try { + onView(ViewMatchers.withText("OK")).perform(click()); + } catch (NoMatchingViewException ex) { + // Ignored. Not the best way of doing this, but Espresso rightly makes + // conditional flow difficult. + } + } +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/AccountSetupPage.java b/tests/src/com/fsck/k9/endtoend/pages/AccountSetupPage.java new file mode 100644 index 000000000..2fa659e90 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/AccountSetupPage.java @@ -0,0 +1,27 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + +public class AccountSetupPage extends AbstractPage { + + public AccountSetupPage inputEmailAddress(String emailAddress) { + onView(withId(R.id.account_email)).perform(typeText(emailAddress)); + return this; + } + + public AccountSetupPage inputPassword(String password) { + onView(withId(R.id.account_password)).perform(typeText(password)); + return this; + } + + public AccountTypePage clickManualSetup() { + onView(withId(R.id.manual_setup)).perform(click()); + return new AccountTypePage(); + } + +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/AccountTypePage.java b/tests/src/com/fsck/k9/endtoend/pages/AccountTypePage.java new file mode 100644 index 000000000..386184161 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/AccountTypePage.java @@ -0,0 +1,14 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +public class AccountTypePage extends AbstractPage { + + public IncomingServerSettingsPage clickImap() { + onView(withId(R.id.imap)).perform(click()); + return new IncomingServerSettingsPage(); + } +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/AccountsPage.java b/tests/src/com/fsck/k9/endtoend/pages/AccountsPage.java new file mode 100644 index 000000000..653281523 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/AccountsPage.java @@ -0,0 +1,56 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.fsck.k9.endtoend.framework.AccountForTest; +import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException; +import com.google.android.apps.common.testing.ui.espresso.ViewAssertion; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.longClick; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.doesNotExist; +import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText; + +public class AccountsPage extends AbstractPage { + + private void assertAccount(String accountDisplayName, boolean exists) { + ViewAssertion assertion = exists ? matches(isDisplayed()) : doesNotExist(); + onView(withText(accountDisplayName)).check(assertion); + } + + public AccountSetupPage clickAddNewAccount() { + // need to click twice for some reason? + onView(withId(R.id.add_new_account)).perform(click()); + try { + onView(withId(R.id.add_new_account)).perform(click()); + } catch (NoMatchingViewException ex) { + // Ignore + } + onView(withId(R.id.account_email)).perform(scrollTo()); + return new AccountSetupPage(); + } + + public void assertAccountExists(String accountDisplayName) { + assertAccount(accountDisplayName, true); + } + + public void assertAccountDoesNotExist(String accountDisplayName) { + assertAccount(accountDisplayName, false); + } + + public void clickLongOnAccount(AccountForTest accountForTest) { + onView(withText(accountForTest.description)).perform(longClick()); + } + + public void clickRemoveInAccountMenu() { + onView(withText("Remove account")).perform(click()); + } + + public void clickOK() { + onView(withText("OK")).perform(click()); + } +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/IncomingServerSettingsPage.java b/tests/src/com/fsck/k9/endtoend/pages/IncomingServerSettingsPage.java new file mode 100644 index 000000000..f0b07579c --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/IncomingServerSettingsPage.java @@ -0,0 +1,66 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.fsck.k9.mail.ConnectionSecurity; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData; +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isClickable; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +public class IncomingServerSettingsPage extends AbstractPage { + + public IncomingServerSettingsPage inputImapServer(String imapServer) { + onView(withId(R.id.account_server)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(imapServer)); + return this; + } + + public IncomingServerSettingsPage inputImapSecurity(ConnectionSecurity security) { + onView(withId(R.id.account_security_type)) + .perform(scrollTo()) + .perform(click()); + onData(allOf(is(instanceOf(ConnectionSecurity.class)), is(security))).perform(click()); + return this; + } + + public IncomingServerSettingsPage inputPort(int port) { + onView(withId(R.id.account_port)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(String.valueOf(port))); + return this; + } + + + public OutgoingServerSettingsPage clickNext() { + onView(withId(R.id.next)) +// .perform(scrollTo()) + .check(matches(isClickable())) + .perform(click()); + + // We know this view is on the next page, this functions as a wait. + onView(withText("SMTP server")).perform(scrollTo()); + return new OutgoingServerSettingsPage(); + } + + public IncomingServerSettingsPage inputUsername(String loginUsername) { + onView(withId(R.id.account_username)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(loginUsername)); + return this; + } + +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/OutgoingServerSettingsPage.java b/tests/src/com/fsck/k9/endtoend/pages/OutgoingServerSettingsPage.java new file mode 100644 index 000000000..87ec9723f --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/OutgoingServerSettingsPage.java @@ -0,0 +1,69 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; +import com.fsck.k9.mail.ConnectionSecurity; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData; +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + + +public class OutgoingServerSettingsPage extends AbstractPage { + + public OutgoingServerSettingsPage inputSmtpServer(String serverAddress) { + onView(withId(R.id.account_server)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(serverAddress)); + return this; + + } + + public OutgoingServerSettingsPage inputSmtpSecurity(ConnectionSecurity security) { + onView(withId(R.id.account_security_type)) + .perform(scrollTo()) + .perform(click()); + onData(allOf(is(instanceOf(ConnectionSecurity.class)), is(security))).perform(click()); + return this; + } + + public OutgoingServerSettingsPage inputPort(int port) { + onView(withId(R.id.account_port)) + .perform(scrollTo()) + .perform(clearText()) + .perform(typeText(String.valueOf(port))); + return this; + } + + public OutgoingServerSettingsPage inputRequireSignIn(boolean requireSignInInput) { + onView(withId(R.id.account_require_login)) + .perform(scrollTo()); + /* + * Make this smarter; click if necessary. + */ + if (!requireSignInInput) { + onView(withId(R.id.account_require_login)) + .perform(click()); + } +// Matcher checkedOrNot = requireSignInInput ? isChecked(): isNotChecked(); +// try { +// onView(withId(R.id.account_require_login)).check((matches(checkedOrNot))); +// } catch (AssertionFailedWithCauseError ex) { +// onView(withId(R.id.account_require_login)).perform(click()); +// } + return this; + } + + public AccountOptionsPage clickNext() { + onView(withId(R.id.next)).perform(click()); + return new AccountOptionsPage(); + } + +} diff --git a/tests/src/com/fsck/k9/endtoend/pages/WelcomeMessagePage.java b/tests/src/com/fsck/k9/endtoend/pages/WelcomeMessagePage.java new file mode 100644 index 000000000..723fb3645 --- /dev/null +++ b/tests/src/com/fsck/k9/endtoend/pages/WelcomeMessagePage.java @@ -0,0 +1,15 @@ +package com.fsck.k9.endtoend.pages; + +import com.fsck.k9.R; + +import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView; +import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click; +import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId; + +public class WelcomeMessagePage extends AbstractPage { + + public final AccountSetupPage clickNext() { + onView(withId(R.id.next)).perform(click()); + return new AccountSetupPage(); + } +} From 68c95d0283986f66fa71ee597c259a7459871172 Mon Sep 17 00:00:00 2001 From: Art O Cathain Date: Sun, 7 Dec 2014 14:22:03 +0000 Subject: [PATCH 02/13] Update to be compatible with latest Android Studio --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- plugins/openpgp-api-library/build.gradle | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 73835b378..cca3e9ba2 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.12.2' + classpath 'com.android.tools.build:gradle:1.0.0-rc4' classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09a4e3c6b..8f3ce51cb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jun 14 01:43:40 CEST 2014 +#Sun Dec 07 14:12:42 GMT 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/plugins/openpgp-api-library/build.gradle b/plugins/openpgp-api-library/build.gradle index 91d20ebf6..1a6529c3d 100644 --- a/plugins/openpgp-api-library/build.gradle +++ b/plugins/openpgp-api-library/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.12.2' + classpath 'com.android.tools.build:gradle:1.0.0-rc4' } } From 8630bb0ad40e487bb10a38e701f50b47b9694365 Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 13 Nov 2014 17:09:38 +0100 Subject: [PATCH 03/13] Add simple test to check if writing a parsed message leads to input data --- .../fsck/k9/mail/ReconstructMessageTest.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/src/com/fsck/k9/mail/ReconstructMessageTest.java diff --git a/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java new file mode 100644 index 000000000..45f7c8f5c --- /dev/null +++ b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java @@ -0,0 +1,67 @@ +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" + + "MIME-Version: 1.0\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=\"----Boundary\"\r\n" + + "Content-Transfer-Encoding: 8bit\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: quoted-printable\r\n" + + "\r\n" + + "=2E=2E=2E\r\n" + + "\r\n" + + "------Boundary--\r\n"; + + 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); + } +} From bcb6c75c2e87854f6b5479d21d6d3a7c3e2a9c4f Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 13 Nov 2014 22:40:58 +0100 Subject: [PATCH 04/13] Add support for storing raw header fields --- src/com/fsck/k9/mail/Message.java | 3 + src/com/fsck/k9/mail/Part.java | 2 + .../fsck/k9/mail/internet/MimeBodyPart.java | 5 + src/com/fsck/k9/mail/internet/MimeHeader.java | 110 ++++++++++++++---- .../fsck/k9/mail/internet/MimeMessage.java | 9 +- .../k9/mail/store/local/LocalMessage.java | 5 + 6 files changed, 109 insertions(+), 25 deletions(-) diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java index 5a4d7a34a..3735f1f58 100644 --- a/src/com/fsck/k9/mail/Message.java +++ b/src/com/fsck/k9/mail/Message.java @@ -135,6 +135,9 @@ public abstract class Message implements Part, CompositeBody { @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; diff --git a/src/com/fsck/k9/mail/Part.java b/src/com/fsck/k9/mail/Part.java index 38c1ad9da..d7970d433 100644 --- a/src/com/fsck/k9/mail/Part.java +++ b/src/com/fsck/k9/mail/Part.java @@ -7,6 +7,8 @@ import java.io.OutputStream; public interface Part { public void addHeader(String name, String value) throws MessagingException; + public void addRawHeader(String name, String raw) throws MessagingException; + public void removeHeader(String name) throws MessagingException; public void setHeader(String name, String value) throws MessagingException; diff --git a/src/com/fsck/k9/mail/internet/MimeBodyPart.java b/src/com/fsck/k9/mail/internet/MimeBodyPart.java index c4bca428a..a542e47fc 100644 --- a/src/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/src/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -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); diff --git a/src/com/fsck/k9/mail/internet/MimeHeader.java b/src/com/fsck/k9/mail/internet/MimeHeader.java index 12bd5fd31..f7313a4f2 100644 --- a/src/com/fsck/k9/mail/internet/MimeHeader.java +++ b/src/com/fsck/k9/mail/internet/MimeHeader.java @@ -52,7 +52,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) { @@ -66,7 +72,7 @@ public class MimeHeader { public Set getHeaderNames() { Set names = new LinkedHashSet(); for (Field field : mFields) { - names.add(field.name); + names.add(field.getName()); } return names; } @@ -74,8 +80,8 @@ public class MimeHeader { public String[] getHeader(String name) { List values = new ArrayList(); for (Field field : mFields) { - if (field.name.equalsIgnoreCase(name)) { - values.add(field.value); + if (field.getName().equalsIgnoreCase(name)) { + values.add(field.getValue()); } } if (values.isEmpty()) { @@ -87,7 +93,7 @@ public class MimeHeader { public void removeHeader(String name) { List removeFields = new ArrayList(); for (Field field : mFields) { - if (field.name.equalsIgnoreCase(name)) { + if (field.getName().equalsIgnoreCase(name)) { removeFields.add(field); } } @@ -97,27 +103,35 @@ public class MimeHeader { public void writeTo(OutputStream out) throws IOException { BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); for (Field field : mFields) { - if (!Utility.arrayContains(writeOmitFields, 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 (!Utility.arrayContains(writeOmitFields, field.getName())) { + 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++) { @@ -133,19 +147,67 @@ public class MimeHeader { private static class Field { private final String name; - private final String value; + private final String raw; + + public static Field newNameValueField(String name, String value) { + if (value == null) { + throw new NullPointerException("Argument 'value' cannot be null"); + } + + return new Field(name, value, null); + } + + public static Field newRawField(String name, String raw) { + if (raw == null) { + throw new NullPointerException("Argument 'raw' cannot be null"); + } + if (name != null && !raw.startsWith(name + ":")) { + throw new IllegalArgumentException("The value of 'raw' needs to start with the supplied field name " + + "followed by a colon"); + } + + return new Field(name, null, raw); + } + + private Field(String name, String value, String raw) { + if (name == null) { + throw new NullPointerException("Argument 'name' cannot be null"); + } - public Field(String name, String value) { this.name = name; this.value = value; + this.raw = raw; + } + + public String getName() { + return name; + } + + public String getValue() { + if (value != null) { + return value; + } + + int delimiterIndex = raw.indexOf(':'); + if (delimiterIndex == raw.length() - 1) { + return ""; + } + + return raw.substring(delimiterIndex + 1).trim(); + } + + public String getRaw() { + return raw; + } + + public boolean hasRawData() { + return raw != null; } @Override public String toString() { - StringBuilder sb = new StringBuilder("("); - sb.append(name).append('=').append(value).append(')'); - return sb.toString(); + return (hasRawData()) ? getRaw() : getName() + ": " + getValue(); } } diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 75fa217d8..c870fda35 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -423,6 +423,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 UnavailableStorageException { mHeader.setHeader(name, value); @@ -598,7 +603,9 @@ public class MimeMessage extends Message { public void field(Field parsedField) throws MimeException { expect(Part.class); try { - ((Part)stack.peek()).addHeader(parsedField.getName(), parsedField.getBody().trim()); + String name = parsedField.getName(); + String raw = parsedField.getRaw().toString(); + ((Part) stack.peek()).addRawHeader(name, raw); } catch (MessagingException me) { throw new Error(me); } diff --git a/src/com/fsck/k9/mail/store/local/LocalMessage.java b/src/com/fsck/k9/mail/store/local/LocalMessage.java index 474a4c849..ba3274b54 100644 --- a/src/com/fsck/k9/mail/store/local/LocalMessage.java +++ b/src/com/fsck/k9/mail/store/local/LocalMessage.java @@ -507,6 +507,11 @@ public class LocalMessage extends MimeMessage { super.addHeader(name, value); } + @Override + public void addRawHeader(String name, String raw) { + throw new RuntimeException("Not supported"); + } + @Override public void setHeader(String name, String value) throws UnavailableStorageException { if (!mHeadersLoaded) From 2404b80b04feb8e1cd04fd636a22d2a4d96e5c5b Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 13 Nov 2014 23:24:27 +0100 Subject: [PATCH 05/13] Fix MessageTest now that we preserve line breaks in headers --- tests/src/com/fsck/k9/mail/MessageTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/src/com/fsck/k9/mail/MessageTest.java b/tests/src/com/fsck/k9/mail/MessageTest.java index 3e56b228d..8a7e135b8 100644 --- a/tests/src/com/fsck/k9/mail/MessageTest.java +++ b/tests/src/com/fsck/k9/mail/MessageTest.java @@ -191,7 +191,8 @@ public class MessageTest extends AndroidTestCase { + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "------Boundary102\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" @@ -200,7 +201,8 @@ public class MessageTest extends AndroidTestCase { + "End of test=2E\r\n" + "\r\n" + "------Boundary102\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" @@ -228,7 +230,8 @@ public class MessageTest extends AndroidTestCase { + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "------Boundary101\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" @@ -237,7 +240,8 @@ public class MessageTest extends AndroidTestCase { + "End of test=2E\r\n" + "\r\n" + "------Boundary101\r\n" - + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Type: text/plain;\r\n" + + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "Testing=2E\r\n" From 51a60b5ad33ed381b3851bb6c87228000015d235 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 14 Nov 2014 00:27:04 +0100 Subject: [PATCH 06/13] Modify ReconstructMessageTest to highlight more problems --- tests/src/com/fsck/k9/mail/ReconstructMessageTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java index 45f7c8f5c..0989581cf 100644 --- a/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java +++ b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java @@ -20,10 +20,10 @@ public class ReconstructMessageTest extends AndroidTestCase { "To: to@example.com\r\n" + "Subject: Test Message \r\n" + "Date: Thu, 13 Nov 2014 17:09:38 +0100\r\n" + - "MIME-Version: 1.0\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" + From d32d6eed0e3d8a7dbd91df156872512450fbcb61 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 14 Nov 2014 00:58:43 +0100 Subject: [PATCH 07/13] Move "magic" from Part.setBody() implementations to MimeMessageHelper.setBody() Now adding message bodies during parsing won't set/modify headers. --- src/com/fsck/k9/activity/MessageCompose.java | 9 ++-- .../k9/controller/MessagingController.java | 5 +- .../fsck/k9/mail/internet/MimeBodyPart.java | 21 +------- .../fsck/k9/mail/internet/MimeMessage.java | 16 ------ .../k9/mail/internet/MimeMessageHelper.java | 52 +++++++++++++++++++ .../fsck/k9/mail/internet/MimeUtility.java | 2 +- src/com/fsck/k9/mail/store/ImapStore.java | 5 +- .../fsck/k9/mail/store/local/LocalFolder.java | 17 +++--- tests/src/com/fsck/k9/mail/MessageTest.java | 4 +- .../fsck/k9/mail/internet/ViewablesTest.java | 10 ++-- 10 files changed, 83 insertions(+), 58 deletions(-) create mode 100644 src/com/fsck/k9/mail/internet/MimeMessageHelper.java diff --git a/src/com/fsck/k9/activity/MessageCompose.java b/src/com/fsck/k9/activity/MessageCompose.java index ebf53391c..31d3a4cd7 100644 --- a/src/com/fsck/k9/activity/MessageCompose.java +++ b/src/com/fsck/k9/activity/MessageCompose.java @@ -104,6 +104,7 @@ import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; @@ -1404,10 +1405,10 @@ public class MessageCompose extends K9Activity implements OnClickListener, MimeMultipart mp = new MimeMultipart(); mp.addBodyPart(new MimeBodyPart(composedMimeMessage)); addAttachmentsToMessage(mp); - message.setBody(mp); + MimeMessageHelper.setBody(message, mp); } else { // If no attachments, our multipart/alternative part is the only one we need. - message.setBody(composedMimeMessage); + MimeMessageHelper.setBody(message, composedMimeMessage); } } else if (mMessageFormat == SimpleMessageFormat.TEXT) { // Text-only message. @@ -1415,10 +1416,10 @@ public class MessageCompose extends K9Activity implements OnClickListener, MimeMultipart mp = new MimeMultipart(); mp.addBodyPart(new MimeBodyPart(body, "text/plain")); addAttachmentsToMessage(mp); - message.setBody(mp); + MimeMessageHelper.setBody(message, mp); } else { // No attachments to include, just stick the text body in the message and call it good. - message.setBody(body); + MimeMessageHelper.setBody(message, body); } } diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 07f2f5e05..c58c97e1b 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -79,6 +79,7 @@ import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Transport; import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mail.store.local.LocalFolder; @@ -2712,7 +2713,7 @@ public class MessagingController implements Runnable { LocalFolder localFolder = (LocalFolder)localStore.getFolder(account.getErrorFolderName()); MimeMessage message = new MimeMessage(); - message.setBody(new TextBody(body)); + MimeMessageHelper.setBody(message, new TextBody(body)); message.setFlag(Flag.X_DOWNLOADED_FULL, true); message.setSubject(subject); @@ -3205,7 +3206,7 @@ public class MessagingController implements Runnable { //FIXME: This is an ugly hack that won't be needed once the Message objects have been united. Message remoteMessage = remoteFolder.getMessage(message.getUid()); - remoteMessage.setBody(message.getBody()); + MimeMessageHelper.setBody(remoteMessage, message.getBody()); remoteFolder.fetchPart(remoteMessage, part, null); localFolder.updateMessage((LocalMessage)message); diff --git a/src/com/fsck/k9/mail/internet/MimeBodyPart.java b/src/com/fsck/k9/mail/internet/MimeBodyPart.java index a542e47fc..32e3e2654 100644 --- a/src/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/src/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -35,7 +35,7 @@ public class MimeBodyPart extends BodyPart { if (mimeType != null) { addHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType); } - setBody(body); + MimeMessageHelper.setBody(this, body); } private String getFirstHeader(String name) { @@ -75,25 +75,6 @@ public class MimeBodyPart extends BodyPart { @Override public void setBody(Body body) throws MessagingException { this.mBody = body; - if (body instanceof Multipart) { - Multipart multipart = ((Multipart)body); - multipart.setParent(this); - String type = multipart.getContentType(); - setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); - if ("multipart/signed".equalsIgnoreCase(type)) { - setEncoding(MimeUtil.ENC_7BIT); - } else { - setEncoding(MimeUtil.ENC_8BIT); - } - } else if (body instanceof TextBody) { - String contentType = String.format("%s;\r\n charset=utf-8", getMimeType()); - String name = MimeUtility.getHeaderParameter(getContentType(), "name"); - if (name != null) { - contentType += String.format(";\r\n name=\"%s\"", name); - } - setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); - setEncoding(MimeUtil.ENC_8BIT); - } } @Override diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index c870fda35..012f2c377 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -396,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) { diff --git a/src/com/fsck/k9/mail/internet/MimeMessageHelper.java b/src/com/fsck/k9/mail/internet/MimeMessageHelper.java new file mode 100644 index 000000000..bc1695607 --- /dev/null +++ b/src/com/fsck/k9/mail/internet/MimeMessageHelper.java @@ -0,0 +1,52 @@ +package com.fsck.k9.mail.internet; + + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; +import org.apache.james.mime4j.util.MimeUtil; + + +public class MimeMessageHelper { + private MimeMessageHelper() { + } + + public static void setBody(Part part, Body body) throws MessagingException { + part.setBody(body); + + if (part instanceof Message) { + part.setHeader("MIME-Version", "1.0"); + } + + if (body instanceof Multipart) { + Multipart multipart = ((Multipart) body); + multipart.setParent(part); + String type = multipart.getContentType(); + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, type); + if ("multipart/signed".equalsIgnoreCase(type)) { + setEncoding(part, MimeUtil.ENC_7BIT); + } else { + setEncoding(part, MimeUtil.ENC_8BIT); + } + } else if (body instanceof TextBody) { + String contentType = String.format("%s;\r\n charset=utf-8", part.getMimeType()); + String name = MimeUtility.getHeaderParameter(part.getContentType(), "name"); + if (name != null) { + contentType += String.format(";\r\n name=\"%s\"", name); + } + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); + + setEncoding(part, MimeUtil.ENC_8BIT); + } + } + + public static void setEncoding(Part part, String encoding) throws MessagingException { + Body body = part.getBody(); + if (body != null) { + body.setEncoding(encoding); + } + part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); + } +} diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 21dad28c7..80efdf5c6 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1067,7 +1067,7 @@ public class MimeUtility { String text = readToString(in, charset); // Replace the body with a TextBody that already contains the decoded text - part.setBody(new TextBody(text)); + MimeMessageHelper.setBody(part, new TextBody(text)); return text; } finally { diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 9042f9d9b..91406c838 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -48,6 +48,7 @@ import java.util.regex.Pattern; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +import com.fsck.k9.mail.internet.MimeMessageHelper; import javax.net.ssl.SSLException; import org.apache.commons.io.IOUtils; @@ -1615,7 +1616,7 @@ public class ImapStore extends Store { 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()); @@ -1624,7 +1625,7 @@ public class ImapStore extends Store { .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.decodeBody(bodyStream, contentTransferEncoding, contentType)); } else { // This shouldn't happen diff --git a/src/com/fsck/k9/mail/store/local/LocalFolder.java b/src/com/fsck/k9/mail/store/local/LocalFolder.java index bc5895f04..e84098183 100644 --- a/src/com/fsck/k9/mail/store/local/LocalFolder.java +++ b/src/com/fsck/k9/mail/store/local/LocalFolder.java @@ -19,6 +19,7 @@ import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; +import com.fsck.k9.mail.internet.MimeMessageHelper; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.util.MimeUtil; @@ -774,17 +775,17 @@ public class LocalFolder extends Folder implements Serializable { // triggering T_MIME_NO_TEXT and T_TVD_MIME_NO_HEADERS // SpamAssassin rules. localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "text/plain"); - localMessage.setBody(new TextBody("")); + MimeMessageHelper.setBody(localMessage, new TextBody("")); } else if (mp.getCount() == 1 && (mp.getBodyPart(0) instanceof LocalAttachmentBodyPart) == false) { // If we have only one part, drop the MimeMultipart container. BodyPart part = mp.getBodyPart(0); localMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, part.getContentType()); - localMessage.setBody(part.getBody()); + MimeMessageHelper.setBody(localMessage, part.getBody()); } else { // Otherwise, attach the MimeMultipart to the message. - localMessage.setBody(mp); + MimeMessageHelper.setBody(localMessage, mp); } } } @@ -1642,11 +1643,13 @@ public class LocalFolder extends Folder implements Serializable { mAccount, attachmentId); if (MimeUtil.isMessage(attachment.getMimeType())) { - attachment.setBody(new LocalAttachmentMessageBody( - contentUri, LocalFolder.this.localStore.mApplication)); + LocalAttachmentMessageBody body = new LocalAttachmentMessageBody( + contentUri, LocalFolder.this.localStore.mApplication); + MimeMessageHelper.setBody(attachment, body); } else { - attachment.setBody(new LocalAttachmentBody( - contentUri, LocalFolder.this.localStore.mApplication)); + LocalAttachmentBody body = new LocalAttachmentBody( + contentUri, LocalFolder.this.localStore.mApplication); + MimeMessageHelper.setBody(attachment, body); } ContentValues cv = new ContentValues(); cv.put("content_uri", contentUri != null ? contentUri.toString() : null); diff --git a/tests/src/com/fsck/k9/mail/MessageTest.java b/tests/src/com/fsck/k9/mail/MessageTest.java index 8a7e135b8..054a3d01a 100644 --- a/tests/src/com/fsck/k9/mail/MessageTest.java +++ b/tests/src/com/fsck/k9/mail/MessageTest.java @@ -5,6 +5,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; + +import com.fsck.k9.mail.internet.MimeMessageHelper; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.util.MimeUtil; @@ -322,7 +324,7 @@ public class MessageTest extends AndroidTestCase { multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_8BIT)); multipartBody.addBodyPart(textBodyPart(MimeUtil.ENC_QUOTED_PRINTABLE)); multipartBody.addBodyPart(binaryBodyPart()); - message.setBody(multipartBody); + MimeMessageHelper.setBody(message, multipartBody); return message; } diff --git a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java b/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java index cceb4bff5..f242eb0b9 100644 --- a/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java +++ b/tests/src/com/fsck/k9/mail/internet/ViewablesTest.java @@ -21,7 +21,7 @@ public class ViewablesTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); - message.setBody(body); + MimeMessageHelper.setBody(message, body); // Extract text ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); @@ -45,7 +45,7 @@ public class ViewablesTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); message.setHeader("Content-Type", "text/html"); - message.setBody(body); + MimeMessageHelper.setBody(message, body); // Extract text ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); @@ -75,7 +75,7 @@ public class ViewablesTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); - message.setBody(multipart); + MimeMessageHelper.setBody(message, multipart); // Extract text ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); @@ -119,7 +119,7 @@ public class ViewablesTest extends AndroidTestCase { innerMessage.setRecipients(RecipientType.TO, new Address[] { new Address("to@example.com") }); innerMessage.setSubject("Subject"); innerMessage.setFrom(new Address("from@example.com")); - innerMessage.setBody(innerBody); + MimeMessageHelper.setBody(innerMessage, innerBody); // Create multipart/mixed part MimeMultipart multipart = new MimeMultipart(); @@ -131,7 +131,7 @@ public class ViewablesTest extends AndroidTestCase { // Create message MimeMessage message = new MimeMessage(); - message.setBody(multipart); + MimeMessageHelper.setBody(message, multipart); // Extract text ViewableContainer container = MimeUtility.extractTextAndAttachments(getContext(), message); From 9f4f0cf6a8705105eea1c3a0da4e36f5841dd7c1 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 25 Nov 2014 19:59:05 +0100 Subject: [PATCH 08/13] Modify BinaryTempFileBody to retain the encoded body For now this breaks a lot of things, e.g. saving messages to the database and making messages 7-bit safe. --- .../k9/mail/internet/BinaryTempFileBody.java | 25 +++---------------- .../fsck/k9/mail/internet/MimeMessage.java | 3 +-- .../fsck/k9/mail/internet/MimeUtility.java | 21 ++++------------ src/com/fsck/k9/mail/store/ImapStore.java | 6 ++--- tests/src/com/fsck/k9/mail/MessageTest.java | 6 ++--- .../fsck/k9/mail/ReconstructMessageTest.java | 5 ++-- 6 files changed, 17 insertions(+), 49 deletions(-) diff --git a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java index f0dd6f473..dceec19e1 100644 --- a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -2,10 +2,7 @@ package com.fsck.k9.mail.internet; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.filter.Base64OutputStream; import org.apache.commons.io.IOUtils; -import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; -import org.apache.james.mime4j.util.MimeUtil; import java.io.*; @@ -27,13 +24,12 @@ public class BinaryTempFileBody implements Body { } public void setEncoding(String encoding) throws MessagingException { - mEncoding = encoding; + mEncoding = encoding; } public BinaryTempFileBody() { if (mTempDirectory == null) { - throw new - RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!"); + throw new RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!"); } } @@ -54,22 +50,7 @@ public class BinaryTempFileBody implements Body { public void writeTo(OutputStream out) throws IOException, MessagingException { InputStream in = getInputStream(); try { - boolean closeStream = false; - if (MimeUtil.isBase64Encoding(mEncoding)) { - out = new Base64OutputStream(out); - closeStream = true; - } else if (MimeUtil.isQuotedPrintableEncoded(mEncoding)){ - out = new QuotedPrintableOutputStream(out, false); - closeStream = true; - } - - try { - IOUtils.copy(in, out); - } finally { - if (closeStream) { - out.close(); - } - } + IOUtils.copy(in, out); } finally { in.close(); } diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 012f2c377..8eb3dfe95 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -531,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); diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index 80efdf5c6..cdd63789e 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1127,24 +1127,11 @@ public class MimeUtility { return false; } - /** - * 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 = - MimeUtility.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; @@ -1154,12 +1141,14 @@ public class MimeUtility { tempBody = new BinaryTempFileBody(); } tempBody.setEncoding(contentTransferEncoding); + OutputStream out = tempBody.getOutputStream(); try { IOUtils.copy(in, out); } finally { out.close(); } + return tempBody; } diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 91406c838..cc714c0e6 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -25,8 +25,6 @@ import java.security.GeneralSecurityException; import java.security.Security; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Deque; @@ -1625,7 +1623,7 @@ public class ImapStore extends Store { .getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; String contentType = part .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; - MimeMessageHelper.setBody(part, MimeUtility.decodeBody(bodyStream, + MimeMessageHelper.setBody(part, MimeUtility.createBody(bodyStream, contentTransferEncoding, contentType)); } else { // This shouldn't happen @@ -3594,7 +3592,7 @@ public class ImapStore extends Store { String contentType = mPart .getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0]; - return MimeUtility.decodeBody(literal, contentTransferEncoding, + return MimeUtility.createBody(literal, contentTransferEncoding, contentType); } return null; diff --git a/tests/src/com/fsck/k9/mail/MessageTest.java b/tests/src/com/fsck/k9/mail/MessageTest.java index 054a3d01a..c5fc39c74 100644 --- a/tests/src/com/fsck/k9/mail/MessageTest.java +++ b/tests/src/com/fsck/k9/mail/MessageTest.java @@ -332,12 +332,12 @@ public class MessageTest extends AndroidTestCase { private MimeBodyPart binaryBodyPart() throws IOException, MessagingException { String encodedTestString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - + "abcdefghijklmnopqrstuvwxyz0123456789+/"; + + "abcdefghijklmnopqrstuvwxyz0123456789+/\r\n"; BinaryTempFileBody tempFileBody = new BinaryTempFileBody(); - InputStream in = new Base64InputStream(new ByteArrayInputStream( - encodedTestString.getBytes("UTF-8"))); + InputStream in = new ByteArrayInputStream( + encodedTestString.getBytes("UTF-8")); OutputStream out = tempFileBody.getOutputStream(); try { diff --git a/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java index 0989581cf..1e6528c99 100644 --- a/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java +++ b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java @@ -37,9 +37,10 @@ public class ReconstructMessageTest extends AndroidTestCase { "\r\n" + "------Boundary\r\n" + "Content-Type: text/plain\r\n" + - "Content-Transfer-Encoding: quoted-printable\r\n" + + "Content-Transfer-Encoding: base64\r\n" + "\r\n" + - "=2E=2E=2E\r\n" + + "VGhpcyBpcyBhIHRl\r\n" + + "c3QgbWVzc2FnZQ==\r\n" + "\r\n" + "------Boundary--\r\n"; From f7d3eaa0068c2d03ecdb57742b39b599e85811a7 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 25 Nov 2014 23:59:03 +0100 Subject: [PATCH 09/13] Fix setUsing7bitTransport() functionality for BinaryTempFileBody --- .../k9/mail/internet/BinaryTempFileBody.java | 44 ++++++++++++++++++- .../internet/BinaryTempFileMessageBody.java | 4 ++ .../fsck/k9/mail/internet/MimeUtility.java | 5 +-- tests/src/com/fsck/k9/mail/MessageTest.java | 5 +-- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java index dceec19e1..677739bbf 100644 --- a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -2,7 +2,10 @@ package com.fsck.k9.mail.internet; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.filter.Base64OutputStream; import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.codec.QuotedPrintableOutputStream; +import org.apache.james.mime4j.util.MimeUtil; import java.io.*; @@ -24,13 +27,50 @@ public class BinaryTempFileBody implements Body { } 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!"); } + + mEncoding = encoding; } public OutputStream getOutputStream() throws IOException { diff --git a/src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java b/src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java index c1503b342..e5fe7c370 100644 --- a/src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java +++ b/src/com/fsck/k9/mail/internet/BinaryTempFileMessageBody.java @@ -18,6 +18,10 @@ import com.fsck.k9.mail.MessagingException; */ public class BinaryTempFileMessageBody extends BinaryTempFileBody implements CompositeBody { + public BinaryTempFileMessageBody(String encoding) { + super(encoding); + } + @Override public void setEncoding(String encoding) throws MessagingException { if (!MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index cdd63789e..b78f8e378 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -1136,11 +1136,10 @@ public class MimeUtility { 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 { diff --git a/tests/src/com/fsck/k9/mail/MessageTest.java b/tests/src/com/fsck/k9/mail/MessageTest.java index c5fc39c74..30a8852b4 100644 --- a/tests/src/com/fsck/k9/mail/MessageTest.java +++ b/tests/src/com/fsck/k9/mail/MessageTest.java @@ -291,8 +291,7 @@ public class MessageTest extends AndroidTestCase { private MimeMessage nestedMessage(MimeMessage subMessage) throws MessagingException, IOException { - BinaryTempFileMessageBody tempMessageBody = new BinaryTempFileMessageBody(); - tempMessageBody.setEncoding(MimeUtil.ENC_8BIT); + BinaryTempFileMessageBody tempMessageBody = new BinaryTempFileMessageBody(MimeUtil.ENC_8BIT); OutputStream out = tempMessageBody.getOutputStream(); try { @@ -334,7 +333,7 @@ public class MessageTest extends AndroidTestCase { String encodedTestString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz0123456789+/\r\n"; - BinaryTempFileBody tempFileBody = new BinaryTempFileBody(); + BinaryTempFileBody tempFileBody = new BinaryTempFileBody(MimeUtil.ENC_BASE64); InputStream in = new ByteArrayInputStream( encodedTestString.getBytes("UTF-8")); From d1d7b60a093407e02259a2b88b9bfd4084005323 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 26 Nov 2014 01:11:26 +0100 Subject: [PATCH 10/13] Add helper method to decode message bodies Depending on whether a Body implements RawDataBody (which indicates the class retains the original encoding) the helper method either strips the transfer encoding or simply returns the result of Body.getInputStream(). This should restore the original functionality. So saving messages in the database should work fine again. --- .../k9/mail/internet/BinaryTempFileBody.java | 7 ++- .../fsck/k9/mail/internet/MimeUtility.java | 49 ++++++++++++++++++- .../fsck/k9/mail/internet/RawDataBody.java | 12 +++++ .../fsck/k9/mail/store/local/LocalFolder.java | 2 +- .../mail/internet/MimeMessageParseTest.java | 8 +-- 5 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/com/fsck/k9/mail/internet/RawDataBody.java diff --git a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java index 677739bbf..17870c140 100644 --- a/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java +++ b/src/com/fsck/k9/mail/internet/BinaryTempFileBody.java @@ -15,7 +15,7 @@ import java.io.*; * and writeTo one time. After writeTo is called, or the InputStream returned from * getInputStream is closed the file is deleted and the Body should be considered disposed of. */ -public class BinaryTempFileBody implements Body { +public class BinaryTempFileBody implements RawDataBody { private static File mTempDirectory; private File mFile; @@ -26,6 +26,11 @@ public class BinaryTempFileBody implements Body { mTempDirectory = tempDirectory; } + @Override + public String getEncoding() { + return mEncoding; + } + public void setEncoding(String encoding) throws MessagingException { if (mEncoding != null && mEncoding.equalsIgnoreCase(encoding)) { return; diff --git a/src/com/fsck/k9/mail/internet/MimeUtility.java b/src/com/fsck/k9/mail/internet/MimeUtility.java index b78f8e378..da3dca5bd 100644 --- a/src/com/fsck/k9/mail/internet/MimeUtility.java +++ b/src/com/fsck/k9/mail/internet/MimeUtility.java @@ -2,6 +2,7 @@ package com.fsck.k9.mail.internet; import android.content.Context; +import android.util.Base64; import android.util.Log; import com.fsck.k9.K9; import com.fsck.k9.R; @@ -10,6 +11,7 @@ import com.fsck.k9.mail.*; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.internet.BinaryTempFileBody.BinaryTempFileBodyInputStream; +import com.fsck.k9.view.MessageHeader; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.codec.Base64InputStream; import org.apache.james.mime4j.codec.QuotedPrintableInputStream; @@ -1026,7 +1028,7 @@ public class MimeUtility { * determine the charset from HTML message. */ if (mimeType.equalsIgnoreCase("text/html") && charset == null) { - InputStream in = part.getBody().getInputStream(); + InputStream in = MimeUtility.decodeBody(part.getBody()); try { byte[] buf = new byte[256]; in.read(buf, 0, buf.length); @@ -1062,7 +1064,7 @@ public class MimeUtility { * Now we read the part into a buffer for further processing. Because * the stream is now wrapped we'll remove any transfer encoding at this point. */ - InputStream in = part.getBody().getInputStream(); + InputStream in = MimeUtility.decodeBody(part.getBody()); try { String text = readToString(in, charset); @@ -1151,6 +1153,49 @@ public class MimeUtility { return tempBody; } + /** + * Get decoded contents of a body. + *

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

+ * The ultimate goal is to get to a point where all classes retain the original data and {@code RawDataBody} can be + * merged into {@link Body}. + */ + public static InputStream decodeBody(Body body) throws MessagingException { + InputStream inputStream; + if (body instanceof RawDataBody) { + RawDataBody rawDataBody = (RawDataBody) body; + String encoding = rawDataBody.getEncoding(); + final InputStream rawInputStream = rawDataBody.getInputStream(); + if (MimeUtil.ENC_7BIT.equalsIgnoreCase(encoding) || MimeUtil.ENC_8BIT.equalsIgnoreCase(encoding)) { + inputStream = rawInputStream; + } else if (MimeUtil.ENC_BASE64.equalsIgnoreCase(encoding)) { + inputStream = new Base64InputStream(rawInputStream, false) { + @Override + public void close() throws IOException { + super.close(); + rawInputStream.close(); + } + }; + } else if (MimeUtil.ENC_QUOTED_PRINTABLE.equalsIgnoreCase(encoding)) { + inputStream = new QuotedPrintableInputStream(rawInputStream) { + @Override + public void close() throws IOException { + super.close(); + rawInputStream.close(); + } + }; + } else { + throw new RuntimeException("Encoding for RawDataBody not supported: " + encoding); + } + } else { + inputStream = body.getInputStream(); + } + + return inputStream; + } /** * Empty base class for the class hierarchy used by diff --git a/src/com/fsck/k9/mail/internet/RawDataBody.java b/src/com/fsck/k9/mail/internet/RawDataBody.java new file mode 100644 index 000000000..e3dee616e --- /dev/null +++ b/src/com/fsck/k9/mail/internet/RawDataBody.java @@ -0,0 +1,12 @@ +package com.fsck.k9.mail.internet; + + +import com.fsck.k9.mail.Body; + + +/** + * See {@link MimeUtility#decodeBody(Body)} + */ +public interface RawDataBody extends Body { + String getEncoding(); +} diff --git a/src/com/fsck/k9/mail/store/local/LocalFolder.java b/src/com/fsck/k9/mail/store/local/LocalFolder.java index e84098183..e264ef877 100644 --- a/src/com/fsck/k9/mail/store/local/LocalFolder.java +++ b/src/com/fsck/k9/mail/store/local/LocalFolder.java @@ -1561,7 +1561,7 @@ public class LocalFolder extends Folder implements Serializable { * If the attachment has a body we're expected to save it into the local store * so we copy the data into a cached attachment file. */ - InputStream in = attachment.getBody().getInputStream(); + InputStream in = MimeUtility.decodeBody(attachment.getBody()); try { tempAttachmentFile = File.createTempFile("att", null, attachmentDirectory); FileOutputStream out = new FileOutputStream(tempAttachmentFile); diff --git a/tests/src/com/fsck/k9/mail/internet/MimeMessageParseTest.java b/tests/src/com/fsck/k9/mail/internet/MimeMessageParseTest.java index 4558d4264..4107a7490 100644 --- a/tests/src/com/fsck/k9/mail/internet/MimeMessageParseTest.java +++ b/tests/src/com/fsck/k9/mail/internet/MimeMessageParseTest.java @@ -63,7 +63,7 @@ public class MimeMessageParseTest extends AndroidTestCase { private static void checkLeafParts(MimeMessage msg, String... expectedParts) throws Exception { List actual = new ArrayList(); for (Body leaf : getLeafParts(msg.getBody())) { - actual.add(streamToString(leaf.getInputStream())); + actual.add(streamToString(MimeUtility.decodeBody(leaf))); } assertEquals(Arrays.asList(expectedParts), actual); } @@ -83,7 +83,7 @@ public class MimeMessageParseTest extends AndroidTestCase { checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); assertEquals("Testmail", msg.getSubject()); assertEquals("text/plain", msg.getContentType()); - assertEquals("this is some test text.", streamToString(msg.getBody().getInputStream())); + assertEquals("this is some test text.", streamToString(MimeUtility.decodeBody(msg.getBody()))); } public static void testSinglePart8BitRecurse() throws Exception { @@ -101,7 +101,7 @@ public class MimeMessageParseTest extends AndroidTestCase { checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); assertEquals("Testmail", msg.getSubject()); assertEquals("text/plain; encoding=ISO-8859-1", msg.getContentType()); - assertEquals("gefährliche Umlaute", streamToString(msg.getBody().getInputStream())); + assertEquals("gefährliche Umlaute", streamToString(MimeUtility.decodeBody(msg.getBody()))); } public static void testSinglePartBase64NoRecurse() throws Exception { @@ -119,7 +119,7 @@ public class MimeMessageParseTest extends AndroidTestCase { checkAddresses(msg.getRecipients(RecipientType.TO), "eva@example.org"); assertEquals("Testmail", msg.getSubject()); assertEquals("text/plain", msg.getContentType()); - assertEquals("this is some more test text.", streamToString(msg.getBody().getInputStream())); + assertEquals("this is some more test text.", streamToString(MimeUtility.decodeBody(msg.getBody()))); } public static void testMultipartSingleLayerNoRecurse() throws Exception { From 3919c9d2d6483f432b14814bfd44875423a872ca Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 7 Dec 2014 03:31:58 +0100 Subject: [PATCH 11/13] Save multi part epilogue in MimeMultipart --- src/com/fsck/k9/mail/internet/MimeMessage.java | 6 ++++++ src/com/fsck/k9/mail/internet/MimeMultipart.java | 8 ++++++++ tests/src/com/fsck/k9/mail/ReconstructMessageTest.java | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 8eb3dfe95..857d1ad76 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -2,6 +2,7 @@ package com.fsck.k9.mail.internet; import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -14,6 +15,7 @@ import java.util.Locale; import java.util.Set; import java.util.UUID; +import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.MimeException; import org.apache.james.mime4j.dom.field.DateTimeField; import org.apache.james.mime4j.field.DefaultFieldParser; @@ -575,6 +577,10 @@ public class MimeMessage extends Message { @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 diff --git a/src/com/fsck/k9/mail/internet/MimeMultipart.java b/src/com/fsck/k9/mail/internet/MimeMultipart.java index 124ca5e4c..6775fdaea 100644 --- a/src/com/fsck/k9/mail/internet/MimeMultipart.java +++ b/src/com/fsck/k9/mail/internet/MimeMultipart.java @@ -11,6 +11,7 @@ import java.util.Random; public class MimeMultipart extends Multipart { private String mPreamble; + private byte[] mEpilogue; private String mContentType; @@ -53,6 +54,10 @@ public class MimeMultipart extends Multipart { this.mPreamble = preamble; } + public void setEpilogue(byte[] epilogue) { + mEpilogue = epilogue; + } + @Override public String getContentType() { return mContentType; @@ -90,6 +95,9 @@ public class MimeMultipart extends Multipart { writer.write(mBoundary); writer.write("--\r\n"); writer.flush(); + if (mEpilogue != null) { + out.write(mEpilogue); + } } @Override diff --git a/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java index 1e6528c99..e36854354 100644 --- a/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java +++ b/tests/src/com/fsck/k9/mail/ReconstructMessageTest.java @@ -42,7 +42,8 @@ public class ReconstructMessageTest extends AndroidTestCase { "VGhpcyBpcyBhIHRl\r\n" + "c3QgbWVzc2FnZQ==\r\n" + "\r\n" + - "------Boundary--\r\n"; + "------Boundary--\r\n" + + "Hi, I'm the epilogue"; BinaryTempFileBody.setTempDirectory(getContext().getCacheDir()); From e374538110fe3083f000b83f74451568941cd23a Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 8 Dec 2014 17:38:30 +0100 Subject: [PATCH 12/13] Store multi part preamble as byte array --- src/com/fsck/k9/mail/internet/MimeMessage.java | 9 +++------ src/com/fsck/k9/mail/internet/MimeMultipart.java | 10 +++------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java index 857d1ad76..a71e2a543 100644 --- a/src/com/fsck/k9/mail/internet/MimeMessage.java +++ b/src/com/fsck/k9/mail/internet/MimeMessage.java @@ -567,12 +567,9 @@ 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 diff --git a/src/com/fsck/k9/mail/internet/MimeMultipart.java b/src/com/fsck/k9/mail/internet/MimeMultipart.java index 6775fdaea..d6ce4377a 100644 --- a/src/com/fsck/k9/mail/internet/MimeMultipart.java +++ b/src/com/fsck/k9/mail/internet/MimeMultipart.java @@ -10,7 +10,7 @@ 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; @@ -46,11 +46,7 @@ public class MimeMultipart extends Multipart { return sb.toString().toUpperCase(Locale.US); } - public String getPreamble() { - return mPreamble; - } - - public void setPreamble(String preamble) { + public void setPreamble(byte[] preamble) { this.mPreamble = preamble; } @@ -72,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"); } From f89b0548a6f2b1554f5adcc6b36b414afdb63a01 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 9 Dec 2014 00:33:38 +0100 Subject: [PATCH 13/13] Add test to verify a signed PGP/MIME message --- build.gradle | 8 +- .../com/fsck/k9/mail/PgpMimeMessageTest.java | 269 ++++++++++++++++++ 2 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 tests/src/com/fsck/k9/mail/PgpMimeMessageTest.java diff --git a/build.gradle b/build.gradle index 5682c80ca..8607e947f 100644 --- a/build.gradle +++ b/build.gradle @@ -43,12 +43,14 @@ dependencies { } androidTestCompile("com.icegreen:greenmail:1.3.1b") { - // Use a better, later version - exclude group: "javax.mail" - } + // 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" } project.ext.preDexLibs = !project.hasProperty('disablePreDex') diff --git a/tests/src/com/fsck/k9/mail/PgpMimeMessageTest.java b/tests/src/com/fsck/k9/mail/PgpMimeMessageTest.java new file mode 100644 index 000000000..75cee3ba2 --- /dev/null +++ b/tests/src/com/fsck/k9/mail/PgpMimeMessageTest.java @@ -0,0 +1,269 @@ +package com.fsck.k9.mail; + + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import android.test.AndroidTestCase; + +import com.fsck.k9.mail.internet.BinaryTempFileBody; +import com.fsck.k9.mail.internet.MimeMessage; +import org.spongycastle.openpgp.PGPCompressedData; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPObjectFactory; +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRingCollection; +import org.spongycastle.openpgp.PGPSignature; +import org.spongycastle.openpgp.PGPSignatureList; +import org.spongycastle.openpgp.PGPUtil; +import org.spongycastle.openpgp.bc.BcPGPObjectFactory; +import org.spongycastle.openpgp.bc.BcPGPPublicKeyRingCollection; +import org.spongycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; + + +public class PgpMimeMessageTest extends AndroidTestCase { + private static final String PUBLIC_KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: GnuPG v1\n" + + "\n" + + "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\n" + + "HzpeeqkwzVU=\n" + + "=3yEX\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + + + public void testSignedMessage() throws IOException, MessagingException, PGPException { + String messageSource = "Date: Mon, 08 Dec 2014 17:44:18 +0100\r\n" + + "From: cketti \r\n" + + "MIME-Version: 1.0\r\n" + + "To: test@example.com\r\n" + + "Subject: OpenPGP signature test\r\n" + + "Content-Type: multipart/signed; micalg=pgp-sha1;\r\n" + + " protocol=\"application/pgp-signature\";\r\n" + + " boundary=\"24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM\"\r\n" + + "\r\n" + + "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)\r\n" + + "--24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=\"------------030308060900040601010501\"\r\n" + + "\r\n" + + "This is a multi-part message in MIME format.\r\n" + + "--------------030308060900040601010501\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "Message body\r\n" + + "goes here\r\n" + + "\r\n" + + "\r\n" + + "--------------030308060900040601010501\r\n" + + "Content-Type: text/plain; charset=UTF-8;\r\n" + + " name=\"attachment.txt\"\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "Content-Disposition: attachment;\r\n" + + " filename=\"attachment.txt\"\r\n" + + "\r\n" + + "VGV4dCBhdHRhY2htZW50Cg==\r\n" + + "--------------030308060900040601010501--\r\n" + + "\r\n" + + "--24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM\r\n" + + "Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n" + + "Content-Description: OpenPGP digital signature\r\n" + + "Content-Disposition: attachment; filename=\"signature.asc\"\r\n" + + "\r\n" + + "-----BEGIN PGP SIGNATURE-----\r\n" + + "Version: GnuPG v1\r\n" + + "\r\n" + + "iQIcBAEBAgAGBQJUhdVqAAoJEO4v7zp9qOKJ8DQP/1+JE8UF7UmirnN1ZO+25hFC\r\n" + + "jAfFMxRWMWXN0gGB+6ySy6ah0bCwmRwHpRBsW/tNcsmOPKb2XBf9zwF06uk/lLp4\r\n" + + "ZmGXxSdQ9XJrlaHk8Sitn9Gi/1L+MNWgrsrLROAZv2jfc9wqN3FOrhN9NC1QXQvO\r\n" + + "+D7sMorSr3l94majoIDrzvxEnfJVfrZWNTUaulJofOJ55GBZ3UJNob1WKjrnculL\r\n" + + "IwmSERmVUoFBUfe/MBqqZH0WDJq9nt//NZFHLunj6nGsrpush1dQRcbR3zzQfXkk\r\n" + + "s7zDLDa8VUv6OxcefjsVN/O7EenoWWgNg6GfW6tY2+oUsLSP2OS3JXvYsylQP4hR\r\n" + + "iU1V9vvsu2Ax6bVb0+uTqw3jNiqVFy3o4mBigVUqp1EFIwBYmyNbe5wj4ACs9Avj\r\n" + + "9t2reFSfXobWQFUS4s71JeMefNAHHJWZI63wNTxE6LOw01YxdJiDaPWGTOyM75MK\r\n" + + "yqn7r5uIfeSv8NypGJaUv4firxKbrcZKk7Wpeh/rZuUSgoPcf3I1IzXfGKKIBHjU\r\n" + + "WUMhTF5SoC5kIZyeXvHrhTM8HszcS8EoG2XcmcYArwgCUlOunFwZNqLPsfdMTRL6\r\n" + + "9rcioaohEtroqoJiGAToJtIz8kqCaamnP/ASBkp9qqJizRd6fqt+tE8BsmJbuPLS\r\n" + + "6lBpS8j0TqmaZMYfB9u4\r\n" + + "=QvET\r\n" + + "-----END PGP SIGNATURE-----\r\n" + + "\r\n" + + "--24Bem7EnUI1Ipn9jNXuLgsetqa6wOkIxM--\r\n"; + + BinaryTempFileBody.setTempDirectory(getContext().getCacheDir()); + + InputStream messageInputStream = new ByteArrayInputStream(messageSource.getBytes()); + MimeMessage message; + try { + message = new MimeMessage(messageInputStream, true); + } finally { + messageInputStream.close(); + } + + Multipart multipartSigned = (Multipart) message.getBody(); + + BodyPart signedPart = multipartSigned.getBodyPart(0); + ByteArrayOutputStream signedPartOutputStream = new ByteArrayOutputStream(); + signedPart.writeTo(signedPartOutputStream); + byte[] signedData = signedPartOutputStream.toByteArray(); + + Body signatureBody = multipartSigned.getBodyPart(1).getBody(); + ByteArrayOutputStream signatureBodyOutputStream = new ByteArrayOutputStream(); + signatureBody.writeTo(signatureBodyOutputStream); + byte[] signatureData = signatureBodyOutputStream.toByteArray(); + + assertTrue(verifySignature(signedData, signatureData)); + } + + private boolean verifySignature(byte[] signedData, byte[] signatureData) throws IOException, PGPException { + InputStream signatureInputStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(signatureData)); + PGPObjectFactory pgpObjectFactory = new BcPGPObjectFactory(signatureInputStream); + Object pgpObject = pgpObjectFactory.nextObject(); + + PGPSignatureList pgpSignatureList; + if (pgpObject instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) pgpObject; + pgpObjectFactory = new BcPGPObjectFactory(compressedData.getDataStream()); + pgpSignatureList = (PGPSignatureList) pgpObjectFactory.nextObject(); + } else { + pgpSignatureList = (PGPSignatureList) pgpObject; + } + PGPSignature signature = pgpSignatureList.get(0); + + InputStream keyInputStream = PGPUtil.getDecoderStream(new ByteArrayInputStream(PUBLIC_KEY.getBytes())); + PGPPublicKeyRingCollection pgpPublicKeyRingCollection = new BcPGPPublicKeyRingCollection(keyInputStream); + PGPPublicKey publicKey = pgpPublicKeyRingCollection.getPublicKey(signature.getKeyID()); + + signature.init(new BcPGPContentVerifierBuilderProvider(), publicKey); + InputStream signedDataInputStream = new ByteArrayInputStream(signedData); + int ch; + while ((ch = signedDataInputStream.read()) >= 0) { + signature.update((byte) ch); + } + + signedDataInputStream.close(); + keyInputStream.close(); + signatureInputStream.close(); + + return signature.verify(); + } +}