Files
FairEmail/app/src/main/java/eu/faircode/email/EmailService.java

837 lines
34 KiB
Java
Raw Normal View History

2019-07-29 11:17:12 +02:00
package eu.faircode.email;
2019-09-18 16:34:07 +02:00
import android.accounts.Account;
import android.accounts.AccountManager;
2020-02-01 10:36:24 +01:00
import android.accounts.AuthenticatorException;
2019-07-29 11:17:12 +02:00
import android.content.Context;
2019-10-21 13:26:15 +02:00
import android.content.SharedPreferences;
2019-09-18 16:34:07 +02:00
import android.text.TextUtils;
2019-07-29 11:17:12 +02:00
2019-12-16 19:09:49 +01:00
import androidx.annotation.NonNull;
2019-10-21 13:26:15 +02:00
import androidx.preference.PreferenceManager;
2019-09-18 16:34:07 +02:00
import com.sun.mail.imap.IMAPFolder;
2019-07-29 11:17:12 +02:00
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.smtp.SMTPTransport;
2019-12-20 17:20:06 +01:00
import net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationService;
2019-12-21 16:03:57 +01:00
import net.openid.appauth.ClientAuthentication;
import net.openid.appauth.ClientSecretPost;
import net.openid.appauth.NoClientAuthentication;
2019-12-20 17:20:06 +01:00
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
2019-12-17 16:11:28 +01:00
import org.jetbrains.annotations.NotNull;
2019-07-29 11:17:12 +02:00
2019-12-16 16:32:25 +01:00
import java.io.IOException;
2019-07-29 11:17:12 +02:00
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
2019-12-17 16:11:28 +01:00
import java.net.Socket;
2019-07-29 11:17:12 +02:00
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
2019-12-17 16:11:28 +01:00
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
2019-12-19 17:15:29 +01:00
import java.security.Principal;
import java.security.cert.CertificateEncodingException;
2019-12-18 14:20:16 +01:00
import java.security.cert.CertificateException;
2019-12-16 16:32:25 +01:00
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
2019-09-18 16:34:07 +02:00
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
2019-07-29 11:17:12 +02:00
import java.util.HashMap;
import java.util.LinkedHashMap;
2019-09-18 16:34:07 +02:00
import java.util.List;
2019-07-29 11:17:12 +02:00
import java.util.Map;
import java.util.Properties;
2019-07-30 20:53:37 +02:00
import java.util.concurrent.ExecutorService;
2019-12-20 17:20:06 +01:00
import java.util.concurrent.Semaphore;
2020-01-29 12:44:55 +01:00
import java.util.regex.Pattern;
2019-07-29 11:17:12 +02:00
2019-09-18 16:34:07 +02:00
import javax.mail.AuthenticationFailedException;
import javax.mail.Folder;
2019-07-29 11:17:12 +02:00
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Service;
import javax.mail.Session;
2019-09-19 17:41:26 +02:00
import javax.mail.Store;
2019-12-09 16:32:37 +01:00
import javax.mail.event.StoreListener;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
2019-12-17 16:11:28 +01:00
import javax.net.ssl.SSLContext;
2020-01-28 16:32:26 +01:00
import javax.net.ssl.SSLSocket;
2019-12-18 14:20:16 +01:00
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
2019-12-17 16:11:28 +01:00
import javax.net.ssl.TrustManagerFactory;
2019-12-18 14:20:16 +01:00
import javax.net.ssl.X509TrustManager;
2019-07-29 11:17:12 +02:00
2020-01-29 21:06:45 +01:00
public class EmailService implements AutoCloseable {
2019-07-29 11:17:12 +02:00
private Context context;
private String protocol;
2019-12-17 16:11:28 +01:00
private boolean insecure;
2020-01-29 12:44:55 +01:00
private boolean harden;
private boolean useip;
2019-07-29 11:17:12 +02:00
private boolean debug;
private Properties properties;
private Session isession;
private Service iservice;
2019-12-09 16:32:37 +01:00
private StoreListener listener;
2019-07-29 11:17:12 +02:00
2019-10-10 13:26:44 +02:00
private ExecutorService executor = Helper.getBackgroundExecutor(0, "mail");
2019-07-30 20:53:37 +02:00
2019-09-18 16:34:07 +02:00
static final int AUTH_TYPE_PASSWORD = 1;
static final int AUTH_TYPE_GMAIL = 2;
2019-12-20 17:20:06 +01:00
static final int AUTH_TYPE_OAUTH = 3;
2019-09-18 16:34:07 +02:00
2020-02-06 13:01:11 +01:00
static final int PURPOSE_CHECK = 1;
static final int PURPOSE_USE = 2;
static final int PURPOSE_SEARCH = 3;
private final static int SEARCH_TIMEOUT = 2 * 60 * 1000; // milliseconds
private final static int CONNECT_TIMEOUT = 60 * 1000; // milliseconds
2019-07-29 16:42:17 +02:00
private final static int WRITE_TIMEOUT = 60 * 1000; // milliseconds
private final static int READ_TIMEOUT = 60 * 1000; // milliseconds
2020-01-21 11:09:57 +01:00
private final static int FETCH_SIZE = 1024 * 1024; // bytes, default 16K
2019-07-29 16:42:17 +02:00
private final static int POOL_TIMEOUT = 45 * 1000; // milliseconds, default 45 sec
private static final int APPEND_BUFFER_SIZE = 4 * 1024 * 1024; // bytes
2020-01-29 12:44:55 +01:00
private static final List<String> SSL_PROTOCOL_BLACKLIST = Collections.unmodifiableList(Arrays.asList(
"SSLv2", "SSLv3", "TLSv1", "TLSv1.1"
));
2020-01-29 21:06:45 +01:00
private EmailService() {
2020-01-29 12:44:55 +01:00
// Prevent instantiation
2019-07-29 11:17:12 +02:00
}
2020-02-06 13:01:11 +01:00
EmailService(Context context, String protocol, String realm, boolean insecure, boolean debug) throws NoSuchProviderException {
this(context, protocol, realm, insecure, PURPOSE_USE, debug);
}
EmailService(Context context, String protocol, String realm, boolean insecure, int purpose, boolean debug) throws NoSuchProviderException {
2019-07-29 16:42:17 +02:00
this.context = context.getApplicationContext();
2019-07-29 11:17:12 +02:00
this.protocol = protocol;
2019-12-17 16:11:28 +01:00
this.insecure = insecure;
2019-07-29 11:17:12 +02:00
this.debug = debug;
2019-10-03 11:39:14 +02:00
2019-07-30 18:53:18 +02:00
properties = MessageHelper.getSessionProperties();
2019-07-29 16:42:17 +02:00
2019-10-21 13:26:15 +02:00
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
2020-01-29 12:44:55 +01:00
this.harden = prefs.getBoolean("ssl_harden", false);
2019-10-21 13:26:15 +02:00
boolean socks_enabled = prefs.getBoolean("socks_enabled", false);
String socks_proxy = prefs.getString("socks_proxy", "localhost:9050");
// SOCKS proxy
if (socks_enabled) {
String[] address = socks_proxy.split(":");
String host = (address.length > 0 ? address[0] : null);
String port = (address.length > 1 ? address[1] : null);
if (TextUtils.isEmpty(host))
host = "localhost";
if (TextUtils.isEmpty(port))
port = "9050";
properties.put("mail." + protocol + ".socks.host", host);
properties.put("mail." + protocol + ".socks.port", port);
Log.i("Using SOCKS proxy=" + host + ":" + port);
}
properties.put("mail.event.scope", "folder");
2019-07-30 20:53:37 +02:00
properties.put("mail.event.executor", executor);
2019-07-29 16:42:17 +02:00
2020-01-23 12:38:31 +01:00
properties.put("mail." + protocol + ".sasl.enable", "true");
properties.put("mail." + protocol + ".sasl.mechanisms", "CRAM-MD5");
2019-09-29 20:25:01 +02:00
properties.put("mail." + protocol + ".sasl.realm", realm == null ? "" : realm);
properties.put("mail." + protocol + ".auth.ntlm.domain", realm == null ? "" : realm);
2019-10-19 10:05:43 +02:00
// TODO: make timeouts configurable?
2020-02-06 13:01:11 +01:00
// writetimeout: one thread overhead
if (purpose == PURPOSE_SEARCH) {
2020-02-06 13:01:11 +01:00
properties.put("mail." + protocol + ".connectiontimeout", Integer.toString(SEARCH_TIMEOUT));
properties.put("mail." + protocol + ".writetimeout", Integer.toString(SEARCH_TIMEOUT));
properties.put("mail." + protocol + ".timeout", Integer.toString(SEARCH_TIMEOUT));
} else {
properties.put("mail." + protocol + ".connectiontimeout", Integer.toString(CONNECT_TIMEOUT));
properties.put("mail." + protocol + ".writetimeout", Integer.toString(WRITE_TIMEOUT));
properties.put("mail." + protocol + ".timeout", Integer.toString(READ_TIMEOUT));
}
2019-10-19 10:05:43 +02:00
2019-10-03 11:39:14 +02:00
if (debug && BuildConfig.DEBUG)
properties.put("mail.debug.auth", "true");
2019-07-29 16:42:17 +02:00
2019-09-19 17:41:26 +02:00
if ("pop3".equals(protocol) || "pop3s".equals(protocol)) {
2019-09-20 09:45:36 +02:00
this.debug = true;
2019-09-19 17:41:26 +02:00
// https://javaee.github.io/javamail/docs/api/com/sun/mail/pop3/package-summary.html#properties
properties.put("mail.pop3s.starttls.enable", "false");
properties.put("mail.pop3.starttls.enable", "true");
2019-10-03 11:39:14 +02:00
properties.put("mail.pop3.starttls.required", Boolean.toString(!insecure));
2019-09-19 17:41:26 +02:00
} else if ("imap".equals(protocol) || "imaps".equals(protocol)) {
2019-07-29 16:42:17 +02:00
// https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
2019-07-30 18:53:18 +02:00
properties.put("mail.imaps.starttls.enable", "false");
2019-07-29 16:42:17 +02:00
2019-07-30 18:53:18 +02:00
properties.put("mail.imap.starttls.enable", "true");
2019-10-03 11:39:14 +02:00
properties.put("mail.imap.starttls.required", Boolean.toString(!insecure));
2019-07-29 16:42:17 +02:00
2019-12-09 16:32:37 +01:00
properties.put("mail." + protocol + ".separatestoreconnection", "true");
2019-07-30 18:53:18 +02:00
properties.put("mail." + protocol + ".connectionpool.debug", "true");
2019-12-09 14:03:18 +01:00
properties.put("mail." + protocol + ".connectionpoolsize", "1");
2019-07-30 18:53:18 +02:00
properties.put("mail." + protocol + ".connectionpooltimeout", Integer.toString(POOL_TIMEOUT));
2019-07-29 16:42:17 +02:00
2019-07-30 18:53:18 +02:00
properties.put("mail." + protocol + ".finalizecleanclose", "false");
2019-10-13 09:54:13 +02:00
//properties.put("mail." + protocol + ".closefoldersonstorefailure", "false");
2019-07-29 16:42:17 +02:00
// https://tools.ietf.org/html/rfc4978
// https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
2019-07-30 18:53:18 +02:00
properties.put("mail." + protocol + ".compress.enable", "true");
//properties.put("mail.imaps.compress.level", "-1");
//properties.put("mail.imaps.compress.strategy", "0");
2019-07-29 16:42:17 +02:00
2019-07-30 18:53:18 +02:00
properties.put("mail." + protocol + ".throwsearchexception", "true");
properties.put("mail." + protocol + ".fetchsize", Integer.toString(FETCH_SIZE));
properties.put("mail." + protocol + ".peek", "true");
properties.put("mail." + protocol + ".appendbuffersize", Integer.toString(APPEND_BUFFER_SIZE));
2019-07-29 16:42:17 +02:00
} else if ("smtp".equals(protocol) || "smtps".equals(protocol)) {
// https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
2019-07-30 18:53:18 +02:00
properties.put("mail.smtps.starttls.enable", "false");
2019-07-29 16:42:17 +02:00
2019-07-30 18:53:18 +02:00
properties.put("mail.smtp.starttls.enable", "true");
2019-10-03 11:39:14 +02:00
properties.put("mail.smtp.starttls.required", Boolean.toString(!insecure));
2019-07-29 16:42:17 +02:00
2019-07-30 18:53:18 +02:00
properties.put("mail." + protocol + ".auth", "true");
2019-07-29 16:42:17 +02:00
} else
throw new NoSuchProviderException(protocol);
2019-07-29 11:17:12 +02:00
}
void setPartialFetch(boolean enabled) {
properties.put("mail." + protocol + ".partialfetch", Boolean.toString(enabled));
}
void setIgnoreBodyStructureSize(boolean enabled) {
properties.put("mail." + protocol + ".ignorebodystructuresize", Boolean.toString(enabled));
2019-07-29 11:17:12 +02:00
}
void setUseIp(boolean enabled) {
2019-07-30 18:53:18 +02:00
useip = enabled;
2019-07-29 11:17:12 +02:00
}
void setLeaveOnServer(boolean keep) {
properties.put("mail." + protocol + ".rsetbeforequit", Boolean.toString(keep));
}
2019-12-09 16:32:37 +01:00
void setListener(StoreListener listener) {
this.listener = listener;
}
2019-07-29 11:17:12 +02:00
public void connect(EntityAccount account) throws MessagingException {
String password = connect(
account.host, account.port,
account.auth_type, account.provider,
account.user, account.password,
account.certificate, account.fingerprint);
2019-09-19 13:21:37 +02:00
if (password != null) {
DB db = DB.getInstance(context);
int count = db.account().setAccountPassword(account.id, account.password);
Log.i(account.name + " token refreshed=" + count);
}
2019-07-29 11:17:12 +02:00
}
public void connect(EntityIdentity identity) throws MessagingException {
String password = connect(
identity.host, identity.port,
identity.auth_type, identity.provider,
identity.user, identity.password,
identity.certificate, identity.fingerprint);
2019-09-19 13:21:37 +02:00
if (password != null) {
DB db = DB.getInstance(context);
int count = db.identity().setIdentityPassword(identity.id, identity.password);
Log.i(identity.email + " token refreshed=" + count);
}
2019-07-29 11:17:12 +02:00
}
2020-01-15 08:58:31 +01:00
public String connect(
String host, int port,
int auth, String provider, String user, String password,
boolean certificate, String fingerprint) throws MessagingException {
2019-12-19 11:53:37 +01:00
SSLSocketFactoryService factory = null;
2019-12-17 16:11:28 +01:00
try {
factory = new SSLSocketFactoryService(host, insecure, harden, certificate, fingerprint);
2019-12-17 16:11:28 +01:00
properties.put("mail." + protocol + ".ssl.socketFactory", factory);
properties.put("mail." + protocol + ".socketFactory.fallback", "false");
2019-12-18 14:20:16 +01:00
properties.put("mail." + protocol + ".ssl.checkserveridentity", "false");
2019-12-17 16:11:28 +01:00
} catch (GeneralSecurityException ex) {
2019-12-18 14:20:16 +01:00
properties.put("mail." + protocol + ".ssl.checkserveridentity", Boolean.toString(!insecure));
if (insecure)
properties.put("mail." + protocol + ".ssl.trust", "*");
2019-12-19 11:53:37 +01:00
Log.e("Trust issues", ex);
2019-12-17 16:11:28 +01:00
}
2019-07-29 11:17:12 +02:00
try {
2019-12-20 17:20:06 +01:00
if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH)
2019-09-18 16:34:07 +02:00
properties.put("mail." + protocol + ".auth.mechanisms", "XOAUTH2");
2019-12-20 17:20:06 +01:00
if (auth == AUTH_TYPE_OAUTH) {
2019-12-21 16:03:57 +01:00
AuthState authState = OAuthRefresh(context, provider, password);
2020-01-15 08:58:31 +01:00
connect(host, port, user, authState.getAccessToken(), factory);
2019-12-20 17:20:06 +01:00
return authState.jsonSerializeString();
} else {
2020-01-15 08:58:31 +01:00
connect(host, port, user, password, factory);
2019-12-20 17:20:06 +01:00
return null;
}
2019-09-18 16:34:07 +02:00
} catch (AuthenticationFailedException ex) {
// Refresh token
if (auth == AUTH_TYPE_GMAIL)
try {
String type = "com.google";
AccountManager am = AccountManager.get(context);
Account[] accounts = am.getAccountsByType(type);
for (Account account : accounts)
if (user.equals(account.name)) {
Log.i("Refreshing token user=" + user);
am.invalidateAuthToken(type, password);
2019-09-18 22:11:48 +02:00
String token = am.blockingGetAuthToken(account, getAuthTokenType(type), true);
if (token == null)
2020-02-01 10:36:24 +01:00
throw new AuthenticatorException("No token on refresh");
2019-09-18 16:34:07 +02:00
2020-01-15 08:58:31 +01:00
connect(host, port, user, token, factory);
2019-09-19 13:21:37 +02:00
return token;
2019-09-18 16:34:07 +02:00
}
2019-09-19 13:21:37 +02:00
2020-02-01 10:36:24 +01:00
throw new AuthenticatorException("Account not found");
2019-09-24 15:34:39 +02:00
} catch (Exception ex1) {
2019-09-18 16:34:07 +02:00
Log.e(ex1);
2019-09-24 15:34:39 +02:00
throw new AuthenticationFailedException(ex.getMessage(), ex1);
2019-09-18 16:34:07 +02:00
}
2019-12-20 17:20:06 +01:00
else if (auth == AUTH_TYPE_OAUTH) {
2019-12-21 16:03:57 +01:00
AuthState authState = OAuthRefresh(context, provider, password);
2020-01-15 08:58:31 +01:00
connect(host, port, user, authState.getAccessToken(), factory);
2019-12-20 17:20:06 +01:00
return authState.jsonSerializeString();
} else
2019-09-18 16:34:07 +02:00
throw ex;
2020-01-15 08:58:31 +01:00
}
}
private void connect(
String host, int port, String user, String password,
SSLSocketFactoryService factory) throws MessagingException {
try {
//if (BuildConfig.DEBUG)
// throw new MailConnectException(
// new SocketConnectException("Debug", new IOException("Test"), host, port, 0));
_connect(host, port, user, password, factory);
} catch (MessagingException ex) {
boolean ioError = false;
Throwable ce = ex;
while (ce != null) {
if (factory != null && ce instanceof CertificateException)
throw new UntrustedException(factory.getFingerPrintSelect(), ex);
2020-01-15 08:58:31 +01:00
if (ce instanceof IOException)
ioError = true;
ce = ce.getCause();
}
if (ioError) {
try {
// Some devices resolve IPv6 addresses while not having IPv6 connectivity
InetAddress[] iaddrs = InetAddress.getAllByName(host);
Log.i("Fallback count=" + iaddrs.length);
if (iaddrs.length > 1)
for (InetAddress iaddr : iaddrs)
try {
Log.i("Falling back to " + iaddr.getHostAddress());
_connect(iaddr.getHostAddress(), port, user, password, factory);
return;
} catch (MessagingException ex1) {
Log.w(ex1);
}
} catch (Throwable ex1) {
Log.w(ex1);
}
}
throw ex;
2019-07-29 11:17:12 +02:00
}
}
2019-12-18 14:20:16 +01:00
private void _connect(
String host, int port, String user, String password,
SSLSocketFactoryService factory) throws MessagingException {
2020-01-15 08:58:31 +01:00
isession = Session.getInstance(properties, null);
isession.setDebug(debug);
//System.setProperty("mail.socket.debug", Boolean.toString(debug));
2019-12-18 14:20:16 +01:00
2020-01-15 08:58:31 +01:00
if ("pop3".equals(protocol) || "pop3s".equals(protocol)) {
isession.setDebug(true);
iservice = isession.getStore(protocol);
iservice.connect(host, port, user, password);
2019-12-18 14:20:16 +01:00
2020-01-15 08:58:31 +01:00
} else if ("imap".equals(protocol) || "imaps".equals(protocol)) {
iservice = isession.getStore(protocol);
if (listener != null)
((IMAPStore) iservice).addStoreListener(listener);
iservice.connect(host, port, user, password);
// https://www.ietf.org/rfc/rfc2971.txt
IMAPStore istore = (IMAPStore) getStore();
if (istore.hasCapability("ID"))
try {
Map<String, String> id = new LinkedHashMap<>();
id.put("name", context.getString(R.string.app_name));
id.put("version", BuildConfig.VERSION_NAME);
Map<String, String> sid = istore.id(id);
if (sid != null) {
Map<String, String> crumb = new HashMap<>();
for (String key : sid.keySet()) {
crumb.put(key, sid.get(key));
EntityLog.log(context, "Server " + key + "=" + sid.get(key));
2019-07-29 11:17:12 +02:00
}
2020-01-15 08:58:31 +01:00
Log.breadcrumb("server", crumb);
2019-12-18 14:20:16 +01:00
}
2020-01-15 08:58:31 +01:00
} catch (MessagingException ex) {
Log.w(ex);
}
2020-01-15 08:58:31 +01:00
} else if ("smtp".equals(protocol) || "smtps".equals(protocol)) {
String[] c = BuildConfig.APPLICATION_ID.split("\\.");
Collections.reverse(Arrays.asList(c));
String hdomain = TextUtils.join(".", c);
String haddr = "[127.0.0.1]";
try {
// This assumes getByName always returns the same address (type)
InetAddress addr = InetAddress.getByName(host);
if (addr instanceof Inet4Address)
haddr = "[" + Inet4Address.getLocalHost().getHostAddress() + "]";
else
haddr = "[IPv6:" + Inet6Address.getLocalHost().getHostAddress() + "]";
} catch (UnknownHostException ex) {
Log.w(ex);
}
2020-01-15 08:58:31 +01:00
Log.i("Using localhost=" + haddr);
properties.put("mail." + protocol + ".localhost", useip ? haddr : hdomain);
2020-01-15 08:58:31 +01:00
iservice = isession.getTransport(protocol);
try {
iservice.connect(host, port, user, password);
} catch (MessagingException ex) {
if (ex.getMessage() != null &&
2020-01-15 08:58:31 +01:00
ex.getMessage().toLowerCase().contains("syntactically invalid")) {
Log.w("Using localhost=" + (useip ? hdomain : haddr), ex);
((SMTPTransport) iservice).setLocalHost(useip ? hdomain : haddr);
2020-01-15 08:58:31 +01:00
iservice.connect(host, port, user, password);
} else
throw ex;
2020-01-12 19:10:10 +01:00
}
2020-01-15 08:58:31 +01:00
} else
throw new NoSuchProviderException(protocol);
2019-07-29 11:17:12 +02:00
}
2019-12-20 17:20:06 +01:00
private static class ErrorHolder {
AuthorizationException error;
}
2019-12-21 16:03:57 +01:00
private static AuthState OAuthRefresh(Context context, String id, String json) throws MessagingException {
2019-12-20 17:20:06 +01:00
try {
AuthState authState = AuthState.jsonDeserialize(json);
2019-12-21 16:03:57 +01:00
ClientAuthentication clientAuth;
EmailProvider provider = EmailProvider.getProvider(context, id);
if (provider.oauth.clientSecret == null)
clientAuth = NoClientAuthentication.INSTANCE;
else
clientAuth = new ClientSecretPost(provider.oauth.clientSecret);
2019-12-20 17:20:06 +01:00
ErrorHolder holder = new ErrorHolder();
2019-12-21 16:03:57 +01:00
Semaphore semaphore = new Semaphore(0);
2019-12-20 17:20:06 +01:00
Log.i("OAuth refresh");
AuthorizationService authService = new AuthorizationService(context);
2019-12-21 16:03:57 +01:00
authState.performActionWithFreshTokens(
authService,
clientAuth,
new AuthState.AuthStateAction() {
@Override
public void execute(String accessToken, String idToken, AuthorizationException error) {
if (error != null)
holder.error = error;
semaphore.release();
}
});
2019-12-20 17:20:06 +01:00
semaphore.acquire();
Log.i("OAuth refreshed");
if (holder.error != null)
throw holder.error;
return authState;
} catch (Exception ex) {
throw new MessagingException("OAuth refresh", ex);
}
}
2019-09-18 16:34:07 +02:00
static String getAuthTokenType(String type) {
// https://developers.google.com/gmail/imap/xoauth2-protocol
if ("com.google".equals(type))
return "oauth2:https://mail.google.com/";
return null;
}
List<EntityFolder> getFolders() throws MessagingException {
List<EntityFolder> folders = new ArrayList<>();
List<EntityFolder> guesses = new ArrayList<>();
for (Folder ifolder : getStore().getDefaultFolder().list("*")) {
String fullName = ifolder.getFullName();
String[] attrs = ((IMAPFolder) ifolder).getAttributes();
String type = EntityFolder.getType(attrs, fullName, true);
Log.i(fullName + " attrs=" + TextUtils.join(" ", attrs) + " type=" + type);
if (type != null) {
EntityFolder folder = new EntityFolder(fullName, type);
folders.add(folder);
if (EntityFolder.USER.equals(type)) {
String guess = EntityFolder.guessType(fullName);
if (guess != null)
guesses.add(folder);
}
}
}
for (EntityFolder guess : guesses) {
boolean has = false;
String gtype = EntityFolder.guessType(guess.name);
for (EntityFolder folder : folders)
if (folder.type.equals(gtype)) {
has = true;
break;
}
if (!has) {
guess.type = gtype;
2020-02-06 12:24:32 +01:00
guess.setProperties();
2019-09-18 16:34:07 +02:00
Log.i(guess.name + " guessed type=" + gtype);
}
}
boolean inbox = false;
boolean drafts = false;
for (EntityFolder folder : folders)
if (EntityFolder.INBOX.equals(folder.type))
inbox = true;
else if (EntityFolder.DRAFTS.equals(folder.type))
drafts = true;
if (!inbox || !drafts)
return null;
return folders;
}
2019-09-19 17:41:26 +02:00
Store getStore() {
return (Store) iservice;
2019-07-29 11:17:12 +02:00
}
SMTPTransport getTransport() {
2019-07-30 18:53:18 +02:00
return (SMTPTransport) iservice;
2019-07-29 11:17:12 +02:00
}
2019-09-19 17:41:26 +02:00
boolean hasCapability(String capability) throws MessagingException {
Store store = getStore();
if (store instanceof IMAPStore)
return ((IMAPStore) getStore()).hasCapability(capability);
else
return false;
}
2019-07-29 11:17:12 +02:00
public void close() throws MessagingException {
try {
2019-10-08 21:59:49 +02:00
if (iservice != null && iservice.isConnected())
2019-07-29 11:17:12 +02:00
iservice.close();
} finally {
2019-07-30 18:53:18 +02:00
context = null;
2019-07-29 11:17:12 +02:00
}
}
2019-12-16 16:32:25 +01:00
2019-12-18 14:20:16 +01:00
private static class SSLSocketFactoryService extends SSLSocketFactory {
2019-12-17 16:11:28 +01:00
// openssl s_client -connect host:port < /dev/null 2>/dev/null | openssl x509 -fingerprint -noout -in /dev/stdin
2019-12-18 16:36:54 +01:00
private String server;
private boolean secure;
2020-01-29 12:44:55 +01:00
private boolean harden;
2019-12-18 16:36:54 +01:00
private String trustedFingerprint;
private SSLSocketFactory factory;
2019-12-17 16:11:28 +01:00
private X509Certificate certificate;
2019-12-17 10:58:42 +01:00
SSLSocketFactoryService(String host, boolean insecure, boolean harden, boolean use_certificate, String fingerprint) throws GeneralSecurityException {
2019-12-18 16:36:54 +01:00
this.server = host;
this.secure = !insecure;
2020-01-29 12:44:55 +01:00
this.harden = harden;
2019-12-18 16:36:54 +01:00
this.trustedFingerprint = fingerprint;
SSLContext sslContext = SSLContext.getInstance("TLS");
2019-12-17 16:11:28 +01:00
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
2019-12-18 19:25:56 +01:00
tmf.init((KeyStore) null);
2019-12-16 16:32:25 +01:00
2019-12-18 16:36:54 +01:00
TrustManager[] tms = tmf.getTrustManagers();
if (tms == null || tms.length == 0 || !(tms[0] instanceof X509TrustManager)) {
Log.e("Missing root trust manager");
sslContext.init(null, tms, null);
} else {
final X509TrustManager rtm = (X509TrustManager) tms[0];
X509TrustManager tm = new X509TrustManager() {
// openssl s_client -connect <host>
2019-12-18 16:36:54 +01:00
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (secure)
rtm.checkClientTrusted(chain, authType);
}
2019-12-18 14:20:16 +01:00
2019-12-18 16:36:54 +01:00
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
certificate = chain[0];
if (secure) {
// Check if selected fingerprint
if (trustedFingerprint != null && matches(certificate, trustedFingerprint)) {
2019-12-18 16:36:54 +01:00
Log.i("Trusted selected fingerprint");
return;
}
2019-12-18 14:20:16 +01:00
2019-12-18 16:36:54 +01:00
// Check certificates
2019-12-19 17:15:29 +01:00
try {
rtm.checkServerTrusted(chain, authType);
} catch (CertificateException ex) {
Principal principal = certificate.getSubjectDN();
if (principal == null)
throw ex;
else
throw new CertificateException(principal.getName(), ex);
}
2019-12-18 14:20:16 +01:00
2019-12-18 16:36:54 +01:00
// Check host name
List<String> names = getDnsNames(certificate);
for (String name : names)
if (matches(server, name)) {
Log.i("Trusted server=" + server + " name=" + name);
return;
}
2019-12-18 14:20:16 +01:00
2019-12-18 16:36:54 +01:00
String error = server + " not in certificate: " + TextUtils.join(",", names);
2020-02-04 09:42:06 +01:00
Log.i(error);
2019-12-18 16:36:54 +01:00
throw new CertificateException(error);
}
2019-12-18 14:20:16 +01:00
}
2019-12-18 16:36:54 +01:00
@Override
public X509Certificate[] getAcceptedIssuers() {
return rtm.getAcceptedIssuers();
2019-12-18 14:20:16 +01:00
}
2019-12-18 16:36:54 +01:00
};
2019-12-18 14:20:16 +01:00
KeyManager[] km = null;
if (use_certificate)
try {
KeyStore ks = KeyStore.getInstance("AndroidCAStore");
ks.load(null, null);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
kmf.init(ks, null);
km = kmf.getKeyManagers();
} catch (Throwable ex) {
Log.e(ex);
}
sslContext.init(km, new TrustManager[]{tm}, null);
2019-12-18 16:36:54 +01:00
}
2019-12-18 14:20:16 +01:00
2019-12-18 16:36:54 +01:00
factory = sslContext.getSocketFactory();
2019-12-17 16:11:28 +01:00
}
2019-12-16 16:32:25 +01:00
2019-12-17 16:11:28 +01:00
@Override
public Socket createSocket() throws IOException {
2019-12-18 16:36:54 +01:00
Log.e("createSocket");
throw new IOException("createSocket");
2019-12-17 16:11:28 +01:00
}
2019-12-16 16:32:25 +01:00
2019-12-17 16:11:28 +01:00
@Override
public Socket createSocket(String host, int port) throws IOException {
2020-01-28 16:32:26 +01:00
return configure(factory.createSocket(server, port));
2019-12-17 16:11:28 +01:00
}
2019-12-16 16:32:25 +01:00
2019-12-17 16:11:28 +01:00
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
2020-01-28 16:32:26 +01:00
return configure(factory.createSocket(s, server, port, autoClose));
2019-12-17 16:11:28 +01:00
}
2019-12-16 16:32:25 +01:00
2019-12-17 16:11:28 +01:00
@Override
public Socket createSocket(InetAddress address, int port) throws IOException {
2019-12-18 16:36:54 +01:00
Log.e("createSocket(address, port)");
throw new IOException("createSocket");
2019-12-17 16:11:28 +01:00
}
@Override
public Socket createSocket(String host, int port, InetAddress clientAddress, int clientPort) throws IOException {
2020-01-28 16:32:26 +01:00
return configure(factory.createSocket(server, port, clientAddress, clientPort));
2019-12-17 16:11:28 +01:00
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress clientAddress, int clientPort) throws IOException {
2019-12-18 16:36:54 +01:00
Log.e("createSocket(address, port, clientAddress, clientPort)");
throw new IOException("createSocket");
2019-12-17 16:11:28 +01:00
}
2020-01-28 16:32:26 +01:00
private Socket configure(Socket socket) {
2020-01-29 12:44:55 +01:00
if (harden && socket instanceof SSLSocket) {
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
SSLSocket sslSocket = (SSLSocket) socket;
List<String> protocols = new ArrayList<>();
for (String protocol : sslSocket.getEnabledProtocols())
if (SSL_PROTOCOL_BLACKLIST.contains(protocol))
Log.i("SSL disabling protocol=" + protocol);
else
protocols.add(protocol);
Log.i("SSL protocols=" + TextUtils.join(",", protocols));
sslSocket.setEnabledProtocols(protocols.toArray(new String[0]));
ArrayList<String> ciphers = new ArrayList<>();
Pattern pattern = Pattern.compile(".*(_DES|DH_|DSS|EXPORT|MD5|NULL|RC4|TLS_FALLBACK_SCSV).*");
for (String cipher : sslSocket.getEnabledCipherSuites()) {
if (pattern.matcher(cipher).matches())
Log.i("SSL disabling cipher=" + cipher);
else
ciphers.add(cipher);
}
Log.i("SSL ciphers=" + TextUtils.join(",", ciphers));
sslSocket.setEnabledCipherSuites(ciphers.toArray(new String[0]));
2020-01-28 16:32:26 +01:00
}
2020-01-29 12:44:55 +01:00
2020-01-28 16:32:26 +01:00
return socket;
}
2019-12-17 16:11:28 +01:00
@Override
public String[] getDefaultCipherSuites() {
2019-12-18 16:36:54 +01:00
return factory.getDefaultCipherSuites();
2019-12-17 16:11:28 +01:00
}
@Override
public String[] getSupportedCipherSuites() {
2019-12-18 16:36:54 +01:00
return factory.getSupportedCipherSuites();
2019-12-17 16:11:28 +01:00
}
private static boolean matches(String server, String name) {
if (name.startsWith("*.")) {
// Wildcard certificate
String domain = name.substring(2);
if (TextUtils.isEmpty(domain))
return false;
int dot = server.indexOf(".");
if (dot < 0)
return false;
String cdomain = server.substring(dot + 1);
if (TextUtils.isEmpty(cdomain))
return false;
return domain.equalsIgnoreCase(cdomain);
2019-12-18 14:20:16 +01:00
} else
2019-12-17 16:11:28 +01:00
return server.equalsIgnoreCase(name);
}
private static List<String> getDnsNames(X509Certificate certificate) throws CertificateParsingException {
List<String> result = new ArrayList<>();
Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
if (altNames == null)
return result;
for (List altName : altNames)
if (altName.get(0).equals(GeneralName.dNSName))
result.add((String) altName.get(1));
return result;
}
private static boolean matches(X509Certificate certificate, @NonNull String trustedFingerprint) {
// Get certificate fingerprint
try {
String fingerprint = getFingerPrint(certificate);
int slash = trustedFingerprint.indexOf('/');
if (slash < 0)
return trustedFingerprint.equals(fingerprint);
else {
String keyId = getKeyId(certificate);
if (trustedFingerprint.substring(slash + 1).equals(keyId))
return true;
return trustedFingerprint.substring(0, slash).equals(fingerprint);
}
} catch (Throwable ex) {
Log.w(ex);
return false;
}
}
private static String getKeyId(X509Certificate certificate) {
try {
byte[] extension = certificate.getExtensionValue(Extension.subjectKeyIdentifier.getId());
if (extension == null)
return null;
byte[] bytes = DEROctetString.getInstance(extension).getOctets();
SubjectKeyIdentifier keyId = SubjectKeyIdentifier.getInstance(bytes);
return Helper.hex(keyId.getKeyIdentifier());
} catch (Throwable ex) {
Log.e(ex);
return null;
}
}
2019-12-17 16:11:28 +01:00
private static String getFingerPrint(X509Certificate certificate) throws CertificateEncodingException, NoSuchAlgorithmException {
return Helper.sha1(certificate.getEncoded());
}
2019-12-16 16:32:25 +01:00
String getFingerPrintSelect() {
2019-12-18 14:20:16 +01:00
try {
String keyId = getKeyId(certificate);
String fingerPrint = getFingerPrint(certificate);
return fingerPrint + (keyId == null ? "" : "/" + keyId);
2019-12-18 14:20:16 +01:00
} catch (Throwable ex) {
Log.e(ex);
return null;
}
2019-12-16 16:32:25 +01:00
}
}
class UntrustedException extends MessagingException {
private String fingerprint;
2019-12-16 19:09:49 +01:00
UntrustedException(@NonNull String fingerprint, @NonNull Exception cause) {
2019-12-18 14:20:16 +01:00
super("Untrusted", cause);
2019-12-16 16:32:25 +01:00
this.fingerprint = fingerprint;
}
String getFingerprint() {
return fingerprint;
}
2019-12-17 16:11:28 +01:00
@NotNull
2019-12-16 16:32:25 +01:00
@Override
public synchronized String toString() {
2019-12-16 19:09:49 +01:00
return getCause().toString();
2019-12-16 16:32:25 +01:00
}
}
2019-07-29 11:17:12 +02:00
}