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

1314 lines
58 KiB
Java
Raw Normal View History

2019-07-29 09:17:12 +00:00
package eu.faircode.email;
2020-10-25 21:20:48 +00:00
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
FairEmail is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
2023-01-01 07:52:55 +00:00
Copyright 2018-2023 by Marcel Bokhorst (M66B)
2020-10-25 21:20:48 +00:00
*/
2021-08-01 16:35:40 +00:00
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_GMAIL;
import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_OAUTH;
2019-07-29 09:17:12 +00:00
import android.content.Context;
2019-10-21 11:26:15 +00:00
import android.content.SharedPreferences;
import android.net.ConnectivityManager;
import android.net.Network;
import android.os.Build;
2020-11-08 14:26:18 +00:00
import android.os.ParcelFileDescriptor;
2020-02-10 15:35:14 +00:00
import android.security.KeyChain;
2020-11-08 19:51:44 +00:00
import android.system.ErrnoException;
2021-11-16 17:47:24 +00:00
import android.system.OsConstants;
2019-09-18 14:34:07 +00:00
import android.text.TextUtils;
2019-07-29 09:17:12 +00:00
2019-12-16 18:09:49 +00:00
import androidx.annotation.NonNull;
2019-10-21 11:26:15 +00:00
import androidx.preference.PreferenceManager;
2020-05-01 19:47:59 +00:00
import com.sun.mail.gimap.GmailSSLProvider;
2022-01-28 17:08:38 +00:00
import com.sun.mail.iap.ProtocolException;
2019-09-18 14:34:07 +00:00
import com.sun.mail.imap.IMAPFolder;
2019-07-29 09:17:12 +00:00
import com.sun.mail.imap.IMAPStore;
2021-08-07 15:06:11 +00:00
import com.sun.mail.pop3.POP3Store;
2019-07-29 09:17:12 +00:00
import com.sun.mail.smtp.SMTPTransport;
2020-02-12 11:56:19 +00:00
import com.sun.mail.util.MailConnectException;
import com.sun.mail.util.SocketConnectException;
import com.sun.mail.util.TraceOutputStream;
2023-11-09 20:17:52 +00:00
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
2020-10-03 20:00:26 +00:00
import java.io.ByteArrayOutputStream;
2019-12-16 15:32:25 +00:00
import java.io.IOException;
2020-10-03 20:00:26 +00:00
import java.io.OutputStream;
import java.io.PrintStream;
2021-11-16 17:47:24 +00:00
import java.net.ConnectException;
2019-07-29 09:17:12 +00:00
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
2019-12-17 15:11:28 +00:00
import java.net.Socket;
2020-08-27 08:15:26 +00:00
import java.net.SocketException;
2019-07-29 09:17:12 +00:00
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
2019-12-17 15:11:28 +00:00
import java.security.KeyStore;
2020-02-20 14:35:10 +00:00
import java.security.PrivateKey;
2019-12-18 13:20:16 +00:00
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
2019-09-18 14:34:07 +00:00
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
2021-10-28 16:08:11 +00:00
import java.util.Date;
2019-07-29 09:17:12 +00:00
import java.util.HashMap;
import java.util.LinkedHashMap;
2019-09-18 14:34:07 +00:00
import java.util.List;
2019-07-29 09:17:12 +00:00
import java.util.Map;
import java.util.Properties;
2020-01-29 11:44:55 +00:00
import java.util.regex.Pattern;
2019-07-29 09:17:12 +00:00
2019-09-18 14:34:07 +00:00
import javax.mail.AuthenticationFailedException;
import javax.mail.Folder;
2019-07-29 09:17:12 +00:00
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Service;
import javax.mail.Session;
2019-09-19 15:41:26 +00:00
import javax.mail.Store;
2019-12-09 15:32:37 +00:00
import javax.mail.event.StoreListener;
2020-08-23 20:50:26 +00:00
import javax.net.SocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
2019-12-17 15:11:28 +00:00
import javax.net.ssl.SSLContext;
2020-06-19 16:35:42 +00:00
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
2020-01-28 15:32:26 +00:00
import javax.net.ssl.SSLSocket;
2019-12-18 13:20:16 +00:00
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
2019-07-29 09:17:12 +00:00
2020-07-17 08:13:04 +00:00
// IMAP standards: https://imapwiki.org/Specs
2020-01-29 20:06:45 +00:00
public class EmailService implements AutoCloseable {
2019-07-29 09:17:12 +00:00
private Context context;
private String protocol;
2019-12-17 15:11:28 +00:00
private boolean insecure;
2020-06-20 17:08:36 +00:00
private int purpose;
2022-01-30 13:56:29 +00:00
private boolean ssl_harden;
2022-07-11 10:21:47 +00:00
private boolean ssl_harden_strict;
2022-01-30 13:54:17 +00:00
private boolean cert_strict;
private boolean useip;
private String ehlo;
2020-10-03 20:00:26 +00:00
private boolean log;
2019-07-29 09:17:12 +00:00
private boolean debug;
private Properties properties;
private Session isession;
private Service iservice;
2019-12-09 15:32:37 +00:00
private StoreListener listener;
2021-11-27 11:58:25 +00:00
private ServiceAuthenticator authenticator;
2021-12-01 16:03:29 +00:00
private RingBuffer<String> breadcrumbs;
2019-07-29 09:17:12 +00:00
2020-02-06 12:01:11 +00:00
static final int PURPOSE_CHECK = 1;
static final int PURPOSE_USE = 2;
static final int PURPOSE_SEARCH = 3;
2020-08-23 07:28:10 +00:00
static final int ENCRYPTION_SSL = 0;
static final int ENCRYPTION_STARTTLS = 1;
static final int ENCRYPTION_NONE = 2;
2021-01-03 16:35:52 +00:00
final static int DEFAULT_CONNECT_TIMEOUT = 20; // seconds
2020-12-25 14:51:04 +00:00
final static boolean SEPARATE_STORE_CONNECTION = false;
2020-07-08 13:37:38 +00:00
private final static int SEARCH_TIMEOUT = 90 * 1000; // milliseconds
2020-01-21 10:09:57 +00:00
private final static int FETCH_SIZE = 1024 * 1024; // bytes, default 16K
2020-12-25 13:04:47 +00:00
private final static int POOL_SIZE = 1; // connections
2021-03-05 07:26:59 +00:00
private final static int POOL_TIMEOUT = 60 * 1000; // milliseconds, default 45 sec
2021-10-28 16:08:11 +00:00
private final static long PROTOCOL_LOG_DURATION = 12 * 3600 * 1000L;
2021-12-01 16:03:29 +00:00
private final static int BREADCRUMBS_SIZE = 100;
2019-07-29 14:42:17 +00:00
2021-06-23 19:43:40 +00:00
private final static int MAX_IPV4 = 2;
private final static int MAX_IPV6 = 1;
2020-11-08 19:51:44 +00:00
private final static int TCP_KEEP_ALIVE_INTERVAL = 9 * 60; // seconds
2019-07-29 14:42:17 +00:00
private static final int APPEND_BUFFER_SIZE = 4 * 1024 * 1024; // bytes
2023-12-13 15:15:33 +00:00
private static final List<String> SSL_PROTOCOL_INSECURE = Collections.unmodifiableList(Arrays.asList(
"SSLv2", "SSLv3"
));
2020-06-02 13:25:14 +00:00
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html#protocols
2020-01-29 11:44:55 +00:00
private static final List<String> SSL_PROTOCOL_BLACKLIST = Collections.unmodifiableList(Arrays.asList(
"SSLv2", "SSLv3", "TLSv1", "TLSv1.1"
));
2022-07-11 10:21:47 +00:00
private static final List<String> SSL_PROTOCOL_BLACKLIST_STRICT = Collections.unmodifiableList(Arrays.asList(
"SSLv2", "SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"
));
2020-06-02 13:25:14 +00:00
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html#cipher-suites
2020-03-21 19:06:31 +00:00
private static final Pattern SSL_CIPHER_BLACKLIST =
Pattern.compile(".*(_DES|DH_|DSS|EXPORT|MD5|NULL|RC4|TLS_FALLBACK_SCSV).*");
2022-07-11 10:21:47 +00:00
private static final Pattern SSL_CIPHER_BLACKLIST_STRICT =
Pattern.compile("(.*(_DES|DH_|DSS|EXPORT|MD5|NULL|RC4|TLS_FALLBACK_SCSV|RSA).*)|(.*SHA$)");
// TLS_FALLBACK_SCSV https://tools.ietf.org/html/rfc7507
// TLS_EMPTY_RENEGOTIATION_INFO_SCSV https://tools.ietf.org/html/rfc5746
2020-05-24 16:20:14 +00:00
2020-01-29 20:06:45 +00:00
private EmailService() {
2020-01-29 11:44:55 +00:00
// Prevent instantiation
2019-07-29 09:17:12 +00:00
}
2022-08-12 20:09:50 +00:00
EmailService(Context context, String protocol, String realm, int encryption, boolean insecure, boolean unicode, boolean debug) throws NoSuchProviderException {
this(context, protocol, realm, encryption, insecure, unicode, PURPOSE_USE, debug);
2020-02-06 12:01:11 +00:00
}
2022-08-12 20:09:50 +00:00
EmailService(Context context, String protocol, String realm, int encryption, boolean insecure, boolean unicode, int purpose, boolean debug) throws NoSuchProviderException {
2019-07-29 14:42:17 +00:00
this.context = context.getApplicationContext();
2019-07-29 09:17:12 +00:00
this.protocol = protocol;
2019-12-17 15:11:28 +00:00
this.insecure = insecure;
2020-06-20 17:08:36 +00:00
this.purpose = purpose;
2019-07-29 09:17:12 +00:00
this.debug = debug;
2019-10-03 09:39:14 +00:00
2022-08-12 20:09:50 +00:00
properties = MessageHelper.getSessionProperties(unicode);
2019-07-29 14:42:17 +00:00
2021-10-28 16:08:11 +00:00
long now = new Date().getTime();
2019-10-21 11:26:15 +00:00
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
2021-10-28 16:08:11 +00:00
long protocol_since = prefs.getLong("protocol_since", 0);
if (protocol_since == 0)
prefs.edit().putLong("protocol_since", now).apply();
else if (protocol_since + PROTOCOL_LOG_DURATION < now && !BuildConfig.TEST_RELEASE)
2021-10-28 16:08:11 +00:00
prefs.edit().putBoolean("protocol", false).apply();
2020-10-03 20:00:26 +00:00
this.log = prefs.getBoolean("protocol", false);
2022-01-30 13:56:29 +00:00
this.ssl_harden = prefs.getBoolean("ssl_harden", false);
2022-07-11 10:21:47 +00:00
this.ssl_harden_strict = prefs.getBoolean("ssl_harden_strict", false);
2023-12-08 07:33:15 +00:00
this.cert_strict = prefs.getBoolean("cert_strict", true);
2020-01-29 11:44:55 +00:00
2020-07-12 09:19:03 +00:00
boolean auth_plain = prefs.getBoolean("auth_plain", true);
boolean auth_login = prefs.getBoolean("auth_login", true);
2021-01-14 11:27:41 +00:00
boolean auth_ntlm = prefs.getBoolean("auth_ntlm", true);
boolean auth_sasl = prefs.getBoolean("auth_sasl", true);
2022-02-03 17:24:56 +00:00
boolean auth_apop = prefs.getBoolean("auth_apop", false);
2022-07-11 09:35:53 +00:00
boolean use_top = prefs.getBoolean("use_top", true);
2023-08-24 11:10:00 +00:00
boolean forget_top = prefs.getBoolean("forget_top", false);
2021-01-14 11:27:41 +00:00
Log.i("Authenticate" +
" plain=" + auth_plain +
" login=" + auth_login +
" ntlm=" + auth_ntlm +
2022-02-03 17:24:56 +00:00
" sasl=" + auth_sasl +
2022-07-09 05:21:35 +00:00
" apop=" + auth_apop +
2023-08-24 07:35:56 +00:00
" use_top=" + use_top +
" forget_top=" + forget_top);
2022-12-28 11:20:58 +00:00
//properties.put("mail.event.scope", "folder");
//properties.put("mail.event.executor", executor);
2019-07-29 14:42:17 +00:00
2020-07-12 09:19:03 +00:00
if (!auth_plain)
properties.put("mail." + protocol + ".auth.plain.disable", "true");
if (!auth_login)
properties.put("mail." + protocol + ".auth.login.disable", "true");
2021-01-14 11:27:41 +00:00
if (!auth_ntlm)
properties.put("mail." + protocol + ".auth.ntlm.disable", "true");
2022-02-03 17:24:56 +00:00
if (auth_apop)
properties.put("mail." + protocol + ".apop.enable", "true");
2022-07-11 09:35:53 +00:00
if (!use_top)
2022-07-09 05:21:35 +00:00
properties.put("mail." + protocol + ".disabletop", "true");
2023-08-24 07:35:56 +00:00
if (forget_top)
properties.put("mail." + protocol + ".forgettopheaders", "true");
2020-07-12 09:19:03 +00:00
2021-01-14 10:57:40 +00:00
// SASL is attempted before other authentication methods
2020-10-25 10:18:18 +00:00
properties.put("mail." + protocol + ".sasl.enable", Boolean.toString(auth_sasl));
properties.put("mail." + protocol + ".sasl.mechanisms", "CRAM-MD5");
properties.put("mail." + protocol + ".sasl.realm", realm == null ? "" : realm);
2021-01-14 10:57:40 +00:00
2019-09-29 18:25:01 +00:00
properties.put("mail." + protocol + ".auth.ntlm.domain", realm == null ? "" : realm);
2020-02-06 12:01:11 +00:00
// writetimeout: one thread overhead
int timeout = prefs.getInt("timeout", DEFAULT_CONNECT_TIMEOUT) * 1000;
Log.i("Timeout=" + timeout);
if (purpose == PURPOSE_SEARCH) {
properties.put("mail." + protocol + ".connectiontimeout", Integer.toString(timeout));
2023-12-07 17:54:32 +00:00
properties.put("mail." + protocol + ".writetimeout", Integer.toString(SEARCH_TIMEOUT));
2020-02-06 12:01:11 +00:00
properties.put("mail." + protocol + ".timeout", Integer.toString(SEARCH_TIMEOUT));
} else {
2021-01-11 07:39:40 +00:00
int factor = 2;
if ("smtp".equals(protocol) || "smtps".equals(protocol))
factor *= 2;
properties.put("mail." + protocol + ".connectiontimeout", Integer.toString(timeout));
2023-12-07 17:54:32 +00:00
properties.put("mail." + protocol + ".writetimeout", Integer.toString(timeout * factor));
2021-01-11 07:39:40 +00:00
properties.put("mail." + protocol + ".timeout", Integer.toString(timeout * factor));
2020-02-06 12:01:11 +00:00
}
2019-10-19 08:05:43 +00:00
boolean idle_done = prefs.getBoolean("idle_done", true);
properties.put("mail.idledone", Boolean.toString(idle_done));
2019-10-03 09:39:14 +00:00
if (debug && BuildConfig.DEBUG)
properties.put("mail.debug.auth", "true");
2019-07-29 14:42:17 +00:00
2020-08-23 07:28:10 +00:00
boolean starttls = (encryption == ENCRYPTION_STARTTLS);
2020-08-23 20:50:26 +00:00
if (encryption == ENCRYPTION_NONE) {
properties.put("mail." + protocol + ".ssl.enable", "false");
properties.put("mail." + protocol + ".socketFactory", new SocketFactoryService());
}
2019-09-19 15:41:26 +00:00
if ("pop3".equals(protocol) || "pop3s".equals(protocol)) {
// https://javaee.github.io/javamail/docs/api/com/sun/mail/pop3/package-summary.html#properties
properties.put("mail.pop3s.starttls.enable", "false");
2020-08-23 07:28:10 +00:00
properties.put("mail.pop3.starttls.enable", Boolean.toString(starttls));
properties.put("mail.pop3.starttls.required", Boolean.toString(starttls && !insecure));
2019-09-19 15:41:26 +00:00
2020-05-01 19:47:59 +00:00
} else if ("imap".equals(protocol) || "imaps".equals(protocol) || "gimaps".equals(protocol)) {
2019-07-29 14:42:17 +00:00
// https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties
2019-07-30 16:53:18 +00:00
properties.put("mail.imaps.starttls.enable", "false");
2019-07-29 14:42:17 +00:00
2020-08-23 07:28:10 +00:00
properties.put("mail.imap.starttls.enable", Boolean.toString(starttls));
properties.put("mail.imap.starttls.required", Boolean.toString(starttls && !insecure));
2019-07-29 14:42:17 +00:00
2020-12-25 14:51:04 +00:00
properties.put("mail." + protocol + ".separatestoreconnection", Boolean.toString(SEPARATE_STORE_CONNECTION));
2019-07-30 16:53:18 +00:00
properties.put("mail." + protocol + ".connectionpool.debug", "true");
properties.put("mail." + protocol + ".connectionpoolsize", Integer.toString(POOL_SIZE));
2019-07-30 16:53:18 +00:00
properties.put("mail." + protocol + ".connectionpooltimeout", Integer.toString(POOL_TIMEOUT));
2019-07-29 14:42:17 +00:00
2019-07-30 16:53:18 +00:00
properties.put("mail." + protocol + ".finalizecleanclose", "false");
2019-10-13 07:54:13 +00:00
//properties.put("mail." + protocol + ".closefoldersonstorefailure", "false");
2019-07-29 14:42:17 +00:00
// https://tools.ietf.org/html/rfc4978
// https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
2019-07-30 16:53:18 +00: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 14:42:17 +00:00
2019-07-30 16:53:18 +00: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 14:42:17 +00:00
if (!"gimaps".equals(protocol) && BuildConfig.DEBUG)
2022-03-25 17:42:54 +00:00
properties.put("mail." + protocol + ".folder.class", IMAPFolderEx.class.getName());
2019-07-29 14:42:17 +00: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 16:53:18 +00:00
properties.put("mail.smtps.starttls.enable", "false");
2019-07-29 14:42:17 +00:00
2020-08-23 07:28:10 +00:00
properties.put("mail.smtp.starttls.enable", Boolean.toString(starttls));
properties.put("mail.smtp.starttls.required", Boolean.toString(starttls && !insecure));
2019-07-29 14:42:17 +00:00
2019-07-30 16:53:18 +00:00
properties.put("mail." + protocol + ".auth", "true");
2019-07-29 14:42:17 +00:00
} else
throw new NoSuchProviderException(protocol);
2019-07-29 09:17:12 +00:00
}
void setPartialFetch(boolean enabled) {
properties.put("mail." + protocol + ".partialfetch", Boolean.toString(enabled));
2023-01-20 23:10:54 +00:00
}
void setRawFetch(boolean enabled) {
properties.put("fairemail.rawfetch", Boolean.toString(enabled));
}
void setIgnoreBodyStructureSize(boolean enabled) {
properties.put("mail." + protocol + ".ignorebodystructuresize", Boolean.toString(enabled));
2019-07-29 09:17:12 +00:00
}
void setUseIp(boolean enabled, String host) {
this.useip = enabled;
this.ehlo = host;
2019-07-29 09:17:12 +00:00
}
void setLeaveOnServer(boolean keep) {
properties.put("mail." + protocol + ".rsetbeforequit", Boolean.toString(keep));
}
2022-06-20 20:02:22 +00:00
void set8BitMime(boolean value) {
2022-06-22 17:11:54 +00:00
// https://datatracker.ietf.org/doc/html/rfc6532
2022-06-20 20:02:22 +00:00
properties.put("mail." + protocol + ".allow8bitmime", Boolean.toString(value));
}
2022-09-02 10:28:38 +00:00
void setRestartIdleInterval(int seconds) {
properties.put("mail." + protocol + ".restartidleinterval", Integer.toString(seconds));
}
2020-09-05 13:52:28 +00:00
// https://tools.ietf.org/html/rfc3461
void setDsnNotify(String what) {
properties.put("mail." + protocol + ".dsn.notify", what);
}
void setReporter(TraceOutputStream.IReport reporter) {
properties.put("mail." + protocol + ".reporter", reporter);
2021-10-07 14:27:01 +00:00
}
2019-12-09 15:32:37 +00:00
void setListener(StoreListener listener) {
this.listener = listener;
}
public void connect(EntityAccount account) throws MessagingException {
connect(
account.host, account.port,
account.auth_type, account.provider,
account.user, account.password,
2020-10-25 21:20:48 +00:00
new ServiceAuthenticator.IAuthenticated() {
@Override
2021-08-01 16:35:40 +00:00
public void onPasswordChanged(Context context, String newPassword) {
2020-10-25 21:20:48 +00:00
DB db = DB.getInstance(context);
2021-11-27 17:48:35 +00:00
account.password = newPassword;
2022-07-16 14:24:54 +00:00
int accounts = db.account().setAccountPassword(account.id, account.password, account.auth_type, account.provider);
int identities = db.identity().setIdentityPassword(account.id, account.user, account.password, account.auth_type, account.auth_type, account.provider);
2021-08-16 12:36:12 +00:00
EntityLog.log(context, EntityLog.Type.Account, account,
"token refreshed=" + accounts + "/" + identities);
2020-10-25 21:20:48 +00:00
}
},
2020-02-10 15:35:14 +00:00
account.certificate_alias, account.fingerprint);
2019-07-29 09:17:12 +00:00
}
public void connect(EntityIdentity identity) throws MessagingException {
connect(
identity.host, identity.port,
identity.auth_type, identity.provider,
identity.user, identity.password,
2020-10-25 21:20:48 +00:00
new ServiceAuthenticator.IAuthenticated() {
@Override
2021-08-01 16:35:40 +00:00
public void onPasswordChanged(Context context, String newPassword) {
2020-10-25 21:20:48 +00:00
DB db = DB.getInstance(context);
2021-11-27 17:48:35 +00:00
identity.password = newPassword;
int count = db.identity().setIdentityPassword(identity.id, identity.password);
2021-08-16 12:36:12 +00:00
EntityLog.log(context, EntityLog.Type.Account, identity.account, null, null,
identity.email + " token refreshed=" + count);
2020-10-25 21:20:48 +00:00
}
},
2020-02-10 15:35:14 +00:00
identity.certificate_alias, identity.fingerprint);
2019-07-29 09:17:12 +00:00
}
public void connect(
2020-10-25 21:20:48 +00:00
String host, int port,
2021-07-17 11:33:25 +00:00
int auth, String provider,
String user, String password,
2020-10-25 21:20:48 +00:00
String certificate, String fingerprint) throws MessagingException {
connect(host, port, auth, provider, user, password, null, certificate, fingerprint);
2020-10-25 21:20:48 +00:00
}
private void connect(
2020-01-15 07:58:31 +00:00
String host, int port,
int auth, String provider,
2021-07-17 11:33:25 +00:00
String user, String password,
2020-10-25 21:20:48 +00:00
ServiceAuthenticator.IAuthenticated intf,
2020-02-10 15:35:14 +00:00
String certificate, String fingerprint) throws MessagingException {
2021-09-02 19:09:35 +00:00
properties.put("fairemail.server", host);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean bind_socket = prefs.getBoolean("bind_socket", false);
if (bind_socket &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
try {
2022-04-13 20:27:33 +00:00
ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class);
Network active = cm.getActiveNetwork();
2021-09-03 05:59:56 +00:00
if (active != null) {
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Binding to" +
" active=" + active);
properties.put("fairemail.factory", active.getSocketFactory());
2021-09-03 05:59:56 +00:00
}
} catch (Throwable ex) {
Log.e(ex);
}
2019-12-19 10:53:37 +00:00
SSLSocketFactoryService factory = null;
2019-12-17 15:11:28 +00:00
try {
2020-02-20 14:35:10 +00:00
PrivateKey key = null;
X509Certificate[] chain = null;
2020-02-10 15:35:14 +00:00
if (certificate != null) {
Log.i("Get client certificate alias=" + certificate);
try {
2020-02-20 14:35:10 +00:00
key = KeyChain.getPrivateKey(context, certificate);
chain = KeyChain.getCertificateChain(context, certificate);
2020-02-10 15:35:14 +00:00
} catch (Throwable ex) {
Log.w(ex);
}
}
2022-11-15 17:15:53 +00:00
boolean strict = ssl_harden_strict;
2022-11-15 20:26:55 +00:00
if (strict)
if ("pop3".equals(protocol) || "pop3s".equals(protocol))
strict = false;
else {
EmailProvider p = EmailProvider.getProviderByHost(context, host);
if (p != null && "1.2".equals(p.maxtls)) {
2022-11-15 17:15:53 +00:00
strict = false;
Log.i(p.name + " maxtls=" + p.maxtls);
}
}
2023-11-09 20:17:52 +00:00
boolean bc = prefs.getBoolean("bouncy_castle", false);
2023-11-10 07:30:22 +00:00
boolean fips = prefs.getBoolean("bc_fips", false);
factory = new SSLSocketFactoryService(host, insecure, ssl_harden, strict, cert_strict, bc, fips, key, chain, fingerprint);
2019-12-17 15:11:28 +00:00
properties.put("mail." + protocol + ".ssl.socketFactory", factory);
properties.put("mail." + protocol + ".socketFactory.fallback", "false");
2019-12-18 13:20:16 +00:00
properties.put("mail." + protocol + ".ssl.checkserveridentity", "false");
2019-12-17 15:11:28 +00:00
} catch (GeneralSecurityException ex) {
2019-12-18 13:20:16 +00:00
properties.put("mail." + protocol + ".ssl.checkserveridentity", Boolean.toString(!insecure));
if (insecure)
properties.put("mail." + protocol + ".ssl.trust", "*");
2019-12-19 10:53:37 +00:00
Log.e("Trust issues", ex);
2019-12-17 15:11:28 +00:00
}
2020-10-25 21:20:48 +00:00
properties.put("mail." + protocol + ".forcepasswordrefresh", "true");
2022-08-17 08:13:05 +00:00
authenticator = new ServiceAuthenticator(context, auth, provider, user, password, intf);
2020-10-25 21:20:48 +00:00
2021-10-24 13:01:54 +00:00
if ("imap.wp.pl".equals(host))
2021-10-24 17:16:23 +00:00
properties.put("mail.idledone", "false");
2021-10-24 13:01:54 +00:00
2019-07-29 09:17:12 +00:00
try {
2021-01-14 11:35:24 +00:00
if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH) {
2019-09-18 14:34:07 +00:00
properties.put("mail." + protocol + ".auth.mechanisms", "XOAUTH2");
2021-01-14 11:35:24 +00:00
properties.put("mail." + protocol + ".auth.xoauth2.disable", "false");
} else
properties.put("mail." + protocol + ".auth.xoauth2.disable", "true");
2019-09-18 14:34:07 +00:00
2020-10-25 21:20:48 +00:00
if (auth == AUTH_TYPE_OAUTH && "imap.mail.yahoo.com".equals(host))
properties.put("mail." + protocol + ".yahoo.guid", "FAIRMAIL_V1");
2022-04-17 09:44:44 +00:00
if (auth == AUTH_TYPE_OAUTH && "pop3s".equals(protocol) && "outlook.office365.com".equals(host))
properties.put("mail." + protocol + ".auth.xoauth2.two.line.authentication.format", "true");
2022-05-18 08:02:52 +00:00
Log.i("Connecting to " + host + ":" + port + " auth=" + auth);
2021-11-27 11:58:25 +00:00
connect(host, port, auth, user, factory);
2019-09-18 14:34:07 +00:00
} catch (AuthenticationFailedException ex) {
2021-09-25 14:29:47 +00:00
//if ("outlook.office365.com".equals(host) &&
// "AUTHENTICATE failed.".equals(ex.getMessage()))
// throw new AuthenticationFailedException(
// "The Outlook IMAP server is currently not accepting logins. " +
// "Synchronizing and configuring accounts will work again after Microsoft has fixed this.",
// ex.getNextException());
2021-09-24 19:53:04 +00:00
2022-06-08 12:04:00 +00:00
if (!BuildConfig.PLAY_STORE_RELEASE)
Log.e(ex);
EntityLog.log(context, ex + "\n" + android.util.Log.getStackTraceString(ex));
2020-10-25 21:20:48 +00:00
if (auth == AUTH_TYPE_GMAIL || auth == AUTH_TYPE_OAUTH) {
2019-09-18 14:34:07 +00:00
try {
2022-07-21 07:18:57 +00:00
EntityLog.log(context, EntityLog.Type.Debug,
ex + "\n" + android.util.Log.getStackTraceString(ex));
2020-10-26 08:31:10 +00:00
authenticator.refreshToken(true);
2021-11-27 11:58:25 +00:00
connect(host, port, auth, user, factory);
2019-09-24 13:34:39 +00:00
} catch (Exception ex1) {
2022-11-12 12:53:52 +00:00
Throwable cause = ex1.getCause();
if (cause == null)
Log.e(ex1);
else
Log.e(new Throwable(ex1.getMessage() + " error=" + cause.getMessage(), ex1));
2022-12-28 17:44:11 +00:00
String msg = ex.getMessage();
if (auth == AUTH_TYPE_GMAIL &&
msg != null && msg.endsWith("Invalid credentials (Failure)"))
2022-12-28 17:44:11 +00:00
msg += "\n" + context.getString(R.string.title_service_token);
Throwable c = ex1;
while (c != null) {
String m = c.getMessage();
if (!TextUtils.isEmpty(m))
msg += "\n" + m;
c = c.getCause();
}
2021-05-01 13:55:03 +00:00
throw new AuthenticationFailedException(
context.getString(R.string.title_service_auth, msg),
2021-05-01 13:55:03 +00:00
ex.getNextException());
2019-09-18 14:34:07 +00:00
}
2022-03-16 18:20:08 +00:00
} else
2020-08-09 11:46:46 +00:00
throw new AuthenticationFailedException(
2022-03-16 18:20:08 +00:00
context.getString(R.string.title_service_auth, ex.getMessage()),
2020-08-09 11:46:46 +00:00
ex.getNextException());
2020-02-12 11:56:19 +00:00
} catch (MailConnectException ex) {
if (ConnectionHelper.vpnActive(context)) {
2020-08-09 11:54:12 +00:00
MailConnectException mex = new MailConnectException(
new SocketConnectException(
context.getString(R.string.title_service_vpn),
new Exception(),
ex.getHost(),
ex.getPort(),
ex.getConnectionTimeout()));
2020-02-12 11:56:19 +00:00
mex.setNextException(ex.getNextException());
throw mex;
} else
throw ex;
2020-03-11 13:12:51 +00:00
} catch (MessagingException ex) {
2022-12-12 09:36:31 +00:00
/*
javax.mail.MessagingException: FY1 BAD Command Error. 10;
nested exception is:
com.sun.mail.iap.BadCommandException: FY1 BAD Command Error. 10
javax.mail.MessagingException: FY1 BAD Command Error. 10;
nested exception is:
com.sun.mail.iap.BadCommandException: FY1 BAD Command Error. 10
at com.sun.mail.imap.IMAPStore.protocolConnect(SourceFile:40)
at javax.mail.Service.connect(SourceFile:31)
at eu.faircode.email.EmailService._connect(SourceFile:31)
at eu.faircode.email.EmailService.connect(SourceFile:99)
at eu.faircode.email.EmailService.connect(SourceFile:40)
at eu.faircode.email.EmailService.connect(SourceFile:4)
at eu.faircode.email.ServiceSynchronize.monitorAccount(SourceFile:40)
at eu.faircode.email.ServiceSynchronize.access$1100(SourceFile:1)
at eu.faircode.email.ServiceSynchronize$4$2.run(SourceFile:1)
at java.lang.Thread.run(Thread.java:919)
Caused by: com.sun.mail.iap.BadCommandException: FY1 BAD Command Error. 10
at com.sun.mail.iap.Protocol.handleResult(SourceFile:7)
at com.sun.mail.imap.protocol.IMAPProtocol.handleLoginResult(SourceFile:4)
at com.sun.mail.imap.protocol.IMAPProtocol.authoauth2(SourceFile:36)
at com.sun.mail.imap.IMAPStore.authenticate(SourceFile:24)
at com.sun.mail.imap.IMAPStore.login(SourceFile:22)
at com.sun.mail.imap.IMAPStore.protocolConnect(SourceFile:24)
*/
if (ex.getMessage() != null && ex.getMessage().contains("Command Error. 10"))
throw new AuthenticationFailedException(context.getString(R.string.title_service_error10), ex);
2020-06-20 17:08:36 +00:00
if (purpose == PURPOSE_CHECK) {
if (port == 995 && !("pop3".equals(protocol) || "pop3s".equals(protocol)))
throw new MessagingException(context.getString(R.string.title_service_port), ex);
else if (ex.getMessage() != null &&
ex.getMessage().contains("Got bad greeting"))
throw new MessagingException(context.getString(R.string.title_service_protocol), ex);
else if (ex.getCause() instanceof SSLException &&
ex.getCause().getMessage() != null &&
ex.getCause().getMessage().contains("Unable to parse TLS packet header"))
throw new MessagingException(context.getString(R.string.title_service_protocol), ex);
else if (ex.getCause() instanceof SSLHandshakeException)
throw new MessagingException(context.getString(R.string.title_service_protocol), ex);
else
throw ex;
} else
2020-03-11 13:12:51 +00:00
throw ex;
2020-01-15 07:58:31 +00:00
}
}
private void connect(
2021-11-27 11:58:25 +00:00
String host, int port, int auth, String user,
2020-01-15 07:58:31 +00:00
SSLSocketFactoryService factory) throws MessagingException {
2022-08-16 05:52:09 +00:00
Map<String, String> crumb = new HashMap<>();
crumb.put("host", host);
crumb.put("port", Integer.toString(port));
crumb.put("auth", Integer.toString(auth));
2020-03-28 07:05:36 +00:00
InetAddress main = null;
2020-06-30 20:50:56 +00:00
boolean require_id = (purpose == PURPOSE_CHECK &&
auth == AUTH_TYPE_OAUTH &&
"outlook.office365.com".equals(host));
Log.i("Require ID=" + require_id);
2020-01-15 07:58:31 +00:00
try {
//if (BuildConfig.DEBUG)
// throw new MailConnectException(
// new SocketConnectException("Debug", new IOException("Test"), host, port, 0));
2020-10-27 12:22:23 +00:00
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
2021-11-12 11:19:38 +00:00
String key = "dns." + host;
try {
main = InetAddress.getByName(host);
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Main address=" + main);
2021-11-12 11:19:38 +00:00
prefs.edit().putString(key, main.getHostAddress()).apply();
} catch (UnknownHostException ex) {
String last = prefs.getString(key, null);
if (TextUtils.isEmpty(last))
2023-11-02 12:19:42 +00:00
throw ex;
2021-11-12 11:19:38 +00:00
else {
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Using " + key + "=" + last);
2021-11-12 11:19:38 +00:00
main = InetAddress.getByName(last);
}
}
boolean prefer_ip4 = prefs.getBoolean("prefer_ip4", true);
2022-03-14 14:18:43 +00:00
if (prefer_ip4 && main instanceof Inet6Address) {
2022-03-14 14:26:37 +00:00
boolean[] has46 = ConnectionHelper.has46(context);
2022-03-14 14:18:43 +00:00
if (has46[0])
try {
for (InetAddress iaddr : InetAddress.getAllByName(host))
if (iaddr instanceof Inet4Address) {
main = iaddr;
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Preferring=" + main);
2022-03-14 14:18:43 +00:00
break;
}
} catch (UnknownHostException ex) {
Log.w(ex);
}
}
2020-10-27 12:22:23 +00:00
2022-08-16 05:52:09 +00:00
Log.breadcrumb("Connecting", crumb);
2021-11-27 11:58:25 +00:00
_connect(main, port, require_id, user, factory);
2022-08-16 05:52:09 +00:00
Log.breadcrumb("Connected", crumb);
2020-03-28 07:05:36 +00:00
} catch (UnknownHostException ex) {
2022-08-16 05:52:09 +00:00
crumb.put("exception", ex + "\n" + android.util.Log.getStackTraceString(ex));
Log.breadcrumb("Connection failed", crumb);
2023-11-02 12:19:42 +00:00
if (ConnectionHelper.vpnActive(context))
throw new MessagingException(ex.getMessage(),
new Exception(context.getString(R.string.title_service_vpn), ex));
else
throw new MessagingException(ex.getMessage(), ex);
2020-01-15 07:58:31 +00:00
} catch (MessagingException ex) {
2022-08-16 05:52:09 +00:00
crumb.put("exception", ex + "\n" + android.util.Log.getStackTraceString(ex));
Log.breadcrumb("Connection failed", crumb);
2021-11-16 17:47:24 +00:00
/*
com.sun.mail.util.MailConnectException: Couldn't connect to host, port: 74.125.140.108, 993; timeout 20000;
nested exception is:
java.net.ConnectException: failed to connect to imap.gmail.com/74.125.140.108 (port 993) from /:: (port 0) after 20000ms: connect failed: EACCES (Permission denied)
at com.sun.mail.imap.IMAPStore.protocolConnect(SourceFile:38)
at com.sun.mail.gimap.GmailStore.protocolConnect(SourceFile:1)
at javax.mail.Service.connect(SourceFile:31)
at eu.faircode.email.EmailService._connect(SourceFile:29)
at eu.faircode.email.EmailService.connect(SourceFile:117)
at eu.faircode.email.EmailService.connect(SourceFile:38)
at eu.faircode.email.EmailService.connect(SourceFile:4)
at eu.faircode.email.ServiceSynchronize.monitorAccount(SourceFile:38)
at eu.faircode.email.ServiceSynchronize.access$1000(SourceFile:1)
at eu.faircode.email.ServiceSynchronize$4$2.run(SourceFile:1)
at java.lang.Thread.run(Thread.java:923)
Caused by: java.net.ConnectException: failed to connect to imap.gmail.com/74.125.140.108 (port 993) from /:: (port 0) after 20000ms: connect failed: EACCES (Permission denied)
at libcore.io.IoBridge.connect(IoBridge.java:142)
at java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:142)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:390)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:230)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:212)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:436)
at java.net.Socket.connect(Socket.java:621)
at com.sun.mail.util.WriteTimeoutSocket.connect(SourceFile:2)
at com.sun.mail.util.SocketFetcher.createSocket(SourceFile:52)
at com.sun.mail.util.SocketFetcher.getSocket(SourceFile:27)
at com.sun.mail.iap.Protocol.<init>(SourceFile:10)
at com.sun.mail.imap.protocol.IMAPProtocol.<init>(SourceFile:1)
at com.sun.mail.gimap.protocol.GmailProtocol.<init>(SourceFile:1)
at com.sun.mail.gimap.GmailStore.newIMAPProtocol(SourceFile:2)
at com.sun.mail.imap.IMAPStore.protocolConnect(SourceFile:17)
... 10 more
Caused by: android.system.ErrnoException: connect failed: EACCES (Permission denied)
at libcore.io.Linux.connect(Native Method)
at libcore.io.ForwardingOs.connect(ForwardingOs.java:94)
at libcore.io.BlockGuardOs.connect(BlockGuardOs.java:138)
at libcore.io.ForwardingOs.connect(ForwardingOs.java:94)
at libcore.io.IoBridge.connectErrno(IoBridge.java:173)
at libcore.io.IoBridge.connect(IoBridge.java:134)
*/
if (ex instanceof MailConnectException &&
ex.getCause() instanceof ConnectException &&
ex.getCause().getCause() instanceof ErrnoException &&
((ErrnoException) ex.getCause().getCause()).errno == OsConstants.EACCES)
throw new SecurityException("Please check 'Restrict data usage' in the Android app settings", ex);
2020-01-15 07:58:31 +00:00
boolean ioError = false;
Throwable ce = ex;
while (ce != null) {
if (factory != null && ce instanceof CertificateException)
throw new UntrustedException(ex, factory.certificate);
2020-01-15 07:58:31 +00:00
if (ce instanceof IOException)
ioError = true;
ce = ce.getCause();
}
if (ioError) {
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Connect ex=" +
ex.getClass().getName() + ":" +
ex + "\n" + android.util.Log.getStackTraceString(ex));
2020-01-15 07:58:31 +00:00
try {
// Some devices resolve IPv6 addresses while not having IPv6 connectivity
InetAddress[] iaddrs = InetAddress.getAllByName(host);
2021-06-23 19:43:40 +00:00
int ip4 = (main instanceof Inet4Address ? 1 : 0);
int ip6 = (main instanceof Inet6Address ? 1 : 0);
2020-03-09 15:32:20 +00:00
2022-03-14 14:26:37 +00:00
boolean[] has46 = ConnectionHelper.has46(context);
2020-03-09 15:32:20 +00:00
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Address main=" + main +
2020-03-10 15:30:53 +00:00
" count=" + iaddrs.length +
2022-03-14 14:18:43 +00:00
" ip4=" + ip4 + " max4=" + MAX_IPV4 + " has4=" + has46[0] +
" ip6=" + ip6 + " max6=" + MAX_IPV6 + " has6=" + has46[1]);
2020-03-10 15:30:53 +00:00
for (InetAddress iaddr : iaddrs) {
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Address resolved=" + iaddr);
2020-03-10 15:30:53 +00:00
if (iaddr.equals(main))
continue;
2020-03-14 17:53:16 +00:00
if (iaddr instanceof Inet4Address) {
2022-03-14 14:18:43 +00:00
if (!has46[0] || ip4 >= MAX_IPV4)
2020-03-14 17:53:16 +00:00
continue;
2021-06-23 19:43:40 +00:00
ip4++;
2020-03-14 17:53:16 +00:00
}
if (iaddr instanceof Inet6Address) {
2022-03-14 14:18:43 +00:00
if (!has46[1] || ip6 >= MAX_IPV6)
2020-03-14 17:53:16 +00:00
continue;
2021-06-23 19:43:40 +00:00
ip6++;
2020-03-10 15:30:53 +00:00
}
try {
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Falling back to " + iaddr);
2021-11-27 11:58:25 +00:00
_connect(iaddr, port, require_id, user, factory);
2020-03-10 15:30:53 +00:00
return;
} catch (MessagingException ex1) {
ex = ex1;
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Fallback ex=" +
ex1.getClass().getName() + ":" +
ex1 + " " + android.util.Log.getStackTraceString(ex1));
2020-03-08 10:24:18 +00:00
}
2020-03-10 15:30:53 +00:00
}
} catch (IOException ex1) {
throw new MessagingException(ex1.getMessage(), ex1);
2020-01-15 07:58:31 +00:00
}
}
throw ex;
2019-07-29 09:17:12 +00:00
}
}
2019-12-18 13:20:16 +00:00
private void _connect(
2021-11-27 11:58:25 +00:00
InetAddress address, int port, boolean require_id, String user,
2019-12-18 13:20:16 +00:00
SSLSocketFactoryService factory) throws MessagingException {
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Network, "Connecting to " + address + ":" + port);
2022-02-01 08:45:01 +00:00
2020-10-25 21:20:48 +00:00
isession = Session.getInstance(properties, authenticator);
2020-10-03 20:00:26 +00:00
2021-12-01 16:03:29 +00:00
breadcrumbs = new RingBuffer<>(BREADCRUMBS_SIZE);
2023-12-14 12:18:41 +00:00
boolean trace = (debug || log);
isession.setDebug(trace);
if (trace)
2020-10-03 20:00:26 +00:00
isession.setDebugOut(new PrintStream(new OutputStream() {
private ByteArrayOutputStream bos = new ByteArrayOutputStream();
@Override
public void write(int b) {
2020-10-04 10:07:12 +00:00
try {
if (((char) b) == '\n') {
String line = bos.toString();
if (!line.endsWith("ignoring socket timeout"))
if (log)
2021-08-16 07:36:23 +00:00
EntityLog.log(context, EntityLog.Type.Protocol, user + " " + line);
2021-02-27 16:45:08 +00:00
else {
2021-12-01 16:03:29 +00:00
breadcrumbs.push(line);
2021-02-27 16:45:08 +00:00
if (BuildConfig.DEBUG)
Log.i("javamail", user + " " + line);
}
2020-10-04 10:07:12 +00:00
bos.reset();
} else
bos.write(b);
} catch (Throwable ex) {
Log.e(ex);
}
2020-10-03 20:00:26 +00:00
}
2020-10-04 10:07:12 +00:00
}, true));
2020-10-03 20:00:26 +00:00
2020-01-15 07:58:31 +00:00
//System.setProperty("mail.socket.debug", Boolean.toString(debug));
2020-05-01 19:47:59 +00:00
isession.addProvider(new GmailSSLProvider());
2019-12-18 13:20:16 +00:00
2020-01-15 07:58:31 +00:00
if ("pop3".equals(protocol) || "pop3s".equals(protocol)) {
iservice = isession.getStore(protocol);
2020-10-25 21:20:48 +00:00
iservice.connect(address.getHostAddress(), port, user, null);
2019-12-18 13:20:16 +00:00
2020-05-01 19:47:59 +00:00
} else if ("imap".equals(protocol) || "imaps".equals(protocol) || "gimaps".equals(protocol)) {
2020-01-15 07:58:31 +00:00
iservice = isession.getStore(protocol);
if (listener != null)
((IMAPStore) iservice).addStoreListener(listener);
2020-10-25 21:20:48 +00:00
iservice.connect(address.getHostAddress(), port, user, null);
2020-01-15 07:58:31 +00:00
// https://www.ietf.org/rfc/rfc2971.txt
IMAPStore istore = (IMAPStore) getStore();
if (istore.hasCapability("ID"))
try {
2021-06-13 15:48:17 +00:00
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean client_id = prefs.getBoolean("client_id", true);
2022-03-03 07:26:01 +00:00
Map<String, String> sid = istore.id(client_id ? getId(context) : null);
2020-01-15 07:58:31 +00:00
if (sid != null) {
Map<String, String> crumb = new HashMap<>();
for (String key : sid.keySet()) {
crumb.put(key, sid.get(key));
2022-03-14 15:32:18 +00:00
EntityLog.log(context, EntityLog.Type.Protocol, "Server " + key + "=" + sid.get(key));
2019-07-29 09:17:12 +00:00
}
2020-01-15 07:58:31 +00:00
Log.breadcrumb("server", crumb);
2019-12-18 13:20:16 +00:00
}
2020-01-15 07:58:31 +00:00
} catch (MessagingException ex) {
Log.w(ex);
2020-07-01 07:53:30 +00:00
// Check for 'User is authenticated but not connected'
2020-06-30 20:50:56 +00:00
if (require_id)
2020-06-30 19:15:25 +00:00
throw ex;
2020-01-15 07:58:31 +00:00
}
2022-01-28 17:08:38 +00:00
// Verizon
2022-01-28 19:43:24 +00:00
if (false && istore.hasCapability("X-UIDONLY") && istore.hasCapability("ENABLE"))
2022-01-28 17:08:38 +00:00
try {
istore.enable("X-UIDONLY");
} catch (ProtocolException ex) {
Log.e(ex);
}
2020-01-15 07:58:31 +00:00
} else if ("smtp".equals(protocol) || "smtps".equals(protocol)) {
2020-04-08 12:59:33 +00:00
// https://tools.ietf.org/html/rfc5321#section-4.1.3
2020-10-10 05:49:18 +00:00
String hdomain = getDefaultEhlo();
2020-04-08 12:59:33 +00:00
String haddr = (address instanceof Inet4Address ? "[127.0.0.1]" : "[IPv6:::1]");
2020-01-15 07:58:31 +00:00
properties.put("mail." + protocol + ".localhost",
ehlo == null ? (useip ? haddr : hdomain) : ehlo);
2020-04-08 12:59:33 +00:00
Log.i("Using localhost=" + properties.getProperty("mail." + protocol + ".localhost"));
2020-01-15 07:58:31 +00:00
iservice = isession.getTransport(protocol);
try {
2020-10-25 21:20:48 +00:00
iservice.connect(address.getHostAddress(), port, user, null);
2020-01-15 07:58:31 +00:00
} catch (MessagingException ex) {
2020-08-23 07:28:10 +00:00
if (ehlo == null && ConnectionHelper.isSyntacticallyInvalid(ex)) {
2020-04-08 12:59:33 +00:00
properties.put("mail." + protocol + ".localhost", useip ? hdomain : haddr);
Log.i("Fallback localhost=" + properties.getProperty("mail." + protocol + ".localhost"));
try {
2020-10-25 21:20:48 +00:00
iservice.connect(address.getHostAddress(), port, user, null);
2020-04-08 12:59:33 +00:00
} catch (MessagingException ex1) {
if (ConnectionHelper.isSyntacticallyInvalid(ex1))
Log.e("Used localhost=" + haddr + "/" + hdomain);
throw ex1;
}
2020-01-15 07:58:31 +00:00
} else
throw ex;
2020-01-12 18:10:10 +00:00
}
2020-01-15 07:58:31 +00:00
} else
throw new NoSuchProviderException(protocol);
2019-07-29 09:17:12 +00:00
}
2022-03-03 07:26:01 +00:00
static Map<String, String> getId(Context context) {
2023-11-29 19:46:22 +00:00
// https://www.rfc-editor.org/rfc/rfc2971.html
2022-03-03 07:26:01 +00:00
Map<String, String> id = new LinkedHashMap<>();
id.put("name", context.getString(R.string.app_name));
id.put("version", BuildConfig.VERSION_NAME);
return id;
}
2020-10-10 05:49:18 +00:00
static String getDefaultEhlo() {
2020-11-11 14:09:58 +00:00
if (BuildConfig.APPLICATION_ID.startsWith("eu.faircode.email"))
return "dummy.faircode.eu";
2020-10-10 05:49:18 +00:00
String[] c = BuildConfig.APPLICATION_ID.split("\\.");
Collections.reverse(Arrays.asList(c));
return TextUtils.join(".", c);
}
2019-09-18 14:34:07 +00:00
List<EntityFolder> getFolders() throws MessagingException {
List<EntityFolder> folders = 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);
2020-05-07 07:31:32 +00:00
if (type != null)
folders.add(new EntityFolder(fullName, type));
2019-09-18 14:34:07 +00:00
}
2021-09-24 07:51:57 +00:00
EntityFolder.guessTypes(folders);
2019-09-18 14:34:07 +00:00
return folders;
}
2019-09-19 15:41:26 +00:00
Store getStore() {
return (Store) iservice;
2019-07-29 09:17:12 +00:00
}
SMTPTransport getTransport() {
2019-07-30 16:53:18 +00:00
return (SMTPTransport) iservice;
2019-07-29 09:17:12 +00:00
}
2020-07-03 07:57:43 +00:00
Long getMaxSize() throws MessagingException {
2020-07-03 08:04:09 +00:00
// https://support.google.com/mail/answer/6584#limit
2020-07-03 07:57:43 +00:00
String size;
if (iservice instanceof SMTPTransport) {
// https://tools.ietf.org/html/rfc1870
size = getTransport().getExtensionParameter("SIZE");
} else if (iservice instanceof IMAPStore) {
// https://tools.ietf.org/html/rfc7889
size = ((IMAPStore) iservice).getCapability("APPENDLIMIT");
} else
return null;
2020-07-02 12:26:41 +00:00
if (!TextUtils.isEmpty(size) && TextUtils.isDigitsOnly(size)) {
long s = Long.parseLong(size);
if (s != 0) // Not infinite
return s;
}
2020-07-01 21:34:53 +00:00
return null;
}
2021-08-07 15:06:11 +00:00
List<String> getCapabilities() throws MessagingException {
List<String> result = new ArrayList<>();
Store store = getStore();
Map<String, String> capabilities;
if (store instanceof IMAPStore)
capabilities = ((IMAPStore) getStore()).getCapabilities();
else if (store instanceof POP3Store)
capabilities = ((POP3Store) getStore()).getCapabilities();
else
capabilities = null;
if (capabilities != null)
result.addAll(capabilities.keySet());
return result;
}
2019-09-19 15:41:26 +00:00
boolean hasCapability(String capability) throws MessagingException {
Store store = getStore();
if (store instanceof IMAPStore)
return ((IMAPStore) getStore()).hasCapability(capability);
else
return false;
}
public Long getAccessTokenExpirationTime() {
return authenticator.getAccessTokenExpirationTime();
2021-11-27 11:58:25 +00:00
}
2020-07-27 13:52:50 +00:00
public boolean isOpen() {
return (iservice != null && iservice.isConnected());
}
2019-07-29 09:17:12 +00:00
public void close() throws MessagingException {
try {
2019-10-08 19:59:49 +00:00
if (iservice != null && iservice.isConnected())
2019-07-29 09:17:12 +00:00
iservice.close();
2021-11-27 11:58:25 +00:00
if (authenticator != null)
authenticator = null;
2019-07-29 09:17:12 +00:00
} finally {
2019-07-30 16:53:18 +00:00
context = null;
2019-07-29 09:17:12 +00:00
}
}
2019-12-16 15:32:25 +00:00
2022-03-29 19:17:53 +00:00
public void dump(String tag) {
EntityLog.log(context, EntityLog.Type.Protocol, "Dump start " + tag);
2021-12-01 16:03:29 +00:00
while (breadcrumbs != null && !breadcrumbs.isEmpty())
2022-01-06 08:47:28 +00:00
EntityLog.log(context, EntityLog.Type.Protocol, "Dump " + breadcrumbs.pop());
2022-05-18 07:45:54 +00:00
EntityLog.log(context, EntityLog.Type.Protocol, "Dump end " + tag);
2021-12-01 16:03:29 +00:00
}
2020-08-23 20:50:26 +00:00
private static class SocketFactoryService extends SocketFactory {
private SocketFactory factory = SocketFactory.getDefault();
@Override
public Socket createSocket() throws IOException {
return configure(factory.createSocket());
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return configure(factory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return configure(factory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return configure(factory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return configure(factory.createSocket(address, port, localAddress, localPort));
}
2020-08-27 08:15:26 +00:00
private Socket configure(Socket socket) throws SocketException {
configureSocketOptions(socket);
2020-08-23 20:50:26 +00:00
return socket;
}
}
2019-12-18 13:20:16 +00:00
private static class SSLSocketFactoryService extends SSLSocketFactory {
2019-12-17 15:11:28 +00:00
// openssl s_client -connect host:port < /dev/null 2>/dev/null | openssl x509 -fingerprint -noout -in /dev/stdin
2022-02-23 14:46:06 +00:00
// nmap --script ssl-enum-ciphers -Pn -p port host
2019-12-18 15:36:54 +00:00
private String server;
private boolean secure;
2022-01-30 13:56:29 +00:00
private boolean ssl_harden;
2022-07-11 10:21:47 +00:00
private boolean ssl_harden_strict;
2022-01-30 13:54:17 +00:00
private boolean cert_strict;
2019-12-18 15:36:54 +00:00
private String trustedFingerprint;
private SSLSocketFactory factory;
2019-12-17 15:11:28 +00:00
private X509Certificate certificate;
2019-12-17 09:58:42 +00:00
2023-11-10 07:30:22 +00:00
SSLSocketFactoryService(String host, boolean insecure,
boolean ssl_harden, boolean ssl_harden_strict, boolean cert_strict,
boolean bc, boolean fips,
PrivateKey key, X509Certificate[] chain, String fingerprint) throws GeneralSecurityException {
2019-12-18 15:36:54 +00:00
this.server = host;
this.secure = !insecure;
2022-01-30 13:56:29 +00:00
this.ssl_harden = ssl_harden;
2022-07-11 10:21:47 +00:00
this.ssl_harden_strict = ssl_harden_strict;
2022-01-30 13:54:17 +00:00
this.cert_strict = cert_strict;
2019-12-18 15:36:54 +00:00
this.trustedFingerprint = fingerprint;
2023-12-09 17:46:43 +00:00
TrustManager[] tms = SSLHelper.getTrustManagers(server, secure, cert_strict, trustedFingerprint,
new SSLHelper.ITrust() {
@Override
public void checkServerTrusted(X509Certificate[] chain) {
certificate = chain[0];
}
});
KeyManager[] km = null;
if (key != null && chain != null)
try {
Log.i("Client certificate init");
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, new char[0]);
ks.setKeyEntry(server, key, new char[0], chain);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, new char[0]);
km = kmf.getKeyManagers();
Log.i("Client certificate initialized");
} catch (Throwable ex) {
Log.e(ex);
}
2023-11-16 08:57:29 +00:00
// https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext
// https://stackoverflow.com/questions/69571364/sslcontext-getinstancetls-vulnerability
2022-02-24 09:27:16 +00:00
// https://developer.android.com/about/versions/oreo/android-8.0-changes.html#security-all
2023-11-09 20:17:52 +00:00
SSLContext sslContext;
String protocol = (insecure ? "SSL" : "TLS");
if (bc)
2023-11-10 07:30:22 +00:00
sslContext = SSLContext.getInstance(protocol, new BouncyCastleJsseProvider(fips));
2023-11-09 20:17:52 +00:00
else
sslContext = SSLContext.getInstance(protocol);
2023-11-10 07:30:22 +00:00
Log.i("Using protocol=" + protocol + " bc=" + bc + " FIPS=" + fips);
2023-12-09 17:46:43 +00:00
sslContext.init(km, tms, null);
2019-12-18 13:20:16 +00:00
2019-12-18 15:36:54 +00:00
factory = sslContext.getSocketFactory();
2019-12-17 15:11:28 +00:00
}
2019-12-16 15:32:25 +00:00
2019-12-17 15:11:28 +00:00
@Override
public Socket createSocket() throws IOException {
2019-12-18 15:36:54 +00:00
Log.e("createSocket");
throw new IOException("createSocket");
2019-12-17 15:11:28 +00:00
}
2019-12-16 15:32:25 +00:00
2019-12-17 15:11:28 +00:00
@Override
public Socket createSocket(String host, int port) throws IOException {
2023-12-07 18:42:28 +00:00
ApplicationSecure.waitProviderInstalled();
2020-01-28 15:32:26 +00:00
return configure(factory.createSocket(server, port));
2019-12-17 15:11:28 +00:00
}
2019-12-16 15:32:25 +00:00
2019-12-17 15:11:28 +00:00
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
2021-08-25 19:38:55 +00:00
configureSocketOptions(s);
2023-12-07 18:42:28 +00:00
ApplicationSecure.waitProviderInstalled();
2023-12-08 09:14:01 +00:00
return configure(factory.createSocket(s, server, port, autoClose));
2019-12-17 15:11:28 +00:00
}
2019-12-16 15:32:25 +00:00
2019-12-17 15:11:28 +00:00
@Override
public Socket createSocket(InetAddress address, int port) throws IOException {
2019-12-18 15:36:54 +00:00
Log.e("createSocket(address, port)");
throw new IOException("createSocket");
2019-12-17 15:11:28 +00:00
}
@Override
public Socket createSocket(String host, int port, InetAddress clientAddress, int clientPort) throws IOException {
2023-12-07 18:42:28 +00:00
ApplicationSecure.waitProviderInstalled();
2020-01-28 15:32:26 +00:00
return configure(factory.createSocket(server, port, clientAddress, clientPort));
2019-12-17 15:11:28 +00:00
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress clientAddress, int clientPort) throws IOException {
2019-12-18 15:36:54 +00:00
Log.e("createSocket(address, port, clientAddress, clientPort)");
throw new IOException("createSocket");
2019-12-17 15:11:28 +00:00
}
2020-08-27 08:15:26 +00:00
private Socket configure(Socket socket) throws SocketException {
if (socket instanceof SSLSocket) {
2020-01-29 11:44:55 +00:00
SSLSocket sslSocket = (SSLSocket) socket;
2020-05-24 16:59:00 +00:00
if (!secure) {
2023-12-13 15:15:33 +00:00
Log.i("SSL insecure");
2022-02-23 11:30:38 +00:00
// Protocols
2020-05-24 16:59:00 +00:00
sslSocket.setEnabledProtocols(sslSocket.getSupportedProtocols());
2022-02-23 11:30:38 +00:00
// Ciphers
2020-05-24 16:59:00 +00:00
List<String> ciphers = new ArrayList<>();
2022-02-23 10:31:00 +00:00
ciphers.addAll(Arrays.asList(sslSocket.getSupportedCipherSuites()));
ciphers.remove("TLS_FALLBACK_SCSV");
2020-05-24 16:59:00 +00:00
sslSocket.setEnabledCipherSuites(ciphers.toArray(new String[0]));
2022-07-11 10:21:47 +00:00
} else if (ssl_harden && ssl_harden_strict &&
!BuildConfig.PLAY_STORE_RELEASE &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
2023-12-13 15:15:33 +00:00
Log.i("SSL harden strict");
2022-07-11 10:21:47 +00:00
// Protocols
List<String> protocols = new ArrayList<>();
for (String protocol : sslSocket.getSupportedProtocols())
2022-07-11 10:21:47 +00:00
if (SSL_PROTOCOL_BLACKLIST_STRICT.contains(protocol))
Log.i("SSL disabling protocol=" + protocol);
else
protocols.add(protocol);
sslSocket.setEnabledProtocols(protocols.toArray(new String[0]));
// Ciphers
List<String> ciphers = new ArrayList<>();
for (String cipher : sslSocket.getEnabledCipherSuites()) {
if (SSL_CIPHER_BLACKLIST_STRICT.matcher(cipher).matches())
Log.i("SSL disabling cipher=" + cipher);
else
ciphers.add(cipher);
}
sslSocket.setEnabledCipherSuites(ciphers.toArray(new String[0]));
2022-01-30 13:56:29 +00:00
} else if (ssl_harden) {
2023-12-13 15:15:33 +00:00
Log.i("SSL harden");
2022-02-23 11:30:38 +00:00
// Protocols
2020-05-24 16:59:00 +00:00
List<String> protocols = new ArrayList<>();
for (String protocol : sslSocket.getSupportedProtocols())
2020-05-24 16:59:00 +00:00
if (SSL_PROTOCOL_BLACKLIST.contains(protocol))
Log.i("SSL disabling protocol=" + protocol);
else
protocols.add(protocol);
sslSocket.setEnabledProtocols(protocols.toArray(new String[0]));
2022-02-23 11:30:38 +00:00
// Ciphers
2020-05-24 16:59:00 +00:00
List<String> ciphers = new ArrayList<>();
for (String cipher : sslSocket.getEnabledCipherSuites()) {
if (SSL_CIPHER_BLACKLIST.matcher(cipher).matches())
Log.i("SSL disabling cipher=" + cipher);
else
ciphers.add(cipher);
}
sslSocket.setEnabledCipherSuites(ciphers.toArray(new String[0]));
2020-05-24 17:11:39 +00:00
} else {
2023-12-13 15:15:33 +00:00
Log.i("SSL default");
// Protocols
List<String> protocols = new ArrayList<>();
for (String protocol : sslSocket.getSupportedProtocols())
if (SSL_PROTOCOL_INSECURE.contains(protocol))
Log.i("SSL disabling protocol=" + protocol);
else
protocols.add(protocol);
sslSocket.setEnabledProtocols(protocols.toArray(new String[0]));
2022-02-23 11:30:38 +00:00
// Ciphers
2020-05-24 17:11:39 +00:00
List<String> ciphers = new ArrayList<>();
ciphers.addAll(Arrays.asList(sslSocket.getEnabledCipherSuites()));
for (String cipher : sslSocket.getSupportedCipherSuites())
2022-02-23 11:30:38 +00:00
if (!ciphers.contains(cipher) && cipher.contains("3DES")) {
2020-05-24 17:11:39 +00:00
// Some servers support 3DES and RC4 only
Log.i("SSL enabling cipher=" + cipher);
ciphers.add(cipher);
}
sslSocket.setEnabledCipherSuites(ciphers.toArray(new String[0]));
2020-01-29 11:44:55 +00:00
}
2020-05-24 16:59:00 +00:00
Log.i("SSL protocols=" + TextUtils.join(",", sslSocket.getEnabledProtocols()));
Log.i("SSL ciphers=" + TextUtils.join(",", sslSocket.getEnabledCipherSuites()));
2020-01-28 15:32:26 +00:00
}
2020-01-29 11:44:55 +00:00
2020-01-28 15:32:26 +00:00
return socket;
}
2019-12-17 15:11:28 +00:00
@Override
public String[] getDefaultCipherSuites() {
2019-12-18 15:36:54 +00:00
return factory.getDefaultCipherSuites();
2019-12-17 15:11:28 +00:00
}
@Override
public String[] getSupportedCipherSuites() {
2019-12-18 15:36:54 +00:00
return factory.getSupportedCipherSuites();
2019-12-17 15:11:28 +00:00
}
2019-12-16 15:32:25 +00:00
}
2020-08-27 08:15:26 +00:00
private static void configureSocketOptions(Socket socket) throws SocketException {
2020-08-27 12:46:04 +00:00
int timeout = socket.getSoTimeout();
boolean keepAlive = socket.getKeepAlive();
int linger = socket.getSoLinger();
2021-10-29 17:31:31 +00:00
boolean reuse = socket.getReuseAddress();
boolean delay = socket.getTcpNoDelay();
2020-08-27 12:46:04 +00:00
2020-11-09 07:25:00 +00:00
Log.i("Socket type=" + socket.getClass().getName() +
2020-08-27 12:46:04 +00:00
" timeout=" + timeout +
" keep-alive=" + keepAlive +
2021-10-29 17:31:31 +00:00
" linger=" + linger +
" reuse=" + reuse +
" delay=" + delay);
2020-08-27 08:15:26 +00:00
2023-09-08 16:51:55 +00:00
if (keepAlive) {
2020-08-27 12:46:04 +00:00
Log.e("Socket keep-alive=" + keepAlive);
2020-11-08 14:26:18 +00:00
socket.setKeepAlive(false); // sets SOL_SOCKET/SO_KEEPALIVE
2020-08-27 08:15:26 +00:00
}
2020-08-27 12:46:04 +00:00
if (linger >= 0) {
Log.e("Socket linger=" + linger);
socket.setSoLinger(false, -1);
}
2020-11-08 14:26:18 +00:00
2021-12-11 06:44:29 +00:00
if (reuse) {
Log.e("Socket reuse=" + reuse);
socket.setReuseAddress(false);
}
if (delay) {
Log.e("Socket delay=" + delay);
socket.setTcpNoDelay(false);
}
2020-11-08 19:51:44 +00:00
try {
boolean tcp_keep_alive = Boolean.parseBoolean(System.getProperty("fairemail.tcp_keep_alive"));
if (tcp_keep_alive) {
Log.i("Enabling TCP keep alive");
int fd = ParcelFileDescriptor.fromSocket(socket).getFd();
2021-10-08 08:32:50 +00:00
int errno = ConnectionHelper.jni_socket_keep_alive(fd, TCP_KEEP_ALIVE_INTERVAL);
2020-11-09 07:56:31 +00:00
if (errno == 0)
Log.i("Enabled TCP keep alive");
else
throw new ErrnoException("jni_socket_keep_alive", errno);
2020-11-08 19:51:44 +00:00
}
} catch (Throwable ex) {
Log.e(ex);
}
2020-08-27 08:15:26 +00:00
}
2023-04-30 07:11:35 +00:00
static String getEncryptionName(int type) {
switch (type) {
case ENCRYPTION_SSL:
return "ssl";
case ENCRYPTION_STARTTLS:
return "starttls";
case ENCRYPTION_NONE:
return "none";
default:
return Integer.toString(type);
}
}
static class UntrustedException extends MessagingException {
2021-08-14 05:06:48 +00:00
private X509Certificate certificate;
2019-12-16 15:32:25 +00:00
UntrustedException(@NonNull Exception cause, @NonNull X509Certificate certificate) {
2019-12-18 13:20:16 +00:00
super("Untrusted", cause);
2021-08-14 05:06:48 +00:00
this.certificate = certificate;
}
X509Certificate getCertificate() {
return certificate;
2019-12-16 15:32:25 +00:00
}
2020-09-10 09:14:51 +00:00
@NonNull
2019-12-16 15:32:25 +00:00
@Override
public synchronized String toString() {
2019-12-16 18:09:49 +00:00
return getCause().toString();
2019-12-16 15:32:25 +00:00
}
}
}