diff --git a/app/build.gradle b/app/build.gradle index 196b58df7f..de75d591be 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,7 @@ android { exclude 'META-INF/LICENSE.txt' exclude 'META-INF/README.md' exclude 'META-INF/CHANGES' + exclude 'META-INF/jersey-module-version' } signingConfigs { @@ -202,6 +203,7 @@ dependencies { def photoview_version = "2.3.0" def relinker_version = "1.3.1" def markwon_version = "4.1.1" + def msal_version = "1.0.0" // https://developer.android.com/jetpack/androidx/releases/ @@ -319,4 +321,7 @@ dependencies { // // https://github.com/QuadFlask/colorpicker implementation project(':qcolorpicker') + + // https://github.com/AzureAD/microsoft-authentication-library-for-android + implementation "com.microsoft.identity.client:msal:$msal_version" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 6a37aca655..7714e6ac4b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -73,6 +73,12 @@ -keepnames class biweekly.** {*;} -dontwarn biweekly.io.json.** +#MSAL +-keep class com.microsoft.aad.adal.** {*;} +-keep class com.microsoft.identity.common.** {*;} +-dontwarn com.nimbusds.jose.** +-keepclassmembers enum * {*;} #GSON + #Notes -dontnote com.google.android.material.** -dontnote com.sun.mail.** diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 79e95cc63e..adeb36e621 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -205,6 +205,20 @@ + + + + + + + + + + + claims = result.getAccount().getClaims(); + if (claims != null) + for (String key : claims.keySet()) + Log.i(key + "=" + claims.get(key)); + + new SimpleTask() { + @Override + protected JSONObject onExecute(Context context, Bundle args) throws Throwable { + String token = args.getString("token"); + + // https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#http-request + URL url = new URL("https://graph.microsoft.com/v1.0/me" + + "?$select=displayName,otherMails"); + Log.i("MSAL fetching " + url); + + HttpURLConnection request = (HttpURLConnection) url.openConnection(); + request.setReadTimeout(15 * 1000); + request.setConnectTimeout(15 * 1000); + request.setRequestMethod("GET"); + request.setDoInput(true); + request.setRequestProperty("Authorization", "Bearer " + token); + request.setRequestProperty("Content-Type", "application/json"); + request.connect(); + + try { + Log.i("MSAL getting response"); + String json = Helper.readStream(request.getInputStream(), StandardCharsets.UTF_8.name()); + return new JSONObject(json); + } finally { + request.disconnect(); + } + } + + @Override + protected void onExecuted(Bundle args, JSONObject data) { + Log.i("MSAL " + data); + + try { + JSONArray otherMails = data.getJSONArray("otherMails"); + + args.putString("displayName", data.getString("displayName")); + args.putString("email", (String) otherMails.get(0)); + + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + String token = args.getString("token"); + String email = args.getString("id"); + String displayName = args.getString("displayName"); + + List folders; + + // https://msdn.microsoft.com/en-us/windows/desktop/dn440163 + String host = "imap-mail.outlook.com"; + int port = 993; + boolean starttls = false; + String user = email; + String password = token; + try (MailService iservice = new MailService(context, "imaps", null, false, true, true)) { + iservice.connect(host, port, MailService.AUTH_TYPE_OUTLOOK, user, password); + + folders = iservice.getFolders(); + + DB db = DB.getInstance(context); + try { + db.beginTransaction(); + + EntityAccount primary = db.account().getPrimaryAccount(); + + // Create account + EntityAccount account = new EntityAccount(); + + account.host = host; + account.starttls = starttls; + account.port = port; + account.auth_type = MailService.AUTH_TYPE_OUTLOOK; + account.user = user; + account.password = password; + + account.name = "OutLook"; + + account.synchronize = true; + account.primary = (primary == null); + + account.created = new Date().getTime(); + account.last_connected = account.created; + + account.id = db.account().insertAccount(account); + args.putLong("account", account.id); + EntityLog.log(context, "OutLook account=" + account.name); + + // Create folders + for (EntityFolder folder : folders) { + folder.account = account.id; + folder.id = db.folder().insertFolder(folder); + EntityLog.log(context, "OutLook folder=" + folder.name + " type=" + folder.type); + } + + // Set swipe left/right folder + for (EntityFolder folder : folders) + if (EntityFolder.TRASH.equals(folder.type)) + account.swipe_left = folder.id; + else if (EntityFolder.ARCHIVE.equals(folder.type)) + account.swipe_right = folder.id; + + db.account().updateAccount(account); + + // Create identity + EntityIdentity identity = new EntityIdentity(); + identity.name = displayName; + identity.email = user; + identity.account = account.id; + + identity.host = "smtp-mail.outlook.com"; + identity.starttls = true; + identity.port = 587; + identity.auth_type = MailService.AUTH_TYPE_OUTLOOK; + identity.user = user; + identity.password = password; + identity.synchronize = true; + identity.primary = true; + + identity.id = db.identity().insertIdentity(identity); + args.putLong("identity", identity.id); + EntityLog.log(context, "Gmail identity=" + identity.name + " email=" + identity.email); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + return null; + } + + @Override + protected void onException(Bundle args, Throwable ex) { + + } + }.execute(ActivitySetup.this, args, "outlook:account"); + } catch (JSONException ex) { + Log.e(ex); + } + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getSupportFragmentManager(), ex); + } + }.execute(ActivitySetup.this, args, "graph:profile"); + } + + @Override + public void onError(MsalException ex) { + Log.e(ex); + } + + @Override + public void onCancel() { + Log.w("MSAL cancelled"); + } + }); + } + + @Override + public void onError(MsalException ex) { + Log.e("MSAL", ex); + } + }); + } + private void onViewQuickSetup(Intent intent) { FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(R.id.content_frame, new FragmentQuickSetup()).addToBackStack("quick"); @@ -1159,6 +1367,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_OUTLOOK.equals(action)) + onOutlook(intent); else if (ACTION_QUICK_SETUP.equals(action)) onViewQuickSetup(intent); else if (ACTION_VIEW_ACCOUNTS.equals(action)) diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java index 3eb8fe51fd..307e542886 100644 --- a/app/src/main/java/eu/faircode/email/FragmentSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java @@ -166,7 +166,8 @@ 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_other, 2, R.string.title_setup_other); + //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_other, 3, R.string.title_setup_other); popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override @@ -179,6 +180,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_outlook: + lbm.sendBroadcast(new Intent(ActivitySetup.ACTION_QUICK_OUTLOOK)); + return true; case R.string.title_setup_other: lbm.sendBroadcast(new Intent(ActivitySetup.ACTION_QUICK_SETUP)); return true; diff --git a/app/src/main/java/eu/faircode/email/MailService.java b/app/src/main/java/eu/faircode/email/MailService.java index a57b1629fc..c914e75c50 100644 --- a/app/src/main/java/eu/faircode/email/MailService.java +++ b/app/src/main/java/eu/faircode/email/MailService.java @@ -45,6 +45,7 @@ public class MailService implements AutoCloseable { static final int AUTH_TYPE_PASSWORD = 1; static final int AUTH_TYPE_GMAIL = 2; + static final int AUTH_TYPE_OUTLOOK = 3; private final static int CHECK_TIMEOUT = 15 * 1000; // milliseconds private final static int CONNECT_TIMEOUT = 20 * 1000; // milliseconds @@ -175,7 +176,7 @@ public class MailService implements AutoCloseable { public String connect(String host, int port, int auth, String user, String password) throws MessagingException { try { - if (auth == AUTH_TYPE_GMAIL) + if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OUTLOOK) properties.put("mail." + protocol + ".auth.mechanisms", "XOAUTH2"); //if (BuildConfig.DEBUG) diff --git a/app/src/main/res/raw/msal_config.json b/app/src/main/res/raw/msal_config.json new file mode 100644 index 0000000000..2f739eafe8 --- /dev/null +++ b/app/src/main/res/raw/msal_config.json @@ -0,0 +1,14 @@ +{ + "client_id": "3514cf2c-e7a3-45a2-80d4-6a3c3498eca0", + "authorization_user_agent": "DEFAULT", + "redirect_uri": "msauth://eu.faircode.email/F7oVwa9V2SX5i5nOpDddTN9MF0s%3D", + "authorities": [ + { + "type": "AAD", + "audience": { + "type": "AzureADMultipleOrgs", + "tenant_id": "organizations" + } + } + ] +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f8d3ba2ad..35a8b4967a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,6 +138,7 @@ Wizard Go \'back\' to go to the inbox Gmail + Outlook Other provider Authorizing Google accounts will work in official versions only because Android checks the app signature Please grant permissions to select an account and read your name