POP3 support

pull/162/head
M66B 5 years ago
parent 70cce65e61
commit 0604a1b077

@ -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
<br />
<a name="faq11"></a>
**(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.~~
<br />

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

@ -192,7 +192,8 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast(
new Intent(settings ? ActivitySetup.ACTION_EDIT_ACCOUNT : ActivityView.ACTION_VIEW_FOLDERS)
.putExtra("id", account.id));
.putExtra("id", account.id)
.putExtra("pop", account.pop));
}
}
@ -213,7 +214,7 @@ public class AdapterAccount extends RecyclerView.Adapter<AdapterAccount.ViewHold
popupMenu.getMenu().add(Menu.NONE, R.string.title_enabled, 1, R.string.title_enabled)
.setCheckable(true).setChecked(account.synchronize);
if (account.notify && Build.VERSION.SDK_INT >= 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<AdapterAccount.ViewHold
popupMenu.getMenu().add(Menu.NONE, R.string.title_edit_channel, 2, R.string.title_edit_channel);
}
if (settings)
if (!account.pop && settings)
popupMenu.getMenu().add(Menu.NONE, R.string.title_copy, 3, R.string.title_copy);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {

@ -273,12 +273,19 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
ivSync.setImageResource(R.drawable.baseline_sync_24);
} else {
StringBuilder a = new StringBuilder();
a.append(NF.format(folder.sync_days));
if (folder.sync_days == Integer.MAX_VALUE)
a.append('∞');
else
a.append(NF.format(folder.sync_days));
a.append('/');
if (folder.keep_days == Integer.MAX_VALUE)
a.append('∞');
else
a.append(NF.format(folder.keep_days));
tvAfter.setText(a.toString());
ivSync.setImageResource(folder.synchronize ? R.drawable.baseline_sync_24 : R.drawable.baseline_sync_disabled_24);
}
@ -368,13 +375,14 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
if (!folder.selectable || folder.tbd != null)
return false;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, view);
popupMenu.getMenu().add(Menu.NONE, 0, 0, folder.getDisplayName(context)).setEnabled(false);
popupMenu.getMenu().add(Menu.NONE, R.string.title_synchronize_now, 1, R.string.title_synchronize_now);
if (folder.account != null) {
if (folder.account != null && !folder.accountPop) {
popupMenu.getMenu().add(Menu.NONE, R.string.title_synchronize_all, 2, R.string.title_synchronize_all);
popupMenu.getMenu().add(Menu.NONE, R.string.title_delete_local, 3, R.string.title_delete_local);
@ -382,8 +390,9 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
if (EntityFolder.TRASH.equals(folder.type))
popupMenu.getMenu().add(Menu.NONE, R.string.title_empty_trash, 5, R.string.title_empty_trash);
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (folder.account != null) {
String startup = prefs.getString("startup", "unified");
if (!"accounts".equals(startup))
popupMenu.getMenu().add(Menu.NONE, R.string.title_unified_folder, 6, R.string.title_unified_folder)
@ -394,7 +403,9 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
popupMenu.getMenu().add(Menu.NONE, R.string.title_notify_folder, 8, R.string.title_notify_folder)
.setCheckable(true).setChecked(folder.notify);
}
if (folder.account != null && !folder.accountPop) {
boolean subscriptions = prefs.getBoolean("subscriptions", false);
if (folder.subscribed != null && subscriptions)
popupMenu.getMenu().add(Menu.NONE, R.string.title_subscribe, 9, R.string.title_subscribe)

@ -654,7 +654,7 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
view.setAlpha(
(EntityFolder.OUTBOX.equals(message.folderType)
? message.identitySynchronize == null || !message.identitySynchronize
: message.uid == null)
: message.uid == null && !message.accountPop)
? Helper.LOW_LIGHT : 1.0f);
// Duplicate
@ -2426,6 +2426,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
popupMenu.getMenu().findItem(R.id.menu_resync).setEnabled(message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_create_rule).setEnabled(!message.accountPop);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem target) {

@ -339,7 +339,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
try {
// https://tools.ietf.org/html/rfc3501#section-6.4.4
Argument arg = new Argument();
if (query.startsWith("raw:") && state.iservice.getStore().hasCapability("X-GM-EXT-1")) {
if (query.startsWith("raw:") && state.iservice.hasCapability("X-GM-EXT-1")) {
// https://support.google.com/mail/answer/7190
// https://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw
arg.writeAtom("X-GM-RAW");

@ -49,6 +49,9 @@ import com.sun.mail.imap.protocol.FLAGS;
import com.sun.mail.imap.protocol.FetchResponse;
import com.sun.mail.imap.protocol.IMAPProtocol;
import com.sun.mail.imap.protocol.UID;
import com.sun.mail.pop3.POP3Folder;
import com.sun.mail.pop3.POP3Message;
import com.sun.mail.pop3.POP3Store;
import com.sun.mail.util.MessageRemovedIOException;
import org.json.JSONArray;
@ -168,10 +171,8 @@ class Core {
!EntityOperation.SYNC.equals(op.name) &&
!EntityOperation.SUBSCRIBE.equals(op.name))
throw new MessageRemovedException();
} else {
} else
db.message().setMessageError(message.id, null);
ensureUid(context, folder, message, op, (IMAPFolder) ifolder);
}
// Operations should use database transaction when needed
@ -204,73 +205,99 @@ class Core {
continue;
}
switch (op.name) {
case EntityOperation.SEEN:
onSeen(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
if (istore instanceof POP3Store)
switch (op.name) {
case EntityOperation.SEEN:
db.message().setMessageUiSeen(folder.id, jargs.getBoolean(0));
break;
case EntityOperation.ANSWERED:
case EntityOperation.ADD:
case EntityOperation.EXISTS:
break;
case EntityOperation.DELETE:
if (!EntityFolder.INBOX.equals(folder.type))
db.message().deleteMessage(folder.id, op.message);
break;
case EntityOperation.SYNC:
if (ifolder == null)
db.folder().setFolderSyncState(folder.id, null);
else
onSynchronizeMessages(context, jargs, account, folder, (POP3Folder) ifolder, state);
break;
default:
Log.w(folder.name + " ignored=" + op.name);
}
else {
ensureUid(context, folder, message, op, (IMAPFolder) ifolder);
switch (op.name) {
case EntityOperation.SEEN:
onSeen(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.FLAG:
onFlag(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.FLAG:
onFlag(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.ANSWERED:
onAnswered(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.ANSWERED:
onAnswered(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.KEYWORD:
onKeyword(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.KEYWORD:
onKeyword(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.ADD:
onAdd(context, jargs, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break;
case EntityOperation.ADD:
onAdd(context, jargs, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break;
case EntityOperation.MOVE:
onMove(context, jargs, false, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break;
case EntityOperation.MOVE:
onMove(context, jargs, false, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break;
case EntityOperation.COPY:
onMove(context, jargs, true, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break;
case EntityOperation.COPY:
onMove(context, jargs, true, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break;
case EntityOperation.FETCH:
onFetch(context, jargs, folder, (IMAPFolder) ifolder, state);
break;
case EntityOperation.FETCH:
onFetch(context, jargs, folder, (IMAPFolder) ifolder, state);
break;
case EntityOperation.DELETE:
onDelete(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.DELETE:
onDelete(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.HEADERS:
onHeaders(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.HEADERS:
onHeaders(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.RAW:
onRaw(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.RAW:
onRaw(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.BODY:
onBody(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.BODY:
onBody(context, jargs, folder, message, (IMAPFolder) ifolder);
break;
case EntityOperation.ATTACHMENT:
onAttachment(context, jargs, folder, message, op, (IMAPFolder) ifolder);
break;
case EntityOperation.ATTACHMENT:
onAttachment(context, jargs, folder, message, op, (IMAPFolder) ifolder);
break;
case EntityOperation.EXISTS:
onExists(context, jargs, folder, message, op, (IMAPFolder) ifolder);
break;
case EntityOperation.EXISTS:
onExists(context, jargs, folder, message, op, (IMAPFolder) ifolder);
break;
case EntityOperation.SYNC:
onSynchronizeMessages(context, jargs, account, folder, (IMAPFolder) ifolder, state);
break;
case EntityOperation.SYNC:
onSynchronizeMessages(context, jargs, account, folder, (IMAPFolder) ifolder, state);
break;
case EntityOperation.SUBSCRIBE:
onSubscribeFolder(context, jargs, folder, (IMAPFolder) ifolder);
break;
case EntityOperation.SUBSCRIBE:
onSubscribeFolder(context, jargs, folder, (IMAPFolder) ifolder);
break;
default:
throw new IllegalArgumentException("Unknown operation=" + op.name);
default:
throw new IllegalArgumentException("Unknown operation=" + op.name);
}
}
// Operation succeeded
@ -343,7 +370,7 @@ class Core {
}
private static void ensureUid(Context context, EntityFolder folder, EntityMessage message, EntityOperation op, IMAPFolder ifolder) throws MessagingException {
if (message.uid != null)
if (message == null || message.uid != null)
return;
if (EntityOperation.ADD.equals(op.name))
return;
@ -1089,6 +1116,130 @@ class Core {
Log.i(folder.name + " subscribed=" + subscribe);
}
private static void onSynchronizeMessages(
Context context, JSONArray jargs,
EntityAccount account, final EntityFolder folder,
final POP3Folder ifolder, State state) throws MessagingException {
DB db = DB.getInstance(context);
try {
db.folder().setFolderSyncState(folder.id, "syncing");
Message[] imessages = ifolder.getMessages();
Log.i(folder.name + " POP messages=" + imessages.length);
db.folder().setFolderSyncState(folder.id, "downloading");
for (Message imessage : imessages)
try {
if (!state.isRunning())
break;
MessageHelper helper = new MessageHelper((MimeMessage) imessage);
String msgid = helper.getMessageID();
if (msgid == null) {
Log.w(folder.name + " POP no message ID");
continue;
}
List<EntityMessage> 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();

@ -35,7 +35,8 @@ public interface DaoFolder {
List<EntityFolder> 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<TupleFolderSort> 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<List<TupleFolderEx>> 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<Integer> 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" +

@ -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<TupleMessageEx> 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" +

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

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

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

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

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

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

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

@ -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 <http://www.gnu.org/licenses/>.
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<Boolean>() {
@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<EntityIdentity> 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<EntityAccount>() {
@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<EntityAccount>() {
@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<Void>() {
@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");
}
}

@ -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<String, String> id = new LinkedHashMap<>();
id.put("name", context.getString(R.string.app_name));
id.put("version", BuildConfig.VERSION_NAME);
Map<String, String> sid = getStore().id(id);
Map<String, String> sid = istore.id(id);
if (sid != null) {
Map<String, String> 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)

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

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

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

@ -101,19 +101,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnAutoConfig" />
<TextView
android:id="@+id/tvPopSupport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:drawableEnd="@drawable/baseline_open_in_new_24"
android:drawablePadding="6dp"
android:text="@string/title_pop3_support"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorLink"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvImap" />
<TextView
android:id="@+id/tvActiveSyncSupport"
android:layout_width="wrap_content"
@ -125,7 +112,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorLink"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPopSupport" />
app:layout_constraintTop_toBottomOf="@id/tvImap" />
<!-- host -->
@ -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" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpAuthorize"

@ -0,0 +1,339 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="12dp"
android:scrollbarStyle="outsideOverlay"
tools:context=".ActivitySetup">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvHost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_host"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/etHost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="pop.domain.tld"
android:inputType="textUri"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvHost" />
<!-- SSL/STARTTLS -->
<RadioGroup
android:id="@+id/rgEncryption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etHost">
<RadioButton
android:id="@+id/radio_ssl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_ssl" />
<RadioButton
android:id="@+id/radio_starttls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/title_starttls" />
</RadioGroup>
<CheckBox
android:id="@+id/cbInsecure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_allow_insecure"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rgEncryption" />
<!-- port -->
<TextView
android:id="@+id/tvPort"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_port"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbInsecure" />
<EditText
android:id="@+id/etPort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="995"
android:inputType="number"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPort" />
<!-- user -->
<TextView
android:id="@+id/tvUser"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:labelFor="@+id/etUser"
android:text="@string/title_user"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etPort" />
<EditText
android:id="@+id/etUser"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="username"
android:inputType="text"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvUser" />
<!-- password -->
<TextView
android:id="@+id/tvPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_password"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etUser" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPassword"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="password"
android:inputType="textPassword"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" />
</com.google.android.material.textfield.TextInputLayout>
<!-- name -->
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_account_name"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tilPassword" />
<TextView
android:id="@+id/tvNameRemark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/title_account_name_hint"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvName" />
<EditText
android:id="@+id/etName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title_optional"
android:inputType="textCapSentences|textAutoCorrect"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvNameRemark" />
<TextView
android:id="@+id/tvColor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_color"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etName" />
<Button
android:id="@+id/btnColor"
style="@style/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp"
android:tag="disable"
android:text="@string/title_select"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvColor" />
<View
android:id="@+id/vwColor"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:background="?attr/colorAccent"
app:layout_constraintBottom_toBottomOf="@id/btnColor"
app:layout_constraintStart_toEndOf="@id/btnColor"
app:layout_constraintTop_toTopOf="@id/btnColor" />
<ImageButton
android:id="@+id/ibColorDefault"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/title_legend_default_color"
app:layout_constraintBottom_toBottomOf="@id/btnColor"
app:layout_constraintStart_toEndOf="@id/vwColor"
app:layout_constraintTop_toTopOf="@id/btnColor"
app:srcCompat="@drawable/baseline_delete_24" />
<TextView
android:id="@+id/tvColorHint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_identity_color_hint"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnColor" />
<TextView
android:id="@+id/tvColorPro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/title_pro_feature"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorLink"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvColorHint" />
<CheckBox
android:id="@+id/cbSynchronize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_enabled"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvColorPro" />
<CheckBox
android:id="@+id/cbPrimary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_primary_account"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbSynchronize" />
<!-- keep alive -->
<TextView
android:id="@+id/tvInterval"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_keep_alive_interval"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/cbPrimary" />
<EditText
android:id="@+id/etInterval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="19"
android:inputType="number"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvInterval" />
<TextView
android:id="@+id/tvIntervalRemark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_account_interval_hint"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etInterval" />
<!-- save -->
<Button
android:id="@+id/btnSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:tag="disable"
android:text="@string/title_save"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvIntervalRemark" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbSave"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="12dp"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="@id/btnSave"
app:layout_constraintStart_toEndOf="@id/btnSave"
app:layout_constraintTop_toTopOf="@id/btnSave" />
<TextView
android:id="@+id/tvError"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:autoLink="web"
android:text="error"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:textColor="?attr/colorWarning"
android:textIsSelectable="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnSave" />
<eu.faircode.email.ContentLoadingProgressBar
android:id="@+id/pbWait"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

@ -415,7 +415,6 @@
<string name="title_identity_delete">Delete this identity permanently?</string>
<string name="title_edit_html">Edit as HTML</string>
<string name="title_last_connected">Last connected: %1$s</string>
<string name="title_pop3_support">POP3 is not supported</string>
<string name="title_activesync_support">ActiveSync is not supported</string>
<string name="title_oauth_support">OAuth is not supported</string>
<string name="title_authorize">Authorize</string>

Loading…
Cancel
Save