Added Outlook graph send

pull/212/head
M66B 2 years ago
parent 106f976f1b
commit ad83451220

@ -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<AdapterIdentity.ViewHo
ivSync.setContentDescription(context.getString(identity.synchronize ? R.string.title_legend_synchronize_on : R.string.title_legend_synchronize_off));
ivOAuth.setVisibility(identity.auth_type == AUTH_TYPE_PASSWORD ? View.GONE : View.VISIBLE);
ivOAuth.setImageResource(identity.auth_type == AUTH_TYPE_OAUTH
? R.drawable.twotone_security_24
: R.drawable.twotone_show_chart_24);
ivPrimary.setVisibility(identity.primary ? View.VISIBLE : View.GONE);
ivGroup.setVisibility(identity.self ? View.GONE : View.VISIBLE);
tvName.setText(identity.getDisplayName());

@ -99,6 +99,7 @@ public class EmailProvider implements Parcelable {
public Server smtp = new Server();
public Server pop;
public OAuth oauth;
public OAuth graph;
public UserType user = UserType.EMAIL;
public String username;
public StringBuilder documentation; // html
@ -305,6 +306,19 @@ public class EmailProvider implements Parcelable {
provider.oauth.redirectUri = xml.getAttributeValue(null, "redirectUri");
provider.oauth.privacy = xml.getAttributeValue(null, "privacy");
provider.oauth.prompt = xml.getAttributeValue(null, "prompt");
} else if ("graph".equals(name)) {
provider.graph = new OAuth();
provider.graph.enabled = getAttributeBooleanValue(xml, "enabled", false);
provider.graph.askAccount = getAttributeBooleanValue(xml, "askAccount", false);
provider.graph.clientId = xml.getAttributeValue(null, "clientId");
provider.graph.clientSecret = xml.getAttributeValue(null, "clientSecret");
provider.graph.scopes = xml.getAttributeValue(null, "scopes").split(",");
provider.graph.authorizationEndpoint = xml.getAttributeValue(null, "authorizationEndpoint");
provider.graph.tokenEndpoint = xml.getAttributeValue(null, "tokenEndpoint");
provider.graph.tokenScopes = getAttributeBooleanValue(xml, "tokenScopes", false);
provider.graph.redirectUri = xml.getAttributeValue(null, "redirectUri");
provider.graph.privacy = xml.getAttributeValue(null, "privacy");
provider.graph.prompt = xml.getAttributeValue(null, "prompt");
} else if ("parameter".equals(name)) {
if (provider.oauth.parameters == null)
provider.oauth.parameters = new LinkedHashMap<>();

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

@ -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<String, String> params = (provider.oauth.parameters == null
Map<String, String> 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.<String, String>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<String> 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();

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

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

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

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

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M13.5,13.48l-4,-4L2,16.99l1.5,1.5 6,-6.01 4,4L22,6.92l-1.41,-1.41z"/>
</vector>

@ -217,7 +217,6 @@
<string name="title_setup_oauth" translatable="false">%1$s (OAuth)</string>
<string name="title_setup_android" translatable="false">%1$s (Android)</string>
<string name="title_setup_google_sign_in">Sign in with Google</string>
<string name="title_setup_outlook" translatable="false">Outlook/Hotmail/Live</string>
<string name="title_setup_other">Other provider</string>
<string name="title_setup_inbound">Incoming email only (email cannot be sent!)</string>
<string name="title_setup_pop3">POP3 account</string>

@ -199,14 +199,13 @@
<!-- https://learn.microsoft.com/en-us/azure/active-directory/develop/redirect-uris-ios -->
</provider>
<provider
name="Outlook test"
debug="true"
description="Office 365 test"
id="outlooktest"
name="Outlook"
description="Outlook/Hotmail/Live"
id="outlookgraph"
link="https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq14"
maxtls="1.2"
noop="true"
order="5"
order="6"
partial="false"
useip="false">
<imap
@ -223,19 +222,30 @@
starttls="false" />
<oauth
askAccount="true"
authorizationEndpoint="https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"
clientId="835c3383-543c-4aa2-98ca-c465eb039987"
enabled="false"
authorizationEndpoint="https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize"
clientId="3514cf2c-e7a3-45a2-80d4-6a3c3498eca0"
enabled="true"
privacy="https://privacy.microsoft.com/privacystatement"
prompt="login"
redirectUri="msauth://eu.faircode.email/xSLzBBuLJunOQPB89rtzM54FXx4%3D"
scopes="profile,openid,email,offline_access,https://outlook.office.com/IMAP.AccessAsUser.All,https://outlook.office.com/SMTP.Send"
tokenEndpoint="https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
redirectUri="msauth.eu.faircode.email://auth"
scopes="profile,openid,email,offline_access,https://outlook.office.com/IMAP.AccessAsUser.All,https://outlook.office.com/SMTP.Send,https://outlook.office.com/POP.AccessAsUser.All"
tokenEndpoint="https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
tokenScopes="true">
<!--parameter
key="domain_hint"
value="9188040d-6c67-4c5b-b112-36a304b66dad" /-->
</oauth>
<graph
askAccount="true"
authorizationEndpoint="https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
clientId="835c3383-543c-4aa2-98ca-c465eb039987"
enabled="true"
privacy="https://privacy.microsoft.com/privacystatement"
prompt="login"
redirectUri="msauth://eu.faircode.email/xSLzBBuLJunOQPB89rtzM54FXx4%3D"
scopes="offline_access,https://graph.microsoft.com/Mail.Send"
tokenEndpoint="https://login.microsoftonline.com/common/oauth2/v2.0/token"
tokenScopes="true" />
<!-- https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow -->
<!-- https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth -->
<!-- https://learn.microsoft.com/en-us/azure/active-directory/develop/redirect-uris-ios -->
@ -293,7 +303,7 @@
<provider
name="Yahoo.co.jp"
domain="yahoo\\.co\\.jp"
order="6"
order="7"
user="local">
<imap
host="imap.mail.yahoo.co.jp"
@ -314,7 +324,7 @@
id="yahoo"
link="https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq88"
noop="true"
order="7"
order="8"
partial="true">
<imap
host="imap.mail.yahoo.com"
@ -348,7 +358,7 @@
id="yahoo2"
link="https://github.com/M66B/FairEmail/blob/master/FAQ.md#user-content-faq88"
noop="true"
order="7"
order="8"
partial="true">
<imap
host="imap.mail.yahoo.com"

Loading…
Cancel
Save