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: Related questions:
* [Why is POP not supported?](#user-content-faq11)
* [Why is ActiveSync not supported?](#user-content-faq133) * [Why is ActiveSync not supported?](#user-content-faq133)
* [Why is OAuth not supported?](#user-content-faq111) * [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) * [(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) * [(8) Can I use a Microsoft Exchange account?](#user-content-faq8)
* [(9) What are identities / how do I add an alias?](#user-content-faq9) * [(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) * [~~(10) What does 'UIDPLUS not supported' mean?~~](#user-content-faq10)
* [(12) How does encryption/decryption work?](#user-content-faq12) * [(12) How does encryption/decryption work?](#user-content-faq12)
* [(13) How does search on device/server work?](#user-content-faq13) * [(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 /> <br />
<a name="faq11"></a> <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, ~~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. ~~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. ~~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. ~~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. ~~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, ~~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. ~~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 /> <br />

@ -1025,7 +1025,8 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
} }
private void onEditAccount(Intent intent) { 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()); fragment.setArguments(intent.getExtras());
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account"); 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); LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
lbm.sendBroadcast( lbm.sendBroadcast(
new Intent(settings ? ActivitySetup.ACTION_EDIT_ACCOUNT : ActivityView.ACTION_VIEW_FOLDERS) 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) popupMenu.getMenu().add(Menu.NONE, R.string.title_enabled, 1, R.string.title_enabled)
.setCheckable(true).setChecked(account.synchronize); .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); String channelId = EntityAccount.getNotificationChannelId(account.id);
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = nm.getNotificationChannel(channelId); 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); 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.getMenu().add(Menu.NONE, R.string.title_copy, 3, R.string.title_copy);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {

@ -273,12 +273,19 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
ivSync.setImageResource(R.drawable.baseline_sync_24); ivSync.setImageResource(R.drawable.baseline_sync_24);
} else { } else {
StringBuilder a = new StringBuilder(); 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('/'); a.append('/');
if (folder.keep_days == Integer.MAX_VALUE) if (folder.keep_days == Integer.MAX_VALUE)
a.append('∞'); a.append('∞');
else else
a.append(NF.format(folder.keep_days)); a.append(NF.format(folder.keep_days));
tvAfter.setText(a.toString()); tvAfter.setText(a.toString());
ivSync.setImageResource(folder.synchronize ? R.drawable.baseline_sync_24 : R.drawable.baseline_sync_disabled_24); 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) if (!folder.selectable || folder.tbd != null)
return false; return false;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, view); PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(context, powner, view);
popupMenu.getMenu().add(Menu.NONE, 0, 0, folder.getDisplayName(context)).setEnabled(false); 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); 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_synchronize_all, 2, R.string.title_synchronize_all);
popupMenu.getMenu().add(Menu.NONE, R.string.title_delete_local, 3, R.string.title_delete_local); 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)) if (EntityFolder.TRASH.equals(folder.type))
popupMenu.getMenu().add(Menu.NONE, R.string.title_empty_trash, 5, R.string.title_empty_trash); 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"); String startup = prefs.getString("startup", "unified");
if (!"accounts".equals(startup)) if (!"accounts".equals(startup))
popupMenu.getMenu().add(Menu.NONE, R.string.title_unified_folder, 6, R.string.title_unified_folder) 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) popupMenu.getMenu().add(Menu.NONE, R.string.title_notify_folder, 8, R.string.title_notify_folder)
.setCheckable(true).setChecked(folder.notify); .setCheckable(true).setChecked(folder.notify);
}
if (folder.account != null && !folder.accountPop) {
boolean subscriptions = prefs.getBoolean("subscriptions", false); boolean subscriptions = prefs.getBoolean("subscriptions", false);
if (folder.subscribed != null && subscriptions) if (folder.subscribed != null && subscriptions)
popupMenu.getMenu().add(Menu.NONE, R.string.title_subscribe, 9, R.string.title_subscribe) 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( view.setAlpha(
(EntityFolder.OUTBOX.equals(message.folderType) (EntityFolder.OUTBOX.equals(message.folderType)
? message.identitySynchronize == null || !message.identitySynchronize ? message.identitySynchronize == null || !message.identitySynchronize
: message.uid == null) : message.uid == null && !message.accountPop)
? Helper.LOW_LIGHT : 1.0f); ? Helper.LOW_LIGHT : 1.0f);
// Duplicate // 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_resync).setEnabled(message.uid != null);
popupMenu.getMenu().findItem(R.id.menu_create_rule).setEnabled(!message.accountPop);
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override @Override
public boolean onMenuItemClick(MenuItem target) { public boolean onMenuItemClick(MenuItem target) {

@ -339,7 +339,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
try { try {
// https://tools.ietf.org/html/rfc3501#section-6.4.4 // https://tools.ietf.org/html/rfc3501#section-6.4.4
Argument arg = new Argument(); 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://support.google.com/mail/answer/7190
// https://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw // https://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw
arg.writeAtom("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.FetchResponse;
import com.sun.mail.imap.protocol.IMAPProtocol; import com.sun.mail.imap.protocol.IMAPProtocol;
import com.sun.mail.imap.protocol.UID; 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 com.sun.mail.util.MessageRemovedIOException;
import org.json.JSONArray; import org.json.JSONArray;
@ -168,10 +171,8 @@ class Core {
!EntityOperation.SYNC.equals(op.name) && !EntityOperation.SYNC.equals(op.name) &&
!EntityOperation.SUBSCRIBE.equals(op.name)) !EntityOperation.SUBSCRIBE.equals(op.name))
throw new MessageRemovedException(); throw new MessageRemovedException();
} else { } else
db.message().setMessageError(message.id, null); db.message().setMessageError(message.id, null);
ensureUid(context, folder, message, op, (IMAPFolder) ifolder);
}
// Operations should use database transaction when needed // Operations should use database transaction when needed
@ -204,73 +205,99 @@ class Core {
continue; continue;
} }
switch (op.name) { if (istore instanceof POP3Store)
case EntityOperation.SEEN: switch (op.name) {
onSeen(context, jargs, folder, message, (IMAPFolder) ifolder); case EntityOperation.SEEN:
break; 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: case EntityOperation.FLAG:
onFlag(context, jargs, folder, message, (IMAPFolder) ifolder); onFlag(context, jargs, folder, message, (IMAPFolder) ifolder);
break; break;
case EntityOperation.ANSWERED: case EntityOperation.ANSWERED:
onAnswered(context, jargs, folder, message, (IMAPFolder) ifolder); onAnswered(context, jargs, folder, message, (IMAPFolder) ifolder);
break; break;
case EntityOperation.KEYWORD: case EntityOperation.KEYWORD:
onKeyword(context, jargs, folder, message, (IMAPFolder) ifolder); onKeyword(context, jargs, folder, message, (IMAPFolder) ifolder);
break; break;
case EntityOperation.ADD: case EntityOperation.ADD:
onAdd(context, jargs, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder); onAdd(context, jargs, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break; break;
case EntityOperation.MOVE: case EntityOperation.MOVE:
onMove(context, jargs, false, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder); onMove(context, jargs, false, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break; break;
case EntityOperation.COPY: case EntityOperation.COPY:
onMove(context, jargs, true, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder); onMove(context, jargs, true, folder, message, (IMAPStore) istore, (IMAPFolder) ifolder);
break; break;
case EntityOperation.FETCH: case EntityOperation.FETCH:
onFetch(context, jargs, folder, (IMAPFolder) ifolder, state); onFetch(context, jargs, folder, (IMAPFolder) ifolder, state);
break; break;
case EntityOperation.DELETE: case EntityOperation.DELETE:
onDelete(context, jargs, folder, message, (IMAPFolder) ifolder); onDelete(context, jargs, folder, message, (IMAPFolder) ifolder);
break; break;
case EntityOperation.HEADERS: case EntityOperation.HEADERS:
onHeaders(context, jargs, folder, message, (IMAPFolder) ifolder); onHeaders(context, jargs, folder, message, (IMAPFolder) ifolder);
break; break;
case EntityOperation.RAW: case EntityOperation.RAW:
onRaw(context, jargs, folder, message, (IMAPFolder) ifolder); onRaw(context, jargs, folder, message, (IMAPFolder) ifolder);
break; break;
case EntityOperation.BODY: case EntityOperation.BODY:
onBody(context, jargs, folder, message, (IMAPFolder) ifolder); onBody(context, jargs, folder, message, (IMAPFolder) ifolder);
break; break;
case EntityOperation.ATTACHMENT: case EntityOperation.ATTACHMENT:
onAttachment(context, jargs, folder, message, op, (IMAPFolder) ifolder); onAttachment(context, jargs, folder, message, op, (IMAPFolder) ifolder);
break; break;
case EntityOperation.EXISTS: case EntityOperation.EXISTS:
onExists(context, jargs, folder, message, op, (IMAPFolder) ifolder); onExists(context, jargs, folder, message, op, (IMAPFolder) ifolder);
break; break;
case EntityOperation.SYNC: case EntityOperation.SYNC:
onSynchronizeMessages(context, jargs, account, folder, (IMAPFolder) ifolder, state); onSynchronizeMessages(context, jargs, account, folder, (IMAPFolder) ifolder, state);
break; break;
case EntityOperation.SUBSCRIBE: case EntityOperation.SUBSCRIBE:
onSubscribeFolder(context, jargs, folder, (IMAPFolder) ifolder); onSubscribeFolder(context, jargs, folder, (IMAPFolder) ifolder);
break; break;
default: default:
throw new IllegalArgumentException("Unknown operation=" + op.name); throw new IllegalArgumentException("Unknown operation=" + op.name);
}
} }
// Operation succeeded // Operation succeeded
@ -343,7 +370,7 @@ class Core {
} }
private static void ensureUid(Context context, EntityFolder folder, EntityMessage message, EntityOperation op, IMAPFolder ifolder) throws MessagingException { 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; return;
if (EntityOperation.ADD.equals(op.name)) if (EntityOperation.ADD.equals(op.name))
return; return;
@ -1089,6 +1116,130 @@ class Core {
Log.i(folder.name + " subscribed=" + subscribe); 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( private static void onSynchronizeMessages(
Context context, JSONArray jargs, Context context, JSONArray jargs,
EntityAccount account, final EntityFolder folder, EntityAccount account, final EntityFolder folder,
@ -1621,18 +1772,6 @@ class Core {
message.warning = Helper.formatThrowable(ex, false); 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 { try {
db.beginTransaction(); db.beginTransaction();

@ -35,7 +35,8 @@ public interface DaoFolder {
List<EntityFolder> getFolders(long account, boolean writable, boolean selectable); List<EntityFolder> getFolders(long account, boolean writable, boolean selectable);
@Query("SELECT folder.*" + @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 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.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" + ", 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(); List<TupleFolderSort> getSortedFolders();
@Query("SELECT folder.*" + @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 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.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" + ", 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); LiveData<List<TupleFolderEx>> liveFolders(Long account);
@Query("SELECT folder.*" + @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 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.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" + ", 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(); LiveData<Integer> liveSynchronizing();
@Query("SELECT folder.*" + @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 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.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" + ", 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 + "'"; String is_outbox = "folder.type = '" + EntityFolder.OUTBOX + "'";
@Query("SELECT message.*" + @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" + ", 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" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" +
", '[' || group_concat(message.`from`, ',') || ']' AS senders" + ", '[' || group_concat(message.`from`, ',') || ']' AS senders" +
@ -90,7 +90,7 @@ public interface DaoMessage {
boolean debug); boolean debug);
@Query("SELECT message.*" + @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" + ", 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" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" +
", '[' || group_concat(message.`from`, ',') || ']' AS senders" + ", '[' || group_concat(message.`from`, ',') || ']' AS senders" +
@ -132,7 +132,7 @@ public interface DaoMessage {
boolean debug); boolean debug);
@Query("SELECT message.*" + @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" + ", 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" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" +
", message.`from` AS senders" + ", message.`from` AS senders" +
@ -231,7 +231,7 @@ public interface DaoMessage {
int countMessageByMsgId(long folder, String msgid); int countMessageByMsgId(long folder, String msgid);
@Query("SELECT message.*" + @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" + ", 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" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" +
", message.`from` AS senders" + ", message.`from` AS senders" +
@ -249,7 +249,7 @@ public interface DaoMessage {
LiveData<TupleMessageEx> liveMessage(long id); LiveData<TupleMessageEx> liveMessage(long id);
@Query("SELECT message.*" + @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" + ", 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" + ", identity.name AS identityName, identity.email AS identityEmail, identity.synchronize AS identitySynchronize" +
", message.`from` AS senders" + ", message.`from` AS senders" +

@ -103,7 +103,7 @@ public class EntityAccount extends EntityOrder implements Serializable {
public Long last_connected; public Long last_connected;
String getProtocol() { String getProtocol() {
return "imap" + (starttls ? "" : "s"); return (pop ? "pop3" : "imap") + (starttls ? "" : "s");
} }
static String getNotificationChannelId(long id) { static String getNotificationChannelId(long id) {
@ -146,6 +146,7 @@ public class EntityAccount extends EntityOrder implements Serializable {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("id", id); json.put("id", id);
json.put("order", order); json.put("order", order);
json.put("pop", pop);
json.put("host", host); json.put("host", host);
json.put("starttls", starttls); json.put("starttls", starttls);
json.put("insecure", insecure); json.put("insecure", insecure);
@ -184,6 +185,9 @@ public class EntityAccount extends EntityOrder implements Serializable {
if (json.has("order")) if (json.has("order"))
account.order = json.getInt("order"); account.order = json.getInt("order");
if (json.has("pop"))
account.pop = json.getBoolean("pop");
account.host = json.getString("host"); account.host = json.getString("host");
account.starttls = (json.has("starttls") && json.getBoolean("starttls")); account.starttls = (json.has("starttls") && json.getBoolean("starttls"));
account.insecure = (json.has("insecure") && json.getBoolean("insecure")); account.insecure = (json.has("insecure") && json.getBoolean("insecure"));

@ -79,7 +79,6 @@ public class EntityOperation {
static final String COPY = "copy"; static final String COPY = "copy";
static final String FETCH = "fetch"; static final String FETCH = "fetch";
static final String DELETE = "delete"; static final String DELETE = "delete";
static final String DELETED = "deleted";
static final String SEEN = "seen"; static final String SEEN = "seen";
static final String ANSWERED = "answered"; static final String ANSWERED = "answered";
static final String FLAG = "flag"; static final String FLAG = "flag";
@ -228,7 +227,7 @@ public class EntityOperation {
name = RAW; name = RAW;
} else if (DELETE.equals(name)) } else if (DELETE.equals(name))
db.message().setMessageUiHide(message.id, new Date().getTime()); db.message().setMessageUiHide(message.id, Long.MAX_VALUE);
} catch (JSONException ex) { } catch (JSONException ex) {
Log.e(ex); Log.e(ex);

@ -84,7 +84,6 @@ public class FragmentAccount extends FragmentBase {
private Button btnAutoConfig; private Button btnAutoConfig;
private ContentLoadingProgressBar pbAutoConfig; private ContentLoadingProgressBar pbAutoConfig;
private TextView tvPopSupport;
private TextView tvActiveSyncSupport; private TextView tvActiveSyncSupport;
private EditText etHost; private EditText etHost;
private RadioGroup rgEncryption; private RadioGroup rgEncryption;
@ -181,7 +180,6 @@ public class FragmentAccount extends FragmentBase {
btnAutoConfig = view.findViewById(R.id.btnAutoConfig); btnAutoConfig = view.findViewById(R.id.btnAutoConfig);
pbAutoConfig = view.findViewById(R.id.pbAutoConfig); pbAutoConfig = view.findViewById(R.id.pbAutoConfig);
tvPopSupport = view.findViewById(R.id.tvPopSupport);
tvActiveSyncSupport = view.findViewById(R.id.tvActiveSyncSupport); tvActiveSyncSupport = view.findViewById(R.id.tvActiveSyncSupport);
etHost = view.findViewById(R.id.etHost); etHost = view.findViewById(R.id.etHost);
etPort = view.findViewById(R.id.etPort); 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.setPaintFlags(tvActiveSyncSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvActiveSyncSupport.setOnClickListener(new View.OnClickListener() { tvActiveSyncSupport.setOnClickListener(new View.OnClickListener() {
@Override @Override
@ -586,7 +576,7 @@ public class FragmentAccount extends FragmentBase {
try (MailService iservice = new MailService(context, protocol, realm, insecure, true)) { try (MailService iservice = new MailService(context, protocol, realm, insecure, true)) {
iservice.connect(host, Integer.parseInt(port), auth, user, password); iservice.connect(host, Integer.parseInt(port), auth, user, password);
result.idle = iservice.getStore().hasCapability("IDLE"); result.idle = iservice.hasCapability("IDLE");
boolean inbox = false; boolean inbox = false;
@ -1164,7 +1154,9 @@ public class FragmentAccount extends FragmentBase {
@Override @Override
protected EntityAccount onExecute(Context context, Bundle args) { protected EntityAccount onExecute(Context context, Bundle args) {
long id = args.getLong("id"); long id = args.getLong("id");
return DB.getInstance(context).account().getAccount(id);
DB db = DB.getInstance(context);
return db.account().getAccount(id);
} }
@Override @Override

@ -34,6 +34,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.constraintlayout.widget.Group; import androidx.constraintlayout.widget.Group;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
@ -108,11 +109,37 @@ public class FragmentAccounts extends FragmentBase {
fab.setOnClickListener(new View.OnClickListener() { fab.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
FragmentAccount fragment = new FragmentAccount(); PopupMenuLifecycle popupMenu = new PopupMenuLifecycle(getContext(), getViewLifecycleOwner(), fab);
fragment.setArguments(new Bundle());
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); popupMenu.getMenu().add(Menu.NONE, R.string.title_imap, 1, R.string.title_imap)
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("account"); .setEnabled(Helper.hasValidFingerprint(getContext()));
fragmentTransaction.commit(); 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; draft.plain_only = ref.plain_only;
if (answer > 0) if (answer > 0)
body = EntityAnswer.getAnswerText(context, answer, draft.to) + body; body = EntityAnswer.getAnswerText(context, answer, draft.to) + body;
EntityOperation.queue(context, ref, EntityOperation.SEEN, true);
} }
if (plain_only) if (plain_only)

@ -270,7 +270,7 @@ public class FragmentFolders extends FragmentBase {
else else
fabError.hide(); fabError.hide();
if (account == null) if (account == null || account.pop)
fab.hide(); fab.hide();
else else
fab.show(); fab.show();

@ -838,8 +838,9 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
int order = 1; int order = 1;
for (EntityAccount account : accounts) for (EntityAccount account : accounts)
popupMenu.getMenu().add(Menu.NONE, 0, order++, account.name) if (!account.pop)
.setIntent(new Intent().putExtra("account", account.id)); popupMenu.getMenu().add(Menu.NONE, 0, order++, account.name)
.setIntent(new Intent().putExtra("account", account.id));
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@Override @Override
@ -1749,7 +1750,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
if (result.hasTrash == null) result.hasTrash = false; if (result.hasTrash == null) result.hasTrash = false;
if (result.hasJunk == null) result.hasJunk = 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; return result;
} }
@ -3211,7 +3215,14 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
if (folder == null) if (folder == null)
return 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) if (!message.content)
EntityOperation.queue(context, message, EntityOperation.BODY); 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.NoSuchProviderException;
import javax.mail.Service; import javax.mail.Service;
import javax.mail.Session; import javax.mail.Session;
import javax.mail.Store;
public class MailService implements AutoCloseable { public class MailService implements AutoCloseable {
private Context context; private Context context;
@ -66,7 +67,22 @@ public class MailService implements AutoCloseable {
String checkserveridentity = Boolean.toString(!insecure).toLowerCase(); 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 // 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.checkserveridentity", checkserveridentity);
properties.put("mail." + protocol + ".ssl.trust", "*"); properties.put("mail." + protocol + ".ssl.trust", "*");
@ -218,17 +234,22 @@ public class MailService implements AutoCloseable {
isession.setDebug(debug); isession.setDebug(debug);
//System.setProperty("mail.socket.debug", Boolean.toString(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 = isession.getStore(protocol);
iservice.connect(host, port, user, password); iservice.connect(host, port, user, password);
// https://www.ietf.org/rfc/rfc2971.txt // https://www.ietf.org/rfc/rfc2971.txt
if (getStore().hasCapability("ID")) IMAPStore istore = (IMAPStore) getStore();
if (istore.hasCapability("ID"))
try { try {
Map<String, String> id = new LinkedHashMap<>(); Map<String, String> id = new LinkedHashMap<>();
id.put("name", context.getString(R.string.app_name)); id.put("name", context.getString(R.string.app_name));
id.put("version", BuildConfig.VERSION_NAME); id.put("version", BuildConfig.VERSION_NAME);
Map<String, String> sid = getStore().id(id); Map<String, String> sid = istore.id(id);
if (sid != null) { if (sid != null) {
Map<String, String> crumb = new HashMap<>(); Map<String, String> crumb = new HashMap<>();
for (String key : sid.keySet()) { for (String key : sid.keySet()) {
@ -322,14 +343,22 @@ public class MailService implements AutoCloseable {
return folders; return folders;
} }
IMAPStore getStore() { Store getStore() {
return (IMAPStore) iservice; return (Store) iservice;
} }
SMTPTransport getTransport() { SMTPTransport getTransport() {
return (SMTPTransport) iservice; 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 { public void close() throws MessagingException {
try { try {
if (iservice != null) if (iservice != null)

@ -701,7 +701,7 @@ public class ServiceSynchronize extends ServiceBase {
throw ex; throw ex;
} }
final boolean capIdle = iservice.getStore().hasCapability("IDLE"); final boolean capIdle = iservice.hasCapability("IDLE");
Log.i(account.name + " idle=" + capIdle); Log.i(account.name + " idle=" + capIdle);
db.account().setAccountState(account.id, "connected"); db.account().setAccountState(account.id, "connected");
@ -792,7 +792,8 @@ public class ServiceSynchronize extends ServiceBase {
}); });
// Update folder list // Update folder list
Core.onSynchronizeFolders(this, account, iservice.getStore(), state); if (!account.pop)
Core.onSynchronizeFolders(this, account, iservice.getStore(), state);
// Open synchronizing folders // Open synchronizing folders
final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory); final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
@ -993,12 +994,13 @@ public class ServiceSynchronize extends ServiceBase {
// Get folder // Get folder
Folder ifolder = mapFolders.get(folder); // null when polling 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 { try {
Log.i(folder.name + " run " + (shouldClose ? "offline" : "online")); Log.i(folder.name + " run " + (shouldClose ? "offline" : "online"));
if (ifolder == null) { if (shouldClose) {
// Prevent unnecessary folder connections // Prevent unnecessary folder connections
if (db.operation().getOperationCount(folder.id, null) == 0) if (db.operation().getOperationCount(folder.id, null) == 0)
return; return;

@ -38,6 +38,7 @@ import java.util.Objects;
public class TupleFolderEx extends EntityFolder implements Serializable { public class TupleFolderEx extends EntityFolder implements Serializable {
public Long accountId; public Long accountId;
public Boolean accountPop;
public Integer accountOrder; public Integer accountOrder;
public String accountName; public String accountName;
public Integer accountColor; public Integer accountColor;
@ -66,6 +67,7 @@ public class TupleFolderEx extends EntityFolder implements Serializable {
TupleFolderEx other = (TupleFolderEx) obj; TupleFolderEx other = (TupleFolderEx) obj;
return (super.equals(obj) && return (super.equals(obj) &&
Objects.equals(this.accountId, other.accountId) && Objects.equals(this.accountId, other.accountId) &&
Objects.equals(this.accountPop, other.accountPop) &&
Objects.equals(this.accountName, other.accountName) && Objects.equals(this.accountName, other.accountName) &&
Objects.equals(this.accountColor, other.accountColor) && Objects.equals(this.accountColor, other.accountColor) &&
Objects.equals(this.accountState, other.accountState) && Objects.equals(this.accountState, other.accountState) &&

@ -26,6 +26,7 @@ import java.util.Objects;
import javax.mail.Address; import javax.mail.Address;
public class TupleMessageEx extends EntityMessage { public class TupleMessageEx extends EntityMessage {
public boolean accountPop;
public String accountName; public String accountName;
public Integer accountColor; public Integer accountColor;
public boolean accountNotify; public boolean accountNotify;
@ -52,6 +53,7 @@ public class TupleMessageEx extends EntityMessage {
if (obj instanceof TupleMessageEx) { if (obj instanceof TupleMessageEx) {
TupleMessageEx other = (TupleMessageEx) obj; TupleMessageEx other = (TupleMessageEx) obj;
return (super.equals(obj) && return (super.equals(obj) &&
this.accountPop == other.accountPop &&
Objects.equals(this.accountName, other.accountName) && Objects.equals(this.accountName, other.accountName) &&
Objects.equals(this.accountColor, other.accountColor) && Objects.equals(this.accountColor, other.accountColor) &&
this.accountNotify == other.accountNotify && this.accountNotify == other.accountNotify &&

@ -101,19 +101,6 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/btnAutoConfig" /> 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 <TextView
android:id="@+id/tvActiveSyncSupport" android:id="@+id/tvActiveSyncSupport"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -125,7 +112,7 @@
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?android:attr/textColorLink" android:textColor="?android:attr/textColorLink"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPopSupport" /> app:layout_constraintTop_toBottomOf="@id/tvImap" />
<!-- host --> <!-- host -->
@ -806,7 +793,7 @@
android:layout_height="0dp" android:layout_height="0dp"
app:constraint_referenced_ids=" app:constraint_referenced_ids="
tvDomain,tvDomainHint,etDomain,btnAutoConfig, 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 <androidx.constraintlayout.widget.Group
android:id="@+id/grpAuthorize" 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_identity_delete">Delete this identity permanently?</string>
<string name="title_edit_html">Edit as HTML</string> <string name="title_edit_html">Edit as HTML</string>
<string name="title_last_connected">Last connected: %1$s</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_activesync_support">ActiveSync is not supported</string>
<string name="title_oauth_support">OAuth is not supported</string> <string name="title_oauth_support">OAuth is not supported</string>
<string name="title_authorize">Authorize</string> <string name="title_authorize">Authorize</string>

Loading…
Cancel
Save