Basic POP3 support

pull/147/head
M66B 6 years ago
parent e64178d530
commit e7dd1a01b1

File diff suppressed because it is too large Load Diff

@ -238,15 +238,16 @@ public class AdapterFolder extends RecyclerView.Adapter<AdapterFolder.ViewHolder
popupMenu.getMenu().add(Menu.NONE, action_synchronize_now, 1, R.string.title_synchronize_now); popupMenu.getMenu().add(Menu.NONE, action_synchronize_now, 1, R.string.title_synchronize_now);
if (folder.account != null) if (folder.account != null && !folder.accountPop)
popupMenu.getMenu().add(Menu.NONE, action_delete_local, 2, R.string.title_delete_local); popupMenu.getMenu().add(Menu.NONE, action_delete_local, 2, R.string.title_delete_local);
if (EntityFolder.TRASH.equals(folder.type)) if (EntityFolder.TRASH.equals(folder.type) && !folder.accountPop)
popupMenu.getMenu().add(Menu.NONE, action_empty_trash, 3, R.string.title_empty_trash); popupMenu.getMenu().add(Menu.NONE, action_empty_trash, 3, R.string.title_empty_trash);
if (folder.account != null) { if (folder.account != null) {
popupMenu.getMenu().add(Menu.NONE, action_edit_properties, 4, R.string.title_edit_properties); popupMenu.getMenu().add(Menu.NONE, action_edit_properties, 4, R.string.title_edit_properties);
popupMenu.getMenu().add(Menu.NONE, action_edit_rules, 5, R.string.title_edit_rules); if (!folder.accountPop)
popupMenu.getMenu().add(Menu.NONE, action_edit_rules, 5, R.string.title_edit_rules);
} }
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {

@ -49,7 +49,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html // https://developer.android.com/topic/libraries/architecture/room.html
@Database( @Database(
version = 42, version = 43,
entities = { entities = {
EntityIdentity.class, EntityIdentity.class,
EntityAccount.class, EntityAccount.class,
@ -489,6 +489,13 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `identity` ADD COLUMN `plain_only` INTEGER NOT NULL DEFAULT 0"); db.execSQL("ALTER TABLE `identity` ADD COLUMN `plain_only` INTEGER NOT NULL DEFAULT 0");
} }
}) })
.addMigrations(new Migration(42, 43) {
@Override
public void migrate(SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `account` ADD COLUMN `pop` INTEGER NOT NULL DEFAULT 0");
}
})
.build(); .build();
} }

