From ad83451220894097e3addc3fd79af68e88546942 Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 4 Mar 2023 08:57:12 +0100 Subject: [PATCH] Added Outlook graph send --- .../eu/faircode/email/AdapterIdentity.java | 4 + .../java/eu/faircode/email/EmailProvider.java | 14 + .../java/eu/faircode/email/EntityAccount.java | 2 +- .../java/eu/faircode/email/FragmentOAuth.java | 152 ++++++---- .../faircode/email/FragmentOptionsMisc.java | 1 + .../java/eu/faircode/email/FragmentSetup.java | 8 - .../faircode/email/ServiceAuthenticator.java | 3 +- .../java/eu/faircode/email/ServiceSend.java | 286 +++++++++++------- .../res/drawable/twotone_show_chart_24.xml | 11 + app/src/main/res/values/strings.xml | 1 - app/src/main/res/xml/providers.xml | 38 ++- 11 files changed, 326 insertions(+), 194 deletions(-) create mode 100644 app/src/main/res/drawable/twotone_show_chart_24.xml diff --git a/app/src/main/java/eu/faircode/email/AdapterIdentity.java b/app/src/main/java/eu/faircode/email/AdapterIdentity.java index cf82d81416..609ad040cd 100644 --- a/app/src/main/java/eu/faircode/email/AdapterIdentity.java +++ b/app/src/main/java/eu/faircode/email/AdapterIdentity.java @@ -19,6 +19,7 @@ package eu.faircode.email; Copyright 2018-2023 by Marcel Bokhorst (M66B) */ +import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_OAUTH; import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD; import android.content.Context; @@ -137,6 +138,9 @@ public class AdapterIdentity extends RecyclerView.Adapter(); diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index 70071e4860..c2de4c092f 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -181,7 +181,7 @@ public class EntityAccount extends EntityOrder implements Serializable { } static boolean isOutlook(String id) { - return ("office365".equals(id) || "office365pcke".equals(id) || "outlook".equals(id) || "outlooktest".equals(id)); + return ("office365".equals(id) || "office365pcke".equals(id) || "outlook".equals(id) || "outlookgraph".equals(id)); } boolean isYahooJp() { diff --git a/app/src/main/java/eu/faircode/email/FragmentOAuth.java b/app/src/main/java/eu/faircode/email/FragmentOAuth.java index af5346f36f..8d511c03d8 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOAuth.java +++ b/app/src/main/java/eu/faircode/email/FragmentOAuth.java @@ -20,6 +20,7 @@ package eu.faircode.email; */ import static android.app.Activity.RESULT_OK; +import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GRAPH; import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_OAUTH; import android.content.ActivityNotFoundException; @@ -239,7 +240,7 @@ public class FragmentOAuth extends FragmentBase { btnOAuth.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - onAuthorize(); + onAuthorize(false); } }); @@ -309,7 +310,7 @@ public class FragmentOAuth extends FragmentBase { } } - private void onAuthorize() { + private void onAuthorize(boolean graph) { try { if (askAccount) { String name = etName.getText().toString().trim(); @@ -350,6 +351,7 @@ public class FragmentOAuth extends FragmentBase { final Context context = getContext(); PackageManager pm = context.getPackageManager(); EmailProvider provider = EmailProvider.getProvider(context, id); + EmailProvider.OAuth oauth = (graph ? provider.graph : provider.oauth); int flags = PackageManager.GET_RESOLVED_FILTER; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) @@ -416,8 +418,8 @@ public class FragmentOAuth extends FragmentBase { AuthorizationService authService = new AuthorizationService(context, appAuthConfig); - String authorizationEndpoint = provider.oauth.authorizationEndpoint; - String tokenEndpoint = provider.oauth.tokenEndpoint; + String authorizationEndpoint = oauth.authorizationEndpoint; + String tokenEndpoint = oauth.tokenEndpoint; String tenant = etTenant.getText().toString().trim(); if (TextUtils.isEmpty(tenant)) @@ -432,14 +434,15 @@ public class FragmentOAuth extends FragmentBase { AuthState authState = new AuthState(serviceConfig); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putString("oauth." + provider.id, authState.jsonSerializeString()).apply(); + String key = "oauth." + provider.id + (graph ? ":graph" : ""); + prefs.edit().putString(key, authState.jsonSerializeString()).apply(); - Map params = (provider.oauth.parameters == null + Map params = (oauth.parameters == null ? new LinkedHashMap<>() - : provider.oauth.parameters); + : oauth.parameters); - String clientId = provider.oauth.clientId; - Uri redirectUri = Uri.parse(provider.oauth.redirectUri); + String clientId = oauth.clientId; + Uri redirectUri = Uri.parse(oauth.redirectUri); if ("gmail".equals(id) && BuildConfig.DEBUG && false) { clientId = "803253368361-hr8kelm53hqodj7c6brdjeb2ctn5jg3p.apps.googleusercontent.com"; redirectUri = Uri.parse("eu.faircode.email.debug:/"); @@ -452,8 +455,8 @@ public class FragmentOAuth extends FragmentBase { clientId, ResponseTypeValues.CODE, redirectUri) - .setScopes(provider.oauth.scopes) - .setState(provider.id) + .setScopes(oauth.scopes) + .setState(provider.id + (graph ? ":graph" : "")) .setAdditionalParameters(params); if (askAccount) { @@ -465,8 +468,8 @@ public class FragmentOAuth extends FragmentBase { authRequestBuilder.setLoginHint(address); } - if (!TextUtils.isEmpty(provider.oauth.prompt)) - authRequestBuilder.setPrompt(provider.oauth.prompt); + if (!TextUtils.isEmpty(oauth.prompt)) + authRequestBuilder.setPrompt(oauth.prompt); AuthorizationRequest authRequest = authRequestBuilder.build(); @@ -509,26 +512,26 @@ public class FragmentOAuth extends FragmentBase { throw ex; } - final EmailProvider provider = EmailProvider.getProvider(getContext(), auth.state); + String id = auth.state.split(":")[0]; + final EmailProvider provider = EmailProvider.getProvider(getContext(), id); + EmailProvider.OAuth oauth = (auth.state.endsWith(":graph") ? provider.graph : provider.oauth); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - String json = prefs.getString("oauth." + provider.id, null); - prefs.edit().remove("oauth." + provider.id).apply(); + String json = prefs.getString("oauth." + auth.state, null); + prefs.edit().remove("oauth." + auth.state).apply(); final AuthState authState = AuthState.jsonDeserialize(json); - Log.i("OAuth get token provider=" + provider.id); + Log.i("OAuth get token provider=" + provider.id + " state=" + auth.state); authState.update(auth, null); - if (BuildConfig.DEBUG) - Log.i("OAuth response=" + authState.jsonSerializeString()); AuthorizationService authService = new AuthorizationService(getContext()); ClientAuthentication clientAuth; - if (provider.oauth.clientSecret == null) + if (oauth.clientSecret == null) clientAuth = NoClientAuthentication.INSTANCE; else - clientAuth = new ClientSecretPost(provider.oauth.clientSecret); + clientAuth = new ClientSecretPost(oauth.clientSecret); TokenRequest.Builder builder = new TokenRequest.Builder( auth.request.configuration, @@ -540,8 +543,8 @@ public class FragmentOAuth extends FragmentBase { .setAdditionalParameters(Collections.emptyMap()) .setNonce(auth.request.nonce); - if (provider.oauth.tokenScopes) - builder.setScope(TextUtils.join(" ", provider.oauth.scopes)); + if (oauth.tokenScopes) + builder.setScope(TextUtils.join(" ", oauth.scopes)); TokenRequest request = builder.build(); @@ -555,17 +558,36 @@ public class FragmentOAuth extends FragmentBase { if (access == null) throw error; - Log.i("OAuth got token provider=" + provider.id); - if (BuildConfig.DEBUG) - Log.i("TokenResponse=" + access.jsonSerializeString()); - authState.update(access, null); - if (BuildConfig.DEBUG) - Log.i("OAuth response=" + authState.jsonSerializeString()); + String[] scopes = access.getScopeSet().toArray(new String[0]); + Log.i("OAuth got token provider=" + provider.id + + " state=" + auth.state + + " scopes=" + TextUtils.join(",", scopes)); if (TextUtils.isEmpty(access.refreshToken)) throw new IllegalStateException("No refresh token"); - onOAuthorized(access.accessToken, access.idToken, authState); + authState.update(access, null); + + if (provider.graph == null || !provider.graph.enabled) + onOAuthorized( + new String[]{access.accessToken}, + new String[]{access.idToken}, + new AuthState[]{authState}); + else { + if (auth.state.endsWith(":graph")) { + String key0 = "oauth." + provider.id; + String json0 = prefs.getString(key0, null); + prefs.edit().remove(key0).apply(); + AuthState state0 = AuthState.jsonDeserialize(json0); + onOAuthorized( + new String[]{state0.getAccessToken(), authState.getAccessToken()}, + new String[]{state0.getIdToken(), authState.getIdToken()}, + new AuthState[]{state0, authState}); + } else { + prefs.edit().putString("oauth." + provider.id, authState.jsonSerializeString()).apply(); + onAuthorize(true); + } + } } catch (Throwable ex) { showError(ex); } @@ -576,18 +598,22 @@ public class FragmentOAuth extends FragmentBase { } } - private void onOAuthorized(String accessToken, String idToken, AuthState state) { + private void onOAuthorized(String[] accessToken, String[] idToken, AuthState[] state) { Log.breadcrumb("onOAuthorized", "id", id); if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) return; + List states = new ArrayList<>(); + for (AuthState s : state) + states.add(s.jsonSerializeString()); + Bundle args = new Bundle(); args.putString("id", id); args.putString("name", name); - args.putString("token", accessToken); - args.putString("jwt", idToken); - args.putString("state", state.jsonSerializeString()); + args.putStringArray("token", accessToken); + args.putStringArray("jwt", idToken); + args.putStringArray("state", states.toArray(new String[0])); args.putBoolean("askAccount", askAccount); args.putString("personal", etName.getText().toString().trim()); args.putString("address", etEmail.getText().toString().trim()); @@ -611,9 +637,9 @@ public class FragmentOAuth extends FragmentBase { protected Void onExecute(Context context, Bundle args) throws Throwable { String id = args.getString("id"); String name = args.getString("name"); - String token = args.getString("token"); - String jwt = args.getString("jwt"); - String state = args.getString("state"); + String[] token = args.getStringArray("token"); + String[] jwt = args.getStringArray("jwt"); + String[] state = args.getStringArray("state"); boolean askAccount = args.getBoolean("askAccount", false); String personal = args.getString("personal"); String address = args.getString("address"); @@ -653,12 +679,10 @@ public class FragmentOAuth extends FragmentBase { usernames.add(sharedname == null ? username : sharedname); EntityLog.log(context, "OAuth id=" + id + " user=" + username + " shared=" + sharedname); - EntityLog.log(context, "OAuth token=" + token); - EntityLog.log(context, "OAuth jwt=" + jwt); - if (token != null && sharedname == null && !"gmail".equals(id)) { + if (token[0] != null && sharedname == null && !"gmail".equals(id)) { // https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens - String[] segments = token.split("\\."); + String[] segments = token[0].split("\\."); if (segments.length > 1) try { String payload = new String(Base64.decode(segments[1], Base64.DEFAULT)); @@ -687,9 +711,9 @@ public class FragmentOAuth extends FragmentBase { } } - if (jwt != null && sharedname == null) { + if (jwt[0] != null && sharedname == null) { // https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens - String[] segments = jwt.split("\\."); + String[] segments = jwt[0].split("\\."); if (segments.length > 1) try { // https://jwt.ms/ @@ -756,17 +780,19 @@ public class FragmentOAuth extends FragmentBase { aservice.connect( inbound.host, inbound.port, AUTH_TYPE_OAUTH, provider.id, - alt, state, + alt, state[0], null, null); } - try (EmailService iservice = new EmailService( - context, iprotocol, null, iencryption, false, false, - EmailService.PURPOSE_CHECK, true)) { - iservice.connect( - provider.smtp.host, provider.smtp.port, - AUTH_TYPE_OAUTH, provider.id, - alt, state, - null, null); + if (state.length == 1) { + try (EmailService iservice = new EmailService( + context, iprotocol, null, iencryption, false, false, + EmailService.PURPOSE_CHECK, true)) { + iservice.connect( + provider.smtp.host, provider.smtp.port, + AUTH_TYPE_OAUTH, provider.id, + alt, state[0], + null, null); + } } EntityLog.log(context, "Using username=" + alt); username = alt; @@ -781,7 +807,7 @@ public class FragmentOAuth extends FragmentBase { if (askAccount) identities.add(new Pair<>(username, personal)); else if ("mailru".equals(id)) { - URL url = new URL("https://oauth.mail.ru/userinfo?access_token=" + token); + URL url = new URL("https://oauth.mail.ru/userinfo?access_token=" + token[0]); Log.i("GET " + url); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); connection.setRequestMethod("GET"); @@ -829,7 +855,7 @@ public class FragmentOAuth extends FragmentBase { aservice.connect( inbound.host, inbound.port, AUTH_TYPE_OAUTH, provider.id, - sharedname == null ? username : sharedname, state, + sharedname == null ? username : sharedname, state[0], null, null); if (pop) @@ -839,7 +865,7 @@ public class FragmentOAuth extends FragmentBase { } Long max_size = null; - if (!inbound_only) { + if (!inbound_only && state.length == 1) { EntityLog.log(context, "OAuth checking SMTP provider=" + provider.id); try (EmailService iservice = new EmailService( @@ -848,7 +874,7 @@ public class FragmentOAuth extends FragmentBase { iservice.connect( provider.smtp.host, provider.smtp.port, AUTH_TYPE_OAUTH, provider.id, - username, state, + username, state[0], null, null); max_size = iservice.getMaxSize(); } @@ -881,7 +907,7 @@ public class FragmentOAuth extends FragmentBase { account.auth_type = AUTH_TYPE_OAUTH; account.provider = provider.id; account.user = (sharedname == null ? username : sharedname); - account.password = state; + account.password = state[0]; int at = account.user.indexOf('@'); String user = account.user.substring(0, at); @@ -943,10 +969,10 @@ public class FragmentOAuth extends FragmentBase { ident.host = provider.smtp.host; ident.encryption = iencryption; ident.port = provider.smtp.port; - ident.auth_type = AUTH_TYPE_OAUTH; + ident.auth_type = (state.length == 1 ? AUTH_TYPE_OAUTH : AUTH_TYPE_GRAPH); ident.provider = provider.id; ident.user = username; - ident.password = state; + ident.password = state[state.length - 1]; ident.use_ip = provider.useip; ident.synchronize = true; ident.primary = ident.user.equals(ident.email); @@ -961,8 +987,12 @@ public class FragmentOAuth extends FragmentBase { args.putLong("account", update.id); EntityLog.log(context, "OAuth update account=" + update.name); db.account().setAccountSynchronize(update.id, true); - db.account().setAccountPassword(update.id, state, AUTH_TYPE_OAUTH, provider.id); - db.identity().setIdentityPassword(update.id, username, state, update.auth_type, AUTH_TYPE_OAUTH, provider.id); + db.account().setAccountPassword(update.id, state[0], AUTH_TYPE_OAUTH, provider.id); + db.identity().setIdentityPassword(update.id, username, + state[state.length - 1], + update.auth_type, + (state.length == 1 ? AUTH_TYPE_OAUTH : AUTH_TYPE_GRAPH), + provider.id); } db.setTransactionSuccessful(); diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java index de806653b2..2c5d1dad7f 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java @@ -2106,6 +2106,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc for (String key : prefs.getAll().keySet()) if ((!BuildConfig.DEBUG && key.startsWith("translated_") && cbGeneral.isChecked()) || + key.startsWith("oauth.") || (key.startsWith("announcement.") && cbGeneral.isChecked()) || (key.endsWith(".show_full") && cbFull.isChecked()) || (key.endsWith(".show_images") && cbImages.isChecked()) || diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java index f555990262..f97b871676 100644 --- a/app/src/main/java/eu/faircode/email/FragmentSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java @@ -348,11 +348,6 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS .setNegativeButton(android.R.string.cancel, null) .show(); return true; - } else if (itemId == R.string.title_setup_other || - itemId == R.string.title_setup_outlook) { - lbm.sendBroadcast(new Intent(ActivitySetup.ACTION_QUICK_SETUP) - .putExtra("title", itemId)); - return true; } else if (itemId == R.string.title_setup_classic) { ibManual.setPressed(true); ibManual.setPressed(false); @@ -429,9 +424,6 @@ public class FragmentSetup extends FragmentBase implements SharedPreferences.OnS int resid = res.getIdentifier("provider_" + provider.id, "drawable", pkg); if (resid != 0) item.setIcon(resid); - - if ("office365pcke".equals(provider.id)) - menu.add(alt ? Menu.FIRST : Menu.NONE, R.string.title_setup_outlook, order++, R.string.title_setup_outlook); } return order; diff --git a/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java b/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java index 3cb4887ab8..c7f75bebd2 100644 --- a/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java +++ b/app/src/main/java/eu/faircode/email/ServiceAuthenticator.java @@ -63,6 +63,7 @@ public class ServiceAuthenticator extends Authenticator { static final int AUTH_TYPE_PASSWORD = 1; static final int AUTH_TYPE_GMAIL = 2; static final int AUTH_TYPE_OAUTH = 3; + static final int AUTH_TYPE_GRAPH = 4; static final long MIN_REFRESH_INTERVAL = 15 * 60 * 1000L; static final long MIN_FORCE_REFRESH_INTERVAL = 15 * 60 * 1000L; @@ -155,7 +156,7 @@ public class ServiceAuthenticator extends Authenticator { void onPasswordChanged(Context context, String newPassword); } - private static void OAuthRefresh(Context context, String id, String user, AuthState authState, boolean forceRefresh) + static void OAuthRefresh(Context context, String id, String user, AuthState authState, boolean forceRefresh) throws MessagingException { try { long now = new Date().getTime(); diff --git a/app/src/main/java/eu/faircode/email/ServiceSend.java b/app/src/main/java/eu/faircode/email/ServiceSend.java index b9ef34e200..f5765927e6 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSend.java +++ b/app/src/main/java/eu/faircode/email/ServiceSend.java @@ -19,6 +19,8 @@ package eu.faircode.email; Copyright 2018-2023 by Marcel Bokhorst (M66B) */ +import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GRAPH; + import android.Manifest; import android.app.AlarmManager; import android.app.Notification; @@ -37,6 +39,8 @@ import android.net.Uri; import android.os.PowerManager; import android.os.SystemClock; import android.text.TextUtils; +import android.util.Base64; +import android.util.Base64OutputStream; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; @@ -47,10 +51,17 @@ import androidx.preference.PreferenceManager; import com.sun.mail.smtp.SMTPSendFailedException; import com.sun.mail.util.TraceOutputStream; +import net.openid.appauth.AuthState; + +import org.json.JSONException; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -91,6 +102,8 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar private static final int RETRY_MAX = 3; private static final int CONNECTIVITY_DELAY = 5000; // milliseconds private static final int PROGRESS_UPDATE_INTERVAL = 1000; // milliseconds + private static final int GRAPH_TIMEOUT = 20; // seconds + private static final String GRAPH_ENDPOINT = "https://graph.microsoft.com/v1.0/me/"; static final int PI_SEND = 1; static final int PI_FIX = 2; @@ -555,7 +568,7 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar ServiceSend.start(this); } - private void onSend(EntityMessage message) throws MessagingException, IOException { + private void onSend(EntityMessage message) throws JSONException, MessagingException, IOException { DB db = DB.getInstance(this); // Check if cancelled by user or by errors @@ -714,124 +727,181 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar // Create transport long start, end; Long max_size = null; - EmailService iservice = new EmailService( - this, ident.getProtocol(), ident.realm, ident.encryption, ident.insecure, ident.unicode, debug); - try { - iservice.setUseIp(ident.use_ip, ident.ehlo); - if (!message.isSigned() && !message.isEncrypted()) - iservice.set8BitMime(ident.octetmime); - - // 0=Read receipt - // 1=Delivery receipt - // 2=Read+delivery receipt - - if (message.receipt_request != null && message.receipt_request) { - int receipt_type = prefs.getInt("receipt_type", 2); - if (receipt_type == 1 || receipt_type == 2) // Delivery receipt - iservice.setDsnNotify("SUCCESS,FAILURE,DELAY"); - } + if (ident.auth_type == AUTH_TYPE_GRAPH) { + try { + // https://learn.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0 + db.identity().setIdentityState(ident.id, "connecting"); + + AuthState authState = AuthState.jsonDeserialize(ident.password); + ServiceAuthenticator.OAuthRefresh(ServiceSend.this, ident.provider, ident.user, authState, false); + Long expiration = authState.getAccessTokenExpirationTime(); + if (expiration != null) + EntityLog.log(ServiceSend.this, ident.user + " token expiration=" + new Date(expiration)); + + String newPassword = authState.jsonSerializeString(); + if (!Objects.equals(ident.password, newPassword)) + db.identity().setIdentityPassword(ident.id, newPassword); + + URL url = new URL(GRAPH_ENDPOINT + "sendMail"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setReadTimeout(GRAPH_TIMEOUT * 1000); + connection.setConnectTimeout(GRAPH_TIMEOUT * 1000); + ConnectionHelper.setUserAgent(ServiceSend.this, connection); + connection.setRequestProperty("Authorization", "Bearer " + authState.getAccessToken()); + connection.setRequestProperty("Content-Type", "text/plain"); + connection.connect(); - // Connect transport - db.identity().setIdentityState(ident.id, "connecting"); - iservice.connect(ident); - if (BuildConfig.DEBUG && false) - throw new IOException("Test"); - db.identity().setIdentityState(ident.id, "connected"); - - if (ident.max_size == null) - max_size = iservice.getMaxSize(); - - List
recipients = new ArrayList<>(); - if (message.headers == null || !Boolean.TRUE.equals(message.resend)) { - Address[] all = imessage.getAllRecipients(); - if (all != null) - recipients.addAll(Arrays.asList(all)); - } else { - String to = imessage.getHeader("Resent-To", ","); - if (to != null) - for (Address a : InternetAddress.parse(to)) - recipients.add(a); - - String cc = imessage.getHeader("Resent-Cc", ","); - if (cc != null) - for (Address a : InternetAddress.parse(cc)) - recipients.add(a); - - String bcc = imessage.getHeader("Resent-Bcc", ","); - if (bcc != null) - for (Address a : InternetAddress.parse(bcc)) - recipients.add(a); + try { + db.identity().setIdentityState(ident.id, "connected"); + + EntityLog.log(this, "Sending via Graph user=" + ident.user); + + start = new Date().getTime(); + imessage.writeTo(new Base64OutputStream(connection.getOutputStream(), Base64.DEFAULT)); + end = new Date().getTime(); + + int status = connection.getResponseCode(); + if (status == HttpURLConnection.HTTP_ACCEPTED) + EntityLog.log(this, "Sent via Graph" + ident.user + " elapse=" + (end - start) + " ms"); + else { + String error = "Error " + status + ": " + connection.getResponseMessage(); + try { + InputStream is = connection.getErrorStream(); + if (is != null) + error += "\n" + Helper.readStream(is); + } catch (Throwable ex) { + Log.w(ex); + } + throw new IOException(error); + } + } finally { + connection.disconnect(); + } + } finally { + db.identity().setIdentityState(ident.id, null); } + } else { + EmailService iservice = new EmailService( + this, ident.getProtocol(), ident.realm, ident.encryption, ident.insecure, ident.unicode, debug); + try { + iservice.setUseIp(ident.use_ip, ident.ehlo); + if (!message.isSigned() && !message.isEncrypted()) + iservice.set8BitMime(ident.octetmime); + + // 0=Read receipt + // 1=Delivery receipt + // 2=Read+delivery receipt + + if (message.receipt_request != null && message.receipt_request) { + int receipt_type = prefs.getInt("receipt_type", 2); + if (receipt_type == 1 || receipt_type == 2) // Delivery receipt + iservice.setDsnNotify("SUCCESS,FAILURE,DELAY"); + } - if (protocol && BuildConfig.DEBUG) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - imessage.writeTo(bos); - for (String line : bos.toString().split("\n")) - EntityLog.log(this, line); - } + // Connect transport + db.identity().setIdentityState(ident.id, "connecting"); + iservice.connect(ident); + if (BuildConfig.DEBUG && false) + throw new IOException("Test"); + db.identity().setIdentityState(ident.id, "connected"); + + if (ident.max_size == null) + max_size = iservice.getMaxSize(); + + List
recipients = new ArrayList<>(); + if (message.headers == null || !Boolean.TRUE.equals(message.resend)) { + Address[] all = imessage.getAllRecipients(); + if (all != null) + recipients.addAll(Arrays.asList(all)); + } else { + String to = imessage.getHeader("Resent-To", ","); + if (to != null) + for (Address a : InternetAddress.parse(to)) + recipients.add(a); + + String cc = imessage.getHeader("Resent-Cc", ","); + if (cc != null) + for (Address a : InternetAddress.parse(cc)) + recipients.add(a); + + String bcc = imessage.getHeader("Resent-Bcc", ","); + if (bcc != null) + for (Address a : InternetAddress.parse(bcc)) + recipients.add(a); + } + + if (protocol && BuildConfig.DEBUG) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + imessage.writeTo(bos); + for (String line : bos.toString().split("\n")) + EntityLog.log(this, line); + } - String via = "via " + ident.host + "/" + ident.user + - " recipients=" + TextUtils.join(", ", recipients); - - iservice.setReporter(new TraceOutputStream.IReport() { - private int progress = -1; - private long last = SystemClock.elapsedRealtime(); - - @Override - public void report(int pos, int total) { - int p = (total == 0 ? 0 : 100 * pos / total); - if (p > progress) { - progress = p; - long now = SystemClock.elapsedRealtime(); - if (now > last + PROGRESS_UPDATE_INTERVAL) { - last = now; - lastProgress = progress; - if (NotificationHelper.areNotificationsEnabled(nm)) - nm.notify(NotificationHelper.NOTIFICATION_SEND, getNotificationService(false)); + String via = "via " + ident.host + "/" + ident.user + + " recipients=" + TextUtils.join(", ", recipients); + + iservice.setReporter(new TraceOutputStream.IReport() { + private int progress = -1; + private long last = SystemClock.elapsedRealtime(); + + @Override + public void report(int pos, int total) { + int p = (total == 0 ? 0 : 100 * pos / total); + if (p > progress) { + progress = p; + long now = SystemClock.elapsedRealtime(); + if (now > last + PROGRESS_UPDATE_INTERVAL) { + last = now; + lastProgress = progress; + if (NotificationHelper.areNotificationsEnabled(nm)) + nm.notify(NotificationHelper.NOTIFICATION_SEND, getNotificationService(false)); + } } } + }); + + // Send message + EntityLog.log(this, "Sending " + via); + start = new Date().getTime(); + iservice.getTransport().sendMessage(imessage, recipients.toArray(new Address[0])); + end = new Date().getTime(); + EntityLog.log(this, "Sent " + via + " elapse=" + (end - start) + " ms"); + } catch (MessagingException ex) { + iservice.dump(ident.email); + Log.e(ex); + + if (ex instanceof SMTPSendFailedException) { + SMTPSendFailedException sem = (SMTPSendFailedException) ex; + ex = new SMTPSendFailedException( + sem.getCommand(), + sem.getReturnCode(), + getString(R.string.title_service_auth, sem.getMessage()), + sem.getNextException(), + sem.getValidSentAddresses(), + sem.getValidUnsentAddresses(), + sem.getInvalidAddresses()); } - }); - - // Send message - EntityLog.log(this, "Sending " + via); - start = new Date().getTime(); - iservice.getTransport().sendMessage(imessage, recipients.toArray(new Address[0])); - end = new Date().getTime(); - EntityLog.log(this, "Sent " + via + " elapse=" + (end - start) + " ms"); - } catch (MessagingException ex) { - iservice.dump(ident.email); - Log.e(ex); - - if (ex instanceof SMTPSendFailedException) { - SMTPSendFailedException sem = (SMTPSendFailedException) ex; - ex = new SMTPSendFailedException( - sem.getCommand(), - sem.getReturnCode(), - getString(R.string.title_service_auth, sem.getMessage()), - sem.getNextException(), - sem.getValidSentAddresses(), - sem.getValidUnsentAddresses(), - sem.getInvalidAddresses()); - } - if (sid != null) - db.message().deleteMessage(sid); + if (sid != null) + db.message().deleteMessage(sid); - db.identity().setIdentityError(ident.id, Log.formatThrowable(ex)); + db.identity().setIdentityError(ident.id, Log.formatThrowable(ex)); - throw ex; - } catch (Throwable ex) { - iservice.dump(ident.email); - throw ex; - } finally { - iservice.close(); - if (lastProgress >= 0) { - lastProgress = -1; - if (NotificationHelper.areNotificationsEnabled(nm)) - nm.notify(NotificationHelper.NOTIFICATION_SEND, getNotificationService(false)); + throw ex; + } catch (Throwable ex) { + iservice.dump(ident.email); + throw ex; + } finally { + iservice.close(); + if (lastProgress >= 0) { + lastProgress = -1; + if (NotificationHelper.areNotificationsEnabled(nm)) + nm.notify(NotificationHelper.NOTIFICATION_SEND, getNotificationService(false)); + } + db.identity().setIdentityState(ident.id, null); } - db.identity().setIdentityState(ident.id, null); } try { diff --git a/app/src/main/res/drawable/twotone_show_chart_24.xml b/app/src/main/res/drawable/twotone_show_chart_24.xml new file mode 100644 index 0000000000..d912ab2ad2 --- /dev/null +++ b/app/src/main/res/drawable/twotone_show_chart_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0580c3998..6ee264ec35 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -217,7 +217,6 @@ %1$s (OAuth) %1$s (Android) Sign in with Google - Outlook/Hotmail/Live Other provider Incoming email only (email cannot be sent!) POP3 account diff --git a/app/src/main/res/xml/providers.xml b/app/src/main/res/xml/providers.xml index 3e807d495f..ca42f1d059 100644 --- a/app/src/main/res/xml/providers.xml +++ b/app/src/main/res/xml/providers.xml @@ -199,14 +199,13 @@ + @@ -293,7 +303,7 @@