diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 3138144edd..69d8d2fc91 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -19,3 +19,4 @@ FairEmail uses: * [Markwon](https://github.com/noties/Markwon). Copyright 2019 Dimitry Ivanov. [Apache License 2.0](https://github.com/noties/Markwon/blob/master/LICENSE). * [Color Picker](https://github.com/QuadFlask/colorpicker). Copyright 2014-2017 QuadFlask. [Apache License 2.0](https://github.com/QuadFlask/colorpicker#user-content-license). * [Bouncy Castle](https://www.bouncycastle.org/). Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. [MIT License](https://www.bouncycastle.org/licence.html). +* [AppAuth for Android](https://github.com/openid/AppAuth-Android). Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. [Apache License 2.0](https://github.com/openid/AppAuth-Android/blob/master/LICENSE). diff --git a/app/build.gradle b/app/build.gradle index b099c7cc2d..a390fef3a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -214,6 +214,7 @@ dependencies { def msal_version = "1.0.0" def bouncycastle_version = "1.64" def colorpicker_version = "0.0.15" + def appauth_version = "0.7.1" // https://developer.android.com/jetpack/androidx/releases/ @@ -339,4 +340,8 @@ dependencies { // https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on implementation "org.bouncycastle:bcpkix-jdk15to18:$bouncycastle_version" //implementation "org.bouncycastle:bcmail-jdk15to18:$bouncycastle_version" + + // https://github.com/openid/AppAuth-Android + // https://mvnrepository.com/artifact/net.openid/appauth?repo=bt-openid + implementation "net.openid:appauth:$appauth_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 92fca1a889..6515c77c56 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -227,6 +228,22 @@ android:icon="@mipmap/ic_launcher" android:launchMode="singleTask" /> + + + + + + + + + + + diff --git a/app/src/main/assets/ATTRIBUTION.md b/app/src/main/assets/ATTRIBUTION.md index 3138144edd..69d8d2fc91 100644 --- a/app/src/main/assets/ATTRIBUTION.md +++ b/app/src/main/assets/ATTRIBUTION.md @@ -19,3 +19,4 @@ FairEmail uses: * [Markwon](https://github.com/noties/Markwon). Copyright 2019 Dimitry Ivanov. [Apache License 2.0](https://github.com/noties/Markwon/blob/master/LICENSE). * [Color Picker](https://github.com/QuadFlask/colorpicker). Copyright 2014-2017 QuadFlask. [Apache License 2.0](https://github.com/QuadFlask/colorpicker#user-content-license). * [Bouncy Castle](https://www.bouncycastle.org/). Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. [MIT License](https://www.bouncycastle.org/licence.html). +* [AppAuth for Android](https://github.com/openid/AppAuth-Android). Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. [Apache License 2.0](https://github.com/openid/AppAuth-Android/blob/master/LICENSE). diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java index 4cf65ce47d..2e9fd59655 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySetup.java +++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java @@ -73,6 +73,21 @@ import com.microsoft.identity.client.IPublicClientApplication; import com.microsoft.identity.client.PublicClientApplication; import com.microsoft.identity.client.exception.MsalException; +import net.openid.appauth.AppAuthConfiguration; +import net.openid.appauth.AuthorizationException; +import net.openid.appauth.AuthorizationRequest; +import net.openid.appauth.AuthorizationResponse; +import net.openid.appauth.AuthorizationService; +import net.openid.appauth.AuthorizationServiceConfiguration; +import net.openid.appauth.ClientAuthentication; +import net.openid.appauth.ClientSecretPost; +import net.openid.appauth.ResponseTypeValues; +import net.openid.appauth.TokenResponse; +import net.openid.appauth.browser.BrowserBlacklist; +import net.openid.appauth.browser.Browsers; +import net.openid.appauth.browser.VersionRange; +import net.openid.appauth.browser.VersionedBrowserMatcher; + import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemReader; import org.json.JSONArray; @@ -131,8 +146,10 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac static final int REQUEST_CHOOSE_ACCOUNT = 5; static final int REQUEST_DONE = 6; static final int REQUEST_IMPORT_CERTIFICATE = 7; + static final int REQUEST_OAUTH = 8; static final String ACTION_QUICK_GMAIL = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_GMAIL"; + static final String ACTION_QUICK_OAUTH = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_OAUTH"; static final String ACTION_QUICK_OUTLOOK = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_OUTLOOK"; static final String ACTION_QUICK_SETUP = BuildConfig.APPLICATION_ID + ".ACTION_QUICK_SETUP"; static final String ACTION_VIEW_ACCOUNTS = BuildConfig.APPLICATION_ID + ".ACTION_VIEW_ACCOUNTS"; @@ -310,6 +327,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); IntentFilter iff = new IntentFilter(); iff.addAction(ACTION_QUICK_GMAIL); + iff.addAction(ACTION_QUICK_OAUTH); iff.addAction(ACTION_QUICK_OUTLOOK); iff.addAction(ACTION_QUICK_SETUP); iff.addAction(ACTION_VIEW_ACCOUNTS); @@ -383,6 +401,10 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac if (resultCode == RESULT_OK && data != null) handleImportCertificate(data); break; + case REQUEST_OAUTH: + if (resultCode == RESULT_OK && data != null) + onHandleOAuth(data); + break; } } catch (Throwable ex) { Log.e(ex); @@ -1138,6 +1160,78 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac fragmentTransaction.commit(); } + private void onOAuth(Intent intent) { + String name = intent.getStringExtra("name"); + for (EmailProvider provider : EmailProvider.loadProfiles(this)) + if (provider.name.equals(name) && provider.oauth != null) { + AppAuthConfiguration appAuthConfig = new AppAuthConfiguration.Builder() + .setBrowserMatcher(new BrowserBlacklist( + new VersionedBrowserMatcher( + Browsers.SBrowser.PACKAGE_NAME, + Browsers.SBrowser.SIGNATURE_SET, + true, + VersionRange.atMost("5.3") + ))) + .build(); + + AuthorizationService authService = new AuthorizationService(this, appAuthConfig); + + AuthorizationRequest authRequest = + new AuthorizationRequest.Builder( + new AuthorizationServiceConfiguration( + Uri.parse(provider.oauth.authorizationEndpoint), + Uri.parse(provider.oauth.tokenEndpoint)), + provider.oauth.clientId, + ResponseTypeValues.CODE, + Uri.parse(provider.oauth.redirectUri)) + .setScopes(provider.oauth.scopes) + .setState(name) + .build(); + + Intent authIntent = authService.getAuthorizationRequestIntent(authRequest); + startActivityForResult(authIntent, REQUEST_OAUTH); + + return; + } + + Log.unexpectedError(getSupportFragmentManager(), + new IllegalArgumentException("Unknown provider=" + name)); + } + + private void onHandleOAuth(Intent data) { + AuthorizationResponse auth = AuthorizationResponse.fromIntent(data); + if (auth == null) { + AuthorizationException ex = AuthorizationException.fromIntent(data); + Log.unexpectedError(getSupportFragmentManager(), ex); + return; + } + + for (EmailProvider provider : EmailProvider.loadProfiles(this)) + if (provider.name.equals(auth.state)) { + AuthorizationService authService = new AuthorizationService(this); + ClientAuthentication clientAuth = new ClientSecretPost(provider.oauth.clientSecret); + authService.performTokenRequest( + auth.createTokenExchangeRequest(), + clientAuth, + new AuthorizationService.TokenResponseCallback() { + @Override + public void onTokenRequestCompleted(TokenResponse access, AuthorizationException ex) { + if (access == null) { + Log.unexpectedError(getSupportFragmentManager(), ex); + return; + } + + // access.accessToken + } + }); + + return; + } + + Log.unexpectedError(getSupportFragmentManager(), + new IllegalArgumentException("Unknown state=" + auth.state)); + } + private void onOutlook(Intent intent) { PublicClientApplication.createMultipleAccountPublicClientApplication( this, @@ -1484,6 +1578,8 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac String action = intent.getAction(); if (ACTION_QUICK_GMAIL.equals(action)) onGmail(intent); + else if (ACTION_QUICK_OAUTH.equals(action)) + onOAuth(intent); else if (ACTION_QUICK_OUTLOOK.equals(action)) onOutlook(intent); else if (ACTION_QUICK_SETUP.equals(action)) diff --git a/app/src/main/java/eu/faircode/email/EmailProvider.java b/app/src/main/java/eu/faircode/email/EmailProvider.java index 678edf50d2..5386e23b81 100644 --- a/app/src/main/java/eu/faircode/email/EmailProvider.java +++ b/app/src/main/java/eu/faircode/email/EmailProvider.java @@ -67,6 +67,7 @@ public class EmailProvider { public String link; public Server imap = new Server(); public Server smtp = new Server(); + public OAuth oauth; public UserType user = UserType.EMAIL; public StringBuilder documentation; // html @@ -129,6 +130,14 @@ public class EmailProvider { provider.smtp.host = xml.getAttributeValue(null, "host"); provider.smtp.port = xml.getAttributeIntValue(null, "port", 0); provider.smtp.starttls = xml.getAttributeBooleanValue(null, "starttls", false); + } else if ("oauth".equals(name)) { + provider.oauth = new OAuth(); + provider.oauth.clientId = xml.getAttributeValue(null, "clientId"); + provider.oauth.clientSecret = xml.getAttributeValue(null, "clientSecret"); + provider.oauth.scopes = xml.getAttributeValue(null, "scopes").split(","); + provider.oauth.authorizationEndpoint = xml.getAttributeValue(null, "authorizationEndpoint"); + provider.oauth.tokenEndpoint = xml.getAttributeValue(null, "tokenEndpoint"); + provider.oauth.redirectUri = xml.getAttributeValue(null, "redirectUri"); } else throw new IllegalAccessException(name); } else if (eventType == XmlPullParser.END_TAG) { @@ -650,4 +659,13 @@ public class EmailProvider { return host + ":" + port; } } + + public static class OAuth { + String clientId; + String clientSecret; + String[] scopes; + String authorizationEndpoint; + String tokenEndpoint; + String redirectUri; + } } diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java index 75f8b46100..f0bf724fce 100644 --- a/app/src/main/java/eu/faircode/email/FragmentSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java @@ -167,9 +167,16 @@ public class FragmentSetup extends FragmentBase { PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(getContext(), getViewLifecycleOwner(), btnQuick); popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_gmail, 1, R.string.title_setup_gmail); - //popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_outlook, 2, R.string.title_setup_outlook); - popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_activesync, 3, R.string.title_setup_activesync); - popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_other, 4, R.string.title_setup_other); + + // Android 5 Lollipop does not support app links + if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_gmail_oauth, 2, R.string.title_setup_gmail_oauth); + + if (BuildConfig.DEBUG) + popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_outlook, 3, R.string.title_setup_outlook); + + popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_activesync, 4, R.string.title_setup_activesync); + popupMenu.getMenu().add(Menu.NONE, R.string.title_setup_other, 5, R.string.title_setup_other); popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override @@ -182,6 +189,9 @@ public class FragmentSetup extends FragmentBase { else ToastEx.makeText(getContext(), R.string.title_setup_gmail_support, Toast.LENGTH_LONG).show(); return true; + case R.string.title_setup_gmail_oauth: + lbm.sendBroadcast(new Intent(ActivitySetup.ACTION_QUICK_OAUTH).putExtra("name", "Gmail")); + return true; case R.string.title_setup_outlook: lbm.sendBroadcast(new Intent(ActivitySetup.ACTION_QUICK_OUTLOOK)); return true; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 089b1892ac..44e59389bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,6 +140,7 @@ Wizard Go \'back\' to go to the inbox Gmail + Gmail OAuth Outlook Exchange ActiveSync Other provider diff --git a/app/src/main/res/xml/providers.xml b/app/src/main/res/xml/providers.xml index f057c1b4b5..5e8b94c14c 100644 --- a/app/src/main/res/xml/providers.xml +++ b/app/src/main/res/xml/providers.xml @@ -14,6 +14,15 @@ host="smtp.gmail.com" port="465" starttls="false" /> + + +