diff --git a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java index 7968e7b5d6..cbf3357e2a 100644 --- a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java +++ b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java @@ -32,7 +32,6 @@ import com.sun.mail.iap.Argument; import com.sun.mail.iap.Response; import com.sun.mail.imap.IMAPFolder; import com.sun.mail.imap.IMAPMessage; -import com.sun.mail.imap.IMAPStore; import com.sun.mail.imap.protocol.IMAPProtocol; import com.sun.mail.imap.protocol.IMAPResponse; @@ -80,7 +79,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback messages = null; - private IMAPStore istore = null; + private ConnectionHelper.ServiceHolder iservice = null; private IMAPFolder ifolder = null; private Message[] imessages = null; @@ -281,11 +280,11 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback id = new LinkedHashMap<>(); - id.put("name", context.getString(R.string.app_name)); - id.put("version", BuildConfig.VERSION_NAME); - Map sid = istore.id(id); - if (sid != null) { - Map crumb = new HashMap<>(); - for (String key : sid.keySet()) { - crumb.put(key, sid.get(key)); - EntityLog.log(context, "Server " + key + "=" + sid.get(key)); - } - Bugsnag.leaveBreadcrumb("server", BreadcrumbType.LOG, crumb); - } - } catch (MessagingException ex) { - Log.w(ex); - } - } - - static void connect(Context context, SMTPTransport itransport, String host, int port, String user, String password) throws MessagingException { - try { - itransport.connect(host, port, user, password); - } catch (MessagingException ex) { - if (!hasIPv6(host)) - throw ex; - - try { - Log.i("Binding to " + any4); - Field fSession = getDeclaredField(itransport.getClass(), "session"); - fSession.setAccessible(true); - Session isession = (Session) fSession.get(itransport); - isession.getProperties().put("mail.smtp.localaddress", any4); - isession.getProperties().put("mail.smtps.localaddress", any4); - itransport.connect(host, port, user, password); - } catch (Throwable ex1) { - Log.w(ex1); - throw ex; - } - } - } - - private static Field getDeclaredField(Class clazz, String name) throws NoSuchFieldException { - while (clazz != null) { - try { - return clazz.getDeclaredField(name); - } catch (NoSuchFieldException ex) { - clazz = clazz.getSuperclass(); - } - } - throw new NoSuchFieldException(name); } private static boolean hasIPv6(String host) { @@ -424,4 +373,66 @@ public class ConnectionHelper { return ok; } + + static class ServiceHolder implements AutoCloseable { + private String protocol; + private Session issesion; + private Service iservice; + + private ServiceHolder() { + } + + ServiceHolder(String protocol, Session issesion) { + this.protocol = protocol; + this.issesion = issesion; + } + + void connect(Context context, String host, int port, String user, String password) throws MessagingException { + if ("imap".equals(protocol) || "imaps".equals(protocol)) { + iservice = issesion.getStore(protocol); + iservice.connect(host, port, user, password); + + // https://www.ietf.org/rfc/rfc2971.txt + if (getStore().hasCapability("ID")) + try { + Map id = new LinkedHashMap<>(); + id.put("name", context.getString(R.string.app_name)); + id.put("version", BuildConfig.VERSION_NAME); + Map sid = getStore().id(id); + if (sid != null) { + Map crumb = new HashMap<>(); + for (String key : sid.keySet()) { + crumb.put(key, sid.get(key)); + EntityLog.log(context, "Server " + key + "=" + sid.get(key)); + } + Bugsnag.leaveBreadcrumb("server", BreadcrumbType.LOG, crumb); + } + } catch (MessagingException ex) { + Log.w(ex); + } + + } else if ("smtp".equals(protocol) || "smtps".equals(protocol)) { + iservice = issesion.getTransport(protocol); + iservice.connect(host, port, user, password); + } else + throw new NoSuchProviderException(protocol); + } + + Session getSession() { + return this.issesion; + } + + IMAPStore getStore() { + return (IMAPStore) this.iservice; + } + + SMTPTransport getTransport() { + return (SMTPTransport) this.iservice; + } + + public void close() throws MessagingException { + if (iservice != null) + iservice.close(); + } + } } diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index d501bc05a3..fcb06589e0 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -59,7 +59,6 @@ import androidx.lifecycle.Lifecycle; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import com.sun.mail.imap.IMAPFolder; -import com.sun.mail.imap.IMAPStore; import com.sun.mail.imap.protocol.IMAPProtocol; import java.net.UnknownHostException; @@ -535,10 +534,11 @@ public class FragmentAccount extends FragmentBase { Properties props = MessageHelper.getSessionProperties(realm, insecure); Session isession = Session.getInstance(props, null); isession.setDebug(true); - try (IMAPStore istore = (IMAPStore) isession.getStore("imap" + (starttls ? "" : "s"))) { - ConnectionHelper.connect(context, istore, host, Integer.parseInt(port), user, password); + try (ConnectionHelper.ServiceHolder iservice = + new ConnectionHelper.ServiceHolder("imap" + (starttls ? "" : "s"), isession)) { + ConnectionHelper.connect(context, iservice, host, Integer.parseInt(port), user, password); - result.idle = istore.hasCapability("IDLE"); + result.idle = iservice.getStore().hasCapability("IDLE"); boolean inbox = false; boolean archive = false; @@ -552,7 +552,7 @@ public class FragmentAccount extends FragmentBase { EntityFolder altSent = null; EntityFolder altJunk = null; - for (Folder ifolder : istore.getDefaultFolder().list("*")) { + for (Folder ifolder : iservice.getStore().getDefaultFolder().list("*")) { // Check folder attributes String fullName = ifolder.getFullName(); String[] attrs = ((IMAPFolder) ifolder).getAttributes(); @@ -892,10 +892,11 @@ public class FragmentAccount extends FragmentBase { Session isession = Session.getInstance(props, null); isession.setDebug(true); - try (IMAPStore istore = (IMAPStore) isession.getStore("imap" + (starttls ? "" : "s"))) { - ConnectionHelper.connect(context, istore, host, Integer.parseInt(port), user, password); + try (ConnectionHelper.ServiceHolder iservice + = new ConnectionHelper.ServiceHolder("imap" + (starttls ? "" : "s"), isession)) { + ConnectionHelper.connect(context, iservice, host, Integer.parseInt(port), user, password); - for (Folder ifolder : istore.getDefaultFolder().list("*")) { + for (Folder ifolder : iservice.getStore().getDefaultFolder().list("*")) { // Check folder attributes String fullName = ifolder.getFullName(); String[] attrs = ((IMAPFolder) ifolder).getAttributes(); diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java index 122cb558f4..c1617523e7 100644 --- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java +++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java @@ -63,7 +63,6 @@ import androidx.lifecycle.Lifecycle; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; -import com.sun.mail.smtp.SMTPTransport; import java.net.Inet4Address; import java.net.Inet6Address; @@ -702,8 +701,9 @@ public class FragmentIdentity extends FragmentBase { isession.setDebug(true); // Create transport - try (SMTPTransport itransport = (SMTPTransport) isession.getTransport(protocol)) { - ConnectionHelper.connect(context, itransport, host, Integer.parseInt(port), user, password); + try (ConnectionHelper.ServiceHolder iservice = + new ConnectionHelper.ServiceHolder(protocol, isession)) { + ConnectionHelper.connect(context, iservice, host, Integer.parseInt(port), user, password); } } diff --git a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java index c98ee3b48d..d001e6a550 100644 --- a/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentQuickSetup.java @@ -52,8 +52,6 @@ import androidx.constraintlayout.widget.Group; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import com.sun.mail.imap.IMAPFolder; -import com.sun.mail.imap.IMAPStore; -import com.sun.mail.smtp.SMTPTransport; import java.net.UnknownHostException; import java.util.ArrayList; @@ -256,13 +254,14 @@ public class FragmentQuickSetup extends FragmentBase { Properties props = MessageHelper.getSessionProperties(null, false); Session isession = Session.getInstance(props, null); isession.setDebug(true); - try (IMAPStore istore = (IMAPStore) isession.getStore(provider.imap_starttls ? "imap" : "imaps")) { - ConnectionHelper.connect(context, istore, provider.imap_host, provider.imap_port, user, password); + try (ConnectionHelper.ServiceHolder iservice = + new ConnectionHelper.ServiceHolder(provider.imap_starttls ? "imap" : "imaps", isession)) { + ConnectionHelper.connect(context, iservice, provider.imap_host, provider.imap_port, user, password); boolean inbox = false; boolean drafts = false; EntityFolder altDrafts = null; - for (Folder ifolder : istore.getDefaultFolder().list("*")) { + for (Folder ifolder : iservice.getStore().getDefaultFolder().list("*")) { String fullName = ifolder.getFullName(); String[] attrs = ((IMAPFolder) ifolder).getAttributes(); String type = EntityFolder.getType(attrs, fullName, true); @@ -320,8 +319,9 @@ public class FragmentQuickSetup extends FragmentBase { Properties props = MessageHelper.getSessionProperties(null, false); Session isession = Session.getInstance(props, null); isession.setDebug(true); - try (SMTPTransport itransport = (SMTPTransport) isession.getTransport(provider.smtp_starttls ? "smtp" : "smtps")) { - ConnectionHelper.connect(context, itransport, provider.smtp_host, provider.smtp_port, user, password); + try (ConnectionHelper.ServiceHolder iservice + = new ConnectionHelper.ServiceHolder(provider.smtp_starttls ? "smtp" : "smtps", isession)) { + ConnectionHelper.connect(context, iservice, provider.smtp_host, provider.smtp_port, user, password); } } diff --git a/app/src/main/java/eu/faircode/email/ServiceSend.java b/app/src/main/java/eu/faircode/email/ServiceSend.java index 28c726b5c0..6a1891fb46 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSend.java +++ b/app/src/main/java/eu/faircode/email/ServiceSend.java @@ -36,8 +36,6 @@ import androidx.core.content.ContextCompat; import androidx.lifecycle.Observer; import androidx.preference.PreferenceManager; -import com.sun.mail.smtp.SMTPTransport; - import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; @@ -365,15 +363,15 @@ public class ServiceSend extends ServiceBase { } // Create transport - try (SMTPTransport itransport = (SMTPTransport) isession.getTransport(protocol)) { + try (ConnectionHelper.ServiceHolder iservice = new ConnectionHelper.ServiceHolder(protocol, isession)) { // Connect transport db.identity().setIdentityState(ident.id, "connecting"); - ConnectionHelper.connect(this, itransport, ident); + ConnectionHelper.connect(this, iservice, ident); db.identity().setIdentityState(ident.id, "connected"); // Send message Address[] to = imessage.getAllRecipients(); - itransport.sendMessage(imessage, to); + iservice.getTransport().sendMessage(imessage, to); long time = new Date().getTime(); EntityLog.log(this, "Sent via " + ident.host + "/" + ident.user + diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index b4b5a4350d..90202e1d51 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -46,7 +46,6 @@ import com.bugsnag.android.BreadcrumbType; import com.bugsnag.android.Bugsnag; import com.sun.mail.imap.IMAPFolder; import com.sun.mail.imap.IMAPMessage; -import com.sun.mail.imap.IMAPStore; import java.io.IOException; import java.text.DateFormat; @@ -625,13 +624,60 @@ public class ServiceSynchronize extends ServiceBase { isession.setDebug(debug); // adb -t 1 logcat | grep "fairemail\|System.out" - final IMAPStore istore = (IMAPStore) isession.getStore(account.getProtocol()); + final ConnectionHelper.ServiceHolder iservice = + new ConnectionHelper.ServiceHolder(account.getProtocol(), isession); final Map mapFolders = new HashMap<>(); List idlers = new ArrayList<>(); try { + // Initiate connection + EntityLog.log(this, account.name + " connecting"); + db.folder().setFolderStates(account.id, null); + db.account().setAccountState(account.id, "connecting"); + + try { + ConnectionHelper.connect(this, iservice, account); + } catch (Throwable ex) { + if (ex instanceof AuthenticationFailedException) { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify("receive:" + account.id, 1, + Core.getNotificationError(this, "error", account.name, ex) + .build()); + throw ex; + } + + // Report account connection error + if (account.last_connected != null && !ConnectionHelper.airplaneMode(this)) { + EntityLog.log(this, account.name + " last connected: " + new Date(account.last_connected)); + + long now = new Date().getTime(); + long delayed = now - account.last_connected - account.poll_interval * 60 * 1000L; + if (delayed > ACCOUNT_ERROR_AFTER * 60 * 1000L && backoff > BACKOFF_ERROR_AFTER) { + Log.i("Reporting sync error after=" + delayed); + Throwable warning = new Throwable( + getString(R.string.title_no_sync, + Helper.getDateTimeInstance(this, DateFormat.SHORT, DateFormat.SHORT) + .format(account.last_connected)), ex); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify("receive:" + account.id, 1, + Core.getNotificationError(this, "warning", account.name, warning) + .build()); + } + } + + throw ex; + } + + final boolean capIdle = iservice.getStore().hasCapability("IDLE"); + Log.i(account.name + " idle=" + capIdle); + + db.account().setAccountState(account.id, "connected"); + db.account().setAccountError(account.id, null); + db.account().setAccountWarning(account.id, null); + EntityLog.log(this, account.name + " connected"); + // Listen for store events - istore.addStoreListener(new StoreListener() { + iservice.getStore().addStoreListener(new StoreListener() { @Override public void notification(StoreEvent e) { if (e.getMessageType() == StoreEvent.NOTICE) @@ -664,7 +710,7 @@ public class ServiceSynchronize extends ServiceBase { }); // Listen for folder events - istore.addFolderListener(new FolderAdapter() { + iservice.getStore().addFolderListener(new FolderAdapter() { @Override public void folderCreated(FolderEvent e) { try { @@ -713,7 +759,7 @@ public class ServiceSynchronize extends ServiceBase { }); // Listen for connection events - istore.addConnectionListener(new ConnectionAdapter() { + iservice.getStore().addConnectionListener(new ConnectionAdapter() { @Override public void opened(ConnectionEvent e) { Log.i(account.name + " opened event"); @@ -730,55 +776,8 @@ public class ServiceSynchronize extends ServiceBase { } }); - - // Initiate connection - EntityLog.log(this, account.name + " connecting"); - db.folder().setFolderStates(account.id, null); - db.account().setAccountState(account.id, "connecting"); - - try { - ConnectionHelper.connect(this, istore, account); - } catch (Throwable ex) { - if (ex instanceof AuthenticationFailedException) { - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify("receive:" + account.id, 1, - Core.getNotificationError(this, "error", account.name, ex) - .build()); - throw ex; - } - - // Report account connection error - if (account.last_connected != null && !ConnectionHelper.airplaneMode(this)) { - EntityLog.log(this, account.name + " last connected: " + new Date(account.last_connected)); - - long now = new Date().getTime(); - long delayed = now - account.last_connected - account.poll_interval * 60 * 1000L; - if (delayed > ACCOUNT_ERROR_AFTER * 60 * 1000L && backoff > BACKOFF_ERROR_AFTER) { - Log.i("Reporting sync error after=" + delayed); - Throwable warning = new Throwable( - getString(R.string.title_no_sync, - Helper.getDateTimeInstance(this, DateFormat.SHORT, DateFormat.SHORT) - .format(account.last_connected)), ex); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify("receive:" + account.id, 1, - Core.getNotificationError(this, "warning", account.name, warning) - .build()); - } - } - - throw ex; - } - - final boolean capIdle = istore.hasCapability("IDLE"); - Log.i(account.name + " idle=" + capIdle); - - db.account().setAccountState(account.id, "connected"); - db.account().setAccountError(account.id, null); - db.account().setAccountWarning(account.id, null); - EntityLog.log(this, account.name + " connected"); - // Update folder list - Core.onSynchronizeFolders(this, account, istore, state); + Core.onSynchronizeFolders(this, account, iservice.getStore(), state); // Open synchronizing folders final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); @@ -803,7 +802,7 @@ public class ServiceSynchronize extends ServiceBase { db.folder().setFolderState(folder.id, "connecting"); - final IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name); + final IMAPFolder ifolder = (IMAPFolder) iservice.getStore().getFolder(folder.name); try { if (BuildConfig.DEBUG && "Postausgang".equals(folder.name)) throw new ReadOnlyFolderException(ifolder); @@ -1080,7 +1079,7 @@ public class ServiceSynchronize extends ServiceBase { db.folder().setFolderState(folder.id, "connecting"); - ifolder = istore.getFolder(folder.name); + ifolder = iservice.getStore().getFolder(folder.name); ifolder.open(Folder.READ_WRITE); db.folder().setFolderState(folder.id, "connected"); @@ -1090,7 +1089,7 @@ public class ServiceSynchronize extends ServiceBase { Core.processOperations(ServiceSynchronize.this, account, folder, - isession, istore, ifolder, + isession, iservice.getStore(), ifolder, state); } catch (Throwable ex) { @@ -1148,18 +1147,18 @@ public class ServiceSynchronize extends ServiceBase { try { while (state.running()) { if (!state.recoverable()) - throw new StoreClosedException(istore, "Unrecoverable"); + throw new StoreClosedException(iservice.getStore(), "Unrecoverable"); // Sends store NOOP - if (!istore.isConnected()) - throw new StoreClosedException(istore, "NOOP"); + if (!iservice.getStore().isConnected()) + throw new StoreClosedException(iservice.getStore(), "NOOP"); for (EntityFolder folder : mapFolders.keySet()) if (folder.synchronize) if (!folder.poll && capIdle) { // Sends folder NOOP if (!mapFolders.get(folder).isOpen()) - throw new StoreClosedException(istore, folder.name); + throw new StoreClosedException(iservice.getStore(), folder.name); } else EntityOperation.sync(this, folder.id, false); @@ -1231,7 +1230,7 @@ public class ServiceSynchronize extends ServiceBase { // Close store try { EntityLog.log(this, account.name + " store closing"); - istore.close(); + iservice.close(); EntityLog.log(this, account.name + " store closed"); } catch (Throwable ex) { Log.w(account.name, ex);