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);
if (folder.account != null)
if (folder.account != null && !folder.accountPop)
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);
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_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() {

@ -49,7 +49,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 42,
version = 43,
entities = {
EntityIdentity.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");
}
})
.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();
}

@ -55,7 +55,7 @@ public interface DaoFolder {
" AND (:search OR (account.synchronize AND account.browse))")
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" +
", 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" +
@ -69,7 +69,7 @@ public interface DaoFolder {
" GROUP BY folder.id")
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" +
", 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" +
@ -97,7 +97,7 @@ public interface DaoFolder {
" AND (account.id = :account OR (:account IS NULL AND account.`primary`))")
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" +
", 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" +
@ -118,6 +118,16 @@ public interface DaoFolder {
@Query("SELECT * FROM folder WHERE id = :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")
EntityFolder getFolderByName(Long account, String name);

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

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

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

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

@ -131,8 +131,7 @@ public class FragmentFolders extends FragmentBase {
tbShowHidden.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE);
pbWait.setVisibility(View.VISIBLE);
if (account < 0)
fab.hide();
fab.hide();
return view;
}
@ -155,6 +154,10 @@ public class FragmentFolders extends FragmentBase {
@Override
public void onChanged(@Nullable EntityAccount account) {
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
if (check) {
String transportType = (starttls ? "smtp" : "smtps");
String protocol = (starttls ? "smtp" : "smtps");
Properties props = MessageHelper.getSessionProperties(auth_type, realm, insecure);
Session isession = Session.getInstance(props, null);
isession.setDebug(true);
Transport itransport = isession.getTransport(transportType);
Transport itransport = isession.getTransport(protocol);
try {
try {
itransport.connect(host, Integer.parseInt(port), user, password);

@ -56,7 +56,6 @@ import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.sun.mail.imap.IMAPStore;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@ -86,6 +85,7 @@ import java.util.concurrent.ThreadFactory;
import javax.mail.Address;
import javax.mail.AuthenticationFailedException;
import javax.mail.MessagingException;
import javax.mail.Store;
import javax.mail.internet.InternetAddress;
import androidx.annotation.NonNull;
@ -779,7 +779,7 @@ public class Helper {
return true;
}
static void connect(Context context, IMAPStore istore, EntityAccount account) throws
static void connect(Context context, Store istore, EntityAccount account) throws
MessagingException {
try {
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.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.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.IMAPProtocol;
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.MailConnectException;
@ -104,6 +106,7 @@ import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.StoreClosedException;
import javax.mail.Transport;
import javax.mail.UIDFolder;
@ -821,8 +824,8 @@ public class ServiceSynchronize extends LifecycleService {
isession.setDebug(debug);
// adb -t 1 logcat | grep "fairemail\|System.out"
final IMAPStore istore = (IMAPStore) isession.getStore(account.starttls ? "imap" : "imaps");
final Map<EntityFolder, IMAPFolder> folders = new HashMap<>();
final Store istore = isession.getStore(account.getProtocol());
final Map<EntityFolder, Folder> folders = new HashMap<>();
List<Thread> idlers = new ArrayList<>();
List<Handler> handlers = new ArrayList<>();
try {
@ -935,8 +938,10 @@ public class ServiceSynchronize extends LifecycleService {
throw ex;
}
final boolean capIdle = istore.hasCapability("IDLE");
final boolean capUidPlus = istore.hasCapability("UIDPLUS");
final boolean capIdle =
(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);
db.account().setAccountState(account.id, "connected");
@ -957,7 +962,7 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderState(folder.id, "connecting");
final IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name);
final Folder ifolder = istore.getFolder(folder.name);
try {
ifolder.open(Folder.READ_WRITE);
} catch (MessagingException ex) {
@ -1002,7 +1007,7 @@ public class ServiceSynchronize extends LifecycleService {
db.beginTransaction();
message = synchronizeMessage(
ServiceSynchronize.this,
folder, ifolder, (IMAPMessage) imessage,
folder, (IMAPFolder) ifolder, (IMAPMessage) imessage,
false,
db.rule().getEnabledRules(folder.id));
db.setTransactionSuccessful();
@ -1014,7 +1019,7 @@ public class ServiceSynchronize extends LifecycleService {
try {
db.beginTransaction();
downloadMessage(ServiceSynchronize.this,
folder, ifolder,
folder, (IMAPFolder) ifolder,
(IMAPMessage) imessage, message.id);
db.setTransactionSuccessful();
} finally {
@ -1051,7 +1056,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(folder.name + " messages removed");
for (Message imessage : e.getMessages())
try {
long uid = ifolder.getUID(imessage);
long uid = ((IMAPFolder) ifolder).getUID(imessage);
DB db = DB.getInstance(ServiceSynchronize.this);
int count = db.message().deleteMessage(folder.id, uid);
@ -1092,7 +1097,7 @@ public class ServiceSynchronize extends LifecycleService {
db.beginTransaction();
message = synchronizeMessage(
ServiceSynchronize.this,
folder, ifolder, (IMAPMessage) e.getMessage(),
folder, (IMAPFolder) ifolder, (IMAPMessage) e.getMessage(),
false,
db.rule().getEnabledRules(folder.id));
db.setTransactionSuccessful();
@ -1104,7 +1109,7 @@ public class ServiceSynchronize extends LifecycleService {
try {
db.beginTransaction();
downloadMessage(ServiceSynchronize.this,
folder, ifolder,
folder, (IMAPFolder) ifolder,
(IMAPMessage) e.getMessage(), message.id);
db.setTransactionSuccessful();
} finally {
@ -1143,7 +1148,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(folder.name + " start idle");
while (state.running()) {
Log.i(folder.name + " do idle");
ifolder.idle(false);
((IMAPFolder) ifolder).idle(false);
}
} catch (Throwable ex) {
Log.e(folder.name, ex);
@ -1210,7 +1215,7 @@ public class ServiceSynchronize extends LifecycleService {
Log.i(folder.name + " process");
// Get folder
IMAPFolder ifolder = folders.get(folder); // null when polling
Folder ifolder = folders.get(folder); // null when polling
final boolean shouldClose = (ifolder == null);
try {
@ -1223,7 +1228,7 @@ public class ServiceSynchronize extends LifecycleService {
db.folder().setFolderState(folder.id, "connecting");
ifolder = (IMAPFolder) istore.getFolder(folder.name);
ifolder = istore.getFolder(folder.name);
ifolder.open(Folder.READ_WRITE);
db.folder().setFolderState(folder.id, "connected");
@ -1437,7 +1442,7 @@ public class ServiceSynchronize extends LifecycleService {
private void processOperations(
EntityAccount account, EntityFolder folder,
Session isession, IMAPStore istore, IMAPFolder ifolder,
Session isession, Store istore, Folder ifolder,
ServiceState state)
throws MessagingException, JSONException, IOException {
try {
@ -1479,46 +1484,50 @@ public class ServiceSynchronize extends LifecycleService {
// Operations should use database transaction when needed
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))
doFlag(folder, ifolder, message, jargs, db);
doFlag(folder, (IMAPFolder) ifolder, message, jargs, db);
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))
doKeyword(folder, ifolder, message, jargs, db);
doKeyword(folder, (IMAPFolder) ifolder, message, jargs, db);
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))
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))
doDelete(folder, ifolder, message, jargs, db);
doDelete(folder, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.SEND.equals(op.name))
doSend(message, db);
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))
doRaw(folder, ifolder, message, jargs, db);
doRaw(folder, (IMAPFolder) ifolder, message, jargs, db);
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))
doAttachment(folder, op, ifolder, message, jargs, db);
doAttachment(folder, op, (IMAPFolder) ifolder, message, jargs, db);
else if (EntityOperation.SYNC.equals(op.name))
if (EntityFolder.OUTBOX.equals(folder.type))
db.folder().setFolderError(folder.id, null);
else
synchronizeMessages(account, folder, ifolder, jargs, state);
else {
if (ifolder instanceof IMAPFolder)
synchronizeMessages(account, folder, (IMAPFolder) ifolder, jargs, state);
else if (ifolder instanceof POP3Folder)
synchronizeMessages(account, folder, (POP3Folder) ifolder, jargs, state);
}
else
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);
}
String transportType = (ident.starttls ? "smtp" : "smtps");
// Get properties
Properties props = MessageHelper.getSessionProperties(ident.auth_type, ident.realm, ident.insecure);
props.put("mail.smtp.localhost", ident.host);
@ -1933,7 +1940,7 @@ public class ServiceSynchronize extends LifecycleService {
// Create transport
// TODO: cache transport?
Transport itransport = isession.getTransport(transportType);
Transport itransport = isession.getTransport(ident.getProtocol());
try {
// Connect transport
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);
try {
db.beginTransaction();
@ -2205,13 +2212,13 @@ public class ServiceSynchronize extends LifecycleService {
for (EntityFolder folder : db.folder().getFolders(account.id))
if (folder.tbc != null) {
Log.i(folder.name + " creating");
IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name);
Folder ifolder = istore.getFolder(folder.name);
if (!ifolder.exists())
ifolder.create(Folder.HOLDS_MESSAGES);
db.folder().resetFolderTbc(folder.id);
} else if (folder.tbd != null && folder.tbd) {
Log.i(folder.name + " deleting");
IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name);
Folder ifolder = istore.getFolder(folder.name);
if (ifolder.exists())
ifolder.delete(false);
db.folder().deleteFolder(folder.id);
@ -2228,7 +2235,11 @@ public class ServiceSynchronize extends LifecycleService {
for (Folder ifolder : ifolders) {
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);
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 {
final DB db = DB.getInstance(this);
try {

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

@ -158,7 +158,7 @@ public class ViewModelBrowse extends ViewModel {
isession.setDebug(true);
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);
Log.i("Boundary opening folder=" + folder.name);

@ -75,27 +75,37 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/etDomain" />
<!-- IMAP -->
<!-- IMAP/POP3 -->
<TextView
android:id="@+id/tvImap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
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_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" />
<TextView
android:id="@+id/tvPop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_pop"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textStyle="italic"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvImap" />
android:layout_marginStart="6dp"
android:text="@string/title_pop3"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Small"
app:layout_constraintBottom_toBottomOf="@id/swPop"
app:layout_constraintStart_toEndOf="@id/swPop"
app:layout_constraintTop_toTopOf="@id/swPop" />
<!-- host -->
@ -107,7 +117,7 @@
android:text="@string/title_host"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvPop" />
app:layout_constraintTop_toBottomOf="@id/swPop" />
<EditText
android:id="@+id/etHost"
@ -671,24 +681,36 @@
android:id="@+id/grpServer"
android:layout_width="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
android:id="@+id/grpAuthorize"
android:layout_width="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
android:id="@+id/grpAdvanced"
android:layout_width="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
android:id="@+id/grpFolders"
android:layout_width="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>
</ScrollView>

@ -199,5 +199,15 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="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>
</ScrollView>

@ -212,6 +212,7 @@
<string name="title_domain">Domain name</string>
<string name="title_autoconfig">Get settings</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_provider">Provider</string>
<string name="title_custom">Custom</string>

Loading…
Cancel
Save