diff --git a/FAQ.md b/FAQ.md index e0bcd6a491..e8fe7e2998 100644 --- a/FAQ.md +++ b/FAQ.md @@ -28,7 +28,6 @@ Please see [here](#user-content-faq22) for common error messages. Related questions: -* [Why is POP not supported?](#user-content-faq11) * [Why is ActiveSync not supported?](#user-content-faq133) * [Why is OAuth not supported?](#user-content-faq111) @@ -111,7 +110,7 @@ FairEmail follows all the best practices for an email client as decribed in [thi * [(7) Why are sent messages not appearing (directly) in the sent folder?](#user-content-faq7) * [(8) Can I use a Microsoft Exchange account?](#user-content-faq8) * [(9) What are identities / how do I add an alias?](#user-content-faq9) -* [(11) Why is POP not supported?](#user-content-faq11) +* [~~(11) Why is POP not supported?~~](#user-content-faq11) * [~~(10) What does 'UIDPLUS not supported' mean?~~](#user-content-faq10) * [(12) How does encryption/decryption work?](#user-content-faq12) * [(13) How does search on device/server work?](#user-content-faq13) @@ -489,21 +488,21 @@ So, unless your provider can enable this extension, you cannot use FairEmail for
-**(11) Why is POP not supported?** +**~~(11) Why is POP not supported?~~** -Besides that any decent email provider supports [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol) these days, -using [POP](https://en.wikipedia.org/wiki/Post_Office_Protocol) will result in unnecessary extra battery usage and delayed new message notifications. -Moreover, POP is unsuitable for two way synchronization and more often than not people read and write messages on different devices these days. +~~Besides that any decent email provider supports [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol) these days,~~ +~~using [POP](https://en.wikipedia.org/wiki/Post_Office_Protocol) will result in unnecessary extra battery usage and delayed new message notifications.~~ +~~Moreover, POP is unsuitable for two way synchronization and more often than not people read and write messages on different devices these days.~~ -Basically, POP supports only downloading and deleting messages from the inbox. -So, common operations like setting message attributes (read, starred, answered, etc), adding (backing up) and moving messages is not possible. +~~Basically, POP supports only downloading and deleting messages from the inbox.~~ +~~So, common operations like setting message attributes (read, starred, answered, etc), adding (backing up) and moving messages is not possible.~~ -See also [what Google writes about it](https://support.google.com/mail/answer/7104828). +~~See also [what Google writes about it](https://support.google.com/mail/answer/7104828).~~ -For example [Gmail can import messages](https://support.google.com/mail/answer/21289) from another POP account, -which can be used as a workaround for when your provider doesn't support IMAP. +~~For example [Gmail can import messages](https://support.google.com/mail/answer/21289) from another POP account,~~ +~~which can be used as a workaround for when your provider doesn't support IMAP.~~ -tl;dr; consider to switch to IMAP. +~~tl;dr; consider to switch to IMAP.~~
diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java index 08a1173724..5860704fc1 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySetup.java +++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java @@ -1025,7 +1025,8 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac } private void onEditAccount(Intent intent) { - FragmentAccount fragment = new FragmentAccount(); + boolean pop = intent.getBooleanExtra("pop", false); + FragmentBase fragment = pop ? new FragmentPop() : new FragmentAccount(); fragment.setArguments(intent.getExtras()); FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account"); diff --git a/app/src/main/java/eu/faircode/email/AdapterAccount.java b/app/src/main/java/eu/faircode/email/AdapterAccount.java index ae3dc85814..90876532b4 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAccount.java +++ b/app/src/main/java/eu/faircode/email/AdapterAccount.java @@ -192,7 +192,8 @@ public class AdapterAccount extends RecyclerView.Adapter= Build.VERSION_CODES.O) { + if (!account.pop && account.notify && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { String channelId = EntityAccount.getNotificationChannelId(account.id); NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = nm.getNotificationChannel(channelId); @@ -221,7 +222,7 @@ public class AdapterAccount extends RecyclerView.Adapter messages = db.message().getMessageByMsgId(folder.account, msgid); + if (messages.size() > 0) { + Log.i(folder.name + " POP having=" + msgid); + continue; + } + + String authentication = helper.getAuthentication(); + MessageHelper.MessageParts parts = helper.getMessageParts(); + + EntityMessage message = new EntityMessage(); + message.account = folder.account; + message.folder = folder.id; + message.uid = null; + + message.msgid = helper.getMessageID(); + message.references = TextUtils.join(" ", helper.getReferences()); + message.inreplyto = helper.getInReplyTo(); + message.deliveredto = helper.getDeliveredTo(); + message.thread = helper.getThreadId(context, account.id, 0); + message.receipt_request = helper.getReceiptRequested(); + message.receipt_to = helper.getReceiptTo(); + message.dkim = MessageHelper.getAuthentication("dkim", authentication); + message.spf = MessageHelper.getAuthentication("spf", authentication); + message.dmarc = MessageHelper.getAuthentication("dmarc", authentication); + message.from = helper.getFrom(); + message.to = helper.getTo(); + message.cc = helper.getCc(); + message.bcc = helper.getBcc(); + message.reply = helper.getReply(); + message.list_post = helper.getListPost(); + message.unsubscribe = helper.getListUnsubscribe(); + message.subject = helper.getSubject(); + message.size = helper.getSize(); + message.content = false; + message.received = helper.getReceived(); + message.sent = helper.getSent(); + message.seen = false; + message.answered = false; + message.flagged = false; + message.flags = null; + message.keywords = new String[0]; + message.ui_seen = false; + message.ui_answered = false; + message.ui_flagged = false; + message.ui_hide = 0L; + message.ui_found = false; + message.ui_ignored = false; + message.ui_browsed = false; + + EntityIdentity identity = matchIdentity(context, folder, message); + message.identity = (identity == null ? null : identity.id); + + message.sender = MessageHelper.getSortKey(message.from); + Uri lookupUri = ContactInfo.getLookupUri(context, message.from); + message.avatar = (lookupUri == null ? null : lookupUri.toString()); + + try { + db.beginTransaction(); + + message.id = db.message().insertMessage(message); + Log.i(folder.name + " added id=" + message.id + " uid=" + message.uid); + + int sequence = 1; + for (EntityAttachment attachment : parts.getAttachments()) { + Log.i(folder.name + " attachment seq=" + sequence + + " name=" + attachment.name + " type=" + attachment.type + + " cid=" + attachment.cid + " pgp=" + attachment.encryption + + " size=" + attachment.size); + attachment.message = message.id; + attachment.sequence = sequence++; + attachment.id = db.attachment().insertAttachment(attachment); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + String body = parts.getHtml(context); + Helper.writeText(message.getFile(context), body); + db.message().setMessageContent(message.id, + true, + parts.isPlainOnly(), + HtmlHelper.getPreview(body), + parts.getWarnings(message.warning)); + + for (EntityAttachment attachment : parts.getAttachments()) + parts.downloadAttachment(context, attachment); + + } catch (Throwable ex) { + db.folder().setFolderError(folder.id, Helper.formatThrowable(ex)); + } finally { + ((POP3Message) imessage).invalidate(true); + } + } finally { + db.folder().setFolderSyncState(folder.id, null); + } + } + private static void onSynchronizeMessages( Context context, JSONArray jargs, EntityAccount account, final EntityFolder folder, @@ -1621,18 +1772,6 @@ class Core { message.warning = Helper.formatThrowable(ex, false); } - /* - // Authentication is more reliable - Address sender = helper.getSender(); // header - if (sender != null) { - String[] s = ((InternetAddress) sender).getAddress().split("@"); - String[] f = (froms == null || froms.length == 0 ? null - : (((InternetAddress) froms[0]).getAddress()).split("@")); - if (s.length > 1 && (f == null || (f.length > 1 && !s[1].equals(f[1])))) - message.warning = context.getString(R.string.title_via, s[1]); - } - */ - try { db.beginTransaction(); diff --git a/app/src/main/java/eu/faircode/email/DaoFolder.java b/app/src/main/java/eu/faircode/email/DaoFolder.java index a0fc2cf4e4..66a488330f 100644 --- a/app/src/main/java/eu/faircode/email/DaoFolder.java +++ b/app/src/main/java/eu/faircode/email/DaoFolder.java @@ -35,7 +35,8 @@ public interface DaoFolder { List getFolders(long account, boolean writable, boolean selectable); @Query("SELECT folder.*" + - ", account.id AS accountId, account.`order` AS accountOrder, account.name AS accountName, account.color AS accountColor, account.state AS accountState" + + ", account.id AS accountId, account.pop AS accountPop, account.`order` AS accountOrder" + + ", account.name AS accountName, account.color AS accountColor, account.state AS accountState" + ", COUNT(DISTINCT CASE WHEN rule.enabled THEN rule.id ELSE NULL END) rules" + ", COUNT(DISTINCT CASE WHEN message.ui_hide THEN NULL ELSE message.id END) AS messages" + ", COUNT(DISTINCT CASE WHEN message.content = 1 AND NOT message.ui_hide THEN message.id ELSE NULL END) AS content" + @@ -71,7 +72,8 @@ public interface DaoFolder { List getSortedFolders(); @Query("SELECT folder.*" + - ", account.id AS accountId, account.`order` AS accountOrder, account.name AS accountName, account.color AS accountColor, account.state AS accountState" + + ", account.id AS accountId, account.pop AS accountPop, account.`order` AS accountOrder" + + ", account.name AS accountName, account.color AS accountColor, account.state AS accountState" + ", COUNT(DISTINCT CASE WHEN rule.enabled THEN rule.id ELSE NULL END) rules" + ", COUNT(DISTINCT CASE WHEN message.ui_hide THEN NULL ELSE message.id END) AS messages" + ", COUNT(DISTINCT CASE WHEN message.content = 1 AND NOT message.ui_hide THEN message.id ELSE NULL END) AS content" + @@ -90,7 +92,8 @@ public interface DaoFolder { LiveData> liveFolders(Long account); @Query("SELECT folder.*" + - ", account.id AS accountId, account.`order` AS accountOrder, account.name AS accountName, account.color AS accountColor, account.state AS accountState" + + ", account.id AS accountId, account.pop AS accountPop, account.`order` AS accountOrder" + + ", account.name AS accountName, account.color AS accountColor, account.state AS accountState" + ", COUNT(DISTINCT CASE WHEN rule.enabled THEN rule.id ELSE NULL END) rules" + ", COUNT(DISTINCT CASE WHEN message.ui_hide THEN NULL ELSE message.id END) AS messages" + ", COUNT(DISTINCT CASE WHEN message.content = 1 AND NOT message.ui_hide THEN message.id ELSE NULL END) AS content" + @@ -126,7 +129,8 @@ public interface DaoFolder { LiveData liveSynchronizing(); @Query("SELECT folder.*" + - ", account.id AS accountId, account.`order` AS accountOrder, account.name AS accountName, account.color AS accountColor, account.state AS accountState" + + ", account.id AS accountId, account.pop AS accountPop, account.`order` AS accountOrder" + + ", account.name AS accountName, account.color AS accountColor, account.state AS accountState" + ", COUNT(DISTINCT CASE WHEN rule.enabled THEN rule.id ELSE NULL END) rules" + ", COUNT(DISTINCT CASE WHEN message.ui_hide THEN NULL ELSE message.id END) AS messages" + ", COUNT(DISTINCT CASE WHEN message.content = 1 AND NOT message.ui_hide THEN message.id ELSE NULL END) AS content" + diff --git a/app/src/main/java/eu/faircode/email/DaoMessage.java b/app/src/main/java/eu/faircode/email/DaoMessage.java index 1a88e645a7..91133d3f48 100644 --- a/app/src/main/java/eu/faircode/email/DaoMessage.java +++ b/app/src/main/java/eu/faircode/email/DaoMessage.java @@ -42,7 +42,7 @@ public interface DaoMessage { String is_outbox = "folder.type = '" + EntityFolder.OUTBOX + "'"; @Query("SELECT message.*" + - ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + + ", account.pop AS accountPop, account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType, folder.read_only AS folderReadOnly" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" + ", '[' || group_concat(message.`from`, ',') || ']' AS senders" + @@ -90,7 +90,7 @@ public interface DaoMessage { boolean debug); @Query("SELECT message.*" + - ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + + ", account.pop AS accountPop, account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType, folder.read_only AS folderReadOnly" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" + ", '[' || group_concat(message.`from`, ',') || ']' AS senders" + @@ -132,7 +132,7 @@ public interface DaoMessage { boolean debug); @Query("SELECT message.*" + - ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + + ", account.pop AS accountPop, account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType, folder.read_only AS folderReadOnly" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" + ", message.`from` AS senders" + @@ -231,7 +231,7 @@ public interface DaoMessage { int countMessageByMsgId(long folder, String msgid); @Query("SELECT message.*" + - ", account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" + + ", account.pop AS accountPop, account.name AS accountName, identity.color AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType, folder.read_only AS folderReadOnly" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" + ", message.`from` AS senders" + @@ -249,7 +249,7 @@ public interface DaoMessage { LiveData liveMessage(long id); @Query("SELECT message.*" + - ", account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + + ", account.pop AS accountPop, account.name AS accountName, IFNULL(identity.color, account.color) AS accountColor, account.notify AS accountNotify" + ", folder.name AS folderName, folder.display AS folderDisplay, folder.type AS folderType, folder.read_only AS folderReadOnly" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" + ", message.`from` AS senders" + diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index 3bbdcd45f4..a66c25fddc 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -103,7 +103,7 @@ public class EntityAccount extends EntityOrder implements Serializable { public Long last_connected; String getProtocol() { - return "imap" + (starttls ? "" : "s"); + return (pop ? "pop3" : "imap") + (starttls ? "" : "s"); } static String getNotificationChannelId(long id) { @@ -146,6 +146,7 @@ public class EntityAccount extends EntityOrder implements Serializable { JSONObject json = new JSONObject(); json.put("id", id); json.put("order", order); + json.put("pop", pop); json.put("host", host); json.put("starttls", starttls); json.put("insecure", insecure); @@ -184,6 +185,9 @@ public class EntityAccount extends EntityOrder implements Serializable { if (json.has("order")) account.order = json.getInt("order"); + if (json.has("pop")) + account.pop = json.getBoolean("pop"); + account.host = json.getString("host"); account.starttls = (json.has("starttls") && json.getBoolean("starttls")); account.insecure = (json.has("insecure") && json.getBoolean("insecure")); diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java index f88127eda6..ff444b1de8 100644 --- a/app/src/main/java/eu/faircode/email/EntityOperation.java +++ b/app/src/main/java/eu/faircode/email/EntityOperation.java @@ -79,7 +79,6 @@ public class EntityOperation { static final String COPY = "copy"; static final String FETCH = "fetch"; static final String DELETE = "delete"; - static final String DELETED = "deleted"; static final String SEEN = "seen"; static final String ANSWERED = "answered"; static final String FLAG = "flag"; @@ -228,7 +227,7 @@ public class EntityOperation { name = RAW; } else if (DELETE.equals(name)) - db.message().setMessageUiHide(message.id, new Date().getTime()); + db.message().setMessageUiHide(message.id, Long.MAX_VALUE); } catch (JSONException ex) { Log.e(ex); diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index 7dcb840f77..ea65690dfb 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -84,7 +84,6 @@ public class FragmentAccount extends FragmentBase { private Button btnAutoConfig; private ContentLoadingProgressBar pbAutoConfig; - private TextView tvPopSupport; private TextView tvActiveSyncSupport; private EditText etHost; private RadioGroup rgEncryption; @@ -181,7 +180,6 @@ public class FragmentAccount extends FragmentBase { btnAutoConfig = view.findViewById(R.id.btnAutoConfig); pbAutoConfig = view.findViewById(R.id.pbAutoConfig); - tvPopSupport = view.findViewById(R.id.tvPopSupport); tvActiveSyncSupport = view.findViewById(R.id.tvActiveSyncSupport); etHost = view.findViewById(R.id.etHost); etPort = view.findViewById(R.id.etPort); @@ -279,14 +277,6 @@ public class FragmentAccount extends FragmentBase { } }); - tvPopSupport.setPaintFlags(tvPopSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); - tvPopSupport.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Helper.viewFAQ(getContext(), 11); - } - }); - tvActiveSyncSupport.setPaintFlags(tvActiveSyncSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); tvActiveSyncSupport.setOnClickListener(new View.OnClickListener() { @Override @@ -586,7 +576,7 @@ public class FragmentAccount extends FragmentBase { try (MailService iservice = new MailService(context, protocol, realm, insecure, true)) { iservice.connect(host, Integer.parseInt(port), auth, user, password); - result.idle = iservice.getStore().hasCapability("IDLE"); + result.idle = iservice.hasCapability("IDLE"); boolean inbox = false; @@ -1164,7 +1154,9 @@ public class FragmentAccount extends FragmentBase { @Override protected EntityAccount onExecute(Context context, Bundle args) { long id = args.getLong("id"); - return DB.getInstance(context).account().getAccount(id); + + DB db = DB.getInstance(context); + return db.account().getAccount(id); } @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentAccounts.java b/app/src/main/java/eu/faircode/email/FragmentAccounts.java index 7c673f013b..48b88051a5 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccounts.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccounts.java @@ -34,6 +34,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.PopupMenu; import androidx.constraintlayout.widget.Group; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentTransaction; @@ -108,11 +109,37 @@ public class FragmentAccounts extends FragmentBase { fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - FragmentAccount fragment = new FragmentAccount(); - fragment.setArguments(new Bundle()); - FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); - fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account"); - fragmentTransaction.commit(); + PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(getContext(), getViewLifecycleOwner(), fab); + + popupMenu.getMenu().add(Menu.NONE, R.string.title_imap, 1, R.string.title_imap) + .setEnabled(Helper.hasValidFingerprint(getContext())); + popupMenu.getMenu().add(Menu.NONE, R.string.title_pop3, 2, R.string.title_pop3); + + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.string.title_imap: + onCreate(true); + return true; + case R.string.title_pop3: + onCreate(false); + return true; + default: + return false; + } + } + + private void onCreate(boolean imap) { + FragmentBase fragment = imap ? new FragmentAccount() : new FragmentPop(); + fragment.setArguments(new Bundle()); + FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account"); + fragmentTransaction.commit(); + } + }); + + popupMenu.show(); } }); diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index b8641326f5..b6643464cf 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -2187,8 +2187,6 @@ public class FragmentCompose extends FragmentBase { draft.plain_only = ref.plain_only; if (answer > 0) body = EntityAnswer.getAnswerText(context, answer, draft.to) + body; - - EntityOperation.queue(context, ref, EntityOperation.SEEN, true); } if (plain_only) diff --git a/app/src/main/java/eu/faircode/email/FragmentFolders.java b/app/src/main/java/eu/faircode/email/FragmentFolders.java index fd2a5f91c6..48c0ee357e 100644 --- a/app/src/main/java/eu/faircode/email/FragmentFolders.java +++ b/app/src/main/java/eu/faircode/email/FragmentFolders.java @@ -270,7 +270,7 @@ public class FragmentFolders extends FragmentBase { else fabError.hide(); - if (account == null) + if (account == null || account.pop) fab.hide(); else fab.show(); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 31d7a3c4ea..07063bd660 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -838,8 +838,9 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. int order = 1; for (EntityAccount account : accounts) - popupMenu.getMenu().add(Menu.NONE, 0, order++, account.name) - .setIntent(new Intent().putExtra("account", account.id)); + if (!account.pop) + popupMenu.getMenu().add(Menu.NONE, 0, order++, account.name) + .setIntent(new Intent().putExtra("account", account.id)); popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override @@ -1749,7 +1750,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. if (result.hasTrash == null) result.hasTrash = false; if (result.hasJunk == null) result.hasJunk = false; - result.accounts = db.account().getSynchronizingAccounts(); + result.accounts = new ArrayList<>(); + for (EntityAccount account : db.account().getSynchronizingAccounts()) + if (!account.pop) + result.accounts.add(account); return result; } @@ -3211,7 +3215,14 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. if (folder == null) return null; - if (message.uid != null) { + EntityAccount account = db.account().getAccount(folder.account); + if (account == null) + return null; + + if (message.uid == null) { + if (expand_read && !message.ui_seen && account.pop) + EntityOperation.queue(context, message, EntityOperation.SEEN, true); + } else { if (!message.content) EntityOperation.queue(context, message, EntityOperation.BODY); diff --git a/app/src/main/java/eu/faircode/email/FragmentPop.java b/app/src/main/java/eu/faircode/email/FragmentPop.java new file mode 100644 index 0000000000..743be037a0 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/FragmentPop.java @@ -0,0 +1,605 @@ +package eu.faircode.email; + +/* + 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 . + + Copyright 2018-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.RadioGroup; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; + +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputLayout; + +import java.util.Date; +import java.util.List; + +import static android.app.Activity.RESULT_OK; +import static com.google.android.material.textfield.TextInputLayout.END_ICON_NONE; +import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE; + +public class FragmentPop extends FragmentBase { + private ViewGroup view; + private ScrollView scroll; + + private EditText etHost; + private RadioGroup rgEncryption; + private CheckBox cbInsecure; + private EditText etPort; + private EditText etUser; + private TextInputLayout tilPassword; + + private EditText etName; + private Button btnColor; + private View vwColor; + private ImageButton ibColorDefault; + private TextView tvColorPro; + + private CheckBox cbSynchronize; + private CheckBox cbPrimary; + private EditText etInterval; + + private Button btnSave; + private ContentLoadingProgressBar pbSave; + private TextView tvError; + + private ContentLoadingProgressBar pbWait; + + private long id = -1; + private boolean saving = false; + private int color = Color.TRANSPARENT; + + private static final int REQUEST_COLOR = 1; + private static final int REQUEST_DELETE = 2; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Get arguments + Bundle args = getArguments(); + id = args.getLong("id", -1); + } + + @Override + @Nullable + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + setSubtitle(R.string.title_edit_account); + setHasOptionsMenu(true); + + view = (ViewGroup) inflater.inflate(R.layout.fragment_pop, container, false); + scroll = view.findViewById(R.id.scroll); + + // Get controls + etHost = view.findViewById(R.id.etHost); + etPort = view.findViewById(R.id.etPort); + rgEncryption = view.findViewById(R.id.rgEncryption); + cbInsecure = view.findViewById(R.id.cbInsecure); + etUser = view.findViewById(R.id.etUser); + tilPassword = view.findViewById(R.id.tilPassword); + + etName = view.findViewById(R.id.etName); + btnColor = view.findViewById(R.id.btnColor); + vwColor = view.findViewById(R.id.vwColor); + ibColorDefault = view.findViewById(R.id.ibColorDefault); + tvColorPro = view.findViewById(R.id.tvColorPro); + + cbSynchronize = view.findViewById(R.id.cbSynchronize); + cbPrimary = view.findViewById(R.id.cbPrimary); + etInterval = view.findViewById(R.id.etInterval); + + btnSave = view.findViewById(R.id.btnSave); + pbSave = view.findViewById(R.id.pbSave); + + tvError = view.findViewById(R.id.tvError); + + pbWait = view.findViewById(R.id.pbWait); + + setColor(color); + btnColor.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + FragmentDialogColor fragment = new FragmentDialogColor(); + fragment.initialize(R.string.title_color, color, new Bundle(), getContext()); + fragment.setTargetFragment(FragmentPop.this, REQUEST_COLOR); + fragment.show(getFragmentManager(), "account:color"); + } + }); + + ibColorDefault.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setColor(Color.TRANSPARENT); + } + }); + + Helper.linkPro(tvColorPro); + + cbSynchronize.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + cbPrimary.setEnabled(checked); + } + }); + + btnSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onSave(); + } + }); + + // Initialize + Helper.setViewsEnabled(view, false); + + tilPassword.setEndIconMode(id < 0 ? END_ICON_PASSWORD_TOGGLE : END_ICON_NONE); + + pbSave.setVisibility(View.GONE); + tvError.setVisibility(View.GONE); + + return view; + } + + private void onSave() { + Bundle args = new Bundle(); + args.putLong("id", id); + + args.putString("host", etHost.getText().toString()); + args.putBoolean("starttls", rgEncryption.getCheckedRadioButtonId() == R.id.radio_starttls); + args.putBoolean("insecure", cbInsecure.isChecked()); + args.putString("port", etPort.getText().toString()); + args.putString("user", etUser.getText().toString()); + args.putString("password", tilPassword.getEditText().getText().toString()); + + args.putString("name", etName.getText().toString()); + args.putInt("color", color); + + args.putBoolean("primary", cbPrimary.isChecked()); + args.putBoolean("synchronize", cbSynchronize.isChecked()); + args.putString("interval", etInterval.getText().toString()); + + new SimpleTask() { + @Override + protected void onPreExecute(Bundle args) { + saving = true; + getActivity().invalidateOptionsMenu(); + Helper.setViewsEnabled(view, false); + tvError.setVisibility(View.GONE); + } + + @Override + protected void onPostExecute(Bundle args) { + saving = false; + getActivity().invalidateOptionsMenu(); + Helper.setViewsEnabled(view, true); + pbSave.setVisibility(View.GONE); + } + + @Override + protected Boolean onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + + String host = args.getString("host"); + boolean starttls = args.getBoolean("starttls"); + boolean insecure = args.getBoolean("insecure"); + String port = args.getString("port"); + String user = args.getString("user").trim(); + String password = args.getString("password"); + + String name = args.getString("name"); + Integer color = args.getInt("color"); + + boolean synchronize = args.getBoolean("synchronize"); + boolean primary = args.getBoolean("primary"); + String interval = args.getString("interval"); + + boolean pro = ActivityBilling.isPro(context); + + if (host.contains(":")) { + Uri h = Uri.parse(host); + host = h.getHost(); + } + + if (TextUtils.isEmpty(host)) + throw new IllegalArgumentException(context.getString(R.string.title_no_host)); + if (TextUtils.isEmpty(port)) + port = "995"; + if (TextUtils.isEmpty(user)) + throw new IllegalArgumentException(context.getString(R.string.title_no_user)); + if (synchronize && TextUtils.isEmpty(password) && !insecure) + throw new IllegalArgumentException(context.getString(R.string.title_no_password)); + if (TextUtils.isEmpty(interval)) + interval = Integer.toString(EntityAccount.DEFAULT_KEEP_ALIVE_INTERVAL); + + if (TextUtils.isEmpty(name)) + name = user; + if (color == Color.TRANSPARENT || !pro) + color = null; + + long now = new Date().getTime(); + + DB db = DB.getInstance(context); + EntityAccount account = db.account().getAccount(id); + + boolean check = (synchronize && (account == null || + !account.synchronize || account.error != null || + !account.insecure.equals(insecure) || + !host.equals(account.host) || Integer.parseInt(port) != account.port || + !user.equals(account.user) || !password.equals(account.password))); + boolean reload = (check || account == null || + account.synchronize != synchronize || + !account.poll_interval.equals(Integer.parseInt(interval))); + Log.i("Account check=" + check + " reload=" + reload); + + Long last_connected = null; + if (account != null && synchronize == account.synchronize) + last_connected = account.last_connected; + + // Check POP3 server + if (check) { + String protocol = "pop3" + (starttls ? "" : "s"); + try (MailService iservice = new MailService(context, protocol, null, insecure, true)) { + iservice.connect(host, Integer.parseInt(port), MailService.AUTH_TYPE_PASSWORD, user, password); + } + } + + try { + db.beginTransaction(); + + if (account != null && !account.password.equals(password)) { + List identities = db.identity().getIdentities(account.id); + for (EntityIdentity identity : identities) + if (identity.password.equals(account.password) && + ConnectionHelper.isSameDomain(identity.host, account.host)) { + Log.i("Changing identity password host=" + identity.host); + identity.password = password; + db.identity().updateIdentity(identity); + } + } + + boolean update = (account != null); + if (account == null) + account = new EntityAccount(); + + account.pop = true; + account.host = host; + account.starttls = starttls; + account.insecure = insecure; + account.port = Integer.parseInt(port); + account.auth_type = MailService.AUTH_TYPE_PASSWORD; + account.user = user; + account.password = password; + + account.name = name; + account.color = color; + + account.synchronize = synchronize; + account.primary = (account.synchronize && primary); + account.browse = false; + account.poll_interval = Integer.parseInt(interval); + + if (!update) + account.created = now; + + account.warning = null; + account.error = null; + account.last_connected = last_connected; + + if (account.primary) + db.account().resetPrimary(); + + if (update) + db.account().updateAccount(account); + else + account.id = db.account().insertAccount(account); + EntityLog.log(context, (update ? "Updated" : "Added") + " account=" + account.name); + + + EntityFolder inbox = db.folder().getFolderByType(account.id, EntityFolder.INBOX); + if (inbox == null) { + inbox = new EntityFolder(); + inbox.account = account.id; + inbox.name = "INBOX"; + inbox.type = EntityFolder.INBOX; + inbox.synchronize = true; + inbox.unified = true; + inbox.notify = true; + inbox.sync_days = Integer.MAX_VALUE; + inbox.keep_days = Integer.MAX_VALUE; + inbox.initialize = 0; + inbox.id = db.folder().insertFolder(inbox); + } + + EntityFolder drafts = db.folder().getFolderByType(account.id, EntityFolder.DRAFTS); + if (drafts == null) { + drafts = new EntityFolder(); + drafts.account = account.id; + drafts.name = context.getString(R.string.title_folder_drafts); + drafts.type = EntityFolder.DRAFTS; + drafts.synchronize = false; + drafts.unified = false; + drafts.notify = false; + drafts.sync_days = Integer.MAX_VALUE; + drafts.keep_days = Integer.MAX_VALUE; + drafts.initialize = 0; + drafts.id = db.folder().insertFolder(drafts); + } + + EntityFolder sent = db.folder().getFolderByType(account.id, EntityFolder.SENT); + if (sent == null) { + sent = new EntityFolder(); + sent.account = account.id; + sent.name = context.getString(R.string.title_folder_sent); + sent.type = EntityFolder.SENT; + sent.synchronize = false; + sent.unified = false; + sent.notify = false; + sent.sync_days = Integer.MAX_VALUE; + sent.keep_days = Integer.MAX_VALUE; + sent.initialize = 0; + sent.id = db.folder().insertFolder(sent); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + if (reload) + ServiceSynchronize.reload(context, "save account"); + + if (!synchronize) { + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel("receive:" + account.id, 1); + nm.cancel("alert:" + account.id, 1); + } + + return false; + } + + @Override + protected void onExecuted(Bundle args, Boolean dirty) { + getFragmentManager().popBackStack(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + if (ex instanceof IllegalArgumentException) + Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show(); + else { + tvError.setText(Helper.formatThrowable(ex, false)); + tvError.setVisibility(View.VISIBLE); + + new Handler().post(new Runnable() { + @Override + public void run() { + scroll.smoothScrollTo(0, tvError.getBottom()); + } + }); + } + } + }.execute(this, args, "account:save"); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putString("fair:password", tilPassword.getEditText().getText().toString()); + outState.putInt("fair:color", color); + super.onSaveInstanceState(outState); + } + + @Override + public void onActivityCreated(@Nullable final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Bundle args = new Bundle(); + args.putLong("id", id); + + new SimpleTask() { + @Override + protected EntityAccount onExecute(Context context, Bundle args) { + long id = args.getLong("id"); + + DB db = DB.getInstance(context); + return db.account().getAccount(id); + } + + @Override + protected void onExecuted(Bundle args, final EntityAccount account) { + if (savedInstanceState == null) { + etHost.setText(account == null ? null : account.host); + etPort.setText(account == null ? null : Long.toString(account.port)); + + rgEncryption.check(account != null && account.starttls ? R.id.radio_starttls : R.id.radio_ssl); + cbInsecure.setChecked(account == null ? false : account.insecure); + + etUser.setText(account == null ? null : account.user); + tilPassword.getEditText().setText(account == null ? null : account.password); + + etName.setText(account == null ? null : account.name); + + cbSynchronize.setChecked(account == null ? true : account.synchronize); + cbPrimary.setChecked(account == null ? false : account.primary); + etInterval.setText(account == null ? "" : Long.toString(account.poll_interval)); + + color = (account == null || account.color == null ? Color.TRANSPARENT : account.color); + + new SimpleTask() { + @Override + protected EntityAccount onExecute(Context context, Bundle args) { + return DB.getInstance(context).account().getPrimaryAccount(); + } + + @Override + protected void onExecuted(Bundle args, EntityAccount primary) { + if (primary == null) + cbPrimary.setChecked(true); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getFragmentManager(), ex); + } + }.execute(FragmentPop.this, new Bundle(), "account:primary"); + } else { + tilPassword.getEditText().setText(savedInstanceState.getString("fair:password")); + color = savedInstanceState.getInt("fair:color"); + } + + setColor(color); + cbPrimary.setEnabled(cbSynchronize.isChecked()); + + Helper.setViewsEnabled(view, true); + + pbWait.setVisibility(View.GONE); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getFragmentManager(), ex); + } + }.execute(this, args, "account:get"); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_account, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(R.id.menu_delete).setVisible(id > 0 && !saving); + super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_delete: + onMenuDelete(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void setColor(int color) { + this.color = color; + + GradientDrawable border = new GradientDrawable(); + border.setColor(color); + border.setStroke(1, Helper.resolveColor(getContext(), R.attr.colorSeparator)); + vwColor.setBackground(border); + } + + private void onMenuDelete() { + Bundle aargs = new Bundle(); + aargs.putString("question", getString(R.string.title_account_delete)); + + FragmentDialogAsk fragment = new FragmentDialogAsk(); + fragment.setArguments(aargs); + fragment.setTargetFragment(FragmentPop.this, REQUEST_DELETE); + fragment.show(getFragmentManager(), "account:delete"); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + try { + switch (requestCode) { + case REQUEST_COLOR: + if (resultCode == RESULT_OK && data != null) { + if (ActivityBilling.isPro(getContext())) { + Bundle args = data.getBundleExtra("args"); + setColor(args.getInt("color")); + } else + startActivity(new Intent(getContext(), ActivityBilling.class)); + } + break; + case REQUEST_DELETE: + if (resultCode == RESULT_OK) + onDelete(); + break; + } + } catch (Throwable ex) { + Log.e(ex); + } + } + + private void onDelete() { + Bundle args = new Bundle(); + args.putLong("id", id); + + new SimpleTask() { + @Override + protected void onPostExecute(Bundle args) { + Helper.setViewsEnabled(view, false); + pbWait.setVisibility(View.VISIBLE); + } + + @Override + protected Void onExecute(Context context, Bundle args) { + long id = args.getLong("id"); + + DB db = DB.getInstance(context); + db.account().setAccountTbd(id); + + ServiceSynchronize.reload(context, "delete account"); + + return null; + } + + @Override + protected void onExecuted(Bundle args, Void data) { + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) + getFragmentManager().popBackStack(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getFragmentManager(), ex); + } + }.execute(this, args, "account:delete"); + } +} diff --git a/app/src/main/java/eu/faircode/email/MailService.java b/app/src/main/java/eu/faircode/email/MailService.java index 43e50d3454..799c52c895 100644 --- a/app/src/main/java/eu/faircode/email/MailService.java +++ b/app/src/main/java/eu/faircode/email/MailService.java @@ -29,6 +29,7 @@ import javax.mail.MessagingException; import javax.mail.NoSuchProviderException; import javax.mail.Service; import javax.mail.Session; +import javax.mail.Store; public class MailService implements AutoCloseable { private Context context; @@ -66,7 +67,22 @@ public class MailService implements AutoCloseable { String checkserveridentity = Boolean.toString(!insecure).toLowerCase(); - if ("imap".equals(protocol) || "imaps".equals(protocol)) { + 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." + protocol + ".ssl.checkserveridentity", checkserveridentity); + properties.put("mail." + protocol + ".ssl.trust", "*"); + + properties.put("mail.pop3s.starttls.enable", "false"); + + properties.put("mail.pop3.starttls.enable", "true"); + properties.put("mail.pop3.starttls.required", "true"); + + // TODO: make timeouts configurable? + properties.put("mail." + protocol + ".connectiontimeout", Integer.toString(CONNECT_TIMEOUT)); + properties.put("mail." + protocol + ".writetimeout", Integer.toString(WRITE_TIMEOUT)); // one thread overhead + properties.put("mail." + protocol + ".timeout", Integer.toString(READ_TIMEOUT)); + + } else if ("imap".equals(protocol) || "imaps".equals(protocol)) { // https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#properties properties.put("mail." + protocol + ".ssl.checkserveridentity", checkserveridentity); properties.put("mail." + protocol + ".ssl.trust", "*"); @@ -218,17 +234,22 @@ public class MailService implements AutoCloseable { isession.setDebug(debug); //System.setProperty("mail.socket.debug", Boolean.toString(debug)); - if ("imap".equals(protocol) || "imaps".equals(protocol)) { + if ("pop3".equals(protocol) || "pop3s".equals(protocol)) { + iservice = isession.getStore(protocol); + iservice.connect(host, port, user, password); + + } else if ("imap".equals(protocol) || "imaps".equals(protocol)) { iservice = isession.getStore(protocol); iservice.connect(host, port, user, password); // https://www.ietf.org/rfc/rfc2971.txt - if (getStore().hasCapability("ID")) + IMAPStore istore = (IMAPStore) getStore(); + if (istore.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); + Map sid = istore.id(id); if (sid != null) { Map crumb = new HashMap<>(); for (String key : sid.keySet()) { @@ -322,14 +343,22 @@ public class MailService implements AutoCloseable { return folders; } - IMAPStore getStore() { - return (IMAPStore) iservice; + Store getStore() { + return (Store) iservice; } SMTPTransport getTransport() { return (SMTPTransport) iservice; } + boolean hasCapability(String capability) throws MessagingException { + Store store = getStore(); + if (store instanceof IMAPStore) + return ((IMAPStore) getStore()).hasCapability(capability); + else + return false; + } + public void close() throws MessagingException { try { if (iservice != null) diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index bba31104f9..f02dcd81c8 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -701,7 +701,7 @@ public class ServiceSynchronize extends ServiceBase { throw ex; } - final boolean capIdle = iservice.getStore().hasCapability("IDLE"); + final boolean capIdle = iservice.hasCapability("IDLE"); Log.i(account.name + " idle=" + capIdle); db.account().setAccountState(account.id, "connected"); @@ -792,7 +792,8 @@ public class ServiceSynchronize extends ServiceBase { }); // Update folder list - Core.onSynchronizeFolders(this, account, iservice.getStore(), state); + if (!account.pop) + Core.onSynchronizeFolders(this, account, iservice.getStore(), state); // Open synchronizing folders final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); @@ -993,12 +994,13 @@ public class ServiceSynchronize extends ServiceBase { // Get folder Folder ifolder = mapFolders.get(folder); // null when polling - final boolean shouldClose = (ifolder == null); + boolean canOpen = (!account.pop || EntityFolder.INBOX.equals(folder.type)); + final boolean shouldClose = (ifolder == null && canOpen); try { Log.i(folder.name + " run " + (shouldClose ? "offline" : "online")); - if (ifolder == null) { + if (shouldClose) { // Prevent unnecessary folder connections if (db.operation().getOperationCount(folder.id, null) == 0) return; diff --git a/app/src/main/java/eu/faircode/email/TupleFolderEx.java b/app/src/main/java/eu/faircode/email/TupleFolderEx.java index d44394ded0..0e34eb1ceb 100644 --- a/app/src/main/java/eu/faircode/email/TupleFolderEx.java +++ b/app/src/main/java/eu/faircode/email/TupleFolderEx.java @@ -38,6 +38,7 @@ import java.util.Objects; public class TupleFolderEx extends EntityFolder implements Serializable { public Long accountId; + public Boolean accountPop; public Integer accountOrder; public String accountName; public Integer accountColor; @@ -66,6 +67,7 @@ public class TupleFolderEx extends EntityFolder implements Serializable { TupleFolderEx other = (TupleFolderEx) obj; return (super.equals(obj) && Objects.equals(this.accountId, other.accountId) && + Objects.equals(this.accountPop, other.accountPop) && Objects.equals(this.accountName, other.accountName) && Objects.equals(this.accountColor, other.accountColor) && Objects.equals(this.accountState, other.accountState) && diff --git a/app/src/main/java/eu/faircode/email/TupleMessageEx.java b/app/src/main/java/eu/faircode/email/TupleMessageEx.java index d9929e84df..006c114c8a 100644 --- a/app/src/main/java/eu/faircode/email/TupleMessageEx.java +++ b/app/src/main/java/eu/faircode/email/TupleMessageEx.java @@ -26,6 +26,7 @@ import java.util.Objects; import javax.mail.Address; public class TupleMessageEx extends EntityMessage { + public boolean accountPop; public String accountName; public Integer accountColor; public boolean accountNotify; @@ -52,6 +53,7 @@ public class TupleMessageEx extends EntityMessage { if (obj instanceof TupleMessageEx) { TupleMessageEx other = (TupleMessageEx) obj; return (super.equals(obj) && + this.accountPop == other.accountPop && Objects.equals(this.accountName, other.accountName) && Objects.equals(this.accountColor, other.accountColor) && this.accountNotify == other.accountNotify && diff --git a/app/src/main/res/layout/fragment_account.xml b/app/src/main/res/layout/fragment_account.xml index 5d9454e056..74eb97120b 100644 --- a/app/src/main/res/layout/fragment_account.xml +++ b/app/src/main/res/layout/fragment_account.xml @@ -101,19 +101,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/btnAutoConfig" /> - - + app:layout_constraintTop_toBottomOf="@id/tvImap" /> @@ -806,7 +793,7 @@ android:layout_height="0dp" app:constraint_referenced_ids=" tvDomain,tvDomainHint,etDomain,btnAutoConfig, - tvImap,tvPopSupport,tvActiveSyncSupport,tvHost,etHost,rgEncryption,cbInsecure,tvPort,etPort" /> + tvImap,tvActiveSyncSupport,tvHost,etHost,rgEncryption,cbInsecure,tvPort,etPort" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +