Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70fbc5aa6a | ||
|
|
095a885d30 | ||
|
|
38df2f6d24 | ||
|
|
3a53d69b31 | ||
|
|
215fbbb74e | ||
|
|
b867e93291 | ||
|
|
e496278128 | ||
|
|
e7292f0fc9 | ||
|
|
f4a25a359d | ||
|
|
ee4339a971 | ||
|
|
0bf88fcf9e | ||
|
|
11d355c3d5 | ||
|
|
ed881a42ad | ||
|
|
1fe8784b1f | ||
|
|
5fe713180d | ||
|
|
c0fa6acc33 | ||
|
|
09b530dd9d | ||
|
|
3f0aa1b59a | ||
|
|
a4949c6473 | ||
|
|
64493bb573 | ||
|
|
6bb0d05478 | ||
|
|
d3fc9aadcc | ||
|
|
d7c4064f0b | ||
|
|
7a47f40fe1 | ||
|
|
a2ab19206c | ||
|
|
faf36edef0 | ||
|
|
34884e7e85 | ||
|
|
aa138cabc0 | ||
|
|
87817b9412 | ||
|
|
11e9333f14 | ||
|
|
703d8af6e8 | ||
|
|
0c2b035970 | ||
|
|
ad218d7b49 | ||
|
|
a0fc3c4058 | ||
|
|
695bb5bc8a | ||
|
|
6786358082 | ||
|
|
f93e4ca813 | ||
|
|
4fb2e7dbc2 | ||
|
|
84eeadd2f7 | ||
|
|
e41cc0cccf | ||
|
|
4d5d202ed7 |
BIN
.idea/caches/build_file_checksums.ser
generated
20
FAQ.md
@@ -24,6 +24,8 @@ to prevent Android from killing the service that takes care of receiving and sen
|
||||
|
||||
Most, if not all, other email apps don't show a notification with the "side effect" that new email is often not or late being reported.
|
||||
|
||||
Background: this is because of the introduction of [doze mode](https://developer.android.com/training/monitoring-device-state/doze-standby) in Android 6 Marshmallow.
|
||||
|
||||
<a name="FAQ3"></a>
|
||||
**(3) What are operations and why are they pending?**
|
||||
|
||||
@@ -99,8 +101,12 @@ So, unless your provider can enable this extension, you cannot use FairEmail for
|
||||
**(11) Why is STARTTLS for IMAP not supported?**
|
||||
|
||||
STARTTLS starts with a not encrypted connection and is therefore not secure.
|
||||
All known IMAP servers support IMAP with STARTTLS, so there is no need to support STARTTLS for IMAP.
|
||||
If you encounter an IMAP server that requires STARTTLS, please [create an issue](https://github.com/M66B/open-source-email/issues/new).
|
||||
All well known IMAP servers support IMAP with a plain SSL connection, so there is no need to support STARTTLS for IMAP.
|
||||
If you encounter an IMAP server that requires STARTTLS, please let me know.
|
||||
|
||||
For more background information, please see [this article](https://www.eff.org/nl/deeplinks/2018/06/announcing-starttls-everywhere-securing-hop-hop-email-delivery).
|
||||
|
||||
tl;dr; "*Additionally, even if you configure STARTTLS perfectly and use a valid certificate, there’s still no guarantee your communication will be encrypted.*"
|
||||
|
||||
<a name="FAQ13"></a>
|
||||
**(13) How does progressive search work?**
|
||||
@@ -227,6 +233,16 @@ Browse messages on the server will fetch messages from the email server in real
|
||||
when you reach the end of the list of synchronized messages, even when the folder is set to not synchronize.
|
||||
You can disable this feature under *Setup* > *Advanced options* > *Browse messages on the server*.
|
||||
|
||||
<a name="FAQ25"></a>
|
||||
**(25) Why can't I select an image, attachment or a file to export/import?**
|
||||
|
||||
If a menu item to select a file is disabled (dimmed),
|
||||
likely the [storage access framework](https://developer.android.com/guide/topics/providers/document-provider),
|
||||
a standard Android component, is not present,
|
||||
for example because your custom ROM does not include it or because it was removed.
|
||||
FairEmail does not request storage permissions, so this framework is required to select files and folders.
|
||||
No app, except maybe file managers, targetting Android 4.4 KitKat or later should ask for storage permissions because it would allow access to *all* files.
|
||||
|
||||
<br>
|
||||
|
||||
If you have another question, want to request a feature or report a bug, you can use [this forum](https://forum.xda-developers.com/android/apps-games/source-email-t3824168).
|
||||
|
||||
@@ -6,8 +6,8 @@ android {
|
||||
applicationId "eu.faircode.email"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 28
|
||||
versionCode 74
|
||||
versionName "1.74"
|
||||
versionCode 78
|
||||
versionName "1.78"
|
||||
archivesBaseName = "FairEmail-v$versionName"
|
||||
|
||||
javaCompileOptions {
|
||||
|
||||
1000
app/schemas/eu.faircode.email.DB/20.json
Normal file
@@ -125,6 +125,7 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
vwColor.setBackgroundColor(Color.TRANSPARENT);
|
||||
ivFlagged.setVisibility(View.GONE);
|
||||
ivAvatar.setVisibility(View.GONE);
|
||||
tvFrom.setText(null);
|
||||
@@ -158,7 +159,10 @@ public class AdapterMessage extends PagedListAdapter<TupleMessageEx, AdapterMess
|
||||
vwColor.setBackgroundColor(message.accountColor == null ? Color.TRANSPARENT : message.accountColor);
|
||||
vwColor.setVisibility(viewType == ViewType.UNIFIED ? View.VISIBLE : View.GONE);
|
||||
|
||||
ivFlagged.setVisibility(message.count - message.unflagged > 0 ? View.VISIBLE : View.GONE);
|
||||
if (viewType == ViewType.THREAD)
|
||||
ivFlagged.setVisibility(message.unflagged == 1 ? View.GONE : View.VISIBLE);
|
||||
else
|
||||
ivFlagged.setVisibility(message.count - message.unflagged > 0 ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (EntityFolder.DRAFTS.equals(message.folderType) ||
|
||||
EntityFolder.OUTBOX.equals(message.folderType) ||
|
||||
|
||||
@@ -67,6 +67,7 @@ public class ApplicationEx extends Application {
|
||||
getString(R.string.channel_service),
|
||||
NotificationManager.IMPORTANCE_MIN);
|
||||
service.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT);
|
||||
service.setShowBadge(false);
|
||||
nm.createNotificationChannel(service);
|
||||
|
||||
NotificationChannel notification = new NotificationChannel(
|
||||
@@ -79,13 +80,14 @@ public class ApplicationEx extends Application {
|
||||
"error",
|
||||
getString(R.string.channel_error),
|
||||
NotificationManager.IMPORTANCE_HIGH);
|
||||
error.setShowBadge(false);
|
||||
nm.createNotificationChannel(error);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean ownFault(Throwable ex) {
|
||||
if (!Helper.isPlayStoreInstall(this))
|
||||
return true;
|
||||
//if (!Helper.isPlayStoreInstall(this))
|
||||
// return true;
|
||||
|
||||
if (ex instanceof OutOfMemoryError)
|
||||
return false;
|
||||
|
||||
@@ -43,6 +43,7 @@ import javax.mail.UIDFolder;
|
||||
import javax.mail.search.BodyTerm;
|
||||
import javax.mail.search.FromStringTerm;
|
||||
import javax.mail.search.OrTerm;
|
||||
import javax.mail.search.RecipientStringTerm;
|
||||
import javax.mail.search.SubjectTerm;
|
||||
|
||||
import androidx.lifecycle.GenericLifecycleObserver;
|
||||
@@ -90,7 +91,6 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
||||
@Override
|
||||
public void run() {
|
||||
Log.i(Helper.TAG, "Boundary close");
|
||||
DB.getInstance(context).message().deleteFoundMessages();
|
||||
try {
|
||||
if (istore != null)
|
||||
istore.close();
|
||||
@@ -167,10 +167,16 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
||||
else
|
||||
imessages = ifolder.search(
|
||||
new OrTerm(
|
||||
new FromStringTerm(search),
|
||||
new OrTerm(
|
||||
new FromStringTerm(search),
|
||||
new RecipientStringTerm(Message.RecipientType.TO, search)
|
||||
),
|
||||
new OrTerm(
|
||||
new SubjectTerm(search),
|
||||
new BodyTerm(search))));
|
||||
new BodyTerm(search)
|
||||
)
|
||||
)
|
||||
);
|
||||
Log.i(Helper.TAG, "Boundary found messages=" + imessages.length);
|
||||
|
||||
index = imessages.length - 1;
|
||||
@@ -200,11 +206,13 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
|
||||
try {
|
||||
long uid = ifolder.getUID(isub[j]);
|
||||
Log.i(Helper.TAG, "Boundary sync uid=" + uid);
|
||||
if (db.message().getMessageByUid(fid, uid) == null) {
|
||||
EntityMessage message = db.message().getMessageByUid(fid, uid);
|
||||
if (message == null) {
|
||||
ServiceSynchronize.synchronizeMessage(context, folder, ifolder, (IMAPMessage) isub[j], search != null);
|
||||
count++;
|
||||
loaded++;
|
||||
}
|
||||
} else
|
||||
db.message().setMessageFound(message.id, true);
|
||||
} catch (MessageRemovedException ex) {
|
||||
Log.w(Helper.TAG, "Boundary " + ex + "\n" + Log.getStackTraceString(ex));
|
||||
} catch (FolderClosedException ex) {
|
||||
|
||||
@@ -45,7 +45,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
// https://developer.android.com/topic/libraries/architecture/room.html
|
||||
|
||||
@Database(
|
||||
version = 19,
|
||||
version = 20,
|
||||
entities = {
|
||||
EntityIdentity.class,
|
||||
EntityAccount.class,
|
||||
@@ -250,6 +250,13 @@ public abstract class DB extends RoomDatabase {
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `hide` INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
})
|
||||
.addMigrations(new Migration(19, 20) {
|
||||
@Override
|
||||
public void migrate(SupportSQLiteDatabase db) {
|
||||
Log.i(Helper.TAG, "DB migration from version " + startVersion + " to " + endVersion);
|
||||
db.execSQL("ALTER TABLE `folder` ADD COLUMN `poll_interval` INTEGER");
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -90,10 +90,6 @@ public interface DaoFolder {
|
||||
" WHERE account = :account AND type = :type")
|
||||
EntityFolder getFolderByType(long account, String type);
|
||||
|
||||
@Query("SELECT COUNT(folder.id) FROM folder" +
|
||||
" WHERE account = :account AND `synchronize`")
|
||||
int getFolderSyncCount(long account);
|
||||
|
||||
// For debug/crash info
|
||||
@Query("SELECT folder.* FROM folder" +
|
||||
" JOIN account ON account.id = folder.account" +
|
||||
@@ -128,8 +124,9 @@ public interface DaoFolder {
|
||||
", synchronize = :synchronize" +
|
||||
", unified = :unified" +
|
||||
", `after` = :after" +
|
||||
", `poll_interval` = :poll_interval" +
|
||||
" WHERE id = :id")
|
||||
int setFolderProperties(long id, String name, String display, boolean hide, boolean synchronize, boolean unified, int after);
|
||||
int setFolderProperties(long id, String name, String display, boolean hide, boolean synchronize, boolean unified, int after, Integer poll_interval);
|
||||
|
||||
@Query("UPDATE folder SET name = :name WHERE account = :account AND name = :old")
|
||||
int renameFolder(long account, String old, String name);
|
||||
|
||||
@@ -241,7 +241,4 @@ public interface DaoMessage {
|
||||
|
||||
@Query("DELETE FROM message WHERE folder = :folder AND received < :received AND NOT uid IS NULL")
|
||||
int deleteMessagesBefore(long folder, long received);
|
||||
|
||||
@Query("DELETE FROM message WHERE ui_found")
|
||||
int deleteFoundMessages();
|
||||
}
|
||||
@@ -49,14 +49,14 @@ public class EntityAccount {
|
||||
@NonNull
|
||||
public Integer auth_type;
|
||||
@NonNull
|
||||
public Boolean primary;
|
||||
@NonNull
|
||||
public Boolean synchronize;
|
||||
@NonNull
|
||||
public Boolean primary;
|
||||
public Integer color;
|
||||
@NonNull
|
||||
public Boolean store_sent; // obsolete
|
||||
@NonNull
|
||||
public Integer poll_interval; // NOOP interval
|
||||
public Integer poll_interval; // keep-alive interval
|
||||
public Long seen_until;
|
||||
public String state;
|
||||
public String error;
|
||||
@@ -66,12 +66,15 @@ public class EntityAccount {
|
||||
if (obj instanceof EntityAccount) {
|
||||
EntityAccount other = (EntityAccount) obj;
|
||||
return ((this.name == null ? other.name == null : this.name.equals(other.name)) &&
|
||||
(this.signature == null ? other.signature == null : this.signature.equals(other.signature)) &&
|
||||
this.host.equals(other.host) &&
|
||||
this.port.equals(other.port) &&
|
||||
this.user.equals(other.user) &&
|
||||
this.password.equals(other.password) &&
|
||||
this.primary.equals(other.primary) &&
|
||||
this.auth_type.equals(other.auth_type) &&
|
||||
this.synchronize.equals(other.synchronize) &&
|
||||
this.primary.equals(other.primary) &&
|
||||
(this.color == null ? other.color == null : this.color.equals(other.color)) &&
|
||||
this.poll_interval.equals(other.poll_interval) &&
|
||||
(this.seen_until == null ? other.seen_until == null : this.seen_until.equals(other.seen_until)) &&
|
||||
(this.state == null ? other.state == null : this.state.equals(other.state)) &&
|
||||
@@ -89,8 +92,8 @@ public class EntityAccount {
|
||||
json.put("user", user);
|
||||
json.put("password", "");
|
||||
json.put("auth_type", auth_type);
|
||||
json.put("primary", primary);
|
||||
json.put("synchronize", false);
|
||||
json.put("primary", primary);
|
||||
if (color != null)
|
||||
json.put("color", color);
|
||||
json.put("poll_interval", poll_interval);
|
||||
@@ -108,8 +111,8 @@ public class EntityAccount {
|
||||
account.user = json.getString("user");
|
||||
account.password = json.getString("password");
|
||||
account.auth_type = json.getInt("auth_type");
|
||||
account.primary = json.getBoolean("primary");
|
||||
account.synchronize = json.getBoolean("synchronize");
|
||||
account.primary = json.getBoolean("primary");
|
||||
if (json.has("color"))
|
||||
account.color = json.getInt("color");
|
||||
account.poll_interval = json.getInt("poll_interval");
|
||||
|
||||
@@ -19,12 +19,10 @@ package eu.faircode.email;
|
||||
Copyright 2018 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@@ -50,7 +48,7 @@ import static androidx.room.ForeignKey.CASCADE;
|
||||
}
|
||||
)
|
||||
|
||||
public class EntityFolder implements Parcelable {
|
||||
public class EntityFolder implements Serializable {
|
||||
static final String TABLE_NAME = "folder";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@@ -62,6 +60,7 @@ public class EntityFolder implements Parcelable {
|
||||
public String type;
|
||||
@NonNull
|
||||
public Boolean synchronize;
|
||||
public Integer poll_interval;
|
||||
@NonNull
|
||||
public Integer after; // days
|
||||
public String display;
|
||||
@@ -136,7 +135,7 @@ public class EntityFolder implements Parcelable {
|
||||
this.name.equals(other.name) &&
|
||||
this.type.equals(other.type) &&
|
||||
this.synchronize.equals(other.synchronize) &&
|
||||
this.after.equals(other.after) &&
|
||||
(this.poll_interval == null ? other.poll_interval == null : this.poll_interval.equals(other.poll_interval)) && this.after.equals(other.after) &&
|
||||
(this.display == null ? other.display == null : this.display.equals(other.display)) &&
|
||||
this.hide == other.hide &&
|
||||
this.unified == other.unified &&
|
||||
@@ -151,81 +150,16 @@ public class EntityFolder implements Parcelable {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int i) {
|
||||
if (id == null) {
|
||||
parcel.writeByte((byte) 0);
|
||||
} else {
|
||||
parcel.writeByte((byte) 1);
|
||||
parcel.writeLong(id);
|
||||
}
|
||||
if (account == null) {
|
||||
parcel.writeByte((byte) 0);
|
||||
} else {
|
||||
parcel.writeByte((byte) 1);
|
||||
parcel.writeLong(account);
|
||||
}
|
||||
parcel.writeString(name);
|
||||
parcel.writeString(type);
|
||||
parcel.writeByte((byte) (synchronize == null ? 0 : synchronize ? 1 : 2));
|
||||
if (after == null) {
|
||||
parcel.writeByte((byte) 0);
|
||||
} else {
|
||||
parcel.writeByte((byte) 1);
|
||||
parcel.writeInt(after);
|
||||
}
|
||||
parcel.writeString(state);
|
||||
parcel.writeString(error);
|
||||
}
|
||||
|
||||
protected EntityFolder(Parcel in) {
|
||||
if (in.readByte() == 0) {
|
||||
id = null;
|
||||
} else {
|
||||
id = in.readLong();
|
||||
}
|
||||
if (in.readByte() == 0) {
|
||||
account = null;
|
||||
} else {
|
||||
account = in.readLong();
|
||||
}
|
||||
name = in.readString();
|
||||
type = in.readString();
|
||||
byte tmpSynchronize = in.readByte();
|
||||
synchronize = tmpSynchronize == 0 ? null : tmpSynchronize == 1;
|
||||
if (in.readByte() == 0) {
|
||||
after = null;
|
||||
} else {
|
||||
after = in.readInt();
|
||||
}
|
||||
state = in.readString();
|
||||
error = in.readString();
|
||||
}
|
||||
|
||||
public static final Creator<EntityFolder> CREATOR = new Creator<EntityFolder>() {
|
||||
@Override
|
||||
public EntityFolder createFromParcel(Parcel in) {
|
||||
return new EntityFolder(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityFolder[] newArray(int size) {
|
||||
return new EntityFolder[size];
|
||||
}
|
||||
};
|
||||
|
||||
public JSONObject toJSON() throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("name", name);
|
||||
json.put("type", type);
|
||||
json.put("unified", unified);
|
||||
json.put("synchronize", synchronize);
|
||||
json.put("poll_interval", poll_interval);
|
||||
json.put("after", after);
|
||||
json.put("display", display);
|
||||
json.put("hide", hide);
|
||||
json.put("unified", unified);
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -233,9 +167,15 @@ public class EntityFolder implements Parcelable {
|
||||
EntityFolder folder = new EntityFolder();
|
||||
folder.name = json.getString("name");
|
||||
folder.type = json.getString("type");
|
||||
folder.unified = json.getBoolean("unified");
|
||||
folder.synchronize = json.getBoolean("synchronize");
|
||||
if (json.has("poll_interval"))
|
||||
folder.poll_interval = json.getInt("poll_interval");
|
||||
folder.after = json.getInt("after");
|
||||
if (json.has("display"))
|
||||
folder.display = json.getString("display");
|
||||
if (json.has("hide"))
|
||||
folder.hide = json.getBoolean("hide");
|
||||
folder.unified = json.getBoolean("unified");
|
||||
return folder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -253,9 +252,9 @@ public class FragmentAbout extends FragmentEx {
|
||||
}
|
||||
});
|
||||
|
||||
boolean debug = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("debug", false);
|
||||
btnLog.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
|
||||
btnDebugInfo.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
|
||||
//boolean debug = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("debug", false);
|
||||
//btnLog.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
|
||||
//btnDebugInfo.setVisibility(debug || BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ public class FragmentAccount extends FragmentEx {
|
||||
private TextInputLayout tilPassword;
|
||||
|
||||
private Button btnAdvanced;
|
||||
|
||||
private TextView tvName;
|
||||
private EditText etName;
|
||||
private Button btnColor;
|
||||
@@ -109,11 +110,12 @@ public class FragmentAccount extends FragmentEx {
|
||||
private ImageView ibColorDefault;
|
||||
private EditText etSignature;
|
||||
private ImageButton ibPro;
|
||||
|
||||
private CheckBox cbSynchronize;
|
||||
private CheckBox cbPrimary;
|
||||
private EditText etInterval;
|
||||
private Button btnCheck;
|
||||
|
||||
private Button btnCheck;
|
||||
private ProgressBar pbCheck;
|
||||
|
||||
private TextView tvIdle;
|
||||
@@ -169,6 +171,7 @@ public class FragmentAccount extends FragmentEx {
|
||||
tilPassword = view.findViewById(R.id.tilPassword);
|
||||
|
||||
btnAdvanced = view.findViewById(R.id.btnAdvanced);
|
||||
|
||||
etName = view.findViewById(R.id.etName);
|
||||
tvName = view.findViewById(R.id.tvName);
|
||||
btnColor = view.findViewById(R.id.btnColor);
|
||||
@@ -576,18 +579,19 @@ public class FragmentAccount extends FragmentEx {
|
||||
args.putString("password", tilPassword.getEditText().getText().toString());
|
||||
args.putInt("auth_type", authorized == null ? Helper.AUTH_TYPE_PASSWORD : provider.getAuthType());
|
||||
|
||||
args.putBoolean("synchronize", cbSynchronize.isChecked());
|
||||
args.putString("name", etName.getText().toString());
|
||||
args.putInt("color", color);
|
||||
args.putString("signature", Html.toHtml(etSignature.getText()));
|
||||
|
||||
args.putBoolean("synchronize", cbSynchronize.isChecked());
|
||||
args.putBoolean("primary", cbPrimary.isChecked());
|
||||
args.putString("interval", etInterval.getText().toString());
|
||||
|
||||
args.putParcelable("drafts", drafts);
|
||||
args.putParcelable("sent", sent);
|
||||
args.putParcelable("all", all);
|
||||
args.putParcelable("trash", trash);
|
||||
args.putParcelable("junk", junk);
|
||||
args.putSerializable("drafts", drafts);
|
||||
args.putSerializable("sent", sent);
|
||||
args.putSerializable("all", all);
|
||||
args.putSerializable("trash", trash);
|
||||
args.putSerializable("junk", junk);
|
||||
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
@@ -601,15 +605,16 @@ public class FragmentAccount extends FragmentEx {
|
||||
String name = args.getString("name");
|
||||
int color = args.getInt("color");
|
||||
String signature = args.getString("signature");
|
||||
|
||||
boolean synchronize = args.getBoolean("synchronize");
|
||||
boolean primary = args.getBoolean("primary");
|
||||
String interval = args.getString("interval");
|
||||
|
||||
EntityFolder drafts = args.getParcelable("drafts");
|
||||
EntityFolder sent = args.getParcelable("sent");
|
||||
EntityFolder all = args.getParcelable("all");
|
||||
EntityFolder trash = args.getParcelable("trash");
|
||||
EntityFolder junk = args.getParcelable("junk");
|
||||
EntityFolder drafts = (EntityFolder) args.getSerializable("drafts");
|
||||
EntityFolder sent = (EntityFolder) args.getSerializable("sent");
|
||||
EntityFolder all = (EntityFolder) args.getSerializable("all");
|
||||
EntityFolder trash = (EntityFolder) args.getSerializable("trash");
|
||||
EntityFolder junk = (EntityFolder) args.getSerializable("junk");
|
||||
|
||||
if (TextUtils.isEmpty(host))
|
||||
throw new Throwable(getContext().getString(R.string.title_no_host));
|
||||
@@ -660,19 +665,23 @@ public class FragmentAccount extends FragmentEx {
|
||||
boolean update = (account != null);
|
||||
if (account == null)
|
||||
account = new EntityAccount();
|
||||
account.name = name;
|
||||
account.color = color;
|
||||
account.signature = signature;
|
||||
|
||||
account.host = host;
|
||||
account.port = Integer.parseInt(port);
|
||||
account.user = user;
|
||||
account.password = password;
|
||||
account.auth_type = auth_type;
|
||||
|
||||
account.name = name;
|
||||
account.color = color;
|
||||
account.signature = signature;
|
||||
|
||||
account.synchronize = synchronize;
|
||||
account.primary = (account.synchronize && primary);
|
||||
account.store_sent = false;
|
||||
account.poll_interval = Integer.parseInt(interval);
|
||||
|
||||
account.store_sent = false;
|
||||
|
||||
if (!synchronize)
|
||||
account.error = null;
|
||||
|
||||
@@ -1024,7 +1033,7 @@ public class FragmentAccount extends FragmentEx {
|
||||
tilPassword.getEditText().setText(token);
|
||||
} catch (Throwable ex) {
|
||||
Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
|
||||
Helper.unexpectedError(getContext(), ex);
|
||||
snackbar.setText(Helper.formatThrowable(ex));
|
||||
} finally {
|
||||
snackbar.dismiss();
|
||||
}
|
||||
|
||||
@@ -322,7 +322,16 @@ public class FragmentCompose extends FragmentEx {
|
||||
public CharSequence convertToString(Cursor cursor) {
|
||||
int colName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
|
||||
int colEmail = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
|
||||
return cursor.getString(colName) + " <" + cursor.getString(colEmail) + ">";
|
||||
String name = cursor.getString(colName);
|
||||
String email = cursor.getString(colEmail);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (name == null)
|
||||
sb.append(email);
|
||||
else {
|
||||
sb.append(name.replace(",", "")).append(" ");
|
||||
sb.append("<").append(email).append(">");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -413,6 +422,10 @@ public class FragmentCompose extends FragmentEx {
|
||||
menu.findItem(R.id.menu_attachment).setVisible(!free && working >= 0);
|
||||
menu.findItem(R.id.menu_attachment).setEnabled(etBody.isEnabled());
|
||||
menu.findItem(R.id.menu_addresses).setVisible(!free && working >= 0);
|
||||
|
||||
PackageManager pm = getContext().getPackageManager();
|
||||
menu.findItem(R.id.menu_image).setEnabled(getImageIntent().resolveActivity(pm) != null);
|
||||
menu.findItem(R.id.menu_attachment).setEnabled(getAttachmentIntent().resolveActivity(pm) != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -479,17 +492,11 @@ public class FragmentCompose extends FragmentEx {
|
||||
}
|
||||
|
||||
private void onMenuImage() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("image/*");
|
||||
startActivityForResult(intent, ActivityCompose.REQUEST_IMAGE);
|
||||
startActivityForResult(getImageIntent(), ActivityCompose.REQUEST_IMAGE);
|
||||
}
|
||||
|
||||
private void onMenuAttachment() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
startActivityForResult(intent, ActivityCompose.REQUEST_ATTACHMENT);
|
||||
startActivityForResult(getAttachmentIntent(), ActivityCompose.REQUEST_ATTACHMENT);
|
||||
}
|
||||
|
||||
private void onMenuAddresses() {
|
||||
@@ -513,23 +520,18 @@ public class FragmentCompose extends FragmentEx {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExit() {
|
||||
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
|
||||
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
|
||||
.setMessage(R.string.title_ask_delete)
|
||||
.setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
onAction(R.id.action_delete);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.title_no, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
private Intent getImageIntent() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("image/*");
|
||||
return intent;
|
||||
}
|
||||
|
||||
private Intent getAttachmentIntent() {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
return intent;
|
||||
}
|
||||
|
||||
private void handlePickContact(int requestCode, Intent data) {
|
||||
@@ -559,9 +561,7 @@ public class FragmentCompose extends FragmentEx {
|
||||
|
||||
InternetAddress address = new InternetAddress(email, name);
|
||||
StringBuilder sb = new StringBuilder(text);
|
||||
if (sb.length() > 0)
|
||||
sb.append(", ");
|
||||
sb.append(address.toString());
|
||||
sb.append(address.toString().replace(",", "")).append(", ");
|
||||
|
||||
if (requestCode == ActivityCompose.REQUEST_CONTACT_TO)
|
||||
etTo.setText(sb.toString());
|
||||
@@ -622,6 +622,25 @@ public class FragmentCompose extends FragmentEx {
|
||||
}.load(this, args);
|
||||
}
|
||||
|
||||
private void handleExit() {
|
||||
if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED))
|
||||
new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner())
|
||||
.setMessage(R.string.title_ask_delete)
|
||||
.setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
onAction(R.id.action_delete);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.title_no, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onAction(int action) {
|
||||
Helper.setViewsEnabled(view, false);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
@@ -1285,8 +1304,10 @@ public class FragmentCompose extends FragmentEx {
|
||||
long id = Long.parseLong(cid[1].replace(BuildConfig.APPLICATION_ID + ".", ""));
|
||||
File file = EntityAttachment.getFile(getContext(), id);
|
||||
Drawable d = Drawable.createFromPath(file.getAbsolutePath());
|
||||
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
|
||||
return d;
|
||||
if (d != null) {
|
||||
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import javax.mail.Session;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.Group;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
public class FragmentFolder extends FragmentEx {
|
||||
@@ -54,16 +55,16 @@ public class FragmentFolder extends FragmentEx {
|
||||
private CheckBox cbSynchronize;
|
||||
private CheckBox cbUnified;
|
||||
private EditText etAfter;
|
||||
private EditText etInterval;
|
||||
private Button btnSave;
|
||||
private ImageButton ibDelete;
|
||||
private ProgressBar pbSave;
|
||||
private ProgressBar pbWait;
|
||||
private Group grpInterval;
|
||||
|
||||
private long id = -1;
|
||||
private long account = -1;
|
||||
|
||||
private static final int MAX_FOLDER_SYNC = 25;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -88,10 +89,12 @@ public class FragmentFolder extends FragmentEx {
|
||||
cbSynchronize = view.findViewById(R.id.cbSynchronize);
|
||||
cbUnified = view.findViewById(R.id.cbUnified);
|
||||
etAfter = view.findViewById(R.id.etAfter);
|
||||
etInterval = view.findViewById(R.id.etInterval);
|
||||
btnSave = view.findViewById(R.id.btnSave);
|
||||
ibDelete = view.findViewById(R.id.ibDelete);
|
||||
pbSave = view.findViewById(R.id.pbSave);
|
||||
pbWait = view.findViewById(R.id.pbWait);
|
||||
grpInterval = view.findViewById(R.id.grpInterval);
|
||||
|
||||
btnSave.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
@@ -110,6 +113,7 @@ public class FragmentFolder extends FragmentEx {
|
||||
args.putBoolean("unified", cbUnified.isChecked());
|
||||
args.putBoolean("synchronize", cbSynchronize.isChecked());
|
||||
args.putString("after", etAfter.getText().toString());
|
||||
args.putString("interval", etInterval.getText().toString());
|
||||
|
||||
new SimpleTask<Void>() {
|
||||
@Override
|
||||
@@ -122,10 +126,12 @@ public class FragmentFolder extends FragmentEx {
|
||||
boolean unified = args.getBoolean("unified");
|
||||
boolean synchronize = args.getBoolean("synchronize");
|
||||
String after = args.getString("after");
|
||||
String interval = args.getString("interval");
|
||||
|
||||
if (TextUtils.isEmpty(display) || display.equals(name))
|
||||
display = null;
|
||||
int days = (TextUtils.isEmpty(after) ? EntityFolder.DEFAULT_USER_SYNC : Integer.parseInt(after));
|
||||
Integer poll_interval = (TextUtils.isEmpty(interval) ? null : Integer.parseInt(interval));
|
||||
|
||||
IMAPStore istore = null;
|
||||
DB db = DB.getInstance(getContext());
|
||||
@@ -159,6 +165,7 @@ public class FragmentFolder extends FragmentEx {
|
||||
create.unified = unified;
|
||||
create.synchronize = synchronize;
|
||||
create.after = days;
|
||||
create.poll_interval = poll_interval;
|
||||
db.folder().insertFolder(create);
|
||||
} else {
|
||||
Log.i(Helper.TAG, "Renaming folder=" + name);
|
||||
@@ -173,7 +180,7 @@ public class FragmentFolder extends FragmentEx {
|
||||
|
||||
if (folder != null) {
|
||||
Log.i(Helper.TAG, "Updating folder=" + name);
|
||||
db.folder().setFolderProperties(id, name, display, hide, synchronize, unified, days);
|
||||
db.folder().setFolderProperties(id, name, display, hide, synchronize, unified, days, poll_interval);
|
||||
if (!synchronize)
|
||||
db.folder().setFolderError(id, null);
|
||||
}
|
||||
@@ -296,6 +303,7 @@ public class FragmentFolder extends FragmentEx {
|
||||
ibDelete.setVisibility(View.GONE);
|
||||
pbSave.setVisibility(View.GONE);
|
||||
pbWait.setVisibility(View.VISIBLE);
|
||||
grpInterval.setVisibility(View.GONE);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -320,34 +328,70 @@ public class FragmentFolder extends FragmentEx {
|
||||
etDisplay.setHint(folder == null ? null : folder.name);
|
||||
cbHide.setChecked(folder == null ? false : folder.hide);
|
||||
cbUnified.setChecked(folder == null ? false : folder.unified);
|
||||
cbSynchronize.setChecked(folder == null || folder.synchronize);
|
||||
etAfter.setText(Integer.toString(folder == null ? EntityFolder.DEFAULT_USER_SYNC : folder.after));
|
||||
etInterval.setText(folder == null || folder.poll_interval == null ? null : Integer.toString(folder.poll_interval));
|
||||
}
|
||||
|
||||
// Consider previous save as cancelled
|
||||
pbWait.setVisibility(View.GONE);
|
||||
Helper.setViewsEnabled(view, true);
|
||||
etRename.setEnabled(folder == null || EntityFolder.USER.equals(folder.type));
|
||||
cbSynchronize.setEnabled(false);
|
||||
btnSave.setEnabled(true);
|
||||
ibDelete.setVisibility(folder == null || !EntityFolder.USER.equals(folder.type) ? View.GONE : View.VISIBLE);
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("account", folder == null ? account : folder.account);
|
||||
|
||||
new SimpleTask<Integer>() {
|
||||
@Override
|
||||
protected Integer onLoad(Context context, Bundle args) {
|
||||
long account = args.getLong("account");
|
||||
return DB.getInstance(context).folder().getFolderSyncCount(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoaded(Bundle args, Integer count) {
|
||||
cbSynchronize.setChecked((folder == null || folder.synchronize) && count < MAX_FOLDER_SYNC);
|
||||
cbSynchronize.setEnabled(count < MAX_FOLDER_SYNC);
|
||||
}
|
||||
}.load(FragmentFolder.this, args);
|
||||
}
|
||||
});
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", id);
|
||||
args.putLong("account", account);
|
||||
|
||||
new SimpleTask<Boolean>() {
|
||||
@Override
|
||||
protected Boolean onLoad(Context context, Bundle args) throws Throwable {
|
||||
long fid = args.getLong("id");
|
||||
long aid = args.getLong("account");
|
||||
|
||||
IMAPStore istore = null;
|
||||
DB db = DB.getInstance(getContext());
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
EntityAccount account;
|
||||
if (fid < 0)
|
||||
account = db.account().getAccount(aid);
|
||||
else {
|
||||
EntityFolder folder = db.folder().getFolder(fid);
|
||||
account = db.account().getAccount(folder.account);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
||||
Properties props = MessageHelper.getSessionProperties(account.auth_type);
|
||||
Session isession = Session.getInstance(props, null);
|
||||
istore = (IMAPStore) isession.getStore("imaps");
|
||||
istore.connect(account.host, account.port, account.user, account.password);
|
||||
|
||||
return istore.hasCapability("IDLE");
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
if (istore != null)
|
||||
istore.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoaded(Bundle args, Boolean capIdle) {
|
||||
grpInterval.setVisibility(capIdle ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
grpInterval.setVisibility(View.VISIBLE);
|
||||
if (BuildConfig.DEBUG)
|
||||
Helper.unexpectedError(getContext(), ex);
|
||||
}
|
||||
}.load(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,26 +796,16 @@ public class FragmentMessage extends FragmentEx {
|
||||
}
|
||||
|
||||
private void onMenuShowHtml() {
|
||||
new SimpleTask<String>() {
|
||||
@Override
|
||||
protected String onLoad(Context context, Bundle args) throws Throwable {
|
||||
return message.read(context);
|
||||
}
|
||||
Bundle args = new Bundle();
|
||||
args.putLong("id", message.id);
|
||||
args.putString("from", MessageHelper.getFormattedAddresses(message.from, true));
|
||||
|
||||
@Override
|
||||
protected void onLoaded(Bundle a, String html) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString("html", html);
|
||||
args.putString("from", MessageHelper.getFormattedAddresses(message.from, true));
|
||||
FragmentWebView fragment = new FragmentWebView();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentWebView fragment = new FragmentWebView();
|
||||
fragment.setArguments(args);
|
||||
|
||||
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
}.load(this, new Bundle());
|
||||
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
|
||||
fragmentTransaction.replace(R.id.content_frame, fragment).addToBackStack("webview");
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
|
||||
private void onMenuUnseen() {
|
||||
|
||||
@@ -182,7 +182,7 @@ public class FragmentMessages extends FragmentEx {
|
||||
return 0;
|
||||
|
||||
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
|
||||
if (message.threaded || EntityFolder.OUTBOX.equals(message.folderType))
|
||||
if (message == null || message.threaded || EntityFolder.OUTBOX.equals(message.folderType))
|
||||
return 0;
|
||||
|
||||
return makeMovementFlags(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
||||
@@ -200,6 +200,9 @@ public class FragmentMessages extends FragmentEx {
|
||||
return;
|
||||
|
||||
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
boolean inbox = (EntityFolder.ARCHIVE.equals(message.folderType) || EntityFolder.TRASH.equals(message.folderType));
|
||||
|
||||
View itemView = viewHolder.itemView;
|
||||
@@ -237,6 +240,8 @@ public class FragmentMessages extends FragmentEx {
|
||||
return;
|
||||
|
||||
TupleMessageEx message = ((AdapterMessage) rvMessage.getAdapter()).getCurrentList().get(pos);
|
||||
if (message == null)
|
||||
return;
|
||||
Log.i(Helper.TAG, "Swiped dir=" + direction + " message=" + message.id);
|
||||
|
||||
Bundle args = new Bundle();
|
||||
|
||||
@@ -19,6 +19,7 @@ package eu.faircode.email;
|
||||
Copyright 2018 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -82,11 +83,26 @@ public class FragmentWebView extends FragmentEx {
|
||||
String url = args.getString("url");
|
||||
webview.loadUrl(url);
|
||||
setSubtitle(url);
|
||||
} else if (args.containsKey("html")) {
|
||||
String html = args.getString("html");
|
||||
String from = args.getString("from");
|
||||
webview.loadDataWithBaseURL("email://", html, "text/html", "UTF-8", null);
|
||||
setSubtitle(from);
|
||||
} else if (args.containsKey("id")) {
|
||||
new SimpleTask<String>() {
|
||||
@Override
|
||||
protected String onLoad(Context context, Bundle args) throws Throwable {
|
||||
long id = args.getLong("id");
|
||||
return EntityMessage.read(context, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLoaded(Bundle args, String html) {
|
||||
String from = args.getString("from");
|
||||
webview.loadDataWithBaseURL("email://", html, "text/html", "UTF-8", null);
|
||||
setSubtitle(from);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Bundle args, Throwable ex) {
|
||||
Helper.unexpectedError(getContext(), ex);
|
||||
}
|
||||
}.load(this, args);
|
||||
}
|
||||
|
||||
((ActivityBase) getActivity()).addBackPressedListener(new ActivityBase.IBackPressedListener() {
|
||||
|
||||
@@ -57,7 +57,6 @@ import java.util.concurrent.ThreadFactory;
|
||||
|
||||
import javax.mail.Address;
|
||||
import javax.mail.AuthenticationFailedException;
|
||||
import javax.mail.FolderClosedException;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.InternetAddress;
|
||||
|
||||
@@ -142,11 +141,6 @@ public class Helper {
|
||||
}
|
||||
|
||||
static String formatThrowable(Throwable ex) {
|
||||
if (ex instanceof FolderClosedException)
|
||||
return null;
|
||||
if (ex instanceof IllegalStateException)
|
||||
return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(ex.getMessage() == null ? ex.getClass().getName() : ex.getMessage());
|
||||
Throwable cause = ex.getCause();
|
||||
|
||||
@@ -83,7 +83,7 @@ public class MessageHelper {
|
||||
|
||||
// https://tools.ietf.org/html/rfc4978
|
||||
// https://docs.oracle.com/javase/8/docs/api/java/util/zip/Deflater.html
|
||||
if (false) {
|
||||
if (true) {
|
||||
Log.i(Helper.TAG, "IMAP compress enabled");
|
||||
props.put("mail.imaps.compress.enable", "true");
|
||||
//props.put("mail.imaps.compress.level", "-1");
|
||||
@@ -91,6 +91,7 @@ public class MessageHelper {
|
||||
}
|
||||
|
||||
props.put("mail.imaps.fetchsize", Integer.toString(48 * 1024)); // default 16K
|
||||
props.put("mail.imaps.peek", "true");
|
||||
|
||||
// https://javaee.github.io/javamail/docs/api/com/sun/mail/smtp/package-summary.html#properties
|
||||
props.put("mail.smtps.ssl.checkserveridentity", "true");
|
||||
@@ -113,9 +114,6 @@ public class MessageHelper {
|
||||
props.put("mail.smtp.writetimeout", Integer.toString(NETWORK_TIMEOUT)); // one thread overhead
|
||||
props.put("mail.smtp.timeout", Integer.toString(NETWORK_TIMEOUT));
|
||||
|
||||
props.put("mail.imaps.peek", "true");
|
||||
//props.put("mail.imaps.minidletime", "5000");
|
||||
|
||||
props.put("mail.mime.address.strict", "false");
|
||||
props.put("mail.mime.decodetext.strict", "false");
|
||||
|
||||
|
||||
@@ -50,12 +50,10 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.sun.mail.iap.ConnectionException;
|
||||
import com.sun.mail.iap.ProtocolException;
|
||||
import com.sun.mail.imap.AppendUID;
|
||||
import com.sun.mail.imap.IMAPFolder;
|
||||
import com.sun.mail.imap.IMAPMessage;
|
||||
import com.sun.mail.imap.IMAPStore;
|
||||
import com.sun.mail.imap.protocol.IMAPProtocol;
|
||||
import com.sun.mail.util.FolderClosedIOException;
|
||||
import com.sun.mail.util.MailConnectException;
|
||||
|
||||
@@ -129,7 +127,6 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
private static final int CONNECT_BACKOFF_START = 8; // seconds
|
||||
private static final int CONNECT_BACKOFF_MAX = 1024; // seconds (1024 sec ~ 17 min)
|
||||
private static final long STORE_NOOP_INTERVAL = 9 * 60 * 1000L; // milliseconds
|
||||
private static final int SYNC_BATCH_SIZE = 20;
|
||||
private static final int DOWNLOAD_BATCH_SIZE = 20;
|
||||
private static final int MESSAGE_AUTO_DOWNLOAD_SIZE = 32 * 1024; // bytes
|
||||
@@ -214,7 +211,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
cm.unregisterNetworkCallback(serviceManager);
|
||||
|
||||
serviceManager.stop();
|
||||
serviceManager.onLost(null);
|
||||
|
||||
stopForeground(true);
|
||||
|
||||
@@ -326,7 +323,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
builder = new Notification.Builder(this);
|
||||
|
||||
builder
|
||||
.setSmallIcon(R.drawable.baseline_compare_arrows_24)
|
||||
.setSmallIcon(R.drawable.baseline_compare_arrows_white_24)
|
||||
.setContentTitle(getResources().getQuantityString(R.plurals.title_notification_synchronizing, accounts, accounts))
|
||||
.setContentIntent(pi)
|
||||
.setAutoCancel(false)
|
||||
@@ -375,7 +372,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
builder = new Notification.Builder(this, "notification");
|
||||
|
||||
builder
|
||||
.setSmallIcon(R.drawable.baseline_mail_24)
|
||||
.setSmallIcon(R.drawable.baseline_email_white_24)
|
||||
.setContentTitle(getResources().getQuantityString(R.plurals.title_notification_unseen, messages.size(), messages.size()))
|
||||
.setContentText("")
|
||||
.setContentIntent(piView)
|
||||
@@ -559,6 +556,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
// Debug
|
||||
boolean debug = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("debug", false);
|
||||
debug = debug || BuildConfig.DEBUG;
|
||||
System.setProperty("mail.socket.debug", Boolean.toString(debug));
|
||||
|
||||
// Create session
|
||||
@@ -569,7 +567,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
final IMAPStore istore = (IMAPStore) isession.getStore("imaps");
|
||||
final Map<EntityFolder, IMAPFolder> folders = new HashMap<>();
|
||||
List<Thread> noops = new ArrayList<>();
|
||||
List<Thread> pollers = new ArrayList<>();
|
||||
List<Thread> idlers = new ArrayList<>();
|
||||
try {
|
||||
// Listen for store events
|
||||
@@ -672,7 +670,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
db.folder().setFolderError(folder.id, null);
|
||||
|
||||
// Keep folder connection alive
|
||||
Thread noop = new Thread(new Runnable() {
|
||||
Thread poller = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
@@ -811,26 +809,15 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
}
|
||||
});
|
||||
|
||||
Log.i(Helper.TAG, folder.name + " start noop");
|
||||
while (state.running && ifolder.isOpen()) {
|
||||
try {
|
||||
Thread.sleep(account.poll_interval * 60 * 1000L);
|
||||
|
||||
if (capIdle) {
|
||||
Log.i(Helper.TAG, folder.name + " request NOOP");
|
||||
ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
|
||||
public Object doCommand(IMAPProtocol p) throws ProtocolException {
|
||||
Log.i(Helper.TAG, ifolder.getName() + " start NOOP");
|
||||
p.simpleCommand("NOOP", null);
|
||||
Log.i(Helper.TAG, ifolder.getName() + " end NOOP");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} else
|
||||
if (!capIdle) {
|
||||
Log.i(Helper.TAG, folder.name + " start polling");
|
||||
while (state.running) {
|
||||
try {
|
||||
Thread.sleep((folder.poll_interval == null ? 9 : folder.poll_interval) * 60 * 1000L);
|
||||
synchronizeMessages(account, folder, ifolder, state);
|
||||
|
||||
} catch (InterruptedException ex) {
|
||||
Log.w(Helper.TAG, folder.name + " noop " + ex.toString());
|
||||
} catch (InterruptedException ex) {
|
||||
Log.w(Helper.TAG, folder.name + " poll " + ex.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
@@ -843,16 +830,17 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
state.notifyAll();
|
||||
}
|
||||
} finally {
|
||||
Log.i(Helper.TAG, folder.name + " end noop");
|
||||
if (!capIdle)
|
||||
Log.i(Helper.TAG, folder.name + " end polling");
|
||||
}
|
||||
}
|
||||
}, "sync.noop." + folder.id);
|
||||
noop.start();
|
||||
noops.add(noop);
|
||||
}, "sync.poller." + folder.id);
|
||||
poller.start();
|
||||
pollers.add(poller);
|
||||
|
||||
// Receive folder events
|
||||
if (capIdle) {
|
||||
Thread idle = new Thread(new Runnable() {
|
||||
Thread idler = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
@@ -876,8 +864,8 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
}
|
||||
}
|
||||
}, "sync.idle." + folder.id);
|
||||
idle.start();
|
||||
idlers.add(idle);
|
||||
idler.start();
|
||||
idlers.add(idler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,22 +953,22 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
try {
|
||||
// Keep store alive
|
||||
while (state.running && istore.isConnected()) {
|
||||
Log.i(Helper.TAG, "Checking folders");
|
||||
for (EntityFolder folder : folders.keySet())
|
||||
if (!folders.get(folder).isOpen())
|
||||
throw new FolderClosedException(folders.get(folder));
|
||||
|
||||
// Wait for stop or folder error
|
||||
while (state.running) {
|
||||
Log.i(Helper.TAG, account.name + " wait");
|
||||
synchronized (state) {
|
||||
try {
|
||||
state.wait(STORE_NOOP_INTERVAL);
|
||||
state.wait(account.poll_interval * 60 * 1000L);
|
||||
} catch (InterruptedException ex) {
|
||||
Log.w(Helper.TAG, account.name + " wait " + ex.toString());
|
||||
}
|
||||
}
|
||||
Log.i(Helper.TAG, account.name + " waited");
|
||||
|
||||
if (!istore.isConnected())
|
||||
throw new StoreClosedException(istore);
|
||||
|
||||
for (EntityFolder folder : folders.keySet())
|
||||
if (!folders.get(folder).isOpen())
|
||||
throw new FolderClosedException(folders.get(folder));
|
||||
}
|
||||
Log.i(Helper.TAG, account.name + " done running=" + state.running);
|
||||
} finally {
|
||||
@@ -992,11 +980,18 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
db.account().setAccountError(account.id, Helper.formatThrowable(ex));
|
||||
} finally {
|
||||
// Close store
|
||||
EntityLog.log(this, account.name + " closing");
|
||||
db.account().setAccountState(account.id, "closing");
|
||||
for (EntityFolder folder : folders.keySet())
|
||||
db.folder().setFolderState(folder.id, "closing");
|
||||
|
||||
// Stop pollers
|
||||
for (Thread poller : pollers) {
|
||||
poller.interrupt();
|
||||
join(poller);
|
||||
}
|
||||
|
||||
// Close store
|
||||
try {
|
||||
Thread t = new Thread(new Runnable() {
|
||||
@Override
|
||||
@@ -1026,16 +1021,10 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
db.folder().setFolderState(folder.id, null);
|
||||
}
|
||||
|
||||
// Stop noop
|
||||
for (Thread noop : noops) {
|
||||
noop.interrupt();
|
||||
join(noop);
|
||||
}
|
||||
|
||||
// Stop idle
|
||||
for (Thread idle : idlers) {
|
||||
idle.interrupt();
|
||||
join(idle);
|
||||
// Stop idlers
|
||||
for (Thread idler : idlers) {
|
||||
idler.interrupt();
|
||||
join(idler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1330,6 +1319,9 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
private void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException {
|
||||
Message imessage = ifolder.getMessageByUID(message.uid);
|
||||
if (imessage == null)
|
||||
throw new MessageRemovedException();
|
||||
|
||||
Enumeration<Header> headers = imessage.getAllHeaders();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (headers.hasMoreElements()) {
|
||||
@@ -1534,10 +1526,12 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
if (message == null)
|
||||
full.add(imessage);
|
||||
}
|
||||
long headers = SystemClock.elapsedRealtime();
|
||||
ifolder.fetch(full.toArray(new Message[0]), fp);
|
||||
Log.i(Helper.TAG, folder.name + " fetched headers=" + full.size() +
|
||||
" " + (SystemClock.elapsedRealtime() - headers) + " ms");
|
||||
if (full.size() > 0) {
|
||||
long headers = SystemClock.elapsedRealtime();
|
||||
ifolder.fetch(full.toArray(new Message[0]), fp);
|
||||
Log.i(Helper.TAG, folder.name + " fetched headers=" + full.size() +
|
||||
" " + (SystemClock.elapsedRealtime() - headers) + " ms");
|
||||
}
|
||||
|
||||
for (int j = isub.length - 1; j >= 0; j--)
|
||||
try {
|
||||
@@ -1791,7 +1785,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
if (!fetch)
|
||||
for (EntityAttachment attachment : attachments)
|
||||
if (!attachment.available)
|
||||
if (attachment.size != null && attachment.size < ATTACHMENT_AUTO_DOWNLOAD_SIZE) {
|
||||
if (!metered || (attachment.size != null && attachment.size < ATTACHMENT_AUTO_DOWNLOAD_SIZE)) {
|
||||
fetch = true;
|
||||
break;
|
||||
}
|
||||
@@ -1821,7 +1815,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
for (int i = 0; i < attachments.size(); i++) {
|
||||
EntityAttachment attachment = attachments.get(i);
|
||||
if (!attachment.available)
|
||||
if (attachment.size != null && attachment.size < ATTACHMENT_AUTO_DOWNLOAD_SIZE) {
|
||||
if (!metered || (attachment.size != null && attachment.size < ATTACHMENT_AUTO_DOWNLOAD_SIZE)) {
|
||||
if (iattachments == null)
|
||||
iattachments = helper.getAttachments();
|
||||
attachment.part = iattachments.get(i).part;
|
||||
@@ -1864,7 +1858,7 @@ public class ServiceSynchronize extends LifecycleService {
|
||||
|
||||
if (running) {
|
||||
ConnectivityManager cm = getSystemService(ConnectivityManager.class);
|
||||
NetworkInfo ani = cm.getActiveNetworkInfo();
|
||||
NetworkInfo ani = (network == null ? null : cm.getActiveNetworkInfo());
|
||||
EntityLog.log(ServiceSynchronize.this, "Network active=" + (ani == null ? null : ani.toString()));
|
||||
if (ani == null || !ani.isConnected()) {
|
||||
EntityLog.log(ServiceSynchronize.this, "Network disconnected=" + ani);
|
||||
|
||||
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 185 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 236 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 267 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 262 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
app/src/main/res/drawable-hdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 144 B |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 147 B |
|
After Width: | Height: | Size: 140 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 200 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 189 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 203 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 325 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 185 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 209 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/drawable-mdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 322 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 181 B |
|
After Width: | Height: | Size: 200 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 305 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 325 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 322 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 233 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 341 B |
|
After Width: | Height: | Size: 398 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 587 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_36.png
Normal file
|
After Width: | Height: | Size: 585 B |
BIN
app/src/main/res/drawable-xxhdpi/baseline_email_white_48.png
Normal file
|
After Width: | Height: | Size: 759 B |
|
After Width: | Height: | Size: 234 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 484 B |
|
After Width: | Height: | Size: 236 B |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 398 B |
|
After Width: | Height: | Size: 484 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_email_black_18.png
Normal file
|
After Width: | Height: | Size: 421 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_email_black_24.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_email_black_36.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_email_black_48.png
Normal file
|
After Width: | Height: | Size: 1023 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_email_white_18.png
Normal file
|
After Width: | Height: | Size: 419 B |
BIN
app/src/main/res/drawable-xxxhdpi/baseline_email_white_24.png
Normal file
|
After Width: | Height: | Size: 532 B |