@ -55,7 +55,7 @@ public interface DaoFolder {
" AND (:search OR (account.synchronize AND account.browse))") " AND (:search OR (account.synchronize AND account.browse))")
EntityFolder getBrowsableFolder(long folder, boolean search); EntityFolder getBrowsableFolder(long folder, boolean search);
@Query("SELECT folder.*, account.name AS accountName, account.color AS accountColor, account.state AS accountState" + @Query("SELECT folder.*, account.name AS accountName, account.color AS accountColor, account.pop as accountPop, account.state AS accountState" +
", COUNT(message.id) AS messages" + ", COUNT(message.id) AS messages" +
", SUM(CASE WHEN message.content = 1 THEN 1 ELSE 0 END) AS content" + ", SUM(CASE WHEN message.content = 1 THEN 1 ELSE 0 END) AS content" +
", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" + ", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" +
@ -69,7 +69,7 @@ public interface DaoFolder {
" GROUP BY folder.id") " GROUP BY folder.id")
LiveData<List<TupleFolderEx>> liveFolders(Long account); LiveData<List<TupleFolderEx>> liveFolders(Long account);
@Query("SELECT folder.*, account.name AS accountName, account.color AS accountColor, account.state AS accountState" + @Query("SELECT folder.*, account.name AS accountName, account.color AS accountColor, account.pop as accountPop, account.state AS accountState" +
", COUNT(message.id) AS messages" + ", COUNT(message.id) AS messages" +
", SUM(CASE WHEN message.content = 1 THEN 1 ELSE 0 END) AS content" + ", SUM(CASE WHEN message.content = 1 THEN 1 ELSE 0 END) AS content" +
", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" + ", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" +
@ -97,7 +97,7 @@ public interface DaoFolder {
" AND (account.id = :account OR (:account IS NULL AND account.`primary`))") " AND (account.id = :account OR (:account IS NULL AND account.`primary`))")
LiveData<EntityFolder> liveDrafts(Long account); LiveData<EntityFolder> liveDrafts(Long account);
@Query("SELECT folder.*, account.name AS accountName, account.color AS accountColor, account.state AS accountState" + @Query("SELECT folder.*, account.name AS accountName, account.color AS accountColor, account.pop as accountPop, account.state AS accountState" +
", COUNT(message.id) AS messages" + ", COUNT(message.id) AS messages" +
", SUM(CASE WHEN message.content = 1 THEN 1 ELSE 0 END) AS content" + ", SUM(CASE WHEN message.content = 1 THEN 1 ELSE 0 END) AS content" +
", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" + ", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" +
@ -118,6 +118,16 @@ public interface DaoFolder {
@Query("SELECT * FROM folder WHERE id = :id") @Query("SELECT * FROM folder WHERE id = :id")
EntityFolder getFolder(Long id); EntityFolder getFolder(Long id);
@Query("SELECT folder.*, account.name AS accountName, account.color AS accountColor, account.pop as accountPop, account.state AS accountState" +
", COUNT(message.id) AS messages" +
", SUM(CASE WHEN message.content = 1 THEN 1 ELSE 0 END) AS content" +
", SUM(CASE WHEN message.ui_seen = 0 THEN 1 ELSE 0 END) AS unseen" +
" FROM folder" +
" LEFT JOIN account ON account.id = folder.account" +
" LEFT JOIN message ON message.folder = folder.id AND NOT message.ui_hide" +
" WHERE folder.id = :id")
TupleFolderEx getFolderEx(Long id);
@Query("SELECT * FROM folder WHERE account = :account AND name = :name") @Query("SELECT * FROM folder WHERE account = :account AND name = :name")
EntityFolder getFolderByName(Long account, String name); EntityFolder getFolderByName(Long account, String name);

@ -49,7 +49,9 @@ public class EntityAccount implements Serializable {
@NonNull @NonNull
public Integer auth_type; public Integer auth_type;
@NonNull @NonNull
public String host; // IMAP public Boolean pop = false;
@NonNull
public String host; // POP3/IMAP
@NonNull @NonNull
public Boolean starttls; public Boolean starttls;
@NonNull @NonNull
@ -86,6 +88,10 @@ public class EntityAccount implements Serializable {
public String error; public String error;
public Long last_connected; public Long last_connected;
String getProtocol() {
return (pop ? "pop3" : "imap") + (starttls ? "" : "s");
}
static String getNotificationChannelName(long account) { static String getNotificationChannelName(long account) {
return "notification." + account; return "notification." + account;
} }
@ -110,6 +116,7 @@ public class EntityAccount implements Serializable {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("id", id); json.put("id", id);
json.put("auth_type", auth_type); json.put("auth_type", auth_type);
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);
@ -142,6 +149,8 @@ public class EntityAccount implements Serializable {
EntityAccount account = new EntityAccount(); EntityAccount account = new EntityAccount();
// id // id
account.auth_type = json.getInt("auth_type"); account.auth_type = json.getInt("auth_type");
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"));

@ -89,6 +89,10 @@ public class EntityIdentity {
public String error; public String error;
public Long last_connected; public Long last_connected;
String getProtocol() {
return (starttls ? "smtp" : "smtps");
}
public JSONObject toJSON() throws JSONException { public JSONObject toJSON() throws JSONException {
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
json.put("id", id); json.put("id", id);

@ -74,9 +74,11 @@ import java.util.Properties;
import javax.mail.AuthenticationFailedException; import javax.mail.AuthenticationFailedException;
import javax.mail.Folder; import javax.mail.Folder;
import javax.mail.Session; import javax.mail.Session;
import javax.mail.Store;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.constraintlayout.widget.Group; import androidx.constraintlayout.widget.Group;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
@ -91,6 +93,7 @@ public class FragmentAccount extends FragmentBase {
private Button btnAutoConfig; private Button btnAutoConfig;
private Button btnAuthorize; private Button btnAuthorize;
private SwitchCompat swPop;
private EditText etHost; private EditText etHost;
private CheckBox cbStartTls; private CheckBox cbStartTls;
private CheckBox cbInsecure; private CheckBox cbInsecure;
@ -165,6 +168,7 @@ public class FragmentAccount extends FragmentBase {
btnAutoConfig = view.findViewById(R.id.btnAutoConfig); btnAutoConfig = view.findViewById(R.id.btnAutoConfig);
btnAuthorize = view.findViewById(R.id.btnAuthorize); btnAuthorize = view.findViewById(R.id.btnAuthorize);
swPop = view.findViewById(R.id.swPop);
etHost = view.findViewById(R.id.etHost); etHost = view.findViewById(R.id.etHost);
etPort = view.findViewById(R.id.etPort); etPort = view.findViewById(R.id.etPort);
cbStartTls = view.findViewById(R.id.cbStartTls); cbStartTls = view.findViewById(R.id.cbStartTls);
@ -235,6 +239,7 @@ public class FragmentAccount extends FragmentBase {
auth_type = Helper.AUTH_TYPE_PASSWORD; auth_type = Helper.AUTH_TYPE_PASSWORD;
swPop.setChecked(false);
etHost.setText(provider.imap_host); etHost.setText(provider.imap_host);
etPort.setText(provider.imap_host == null ? null : Integer.toString(provider.imap_port)); etPort.setText(provider.imap_host == null ? null : Integer.toString(provider.imap_port));
cbStartTls.setChecked(provider.imap_starttls); cbStartTls.setChecked(provider.imap_starttls);
@ -280,10 +285,37 @@ public class FragmentAccount extends FragmentBase {
} }
}); });
swPop.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
boolean starttls = cbStartTls.isChecked();
if (isChecked) {
etHost.setHint("pop.domain.tld");
etPort.setHint(starttls ? "110" : "995");
etRealm.setText(null);
cbBrowse.setChecked(false);
etPrefix.setText(null);
btnCheck.setVisibility(View.GONE);
btnSave.setVisibility(View.VISIBLE);
} else {
etHost.setHint("imap.domain.tld");
etPort.setHint(starttls ? "143" : "993");
btnCheck.setVisibility(View.VISIBLE);
btnSave.setVisibility(View.GONE);
}
etRealm.setEnabled(!isChecked);
cbBrowse.setEnabled(!isChecked);
etPrefix.setEnabled(!isChecked);
}
});
cbStartTls.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { cbStartTls.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
etPort.setHint(checked ? "143" : "993"); if (swPop.isChecked())
etPort.setHint(checked ? "110" : "995");
else
etPort.setHint(checked ? "143" : "993");
} }
}); });
@ -472,6 +504,7 @@ public class FragmentAccount extends FragmentBase {
@Override @Override
protected void onExecuted(Bundle args, EmailProvider provider) { protected void onExecuted(Bundle args, EmailProvider provider) {
swPop.setChecked(false);
etHost.setText(provider.imap_host); etHost.setText(provider.imap_host);
etPort.setText(Integer.toString(provider.imap_port)); etPort.setText(Integer.toString(provider.imap_port));
cbStartTls.setChecked(provider.imap_starttls); cbStartTls.setChecked(provider.imap_starttls);
@ -491,6 +524,7 @@ public class FragmentAccount extends FragmentBase {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong("id", id); args.putLong("id", id);
args.putInt("auth_type", auth_type); args.putInt("auth_type", auth_type);
args.putBoolean("pop", swPop.isChecked());
args.putString("host", etHost.getText().toString()); args.putString("host", etHost.getText().toString());
args.putBoolean("starttls", cbStartTls.isChecked()); args.putBoolean("starttls", cbStartTls.isChecked());
args.putBoolean("insecure", cbInsecure.isChecked()); args.putBoolean("insecure", cbInsecure.isChecked());
@ -523,6 +557,7 @@ public class FragmentAccount extends FragmentBase {
protected CheckResult onExecute(Context context, Bundle args) throws Throwable { protected CheckResult onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id"); long id = args.getLong("id");
int auth_type = args.getInt("auth_type"); int auth_type = args.getInt("auth_type");
boolean pop = args.getBoolean("pop");
String host = args.getString("host"); String host = args.getString("host");
boolean starttls = args.getBoolean("starttls"); boolean starttls = args.getBoolean("starttls");
boolean insecure = args.getBoolean("insecure"); boolean insecure = args.getBoolean("insecure");
@ -534,7 +569,10 @@ public class FragmentAccount extends FragmentBase {
if (TextUtils.isEmpty(host)) if (TextUtils.isEmpty(host))
throw new IllegalArgumentException(context.getString(R.string.title_no_host)); throw new IllegalArgumentException(context.getString(R.string.title_no_host));
if (TextUtils.isEmpty(port)) if (TextUtils.isEmpty(port))
port = (starttls ? "143" : "993"); if (pop)
port = (starttls ? "110" : "995");
else
port = (starttls ? "143" : "993");
if (TextUtils.isEmpty(user)) if (TextUtils.isEmpty(user))
throw new IllegalArgumentException(context.getString(R.string.title_no_user)); throw new IllegalArgumentException(context.getString(R.string.title_no_user));
if (TextUtils.isEmpty(password) && !insecure) if (TextUtils.isEmpty(password) && !insecure)
@ -553,9 +591,9 @@ public class FragmentAccount extends FragmentBase {
Properties props = MessageHelper.getSessionProperties(auth_type, realm, insecure); Properties props = MessageHelper.getSessionProperties(auth_type, realm, insecure);
Session isession = Session.getInstance(props, null); Session isession = Session.getInstance(props, null);
isession.setDebug(true); isession.setDebug(true);
IMAPStore istore = null; Store istore = null;
try { try {
istore = (IMAPStore) isession.getStore(starttls ? "imap" : "imaps"); istore = isession.getStore((pop ? "pop3" : "imap") + (starttls ? "" : "s"));
try { try {
istore.connect(host, Integer.parseInt(port), user, password); istore.connect(host, Integer.parseInt(port), user, password);
} catch (AuthenticationFailedException ex) { } catch (AuthenticationFailedException ex) {
@ -566,7 +604,8 @@ public class FragmentAccount extends FragmentBase {
throw ex; throw ex;
} }
result.idle = istore.hasCapability("IDLE"); if (istore instanceof IMAPStore)
result.idle = ((IMAPStore) istore).hasCapability("IDLE");
boolean inbox = false; boolean inbox = false;
boolean archive = false; boolean archive = false;
@ -583,7 +622,11 @@ public class FragmentAccount extends FragmentBase {
for (Folder ifolder : istore.getDefaultFolder().list("*")) { for (Folder ifolder : istore.getDefaultFolder().list("*")) {
// Check folder attributes // Check folder attributes
String fullName = ifolder.getFullName(); String fullName = ifolder.getFullName();
String[] attrs = ((IMAPFolder) ifolder).getAttributes(); String[] attrs;
if (ifolder instanceof IMAPFolder)
attrs = ((IMAPFolder) ifolder).getAttributes();
else
attrs = new String[0];
Log.i(fullName + " attrs=" + TextUtils.join(" ", attrs)); Log.i(fullName + " attrs=" + TextUtils.join(" ", attrs));
String type = EntityFolder.getType(attrs, fullName); String type = EntityFolder.getType(attrs, fullName);
@ -660,9 +703,15 @@ public class FragmentAccount extends FragmentBase {
@Override @Override
protected void onExecuted(Bundle args, CheckResult result) { protected void onExecuted(Bundle args, CheckResult result) {
tvIdle.setVisibility(result.idle ? View.GONE : View.VISIBLE); boolean pop = args.getBoolean("pop");
setFolders(result.folders, result.account); tvIdle.setVisibility(result.idle || pop ? View.GONE : View.VISIBLE);
if (pop) {
grpFolders.setVisibility(View.GONE);
btnSave.setVisibility(View.VISIBLE);
} else
setFolders(result.folders, result.account);
new Handler().post(new Runnable() { new Handler().post(new Runnable() {
@Override @Override
@ -721,6 +770,7 @@ public class FragmentAccount extends FragmentBase {
args.putLong("id", id); args.putLong("id", id);
args.putInt("auth_type", auth_type); args.putInt("auth_type", auth_type);
args.putBoolean("pop", swPop.isChecked());
args.putString("host", etHost.getText().toString()); args.putString("host", etHost.getText().toString());
args.putBoolean("starttls", cbStartTls.isChecked()); args.putBoolean("starttls", cbStartTls.isChecked());
args.putBoolean("insecure", cbInsecure.isChecked()); args.putBoolean("insecure", cbInsecure.isChecked());
@ -770,6 +820,7 @@ public class FragmentAccount extends FragmentBase {
long id = args.getLong("id"); long id = args.getLong("id");
int auth_type = args.getInt("auth_type"); int auth_type = args.getInt("auth_type");
boolean pop = args.getBoolean("pop");
String host = args.getString("host"); String host = args.getString("host");
boolean starttls = args.getBoolean("starttls"); boolean starttls = args.getBoolean("starttls");
boolean insecure = args.getBoolean("insecure"); boolean insecure = args.getBoolean("insecure");
@ -799,7 +850,10 @@ public class FragmentAccount extends FragmentBase {
if (TextUtils.isEmpty(host)) if (TextUtils.isEmpty(host))
throw new IllegalArgumentException(context.getString(R.string.title_no_host)); throw new IllegalArgumentException(context.getString(R.string.title_no_host));
if (TextUtils.isEmpty(port)) if (TextUtils.isEmpty(port))
port = (starttls ? "143" : "993"); if (pop)
port = (starttls ? "110" : "995");
else
port = (starttls ? "143" : "993");
if (TextUtils.isEmpty(user)) if (TextUtils.isEmpty(user))
throw new IllegalArgumentException(context.getString(R.string.title_no_user)); throw new IllegalArgumentException(context.getString(R.string.title_no_user));
if (synchronize && TextUtils.isEmpty(password) && !insecure) if (synchronize && TextUtils.isEmpty(password) && !insecure)
@ -825,6 +879,7 @@ public class FragmentAccount extends FragmentBase {
boolean check = (synchronize && (account == null || boolean check = (synchronize && (account == null ||
auth_type != account.auth_type || auth_type != account.auth_type ||
pop != account.pop ||
!host.equals(account.host) || Integer.parseInt(port) != account.port || !host.equals(account.host) || Integer.parseInt(port) != account.port ||
!user.equals(account.user) || !password.equals(account.password) || !user.equals(account.user) || !password.equals(account.password) ||
(realm == null ? accountRealm != null : !realm.equals(accountRealm)))); (realm == null ? accountRealm != null : !realm.equals(accountRealm))));
@ -844,9 +899,9 @@ public class FragmentAccount extends FragmentBase {
Session isession = Session.getInstance(props, null); Session isession = Session.getInstance(props, null);
isession.setDebug(true); isession.setDebug(true);
IMAPStore istore = null; Store istore = null;
try { try {
istore = (IMAPStore) isession.getStore(starttls ? "imap" : "imaps"); istore = isession.getStore((pop ? "pop3" : "imap") + (starttls ? "" : "s"));
try { try {
istore.connect(host, Integer.parseInt(port), user, password); istore.connect(host, Integer.parseInt(port), user, password);
} catch (AuthenticationFailedException ex) { } catch (AuthenticationFailedException ex) {
@ -861,7 +916,11 @@ public class FragmentAccount extends FragmentBase {
for (Folder ifolder : istore.getDefaultFolder().list("*")) { for (Folder ifolder : istore.getDefaultFolder().list("*")) {
// Check folder attributes // Check folder attributes
String fullName = ifolder.getFullName(); String fullName = ifolder.getFullName();
String[] attrs = ((IMAPFolder) ifolder).getAttributes(); String[] attrs;
if (ifolder instanceof IMAPFolder)
attrs = ((IMAPFolder) ifolder).getAttributes();
else
attrs = new String[0];
Log.i(fullName + " attrs=" + TextUtils.join(" ", attrs)); Log.i(fullName + " attrs=" + TextUtils.join(" ", attrs));
String type = EntityFolder.getType(attrs, fullName); String type = EntityFolder.getType(attrs, fullName);
@ -872,6 +931,7 @@ public class FragmentAccount extends FragmentBase {
inbox.synchronize = true; inbox.synchronize = true;
inbox.unified = true; inbox.unified = true;
inbox.notify = true; inbox.notify = true;
inbox.initialize = !pop;
inbox.sync_days = EntityFolder.DEFAULT_SYNC; inbox.sync_days = EntityFolder.DEFAULT_SYNC;
inbox.keep_days = EntityFolder.DEFAULT_KEEP; inbox.keep_days = EntityFolder.DEFAULT_KEEP;
} }
@ -894,6 +954,7 @@ public class FragmentAccount extends FragmentBase {
account = new EntityAccount(); account = new EntityAccount();
account.auth_type = auth_type; account.auth_type = auth_type;
account.pop = pop;
account.host = host; account.host = host;
account.starttls = starttls; account.starttls = starttls;
account.insecure = insecure; account.insecure = insecure;
@ -1111,6 +1172,7 @@ public class FragmentAccount extends FragmentBase {
spProvider.setTag(1); spProvider.setTag(1);
spProvider.setSelection(1); spProvider.setSelection(1);
} }
swPop.setChecked(account.pop);
etHost.setText(account.host); etHost.setText(account.host);
etPort.setText(Long.toString(account.port)); etPort.setText(Long.toString(account.port));
} }
@ -1173,6 +1235,15 @@ public class FragmentAccount extends FragmentBase {
// Consider previous check/save/delete as cancelled // Consider previous check/save/delete as cancelled
pbWait.setVisibility(View.GONE); pbWait.setVisibility(View.GONE);
if (account != null && account.pop) {
etRealm.setEnabled(false);
cbBrowse.setEnabled(false);
etPrefix.setEnabled(false);
grpFolders.setVisibility(View.GONE);
btnSave.setVisibility(View.VISIBLE);
return;
}
args.putLong("account", account == null ? -1 : account.id); args.putLong("account", account == null ? -1 : account.id);
new SimpleTask<List<EntityFolder>>() { new SimpleTask<List<EntityFolder>>() {

@ -41,6 +41,7 @@ import java.util.Calendar;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.Group;
public class FragmentFolder extends FragmentBase { public class FragmentFolder extends FragmentBase {
private ViewGroup view; private ViewGroup view;
@ -58,6 +59,7 @@ public class FragmentFolder extends FragmentBase {
private Button btnSave; private Button btnSave;
private ContentLoadingProgressBar pbSave; private ContentLoadingProgressBar pbSave;
private ContentLoadingProgressBar pbWait; private ContentLoadingProgressBar pbWait;
private Group grpPop;
private long id = -1; private long id = -1;
private long account = -1; private long account = -1;
@ -97,6 +99,7 @@ public class FragmentFolder extends FragmentBase {
btnSave = view.findViewById(R.id.btnSave); btnSave = view.findViewById(R.id.btnSave);
pbSave = view.findViewById(R.id.pbSave); pbSave = view.findViewById(R.id.pbSave);
pbWait = view.findViewById(R.id.pbWait); pbWait = view.findViewById(R.id.pbWait);
grpPop = view.findViewById(R.id.grpPop);
cbUnified.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { cbUnified.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override @Override
@ -136,6 +139,7 @@ public class FragmentFolder extends FragmentBase {
btnSave.setEnabled(false); btnSave.setEnabled(false);
pbSave.setVisibility(View.GONE); pbSave.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE); pbWait.setVisibility(View.VISIBLE);
grpPop.setVisibility(View.GONE);
return view; return view;
} }
@ -361,15 +365,15 @@ public class FragmentFolder extends FragmentBase {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putLong("id", id); args.putLong("id", id);
new SimpleTask<EntityFolder>() { new SimpleTask<TupleFolderEx>() {
@Override @Override
protected EntityFolder onExecute(Context context, Bundle args) { protected TupleFolderEx onExecute(Context context, Bundle args) {
long id = args.getLong("id"); long id = args.getLong("id");
return DB.getInstance(context).folder().getFolder(id); return DB.getInstance(context).folder().getFolderEx(id);
} }
@Override @Override
protected void onExecuted(Bundle args, EntityFolder folder) { protected void onExecuted(Bundle args, TupleFolderEx folder) {
if (savedInstanceState == null) { if (savedInstanceState == null) {
etName.setText(folder == null ? null : folder.name); etName.setText(folder == null ? null : folder.name);
etDisplay.setText(folder == null ? null : folder.display); etDisplay.setText(folder == null ? null : folder.display);
@ -389,6 +393,8 @@ public class FragmentFolder extends FragmentBase {
// Consider previous save as cancelled // Consider previous save as cancelled
pbWait.setVisibility(View.GONE); pbWait.setVisibility(View.GONE);
grpPop.setVisibility(folder == null || !folder.accountPop ? View.VISIBLE : View.GONE);
Helper.setViewsEnabled(view, true); Helper.setViewsEnabled(view, true);
etName.setEnabled(folder == null); etName.setEnabled(folder == null);
etDisplay.setEnabled(folder == null || !EntityFolder.INBOX.equals(folder.type)); etDisplay.setEnabled(folder == null || !EntityFolder.INBOX.equals(folder.type));

@ -131,8 +131,7 @@ public class FragmentFolders extends FragmentBase {
tbShowHidden.setVisibility(View.GONE); tbShowHidden.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE); grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE); pbWait.setVisibility(View.VISIBLE);
if (account < 0) fab.hide();
fab.hide();
return view; return view;
} }
@ -155,6 +154,10 @@ public class FragmentFolders extends FragmentBase {
@Override @Override
public void onChanged(@Nullable EntityAccount account) { public void onChanged(@Nullable EntityAccount account) {
setSubtitle(account == null ? null : account.name); setSubtitle(account == null ? null : account.name);
if (account == null || account.pop)
fab.hide();
else
fab.show();
} }
}); });

@ -585,11 +585,11 @@ public class FragmentIdentity extends FragmentBase {
// Check SMTP server // Check SMTP server
if (check) { if (check) {
String transportType = (starttls ? "smtp" : "smtps"); String protocol = (starttls ? "smtp" : "smtps");
Properties props = MessageHelper.getSessionProperties(auth_type, realm, insecure); Properties props = MessageHelper.getSessionProperties(auth_type, realm, insecure);
Session isession = Session.getInstance(props, null); Session isession = Session.getInstance(props, null);
isession.setDebug(true); isession.setDebug(true);
Transport itransport = isession.getTransport(transportType); Transport itransport = isession.getTransport(protocol);
try { try {
try { try {
itransport.connect(host, Integer.parseInt(port), user, password); itransport.connect(host, Integer.parseInt(port), user, password);

@ -56,7 +56,6 @@ import android.widget.Toast;
import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClient;
import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.sun.mail.imap.IMAPStore;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
@ -86,6 +85,7 @@ import java.util.concurrent.ThreadFactory;
import javax.mail.Address; import javax.mail.Address;
import javax.mail.AuthenticationFailedException; import javax.mail.AuthenticationFailedException;
import javax.mail.MessagingException; import javax.mail.MessagingException;
import javax.mail.Store;
import javax.mail.internet.InternetAddress; import javax.mail.internet.InternetAddress;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -779,7 +779,7 @@ public class Helper {
return true; return true;
} }
static void connect(Context context, IMAPStore istore, EntityAccount account) throws static void connect(Context context, Store istore, EntityAccount account) throws
MessagingException { MessagingException {
try { try {
istore.connect(account.host, account.port, account.user, account.password); istore.connect(account.host, account.port, account.user, account.password);

@ -165,6 +165,26 @@ public class MessageHelper {
props.put("mail.smtp.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead props.put("mail.smtp.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
props.put("mail.smtp.timeout", Integer.toString(NETWORK_TIMEOUT)); props.put("mail.smtp.timeout", Integer.toString(NETWORK_TIMEOUT));
// https://javaee.github.io/javamail/docs/api/com/sun/mail/pop3/package-summary.html
props.put("mail.pop3s.ssl.checkserveridentity", checkserveridentity);
props.put("mail.pop3s.ssl.trust", "*");
props.put("mail.pop3s.starttls.enable", "false");
props.put("mail.pop3s.starttls.required", "false");
props.put("mail.pop3s.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
props.put("mail.pop3s.timeout", Integer.toString(NETWORK_TIMEOUT));
props.put("mail.pop3s.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
props.put("mail.pop3.ssl.checkserveridentity", checkserveridentity);
props.put("mail.pop3.ssl.trust", "*");
props.put("mail.pop3.starttls.enable", "true");
props.put("mail.pop3.starttls.required", "true");
props.put("mail.pop3.connectiontimeout", Integer.toString(NETWORK_TIMEOUT));
props.put("mail.pop3.timeout", Integer.toString(NETWORK_TIMEOUT));
props.put("mail.pop3.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
// MIME
props.put("mail.mime.allowutf8", "true"); // SMTPTransport, MimeMessage props.put("mail.mime.allowutf8", "true"); // SMTPTransport, MimeMessage
props.put("mail.mime.address.strict", "false"); props.put("mail.mime.address.strict", "false");

@ -56,6 +56,8 @@ import com.sun.mail.imap.IMAPStore;
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.util.FolderClosedIOException; import com.sun.mail.util.FolderClosedIOException;
import com.sun.mail.util.MailConnectException; import com.sun.mail.util.MailConnectException;
@ -104,6 +106,7 @@ import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException; import javax.mail.NoSuchProviderException;
import javax.mail.SendFailedException; import javax.mail.SendFailedException;
import javax.mail.Session; import javax.mail.Session;
import javax.mail.Store;
import javax.mail.StoreClosedException; import javax.mail.StoreClosedException;
import javax.mail.Transport; import javax.mail.Transport;
import javax.mail.UIDFolder; import javax.mail.UIDFolder;
@ -821,8 +824,8 @@ public class ServiceSynchronize extends LifecycleService {
isession.setDebug(debug); isession.setDebug(debug);
// adb -t 1 logcat | grep "fairemail\|System.out" // adb -t 1 logcat | grep "fairemail\|System.out"
final IMAPStore istore = (IMAPStore) isession.getStore(account.starttls ? "imap" : "imaps"); final Store istore = isession.getStore(account.getProtocol());
final Map<EntityFolder, IMAPFolder> folders = new HashMap<>(); final Map<EntityFolder, Folder> folders = new HashMap<>();
List<Thread> idlers = new ArrayList<>(); List<Thread> idlers = new ArrayList<>();
List<Handler> handlers = new ArrayList<>(); List<Handler> handlers = new ArrayList<>();
try { try {
@ -935,8 +938,10 @@ public class ServiceSynchronize extends LifecycleService {
throw ex; throw ex;
} }
final boolean capIdle = istore.hasCapability("IDLE"); final boolean capIdle =
final boolean capUidPlus = istore.hasCapability("UIDPLUS"); (istore instanceof IMAPStore ? ((IMAPStore) istore).hasCapability("IDLE") : false);
final boolean capUidPlus =
(istore instanceof IMAPStore ? ((IMAPStore) istore).hasCapability("UIDPLUS") : false);
Log.i(account.name + " idle=" + capIdle + " uidplus=" + capUidPlus); Log.i(account.name + " idle=" + capIdle + " uidplus=" + capUidPlus);
db.account().setAccountState(account.id, "connected"); db.account().setAccountState(account.id, "connected");
@ -957,7 +962,7 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderState(folder.id, "connecting"); db.folder().setFolderState(folder.id, "connecting");
final IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name); final Folder ifolder = istore.getFolder(folder.name);
try { try {
ifolder.open(Folder.READ_WRITE); ifolder.open(Folder.READ_WRITE);
} catch (MessagingException ex) { } catch (MessagingException ex) {
@ -1002,7 +1007,7 @@ public class ServiceSynchronize extends LifecycleService {
db.beginTransaction(); db.beginTransaction();
message = synchronizeMessage( message = synchronizeMessage(
ServiceSynchronize.this, ServiceSynchronize.this,
folder, ifolder, (IMAPMessage) imessage, folder, (IMAPFolder) ifolder, (IMAPMessage) imessage,
false, false,
db.rule().getEnabledRules(folder.id)); db.rule().getEnabledRules(folder.id));
db.setTransactionSuccessful(); db.setTransactionSuccessful();
@ -1014,7 +1019,7 @@ public class ServiceSynchronize extends LifecycleService {
try { try {
db.beginTransaction(); db.beginTransaction();
downloadMessage(ServiceSynchronize.this, downloadMessage(ServiceSynchronize.this,
folder, ifolder, folder, (IMAPFolder) ifolder,
(IMAPMessage) imessage, message.id); (IMAPMessage) imessage, message.id);
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
@ -1051,7 +1056,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(folder.name + " messages removed"); Log.i(folder.name + " messages removed");
for (Message imessage : e.getMessages()) for (Message imessage : e.getMessages())
try { try {
long uid = ifolder.getUID(imessage); long uid = ((IMAPFolder) ifolder).getUID(imessage);
DB db = DB.getInstance(ServiceSynchronize.this); DB db = DB.getInstance(ServiceSynchronize.this);
int count = db.message().deleteMessage(folder.id, uid); int count = db.message().deleteMessage(folder.id, uid);
@ -1092,7 +1097,7 @@ public class ServiceSynchronize extends LifecycleService {
db.beginTransaction(); db.beginTransaction();
message = synchronizeMessage( message = synchronizeMessage(
ServiceSynchronize.this, ServiceSynchronize.this,
folder, ifolder, (IMAPMessage) e.getMessage(), folder, (IMAPFolder) ifolder, (IMAPMessage) e.getMessage(),
false, false,
db.rule().getEnabledRules(folder.id)); db.rule().getEnabledRules(folder.id));
db.setTransactionSuccessful(); db.setTransactionSuccessful();
@ -1104,7 +1109,7 @@ public class ServiceSynchronize extends LifecycleService {
try { try {
db.beginTransaction(); db.beginTransaction();
downloadMessage(ServiceSynchronize.this, downloadMessage(ServiceSynchronize.this,
folder, ifolder, folder, (IMAPFolder) ifolder,
(IMAPMessage) e.getMessage(), message.id); (IMAPMessage) e.getMessage(), message.id);
db.setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
@ -1143,7 +1148,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(folder.name + " start idle"); Log.i(folder.name + " start idle");
while (state.running()) { while (state.running()) {
Log.i(folder.name + " do idle"); Log.i(folder.name + " do idle");
ifolder.idle(false); ((IMAPFolder) ifolder).idle(false);
} }
} catch (Throwable ex) { } catch (Throwable ex) {
Log.e(folder.name, ex); Log.e(folder.name, ex);
@ -1210,7 +1215,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(folder.name + " process"); Log.i(folder.name + " process");
// Get folder // Get folder
IMAPFolder ifolder = folders.get(folder); // null when polling Folder ifolder = folders.get(folder); // null when polling
final boolean shouldClose = (ifolder == null); final boolean shouldClose = (ifolder == null);
try { try {
@ -1223,7 +1228,7 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderState(folder.id, "connecting"); db.folder().setFolderState(folder.id, "connecting");
ifolder = (IMAPFolder) istore.getFolder(folder.name); ifolder = istore.getFolder(folder.name);
ifolder.open(Folder.READ_WRITE); ifolder.open(Folder.READ_WRITE);
db.folder().setFolderState(folder.id, "connected"); db.folder().setFolderState(folder.id, "connected");
@ -1437,7 +1442,7 @@ public class ServiceSynchronize extends LifecycleService {
private void processOperations( private void processOperations(
EntityAccount account, EntityFolder folder, EntityAccount account, EntityFolder folder,
Session isession, IMAPStore istore, IMAPFolder ifolder, Session isession, Store istore, Folder ifolder,
ServiceState state) ServiceState state)
throws MessagingException, JSONException, IOException { throws MessagingException, JSONException, IOException {
try { try {
@ -1479,46 +1484,50 @@ public class ServiceSynchronize extends LifecycleService {
// Operations should use database transaction when needed // Operations should use database transaction when needed
if (EntityOperation.SEEN.equals(op.name)) if (EntityOperation.SEEN.equals(op.name))
doSeen(folder, ifolder, message, jargs, db); doSeen(folder, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.FLAG.equals(op.name)) else if (EntityOperation.FLAG.equals(op.name))
doFlag(folder, ifolder, message, jargs, db); doFlag(folder, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.ANSWERED.equals(op.name)) else if (EntityOperation.ANSWERED.equals(op.name))
doAnswered(folder, ifolder, message, jargs, db); doAnswered(folder, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.KEYWORD.equals(op.name)) else if (EntityOperation.KEYWORD.equals(op.name))
doKeyword(folder, ifolder, message, jargs, db); doKeyword(folder, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.ADD.equals(op.name)) else if (EntityOperation.ADD.equals(op.name))
doAdd(folder, isession, istore, ifolder, message, jargs, db); doAdd(folder, isession, (IMAPStore) istore, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.MOVE.equals(op.name)) else if (EntityOperation.MOVE.equals(op.name))
doMove(folder, isession, istore, ifolder, message, jargs, db); doMove(folder, isession, (IMAPStore) istore, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.DELETE.equals(op.name)) else if (EntityOperation.DELETE.equals(op.name))
doDelete(folder, ifolder, message, jargs, db); doDelete(folder, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.SEND.equals(op.name)) else if (EntityOperation.SEND.equals(op.name))
doSend(message, db); doSend(message, db);
else if (EntityOperation.HEADERS.equals(op.name)) else if (EntityOperation.HEADERS.equals(op.name))
doHeaders(folder, ifolder, message, db); doHeaders(folder, (IMAPFolder) ifolder, message, db);
else if (EntityOperation.RAW.equals(op.name)) else if (EntityOperation.RAW.equals(op.name))
doRaw(folder, ifolder, message, jargs, db); doRaw(folder, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.BODY.equals(op.name)) else if (EntityOperation.BODY.equals(op.name))
doBody(folder, ifolder, message, db); doBody(folder, (IMAPFolder) ifolder, message, db);
else if (EntityOperation.ATTACHMENT.equals(op.name)) else if (EntityOperation.ATTACHMENT.equals(op.name))
doAttachment(folder, op, ifolder, message, jargs, db); doAttachment(folder, op, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.SYNC.equals(op.name)) else if (EntityOperation.SYNC.equals(op.name))
if (EntityFolder.OUTBOX.equals(folder.type)) if (EntityFolder.OUTBOX.equals(folder.type))
db.folder().setFolderError(folder.id, null); db.folder().setFolderError(folder.id, null);
else else {
synchronizeMessages(account, folder, ifolder, jargs, state); if (ifolder instanceof IMAPFolder)
synchronizeMessages(account, folder, (IMAPFolder) ifolder, jargs, state);
else if (ifolder instanceof POP3Folder)
synchronizeMessages(account, folder, (POP3Folder) ifolder, jargs, state);
}
else else
throw new MessagingException("Unknown operation name=" + op.name); throw new MessagingException("Unknown operation name=" + op.name);
@ -1897,8 +1906,6 @@ public class ServiceSynchronize extends LifecycleService {
db.message().setMessageLastAttempt(message.id, message.last_attempt); db.message().setMessageLastAttempt(message.id, message.last_attempt);
} }
String transportType = (ident.starttls ? "smtp" : "smtps");
// Get properties // Get properties
Properties props = MessageHelper.getSessionProperties(ident.auth_type, ident.realm, ident.insecure); Properties props = MessageHelper.getSessionProperties(ident.auth_type, ident.realm, ident.insecure);
props.put("mail.smtp.localhost", ident.host); props.put("mail.smtp.localhost", ident.host);
@ -1933,7 +1940,7 @@ public class ServiceSynchronize extends LifecycleService {
// Create transport // Create transport
// TODO: cache transport? // TODO: cache transport?
Transport itransport = isession.getTransport(transportType); Transport itransport = isession.getTransport(ident.getProtocol());
try { try {
// Connect transport // Connect transport
db.identity().setIdentityState(ident.id, "connecting"); db.identity().setIdentityState(ident.id, "connecting");
@ -2194,7 +2201,7 @@ public class ServiceSynchronize extends LifecycleService {
} }
} }
private void synchronizeFolders(EntityAccount account, IMAPStore istore, ServiceState state) throws MessagingException { private void synchronizeFolders(EntityAccount account, Store istore, ServiceState state) throws MessagingException {
DB db = DB.getInstance(this); DB db = DB.getInstance(this);
try { try {
db.beginTransaction(); db.beginTransaction();
@ -2205,13 +2212,13 @@ public class ServiceSynchronize extends LifecycleService {
for (EntityFolder folder : db.folder().getFolders(account.id)) for (EntityFolder folder : db.folder().getFolders(account.id))
if (folder.tbc != null) { if (folder.tbc != null) {
Log.i(folder.name + " creating"); Log.i(folder.name + " creating");
IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name); Folder ifolder = istore.getFolder(folder.name);
if (!ifolder.exists()) if (!ifolder.exists())
ifolder.create(Folder.HOLDS_MESSAGES); ifolder.create(Folder.HOLDS_MESSAGES);
db.folder().resetFolderTbc(folder.id); db.folder().resetFolderTbc(folder.id);
} else if (folder.tbd != null && folder.tbd) { } else if (folder.tbd != null && folder.tbd) {
Log.i(folder.name + " deleting"); Log.i(folder.name + " deleting");
IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name); Folder ifolder = istore.getFolder(folder.name);
if (ifolder.exists()) if (ifolder.exists())
ifolder.delete(false); ifolder.delete(false);
db.folder().deleteFolder(folder.id); db.folder().deleteFolder(folder.id);
@ -2228,7 +2235,11 @@ public class ServiceSynchronize extends LifecycleService {
for (Folder ifolder : ifolders) { for (Folder ifolder : ifolders) {
String fullName = ifolder.getFullName(); String fullName = ifolder.getFullName();
String[] attrs = ((IMAPFolder) ifolder).getAttributes(); String[] attrs;
if (ifolder instanceof IMAPFolder)
attrs = ((IMAPFolder) ifolder).getAttributes();
else
attrs = new String[0];
String type = EntityFolder.getType(attrs, fullName); String type = EntityFolder.getType(attrs, fullName);
EntityLog.log(this, account.name + ":" + fullName + EntityLog.log(this, account.name + ":" + fullName +
@ -2299,6 +2310,102 @@ public class ServiceSynchronize extends LifecycleService {
} }
} }
private void synchronizeMessages(EntityAccount account, final EntityFolder folder, POP3Folder ifolder, JSONArray jargs, ServiceState state) throws JSONException, MessagingException, IOException {
DB db = DB.getInstance(this);
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.running())
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;
}
EntityMessage message = new EntityMessage();
message.account = folder.account;
message.folder = folder.id;
message.identity = null;
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(0);
message.sender = MessageHelper.getSortKey(helper.getFrom());
message.from = helper.getFrom();
message.to = helper.getTo();
message.cc = helper.getCc();
message.bcc = helper.getBcc();
message.reply = helper.getReply();
message.subject = helper.getSubject();
message.size = helper.getSize();
message.content = false;
message.received = new Date().getTime(); // TODO
message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime());
message.seen = false;
message.answered = false;
message.flagged = false;
message.flags = null;
message.keywords = null;
message.ui_seen = false;
message.ui_answered = false;
message.ui_flagged = false;
message.ui_hide = false;
message.ui_found = false;
message.ui_ignored = false;
message.ui_browsed = false;
Uri lookupUri = ContactInfo.getLookupUri(this, message.from);
message.avatar = (lookupUri == null ? null : lookupUri.toString());
message.id = db.message().insertMessage(message);
Log.i(folder.name + " POP id=" + message.id + " uid=" + message.uid);
MessageHelper.MessageParts parts = helper.getMessageParts();
String body = parts.getHtml(this);
Helper.writeText(EntityMessage.getFile(this, message.id), body);
db.message().setMessageContent(message.id, true, HtmlHelper.getPreview(body));
db.message().setMessageWarning(message.id, parts.getWarnings(message.warning));
int sequence = 1;
for (EntityAttachment attachment : parts.getAttachments()) {
Log.i(folder.name + " POP attachment seq=" + sequence +
" name=" + attachment.name + " type=" + attachment.type +
" cid=" + attachment.cid + " pgp=" + attachment.encryption);
attachment.message = message.id;
attachment.sequence = sequence++;
attachment.id = db.attachment().insertAttachment(attachment);
parts.downloadAttachment(this, db, attachment.id, attachment.sequence);
}
} catch (Throwable ex) {
db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
} finally {
((POP3Message) imessage).invalidate(true);
}
} finally {
db.folder().setFolderSyncState(folder.id, null);
}
}
private void synchronizeMessages(EntityAccount account, final EntityFolder folder, IMAPFolder ifolder, JSONArray jargs, ServiceState state) throws JSONException, MessagingException, IOException { private void synchronizeMessages(EntityAccount account, final EntityFolder folder, IMAPFolder ifolder, JSONArray jargs, ServiceState state) throws JSONException, MessagingException, IOException {
final DB db = DB.getInstance(this); final DB db = DB.getInstance(this);
try { try {

@ -22,6 +22,7 @@ package eu.faircode.email;
public class TupleFolderEx extends EntityFolder { public class TupleFolderEx extends EntityFolder {
public String accountName; public String accountName;
public Integer accountColor; public Integer accountColor;
public boolean accountPop;
public String accountState; public String accountState;
public int messages; public int messages;
public int content; public int content;
@ -34,6 +35,7 @@ public class TupleFolderEx extends EntityFolder {
return (super.equals(obj) && return (super.equals(obj) &&
(this.accountName == null ? other.accountName == null : accountName.equals(other.accountName)) && (this.accountName == null ? other.accountName == null : accountName.equals(other.accountName)) &&
(this.accountColor == null ? other.accountColor == null : this.accountColor.equals(other.accountColor)) && (this.accountColor == null ? other.accountColor == null : this.accountColor.equals(other.accountColor)) &&
this.accountPop == other.accountPop &&
(this.accountState == null ? other.accountState == null : accountState.equals(other.accountState)) && (this.accountState == null ? other.accountState == null : accountState.equals(other.accountState)) &&
this.messages == other.messages && this.messages == other.messages &&
this.content == other.content && this.content == other.content &&

@ -158,7 +158,7 @@ public class ViewModelBrowse extends ViewModel {
isession.setDebug(true); isession.setDebug(true);
Log.i("Boundary connecting account=" + account.name); Log.i("Boundary connecting account=" + account.name);
state.istore = (IMAPStore) isession.getStore(account.starttls ? "imap" : "imaps"); state.istore = (IMAPStore) isession.getStore(account.getProtocol());
Helper.connect(state.context, state.istore, account); Helper.connect(state.context, state.istore, account);
Log.i("Boundary opening folder=" + folder.name); Log.i("Boundary opening folder=" + folder.name);

@ -75,27 +75,37 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etDomain" /> app:layout_constraintTop_toBottomOf="@id/etDomain" />
<!-- IMAP --> <!-- IMAP/POP3 -->
<TextView <TextView
android:id="@+id/tvImap" android:id="@+id/tvImap"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_imap" android:text="@string/title_imap"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@+id/swPop"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/swPop" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/swPop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginTop="12dp"
app:layout_constraintStart_toEndOf="@id/tvImap"
app:layout_constraintTop_toBottomOf="@id/btnAutoConfig" /> app:layout_constraintTop_toBottomOf="@id/btnAutoConfig" />
<TextView <TextView
android:id="@+id/tvPop" android:id="@+id/tvPop"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/title_pop" android:layout_marginStart="6dp"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:text="@string/title_pop3"
android:textStyle="italic" android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@id/swPop"
app:layout_constraintTop_toBottomOf="@id/tvImap" /> app:layout_constraintStart_toEndOf="@id/swPop"
app:layout_constraintTop_toTopOf="@id/swPop" />
<!-- host --> <!-- host -->
@ -107,7 +117,7 @@
android:text="@string/title_host" android:text="@string/title_host"
android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPop" /> app:layout_constraintTop_toBottomOf="@id/swPop" />
<EditText <EditText
android:id="@+id/etHost" android:id="@+id/etHost"
@ -671,24 +681,36 @@
android:id="@+id/grpServer" android:id="@+id/grpServer"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:constraint_referenced_ids="tvDomain,tvDomainHint,etDomain,btnAutoConfig,tvImap,tvPop,tvHost,etHost,cbStartTls,cbInsecure,tvPort,etPort" /> app:constraint_referenced_ids="
tvDomain,tvDomainHint,etDomain,btnAutoConfig,
tvImap,swPop,tvPop,tvHost,etHost,cbStartTls,cbInsecure,tvPort,etPort" />
<androidx.constraintlayout.widget.Group <androidx.constraintlayout.widget.Group
android:id="@+id/grpAuthorize" android:id="@+id/grpAuthorize"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:constraint_referenced_ids="tvUser,etUser,tvPassword,tilPassword,tvRealm,etRealm,tvName,tvNameRemark,etName,btnColor,vwColor,ibColorDefault" /> app:constraint_referenced_ids="
tvUser,etUser,tvPassword,tilPassword,tvRealm,etRealm,
tvName,tvNameRemark,etName,btnColor,vwColor,ibColorDefault" />
<androidx.constraintlayout.widget.Group <androidx.constraintlayout.widget.Group
android:id="@+id/grpAdvanced" android:id="@+id/grpAdvanced"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:constraint_referenced_ids="tvPrefix,tvPrefixRemark,etPrefix,cbNotify,cbSynchronize,cbPrimary,cbBrowse,tvBrowseHint,tvInterval,etInterval" /> app:constraint_referenced_ids="
tvPrefix,tvPrefixRemark,etPrefix,cbNotify,
cbSynchronize,cbPrimary,cbBrowse,tvBrowseHint,tvInterval,etInterval" />
<androidx.constraintlayout.widget.Group <androidx.constraintlayout.widget.Group
android:id="@+id/grpFolders" android:id="@+id/grpFolders"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:constraint_referenced_ids="tvDrafts,spDrafts,tvDraftsRemark,tvSent,spSent,tvAll,spAll,tvTrash,spTrash,tvJunk,spJunk,vSeparatorSwipe,tvLeft,spLeft,tvRight,spRight" /> app:constraint_referenced_ids="
tvDrafts,spDrafts,tvDraftsRemark,
tvSent,spSent,
tvAll,spAll,
tvTrash,spTrash,
tvJunk,spJunk,
vSeparatorSwipe,tvLeft,spLeft,tvRight,spRight" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>

@ -199,5 +199,15 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/grpPop"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="
cbHide,
cbPoll,cbDownload,
tvSyncDays,tvSyncDaysRemark,etSyncDays,
tvKeepDays,etKeepDays,cbKeepAll" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>

@ -212,6 +212,7 @@
<string name="title_domain">Domain name</string> <string name="title_domain">Domain name</string>
<string name="title_autoconfig">Get settings</string> <string name="title_autoconfig">Get settings</string>
<string name="title_imap" translatable="false">IMAP</string> <string name="title_imap" translatable="false">IMAP</string>
<string name="title_pop3" translatable="false">POP3</string>
<string name="title_smtp" translatable="false">SMTP</string> <string name="title_smtp" translatable="false">SMTP</string>
<string name="title_provider">Provider</string> <string name="title_provider">Provider</string>
<string name="title_custom">Custom</string> <string name="title_custom">Custom</string>

Loading…
Cancel
Save