FairEmail/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java

1433 lines
60 KiB
Java
Raw Normal View History

2018-09-02 06:59:49 +00:00
package eu.faircode.email;
2018-09-04 07:02:54 +00:00
/*
This file is part of FairEmail.
FairEmail is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
2018-10-29 10:46:49 +00:00
FairEmail is distributed in the hope that it will be useful,
2018-09-04 07:02:54 +00:00
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
2018-10-29 10:46:49 +00:00
along with FairEmail. If not, see <http://www.gnu.org/licenses/>.
2018-09-04 07:02:54 +00:00
2024-01-01 07:50:49 +00:00
Copyright 2018-2024 by Marcel Bokhorst (M66B)
2018-09-04 07:02:54 +00:00
*/
import android.content.Context;
import android.content.SharedPreferences;
2022-09-26 07:01:55 +00:00
import android.database.sqlite.SQLiteDatabase;
2019-07-24 10:00:47 +00:00
import android.text.TextUtils;
2018-09-02 06:59:49 +00:00
2019-06-12 20:22:17 +00:00
import androidx.annotation.NonNull;
2020-04-09 15:50:29 +00:00
import androidx.annotation.Nullable;
2018-09-02 06:59:49 +00:00
import androidx.paging.PagedList;
import androidx.preference.PreferenceManager;
2018-09-02 06:59:49 +00:00
2020-05-01 19:47:59 +00:00
import com.sun.mail.gimap.GmailFolder;
import com.sun.mail.iap.Argument;
2020-04-09 15:50:29 +00:00
import com.sun.mail.iap.ProtocolException;
import com.sun.mail.iap.Response;
import com.sun.mail.imap.IMAPFolder;
2019-10-05 18:44:45 +00:00
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.imap.protocol.IMAPProtocol;
import com.sun.mail.imap.protocol.IMAPResponse;
2020-05-03 21:28:14 +00:00
import com.sun.mail.imap.protocol.SearchSequence;
import com.sun.mail.util.MessageRemovedIOException;
2021-09-24 11:36:06 +00:00
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
2023-02-25 07:05:42 +00:00
import org.jsoup.nodes.Document;
2021-09-24 11:36:06 +00:00
2019-10-02 19:13:10 +00:00
import java.io.File;
import java.io.IOException;
2020-04-09 15:50:29 +00:00
import java.io.Serializable;
import java.lang.reflect.Constructor;
2020-05-03 21:28:14 +00:00
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
2022-03-18 18:46:23 +00:00
import java.util.Calendar;
2022-10-10 11:59:08 +00:00
import java.util.Comparator;
2020-04-10 09:51:50 +00:00
import java.util.Date;
2021-03-04 19:24:42 +00:00
import java.util.HashMap;
import java.util.List;
2021-03-04 19:24:42 +00:00
import java.util.Map;
2020-04-09 15:50:29 +00:00
import java.util.Objects;
2022-12-31 19:56:14 +00:00
import java.util.concurrent.ExecutorService;
2022-11-09 14:33:37 +00:00
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import javax.mail.Address;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.FolderClosedException;
import javax.mail.Message;
import javax.mail.MessageRemovedException;
import javax.mail.MessagingException;
2019-12-05 19:05:07 +00:00
import javax.mail.ReadOnlyFolderException;
import javax.mail.UIDFolder;
2019-09-26 08:53:27 +00:00
import javax.mail.internet.MimeMessage;
2019-09-13 11:28:46 +00:00
import javax.mail.search.AndTerm;
import javax.mail.search.BodyTerm;
2020-04-10 09:51:50 +00:00
import javax.mail.search.ComparisonTerm;
import javax.mail.search.FlagTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.NotTerm;
import javax.mail.search.OrTerm;
2020-04-10 09:51:50 +00:00
import javax.mail.search.ReceivedDateTerm;
import javax.mail.search.RecipientStringTerm;
import javax.mail.search.SearchTerm;
2020-06-12 16:49:46 +00:00
import javax.mail.search.SizeTerm;
import javax.mail.search.SubjectTerm;
2018-09-02 06:59:49 +00:00
public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMessageEx> {
private Context context;
2021-11-22 07:36:51 +00:00
private AdapterMessage.ViewType viewType;
2020-02-02 16:17:58 +00:00
private Long account;
2019-05-12 09:40:54 +00:00
private Long folder;
2019-05-12 12:28:18 +00:00
private boolean server;
2020-04-09 15:50:29 +00:00
private SearchCriteria criteria;
private int pageSize;
2019-05-14 16:34:09 +00:00
2018-09-02 06:59:49 +00:00
private IBoundaryCallbackMessages intf;
2018-10-20 17:56:09 +00:00
2019-08-09 17:29:17 +00:00
private State state;
2022-12-31 19:56:14 +00:00
private static ExecutorService executor = Helper.getBackgroundExecutor(1, "boundary");
2020-07-08 12:29:39 +00:00
private static final int SEARCH_LIMIT_DEVICE = 1000;
2019-10-29 08:46:39 +00:00
2018-09-02 06:59:49 +00:00
interface IBoundaryCallbackMessages {
void onLoading();
void onLoaded(int found);
2018-09-02 06:59:49 +00:00
void onWarning(String message);
2019-06-13 06:10:47 +00:00
void onException(@NonNull Throwable ex);
2018-09-02 06:59:49 +00:00
}
2021-11-22 07:36:51 +00:00
BoundaryCallbackMessages(
Context context,
AdapterMessage.ViewType viewType, long account, long folder,
boolean server, SearchCriteria criteria,
int pageSize) {
this.context = context.getApplicationContext();
2021-11-22 07:36:51 +00:00
this.viewType = viewType;
2020-02-02 16:17:58 +00:00
this.account = (account < 0 ? null : account);
2019-05-12 09:40:54 +00:00
this.folder = (folder < 0 ? null : folder);
2019-05-12 12:28:18 +00:00
this.server = server;
2020-04-09 15:50:29 +00:00
this.criteria = criteria;
this.pageSize = pageSize;
2019-05-14 16:34:09 +00:00
}
2018-09-02 06:59:49 +00:00
2021-06-18 14:02:46 +00:00
State setCallback(IBoundaryCallbackMessages intf) {
2021-06-18 14:50:09 +00:00
Log.i("Boundary callback=" + intf);
if (Objects.equals(intf, this.intf))
return this.state;
2019-05-14 16:34:09 +00:00
this.intf = intf;
2019-08-09 17:29:17 +00:00
this.state = new State();
2021-06-18 14:50:09 +00:00
2021-06-18 14:02:46 +00:00
return this.state;
2018-09-02 06:59:49 +00:00
}
@Override
public void onZeroItemsLoaded() {
2019-05-12 12:28:18 +00:00
Log.i("Boundary zero loaded");
2021-06-04 19:58:29 +00:00
queue_load(state);
2018-09-02 06:59:49 +00:00
}
@Override
2019-08-09 17:29:17 +00:00
public void onItemAtEndLoaded(@NonNull final TupleMessageEx itemAtEnd) {
2021-06-04 19:58:29 +00:00
Log.i("Boundary at end id=" + itemAtEnd.id);
queue_load(state);
2018-09-02 06:59:49 +00:00
}
2020-03-24 12:20:27 +00:00
void retry() {
2022-12-31 19:56:14 +00:00
executor.submit(new Runnable() {
2020-04-10 07:02:09 +00:00
@Override
public void run() {
2020-07-27 13:52:50 +00:00
close(state, true);
2020-04-10 07:02:09 +00:00
}
});
2021-06-04 19:58:29 +00:00
queue_load(state);
2020-03-24 12:20:27 +00:00
}
2021-06-04 19:58:29 +00:00
private void queue_load(final State state) {
2022-11-09 14:33:37 +00:00
if (state.queued.get() > 1) {
Log.i("Boundary queued =" + state.queued.get());
2021-06-04 19:58:29 +00:00
return;
}
2022-11-09 14:33:37 +00:00
state.queued.incrementAndGet();
Log.i("Boundary queued +" + state.queued.get());
2021-03-01 07:40:35 +00:00
2022-12-31 19:56:14 +00:00
executor.submit(new Runnable() {
2019-01-22 15:55:21 +00:00
@Override
public void run() {
2021-03-06 10:14:23 +00:00
Helper.gc();
2021-03-04 21:03:06 +00:00
2021-03-04 19:24:42 +00:00
int free = Log.getFreeMemMb();
Map<String, String> crumb = new HashMap<>();
2022-11-09 14:33:37 +00:00
crumb.put("queued", Integer.toString(state.queued.get()));
2021-03-04 19:24:42 +00:00
Log.breadcrumb("Boundary run", crumb);
2021-06-04 19:58:29 +00:00
Log.i("Boundary run free=" + free);
2021-02-28 15:48:51 +00:00
int found = 0;
2019-01-22 15:55:21 +00:00
try {
2021-06-18 14:02:46 +00:00
if (state.destroyed || state.error) {
Log.i("Boundary was destroyed");
2019-05-12 12:28:18 +00:00
return;
2021-06-18 14:02:46 +00:00
}
if (!Objects.equals(state, BoundaryCallbackMessages.this.state)) {
Log.i("Boundary changed state");
return;
}
2019-05-12 12:28:18 +00:00
2022-04-17 16:58:43 +00:00
ApplicationEx.getMainHandler().post(new Runnable() {
@Override
public void run() {
if (intf != null)
2019-05-14 16:34:09 +00:00
intf.onLoading();
2022-04-17 16:58:43 +00:00
}
});
2019-05-12 12:28:18 +00:00
if (server)
2020-03-22 09:14:36 +00:00
try {
found = load_server(state);
2020-03-22 09:14:36 +00:00
} catch (Throwable ex) {
if (state.error || ex instanceof IllegalArgumentException)
throw ex;
Log.w("Boundary", ex);
2020-07-27 13:52:50 +00:00
close(state, true);
2020-03-22 09:14:36 +00:00
// Retry
found = load_server(state);
2020-03-22 09:14:36 +00:00
}
2019-05-12 12:28:18 +00:00
else
found = load_device(state);
2019-01-22 15:55:21 +00:00
} catch (final Throwable ex) {
2020-03-24 12:25:16 +00:00
state.error = true;
2019-01-22 15:55:21 +00:00
Log.e("Boundary", ex);
2022-04-17 16:58:43 +00:00
ApplicationEx.getMainHandler().post(new Runnable() {
@Override
public void run() {
if (intf != null)
2019-06-13 06:10:47 +00:00
intf.onException(ex);
2022-04-17 16:58:43 +00:00
}
});
2019-01-22 15:55:21 +00:00
} finally {
2022-11-09 14:33:37 +00:00
state.queued.decrementAndGet();
Log.i("Boundary queued -" + state.queued.get());
2021-03-06 10:14:23 +00:00
Helper.gc();
2021-03-04 21:03:06 +00:00
2022-11-09 14:33:37 +00:00
crumb.put("queued", Integer.toString(state.queued.get()));
2021-03-04 19:24:42 +00:00
Log.breadcrumb("Boundary done", crumb);
2022-04-17 16:58:43 +00:00
final int f = found;
ApplicationEx.getMainHandler().post(new Runnable() {
@Override
public void run() {
if (intf != null)
intf.onLoaded(f);
2022-04-17 16:58:43 +00:00
}
});
2018-09-02 06:59:49 +00:00
}
2019-01-22 15:55:21 +00:00
}
});
2018-09-02 06:59:49 +00:00
}
2019-01-29 08:35:42 +00:00
2019-08-09 17:29:17 +00:00
private int load_device(State state) {
DB db = DB.getInstance(context);
2021-03-14 18:03:57 +00:00
Log.i("Boundary device" +
" index=" + state.index +
" matches=" + (state.matches == null ? null : state.matches.size()));
long[] exclude = new long[0];
if (folder == null) {
List<Long> folders = new ArrayList<>();
if (!criteria.in_trash) {
List<EntityFolder> trash = db.folder().getFoldersByType(EntityFolder.TRASH);
if (trash != null)
for (EntityFolder folder : trash)
folders.add(folder.id);
}
if (!criteria.in_junk) {
List<EntityFolder> junk = db.folder().getFoldersByType(EntityFolder.JUNK);
if (junk != null)
for (EntityFolder folder : junk)
folders.add(folder.id);
}
exclude = Helper.toLongArray(folders);
}
2019-05-12 12:28:18 +00:00
int found = 0;
2020-01-14 20:58:27 +00:00
List<String> word = new ArrayList<>();
List<String> plus = new ArrayList<>();
List<String> minus = new ArrayList<>();
if (criteria.query != null) {
for (String w : criteria.query.trim().split("\\s+"))
if (w.length() > 1 && w.startsWith("+"))
plus.add(w.substring(1));
else if (w.length() > 1 && w.startsWith("-"))
minus.add(w.substring(1));
else
word.add(w);
if (word.size() == 0 && plus.size() > 0)
word.add(plus.get(0));
}
2023-01-05 19:58:09 +00:00
if (criteria.fts && word.size() > 0 && !criteria.in_headers && !criteria.in_html) {
2020-01-14 20:58:27 +00:00
if (state.ids == null) {
2022-09-26 06:13:03 +00:00
SQLiteDatabase sdb = Fts4DbHelper.getInstance(context);
state.ids = Fts4DbHelper.match(sdb, account, folder, exclude, criteria, TextUtils.join(" ", word));
2022-11-04 07:14:54 +00:00
EntityLog.log(context, "Boundary FTS" +
2020-07-07 15:00:52 +00:00
" account=" + account +
" folder=" + folder +
" criteria=" + criteria +
" ids=" + state.ids.size());
2020-01-14 20:58:27 +00:00
}
2021-10-11 13:20:31 +00:00
List<Long> excluded = Helper.fromLongArray(exclude);
2020-01-14 20:58:27 +00:00
try {
db.beginTransaction();
for (; state.index < state.ids.size() && found < pageSize && !state.destroyed; state.index++) {
2021-06-15 06:40:43 +00:00
long id = state.ids.get(state.index);
EntityMessage message = db.message().getMessage(id);
2022-06-07 16:05:10 +00:00
if (message == null || message.ui_hide)
continue;
2021-10-11 13:20:31 +00:00
if (excluded.contains(message.folder))
continue;
2022-12-22 12:27:17 +00:00
if (!matchMessage(context, message, criteria, false))
continue;
2022-10-12 06:54:06 +00:00
found += db.message().setMessageFound(message.id, true);
Log.i("Boundary matched=" + message.id + " found=" + found);
2020-01-14 20:58:27 +00:00
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return found;
2020-01-14 20:58:27 +00:00
}
2021-02-28 15:48:51 +00:00
while (found < pageSize && !state.destroyed) {
if (state.matches == null ||
(state.matches.size() > 0 && state.index >= state.matches.size())) {
String query = (word.size() == 0 ? null : '%' + TextUtils.join("%", word) + '%');
2021-02-28 15:48:51 +00:00
state.matches = db.message().matchMessages(
account, folder, exclude,
query,
2022-10-12 06:54:06 +00:00
//criteria.in_senders,
//criteria.in_recipients,
//criteria.in_subject,
//criteria.in_keywords,
//criteria.in_message,
//criteria.in_notes,
//criteria.in_headers,
2021-02-28 15:48:51 +00:00
criteria.with_unseen,
criteria.with_flagged,
criteria.with_hidden,
criteria.with_encrypted,
criteria.with_attachments,
criteria.with_notes,
2021-02-28 15:48:51 +00:00
criteria.with_types == null ? 0 : criteria.with_types.length,
criteria.with_types == null ? new String[]{} : criteria.with_types,
criteria.with_size,
criteria.after,
criteria.before,
SEARCH_LIMIT_DEVICE, state.offset);
EntityLog.log(context, "Boundary device" +
" account=" + account +
" folder=" + folder +
" criteria=" + criteria +
" query=" + query +
2021-02-28 15:48:51 +00:00
" offset=" + state.offset +
" size=" + state.matches.size());
state.offset += Math.min(state.matches.size(), SEARCH_LIMIT_DEVICE);
state.index = 0;
}
2019-10-29 08:46:39 +00:00
2021-02-28 15:48:51 +00:00
if (state.matches.size() == 0)
break;
2019-10-29 11:09:50 +00:00
2021-02-28 15:48:51 +00:00
for (int i = state.index; i < state.matches.size() && found < pageSize && !state.destroyed; i++) {
state.index = i + 1;
2019-10-29 08:46:39 +00:00
2021-02-28 15:48:51 +00:00
TupleMatch match = state.matches.get(i);
2022-10-12 06:54:06 +00:00
boolean matched = (criteria.query == null || Boolean.TRUE.equals(match.matched));
2022-10-12 06:54:06 +00:00
if (!matched) {
EntityMessage message = db.message().getMessage(match.id);
2022-10-12 06:54:06 +00:00
if (message != null && !message.ui_hide)
2022-12-22 12:27:17 +00:00
matched = matchMessage(context, message, criteria, true);
}
2021-03-15 08:05:54 +00:00
if (matched) {
2021-12-30 07:32:51 +00:00
found += db.message().setMessageFound(match.id, true);
2021-06-15 06:40:43 +00:00
Log.i("Boundary matched=" + match.id + " found=" + found);
2019-05-12 09:40:54 +00:00
}
}
2019-05-12 09:40:54 +00:00
}
2021-03-14 18:03:57 +00:00
Log.i("Boundary device done" +
" found=" + found + "/" + pageSize +
" destroyed=" + state.destroyed +
" memory=" + Log.getFreeMemMb());
2019-05-12 12:28:18 +00:00
return found;
}
2021-06-17 20:35:48 +00:00
private int load_server(final State state) throws MessagingException, ProtocolException, IOException {
2019-05-12 12:28:18 +00:00
DB db = DB.getInstance(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
2020-07-08 07:13:47 +00:00
boolean debug = prefs.getBoolean("debug", false);
2020-04-09 15:50:29 +00:00
final EntityFolder browsable = db.folder().getBrowsableFolder(folder, criteria != null);
2020-12-17 14:05:44 +00:00
if (browsable == null || !browsable.selectable || browsable.local) {
2021-02-27 17:21:22 +00:00
Log.i("Boundary not browsable=" + (folder != null));
2019-06-07 17:41:23 +00:00
return 0;
2020-03-03 09:37:15 +00:00
}
2019-05-12 12:28:18 +00:00
2019-05-12 09:40:54 +00:00
EntityAccount account = db.account().getAccount(browsable.account);
2022-03-31 15:28:58 +00:00
if (account == null || account.protocol != EntityAccount.TYPE_IMAP)
2019-06-07 17:41:23 +00:00
return 0;
2019-08-09 17:29:17 +00:00
if (state.imessages == null)
try {
// Check connectivity
2019-05-12 16:41:51 +00:00
if (!ConnectionHelper.getNetworkState(context).isSuitable())
2019-06-13 06:09:01 +00:00
throw new IllegalStateException(context.getString(R.string.title_no_internet));
2020-07-07 15:00:52 +00:00
EntityLog.log(context, "Boundary server connecting account=" + account.name);
2020-02-06 12:06:10 +00:00
state.iservice = new EmailService(
2022-08-12 20:09:50 +00:00
context, account.getProtocol(), account.realm, account.encryption, account.insecure, account.unicode,
2020-07-25 10:45:00 +00:00
EmailService.PURPOSE_SEARCH, debug || BuildConfig.DEBUG);
2019-08-09 17:29:17 +00:00
state.iservice.setPartialFetch(account.partial_fetch);
2023-01-20 23:10:54 +00:00
state.iservice.setRawFetch(account.raw_fetch);
state.iservice.setIgnoreBodyStructureSize(account.ignore_size);
2019-08-09 17:29:17 +00:00
state.iservice.connect(account);
2020-07-07 15:00:52 +00:00
EntityLog.log(context, "Boundary server opening folder=" + browsable.name);
2019-08-09 17:29:17 +00:00
state.ifolder = (IMAPFolder) state.iservice.getStore().getFolder(browsable.name);
2019-12-05 19:05:07 +00:00
try {
state.ifolder.open(Folder.READ_WRITE);
2021-11-13 15:16:21 +00:00
browsable.read_only = state.ifolder.getUIDNotSticky();
db.folder().setFolderReadOnly(browsable.id, browsable.read_only);
2019-12-05 19:05:07 +00:00
} catch (ReadOnlyFolderException ex) {
state.ifolder.open(Folder.READ_ONLY);
2021-11-13 15:16:21 +00:00
browsable.read_only = true;
db.folder().setFolderReadOnly(browsable.id, browsable.read_only);
2019-12-05 19:05:07 +00:00
}
2020-03-22 10:34:11 +00:00
db.folder().setFolderError(browsable.id, null);
2020-08-08 09:51:41 +00:00
int count = MessageHelper.getMessageCount(state.ifolder);
2022-05-02 13:23:16 +00:00
db.folder().setFolderTotal(browsable.id, count < 0 ? null : count, new Date().getTime());
2019-08-21 18:23:13 +00:00
2020-04-09 15:50:29 +00:00
if (criteria == null) {
2021-11-22 07:36:51 +00:00
boolean filter_seen = prefs.getBoolean(FragmentMessages.getFilter(context, "seen", viewType, browsable.type), false);
boolean filter_unflagged = prefs.getBoolean(FragmentMessages.getFilter(context, "unflagged", viewType, browsable.type), false);
2020-07-07 15:00:52 +00:00
EntityLog.log(context, "Boundary filter seen=" + filter_seen + " unflagged=" + filter_unflagged);
2019-08-15 14:39:07 +00:00
2020-04-09 19:19:44 +00:00
List<SearchTerm> and = new ArrayList<>();
2019-09-13 11:28:46 +00:00
2020-04-09 19:19:44 +00:00
if (filter_seen &&
state.ifolder.getPermanentFlags().contains(Flags.Flag.SEEN))
and.add(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
2019-10-05 14:33:33 +00:00
2020-04-09 19:19:44 +00:00
if (filter_unflagged &&
state.ifolder.getPermanentFlags().contains(Flags.Flag.FLAGGED))
and.add(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
if (and.size() == 0)
2019-10-05 14:33:33 +00:00
state.imessages = state.ifolder.getMessages();
2020-04-09 19:19:44 +00:00
else
state.imessages = state.ifolder.search(new AndTerm(and.toArray(new SearchTerm[0])));
2022-10-10 11:59:08 +00:00
2020-07-07 15:00:52 +00:00
EntityLog.log(context, "Boundary filter messages=" + state.imessages.length);
2021-06-17 20:35:48 +00:00
} else
try {
Object result = state.ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
try {
// https://tools.ietf.org/html/rfc3501#section-6.4.4
if (criteria.query != null &&
criteria.query.startsWith("raw:") &&
protocol.hasCapability("X-GM-EXT-1") &&
EntityFolder.ARCHIVE.equals(browsable.type)) {
// https://support.google.com/mail/answer/7190
// https://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw
Log.i("Boundary raw search=" + criteria.query);
Argument arg = new Argument();
arg.writeAtom("X-GM-RAW");
arg.writeString(criteria.query.substring(4));
Response[] responses = protocol.command("SEARCH", arg);
if (responses.length == 0)
throw new ProtocolException("No response");
if (!responses[responses.length - 1].isOK())
2021-06-17 20:35:48 +00:00
throw new ProtocolException(
context.getString(R.string.title_service_auth, responses[responses.length - 1]));
List<Integer> msgnums = new ArrayList<>();
for (Response response : responses)
if (((IMAPResponse) response).keyEquals("SEARCH")) {
int msgnum;
while ((msgnum = response.readNumber()) != -1)
msgnums.add(msgnum);
}
Message[] imessages = new Message[msgnums.size()];
for (int i = 0; i < msgnums.size(); i++)
imessages[i] = state.ifolder.getMessage(msgnums.get(i));
return imessages;
} else {
EntityLog.log(context, "Boundary server" +
" account=" + account +
" folder=" + folder +
" search=" + criteria);
try {
return search(true, browsable.keywords, protocol, state);
} catch (Throwable ex) {
EntityLog.log(context, ex.toString());
2022-11-12 07:47:59 +00:00
if (ex instanceof ProtocolException && ex.getMessage() != null)
try {
boolean retry = false;
String remark = null;
if (ex.getMessage().contains("full text search not supported")) {
retry = true;
criteria.in_message = false;
} else if (ex.getMessage().contains("invalid search criteria")) {
// invalid SEARCH command syntax, invalid search criteria syntax
retry = true;
criteria.in_keywords = false;
remark = "Keyword search not supported?";
2022-04-30 09:41:47 +00:00
}
2022-11-12 07:47:59 +00:00
if (retry) {
String msg = context.getString(R.string.title_service_auth,
account.host + ": " +
(remark == null ? "" : remark + " - ") +
getMessage(ex));
ApplicationEx.getMainHandler().post(new Runnable() {
@Override
public void run() {
if (intf != null)
intf.onWarning(msg);
}
});
return search(true, browsable.keywords, protocol, state);
}
} catch (Throwable exex) {
Log.w(exex);
}
}
2021-06-17 20:35:48 +00:00
return search(false, browsable.keywords, protocol, state);
2021-06-04 16:12:02 +00:00
}
2021-06-17 20:35:48 +00:00
} catch (Throwable ex) {
2021-06-18 14:55:22 +00:00
ProtocolException pex;
2021-06-17 20:35:48 +00:00
if (ex instanceof ProtocolException)
2021-06-18 14:55:22 +00:00
pex = new ProtocolException(
context.getString(R.string.title_service_auth,
2022-04-30 09:52:08 +00:00
account.host + ": " + getMessage(ex)),
2021-06-18 14:55:22 +00:00
ex.getCause());
2021-06-17 20:35:48 +00:00
else
2021-06-18 14:55:22 +00:00
pex = new ProtocolException("Search " + account.host, ex);
2021-06-17 20:35:48 +00:00
Log.e(pex);
throw pex;
}
}
2021-06-17 20:35:48 +00:00
});
2021-06-17 20:35:48 +00:00
state.imessages = (Message[]) result;
} catch (MessagingException ex) {
if (ex.getCause() instanceof ProtocolException)
throw (ProtocolException) ex.getCause();
else
throw ex;
}
2020-07-08 13:17:34 +00:00
EntityLog.log(context, "Boundary found messages=" + state.imessages.length);
2022-10-10 12:03:49 +00:00
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
state.ifolder.fetch(state.imessages, fp);
Arrays.sort(state.imessages, new Comparator<Message>() {
@Override
public int compare(Message m1, Message m2) {
Date d1 = null;
try {
d1 = m1.getReceivedDate();
} catch (Throwable ex) {
Log.w(ex);
}
Date d2 = null;
try {
d2 = m2.getReceivedDate();
} catch (Throwable ex) {
Log.w(ex);
}
return Long.compare(d1 == null ? 0 : d1.getTime(), d2 == null ? 0 : d2.getTime());
}
});
EntityLog.log(context, "Boundary sorted messages=" + state.imessages.length);
2019-08-09 17:29:17 +00:00
state.index = state.imessages.length - 1;
} catch (Throwable ex) {
2019-08-09 17:29:17 +00:00
state.error = true;
2020-07-08 13:17:34 +00:00
if (ex instanceof FolderClosedException)
Log.w("Search", ex);
else
Log.e("Search", ex);
2019-06-18 19:30:57 +00:00
throw ex;
}
2022-12-24 09:10:21 +00:00
List<EntityRule> rules = db.rule().getEnabledRules(browsable.id, false);
2019-06-13 08:40:09 +00:00
2019-05-12 12:28:18 +00:00
int found = 0;
while (state.index >= 0 && found < pageSize && !state.destroyed) {
Log.i("Boundary server index=" + state.index);
int from = Math.max(0, state.index - (pageSize - found) + 1);
2019-08-09 17:29:17 +00:00
Message[] isub = Arrays.copyOfRange(state.imessages, from, state.index + 1);
2021-03-01 19:03:23 +00:00
Arrays.fill(state.imessages, from, state.index + 1, null);
state.index -= (pageSize - found);
2019-10-05 14:33:33 +00:00
List<Message> add = new ArrayList<>();
for (Message m : isub)
try {
long uid = state.ifolder.getUID(m);
2020-07-24 18:46:42 +00:00
EntityMessage message = db.message().getMessageByUid(browsable.id, uid);
if (message == null)
2019-10-05 14:33:33 +00:00
add.add(m);
2021-05-01 10:41:31 +00:00
} catch (FolderClosedException ex) {
throw ex;
2020-06-14 15:15:39 +00:00
} catch (Throwable ex) {
Log.w(ex);
2019-10-05 14:33:33 +00:00
add.add(m);
}
2020-07-08 13:17:34 +00:00
Log.i("Boundary fetching " + add.size() + "/" + isub.length);
2019-10-05 14:33:33 +00:00
if (add.size() > 0) {
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.FLAGS);
fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
fp.add(UIDFolder.FetchProfileItem.UID);
fp.add(IMAPFolder.FetchProfileItem.HEADERS);
//fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
fp.add(FetchProfile.Item.SIZE);
2022-10-10 12:03:49 +00:00
//fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
2020-06-25 07:14:05 +00:00
if (account.isGmail()) {
2020-05-01 19:47:59 +00:00
fp.add(GmailFolder.FetchProfileItem.THRID);
2020-06-25 07:14:05 +00:00
fp.add(GmailFolder.FetchProfileItem.LABELS);
}
2019-10-05 14:33:33 +00:00
state.ifolder.fetch(add.toArray(new Message[0]), fp);
}
2021-02-28 15:48:51 +00:00
Core.State astate = new Core.State(ConnectionHelper.getNetworkState(context));
for (int j = isub.length - 1; j >= 0 && found < pageSize && !state.destroyed && astate.isRecoverable(); j--)
2021-02-28 15:48:51 +00:00
try {
long uid = state.ifolder.getUID(isub[j]);
Log.i("Boundary server sync uid=" + uid);
EntityMessage message = db.message().getMessageByUid(browsable.id, uid);
2021-08-10 17:17:40 +00:00
if (message == null) {
2021-02-28 15:48:51 +00:00
message = Core.synchronizeMessage(context,
account, browsable,
(IMAPStore) state.iservice.getStore(), state.ifolder, (MimeMessage) isub[j],
true, true,
rules, astate, null);
2021-08-13 16:36:17 +00:00
// SQLiteConstraintException
if (message != null && criteria == null)
found++; // browsed
2021-08-10 17:17:40 +00:00
}
2021-08-13 16:36:17 +00:00
if (message != null && criteria != null)
2021-12-30 07:32:51 +00:00
found += db.message().setMessageFound(message.id, true);
2021-06-15 06:40:43 +00:00
Log.i("Boundary matched=" + (message == null ? null : message.id) + " found=" + found);
} catch (MessageRemovedException | MessageRemovedIOException ex) {
2021-02-28 15:48:51 +00:00
Log.w(browsable.name + " boundary server", ex);
} catch (FolderClosedException ex) {
throw ex;
} catch (IOException ex) {
if (ex.getCause() instanceof MessagingException) {
2019-05-12 12:28:18 +00:00
Log.w(browsable.name + " boundary server", ex);
2019-12-06 07:50:46 +00:00
db.folder().setFolderError(browsable.id, Log.formatThrowable(ex));
2021-02-28 15:48:51 +00:00
} else
throw ex;
} catch (Throwable ex) {
Log.e(browsable.name + " boundary server", ex);
db.folder().setFolderError(browsable.id, Log.formatThrowable(ex));
} finally {
2021-03-01 19:03:23 +00:00
isub[j] = null;
2021-02-28 15:48:51 +00:00
}
}
2020-07-27 13:52:50 +00:00
if (state.index < 0) {
Log.i("Boundary server end");
close(state, false);
}
Log.i("Boundary server done memory=" + Log.getFreeMemMb());
2019-05-12 12:28:18 +00:00
return found;
}
2019-05-14 16:34:09 +00:00
2022-04-30 09:52:08 +00:00
private String getMessage(Throwable ex) {
if (ex instanceof ProtocolException) {
Response r = ((ProtocolException) ex).getResponse();
if (r != null && !TextUtils.isEmpty(r.getRest()))
2023-12-13 10:42:38 +00:00
return r.getRest();
2022-04-30 09:52:08 +00:00
}
2023-12-12 19:55:55 +00:00
return new ThrowableWrapper(ex).toSafeString();
2022-04-30 09:52:08 +00:00
}
2020-07-10 12:43:33 +00:00
private Message[] search(boolean utf8, String[] keywords, IMAPProtocol protocol, State state) throws IOException, MessagingException, ProtocolException {
2020-07-08 11:16:11 +00:00
EntityLog.log(context, "Search utf8=" + utf8);
SearchTerm terms = criteria.getTerms(utf8, state.ifolder.getPermanentFlags(), keywords);
2020-07-08 13:17:34 +00:00
if (terms == null)
return state.ifolder.getMessages();
2020-07-08 11:16:11 +00:00
SearchSequence ss = new SearchSequence(protocol);
Argument args = ss.generateSequence(terms, utf8 ? StandardCharsets.UTF_8.name() : null);
args.writeAtom("ALL");
2023-03-13 15:33:13 +00:00
// https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4
Response[] responses = protocol.command("SEARCH" + (utf8 ? " CHARSET UTF-8" : ""), args);
if (responses != null && responses.length > 0 && !responses[responses.length - 1].isOK()) {
2023-03-15 10:17:29 +00:00
// Normally "NO", but some servers respond with "BAD Could not parse command"
2023-03-13 15:33:13 +00:00
if (!BuildConfig.PLAY_STORE_RELEASE)
Log.e(responses[responses.length - 1].toString());
responses = protocol.command("SEARCH", args);
}
2020-07-08 11:16:11 +00:00
if (responses == null || responses.length == 0)
2020-07-08 13:17:34 +00:00
throw new ProtocolException("No response from server");
2020-07-08 11:16:11 +00:00
for (Response response : responses)
2020-07-08 13:17:34 +00:00
Log.i("Search response=" + response);
2020-07-08 11:16:11 +00:00
if (!responses[responses.length - 1].isOK())
throw new ProtocolException(responses[responses.length - 1]);
List<Integer> msgnums = new ArrayList<>();
2022-10-10 12:03:49 +00:00
for (int r = 0; r < responses.length; r++) {
2020-07-08 11:16:11 +00:00
IMAPResponse response = (IMAPResponse) responses[r];
if (response.keyEquals("SEARCH")) {
int msgnum;
while ((msgnum = response.readNumber()) != -1)
msgnums.add(msgnum);
}
}
EntityLog.log(context, "Search messages=" + msgnums.size());
Message[] imessages = new Message[msgnums.size()];
for (int i = 0; i < msgnums.size(); i++)
imessages[i] = state.ifolder.getMessage(msgnums.get(i));
return imessages;
}
2022-12-22 12:27:17 +00:00
private static boolean matchMessage(Context context, EntityMessage message, SearchCriteria criteria, boolean partial) {
2022-10-12 06:54:06 +00:00
if (criteria.with_unseen) {
if (message.ui_seen)
return false;
}
if (criteria.with_flagged) {
if (!message.ui_flagged)
return false;
}
if (criteria.with_hidden) {
if (message.ui_snoozed == null)
return false;
}
if (criteria.with_encrypted) {
if (message.encrypt == null ||
EntityMessage.ENCRYPT_NONE.equals(message.encrypt))
return false;
}
if (criteria.with_attachments) {
if (message.attachments == 0)
return false;
}
//
if (criteria.with_notes) {
if (message.notes == null)
return false;
}
//
if (criteria.with_size != null) {
if (message.total == null || message.total < criteria.with_size)
return false;
}
//
if (criteria.before != null) {
if (message.received > criteria.before)
return false;
}
//
if (criteria.after != null) {
if (message.received < criteria.after)
return false;
}
if (criteria.in_senders) {
2022-12-22 12:27:17 +00:00
if (contains(message.from, criteria.query, partial))
2022-10-12 06:54:06 +00:00
return true;
}
if (criteria.in_recipients) {
2022-12-22 12:27:17 +00:00
if (contains(message.to, criteria.query, partial) ||
contains(message.cc, criteria.query, partial) ||
contains(message.bcc, criteria.query, partial))
2022-10-12 06:54:06 +00:00
return true;
}
if (criteria.in_subject) {
2022-12-22 12:27:17 +00:00
if (contains(message.subject, criteria.query, partial, false))
2022-10-12 06:54:06 +00:00
return true;
}
if (criteria.in_keywords) {
if (message.keywords != null)
for (String keyword : message.keywords)
2022-12-22 12:27:17 +00:00
if (contains(keyword, criteria.query, partial, false))
2022-10-12 06:54:06 +00:00
return true;
}
if (criteria.in_notes) {
2022-12-22 12:27:17 +00:00
if (contains(message.notes, criteria.query, partial, false))
2022-10-12 06:54:06 +00:00
return true;
}
if (criteria.in_filenames) {
DB db = DB.getInstance(context);
List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
if (attachments != null)
for (EntityAttachment attachment : attachments)
if (!TextUtils.isEmpty(attachment.name) && contains(attachment.name, criteria.query, true, false))
return true; // Partial search to find "filename.extension"
}
2022-10-12 06:54:06 +00:00
if (criteria.in_headers) {
2023-01-03 08:42:03 +00:00
if (message.headers != null && message.headers.contains(criteria.query))
2022-10-12 06:54:06 +00:00
return true;
}
2023-01-03 08:42:03 +00:00
if (criteria.in_html || criteria.in_message)
2022-10-12 06:54:06 +00:00
try {
File file = EntityMessage.getFile(context, message.id);
if (file.exists()) {
2023-02-25 07:05:42 +00:00
String selector = criteria.getJsoup();
if (selector != null) {
Document d = JsoupEx.parse(file);
return (d.select(selector).size() > 0);
}
2022-10-12 06:54:06 +00:00
String html = Helper.readText(file);
2023-01-03 08:42:03 +00:00
if (criteria.in_html) {
if (html.contains(criteria.query))
2022-10-12 06:54:06 +00:00
return true;
}
2023-02-25 07:05:42 +00:00
2023-01-03 08:42:03 +00:00
if (criteria.in_message) {
2023-01-19 17:27:00 +00:00
// This won't match <p>An <b>example</b><p> when searching for "An example"
2023-01-03 08:42:03 +00:00
if (contains(html, criteria.query, partial, true)) {
String text = HtmlHelper.getFullText(html);
if (contains(text, criteria.query, partial, false))
return true;
}
}
2022-10-12 06:54:06 +00:00
}
} catch (IOException ex) {
Log.e(ex);
}
return false;
}
2022-12-22 12:27:17 +00:00
private static boolean contains(Address[] addresses, String query, boolean partial) {
if (addresses == null)
return false;
for (Address address : addresses)
2022-12-22 12:27:17 +00:00
if (contains(address.toString(), query, partial, false))
return true;
return false;
}
2022-12-22 12:27:17 +00:00
private static boolean contains(String text, String query, boolean partial, boolean html) {
2022-09-29 12:34:17 +00:00
if (TextUtils.isEmpty(text))
return false;
2023-09-26 08:50:45 +00:00
text = Fts4DbHelper.processBreakText(text);
List<String> word = new ArrayList<>();
for (String w : query.trim().split("\\s+"))
if (w.length() > 1 && w.startsWith("+")) {
if (!text.contains(Fts4DbHelper.preprocessText(w.substring(1))))
return false;
} else if (w.length() > 1 && w.startsWith("-")) {
if (!html && text.contains(Fts4DbHelper.preprocessText(w.substring(1))))
return false;
} else
2023-09-26 08:50:45 +00:00
word.addAll(Arrays.asList(Fts4DbHelper.processBreakText(w).split("\\s+")));
if (word.size() == 0)
return true;
2023-08-20 07:56:17 +00:00
// \b is limited to [0-9A-Za-z_]
String b = "(^|\\s+)";
String a = "($|\\s+)";
2022-11-13 07:12:11 +00:00
StringBuilder sb = new StringBuilder();
2023-08-20 07:56:17 +00:00
sb.append(partial ? ".*(" : ".*?" + b + "(");
2022-11-13 07:12:11 +00:00
for (int i = 0; i < word.size(); i++) {
if (i > 0)
sb.append("\\s+");
sb.append(Pattern.quote(word.get(i)));
}
2023-08-20 07:56:17 +00:00
sb.append(partial ? ").*" : ")" + a + ".*?");
2022-11-13 07:12:11 +00:00
Pattern pat = Pattern.compile(sb.toString(), Pattern.DOTALL);
return pat.matcher(text).matches();
2022-09-29 12:34:17 +00:00
}
2021-06-18 14:02:46 +00:00
State getState() {
return this.state;
}
2020-04-10 07:02:09 +00:00
2021-06-18 14:02:46 +00:00
void destroy(State state) {
state.destroyed = true;
2022-04-17 15:13:05 +00:00
this.intf = null;
2021-06-18 14:02:46 +00:00
Log.i("Boundary destroy");
2019-05-14 16:34:09 +00:00
2022-12-31 19:56:14 +00:00
executor.submit(new Runnable() {
2019-05-14 16:34:09 +00:00
@Override
public void run() {
2021-06-18 14:02:46 +00:00
close(state, true);
2019-05-14 16:34:09 +00:00
}
});
}
2019-08-09 17:29:17 +00:00
2020-07-27 13:52:50 +00:00
private void close(State state, boolean reset) {
2020-03-22 09:14:36 +00:00
Log.i("Boundary close");
2020-04-10 07:02:09 +00:00
2020-03-22 09:14:36 +00:00
try {
2020-07-27 13:52:50 +00:00
if (state.ifolder != null && state.ifolder.isOpen())
2022-09-23 16:12:42 +00:00
state.ifolder.close(false);
2020-03-22 09:14:36 +00:00
} catch (Throwable ex) {
Log.e("Boundary", ex);
}
2020-04-10 07:02:09 +00:00
2020-03-22 09:14:36 +00:00
try {
2020-07-27 13:52:50 +00:00
if (state.iservice != null && state.iservice.isOpen())
2020-03-22 09:14:36 +00:00
state.iservice.close();
} catch (Throwable ex) {
Log.e("Boundary", ex);
}
2020-04-10 07:02:09 +00:00
2020-07-27 13:52:50 +00:00
if (reset)
state.reset();
2020-03-22 09:14:36 +00:00
}
2021-06-18 14:02:46 +00:00
static class State {
2022-11-09 14:33:37 +00:00
final AtomicInteger queued = new AtomicInteger(0);
2019-08-09 17:29:17 +00:00
boolean destroyed = false;
boolean error = false;
int index = 0;
2019-10-29 08:46:39 +00:00
int offset = 0;
2020-01-14 20:58:27 +00:00
List<Long> ids = null;
2019-10-23 18:01:43 +00:00
List<TupleMatch> matches = null;
2019-08-09 17:29:17 +00:00
2020-01-29 20:06:45 +00:00
EmailService iservice = null;
2019-08-09 17:29:17 +00:00
IMAPFolder ifolder = null;
Message[] imessages = null;
2020-03-22 09:14:36 +00:00
void reset() {
2020-04-09 19:19:44 +00:00
Log.i("Boundary reset");
2022-11-09 14:33:37 +00:00
queued.set(0);
2020-03-22 09:14:36 +00:00
destroyed = false;
error = false;
index = 0;
offset = 0;
ids = null;
matches = null;
iservice = null;
ifolder = null;
imessages = null;
2021-03-04 21:03:06 +00:00
2021-03-06 10:14:23 +00:00
Helper.gc();
2020-03-22 09:14:36 +00:00
}
2019-08-09 17:29:17 +00:00
}
2020-04-09 15:50:29 +00:00
2022-07-19 13:25:12 +00:00
static class SearchCriteria extends EntitySearch implements Serializable {
2020-04-09 15:50:29 +00:00
String query;
2021-02-24 18:18:50 +00:00
boolean fts = false;
2020-04-09 15:50:29 +00:00
boolean in_senders = true;
2020-05-04 08:23:10 +00:00
boolean in_recipients = true;
2020-04-09 15:50:29 +00:00
boolean in_subject = true;
2020-04-10 14:54:20 +00:00
boolean in_keywords = true;
2020-04-09 15:50:29 +00:00
boolean in_message = true;
2021-02-28 15:12:48 +00:00
boolean in_notes = true;
boolean in_filenames = false;
boolean in_headers = false;
boolean in_html = false;
2020-04-09 15:50:29 +00:00
boolean with_unseen;
boolean with_flagged;
boolean with_hidden;
boolean with_encrypted;
boolean with_attachments;
boolean with_notes;
2020-06-19 13:18:43 +00:00
String[] with_types;
2020-06-12 16:49:46 +00:00
Integer with_size = null;
boolean in_trash = true;
boolean in_junk = true;
2020-04-10 10:26:23 +00:00
Long after = null;
Long before = null;
2020-04-09 15:50:29 +00:00
2022-10-08 15:28:14 +00:00
private static final String FROM = "from:";
2022-10-09 06:03:02 +00:00
private static final String TO = "to:";
2022-10-09 20:27:24 +00:00
private static final String CC = "cc:";
private static final String BCC = "bcc:";
2022-10-09 06:03:02 +00:00
private static final String KEYWORD = "keyword:";
2023-02-25 07:05:42 +00:00
private static final String JSOUP_PREFIX = "jsoup:";
String getJsoup() {
if (query == null)
return null;
if (!query.startsWith(JSOUP_PREFIX))
return null;
return query.substring(JSOUP_PREFIX.length());
}
2022-10-08 15:28:14 +00:00
2022-10-08 16:08:22 +00:00
boolean onServer() {
if (query == null)
return false;
for (String w : query.trim().split("\\s+"))
if (w.length() > 1 && w.startsWith("?"))
2022-10-08 16:08:22 +00:00
return true;
else if (w.length() > FROM.length() && w.startsWith(FROM))
return true;
2022-10-09 06:03:02 +00:00
else if (w.length() > TO.length() && w.startsWith(TO))
return true;
2022-10-09 20:27:24 +00:00
else if (w.length() > CC.length() && w.startsWith(CC))
return true;
else if (w.length() > BCC.length() && w.startsWith(BCC))
return true;
2022-10-09 06:03:02 +00:00
else if (w.length() > KEYWORD.length() && w.startsWith(KEYWORD))
return true;
2022-10-08 16:08:22 +00:00
return false;
}
2020-05-03 20:22:55 +00:00
SearchTerm getTerms(boolean utf8, Flags flags, String[] keywords) {
List<SearchTerm> or = new ArrayList<>();
List<SearchTerm> and = new ArrayList<>();
if (query != null) {
String search = query;
if (!utf8) {
2023-11-06 08:53:02 +00:00
// Perhaps: Transliterator.getInstance("de-ASCII");
2020-10-15 14:40:27 +00:00
search = search
.replace("ß", "ss") // Eszett
2021-06-04 16:28:31 +00:00
.replace("ij", "ij")
.replace("ø", "o");
2020-10-13 15:51:32 +00:00
search = Normalizer
.normalize(search, Normalizer.Form.NFKD)
.replaceAll("[^\\p{ASCII}]", "");
2023-03-13 15:33:13 +00:00
if (TextUtils.isEmpty(search)) {
String msg = "Cannot convert to ASCII: " + query;
Log.e(msg);
throw new IllegalArgumentException(msg);
}
2020-05-03 20:22:55 +00:00
}
List<String> word = new ArrayList<>();
List<String> plus = new ArrayList<>();
List<String> minus = new ArrayList<>();
List<String> opt = new ArrayList<>();
2022-10-09 06:03:02 +00:00
List<String> andFrom = new ArrayList<>();
List<String> andTo = new ArrayList<>();
2022-10-09 20:27:24 +00:00
List<String> andCc = new ArrayList<>();
List<String> andBcc = new ArrayList<>();
2022-10-09 06:03:02 +00:00
List<String> andKeyword = new ArrayList<>();
2020-09-09 06:30:19 +00:00
StringBuilder all = new StringBuilder();
for (String w : search.trim().split("\\s+")) {
2020-09-09 06:30:19 +00:00
if (all.length() > 0)
all.append(' ');
if (w.length() > 1 && w.startsWith("+")) {
plus.add(w.substring(1));
2020-09-09 06:30:19 +00:00
all.append(w.substring(1));
} else if (w.length() > 1 && w.startsWith("-")) {
minus.add(w.substring(1));
2020-09-09 06:30:19 +00:00
all.append(w.substring(1));
} else if (w.length() > 1 && w.startsWith("?")) {
opt.add(w.substring(1));
2020-09-09 06:30:19 +00:00
all.append(w.substring(1));
2022-10-09 06:03:02 +00:00
} else if (w.length() > FROM.length() && w.startsWith(FROM))
andFrom.add(w.substring(FROM.length()));
else if (w.length() > TO.length() && w.startsWith(TO))
andTo.add(w.substring(TO.length()));
2022-10-09 20:27:24 +00:00
else if (w.length() > CC.length() && w.startsWith(CC))
andCc.add(w.substring(CC.length()));
else if (w.length() > BCC.length() && w.startsWith(BCC))
andBcc.add(w.substring(BCC.length()));
2022-10-09 06:03:02 +00:00
else if (w.length() > KEYWORD.length() && w.startsWith(KEYWORD))
andKeyword.add(w.substring(KEYWORD.length()));
else {
word.add(w);
2020-09-09 06:30:19 +00:00
all.append(w);
}
}
2022-10-09 06:03:02 +00:00
if (plus.size() + minus.size() + opt.size() +
2022-10-11 14:02:10 +00:00
andFrom.size() + andTo.size() + andCc.size() + andBcc.size() + andKeyword.size() > 0)
2020-09-09 06:30:19 +00:00
search = all.toString();
2020-05-03 20:22:55 +00:00
// Yahoo! does not support keyword search, but uses the flags $Forwarded $Junk $NotJunk
boolean hasKeywords = false;
for (String keyword : keywords)
if (!keyword.startsWith("$")) {
hasKeywords = true;
break;
}
2022-10-09 06:03:02 +00:00
if (andFrom.size() > 0) {
for (String term : andFrom)
2022-10-08 15:28:14 +00:00
and.add(new FromStringTerm(term));
} else {
2022-10-11 14:02:10 +00:00
if (in_senders && !TextUtils.isEmpty(search) &&
plus.size() + minus.size() + opt.size() == 0)
2022-10-08 15:28:14 +00:00
or.add(new FromStringTerm(search));
}
2022-10-09 20:27:24 +00:00
if (andTo.size() + andCc.size() + andBcc.size() > 0) {
2022-10-09 06:03:02 +00:00
for (String term : andTo)
and.add(new RecipientStringTerm(Message.RecipientType.TO, term));
2022-10-09 20:27:24 +00:00
for (String term : andCc)
and.add(new RecipientStringTerm(Message.RecipientType.CC, term));
for (String term : andBcc)
and.add(new RecipientStringTerm(Message.RecipientType.BCC, term));
2022-10-09 06:03:02 +00:00
} else {
2022-10-11 14:02:10 +00:00
if (in_recipients && !TextUtils.isEmpty(search) &&
plus.size() + minus.size() + opt.size() == 0) {
2022-10-09 06:03:02 +00:00
or.add(new RecipientStringTerm(Message.RecipientType.TO, search));
or.add(new RecipientStringTerm(Message.RecipientType.CC, search));
or.add(new RecipientStringTerm(Message.RecipientType.BCC, search));
}
2020-05-03 20:22:55 +00:00
}
2022-10-09 06:03:02 +00:00
if (in_subject && !TextUtils.isEmpty(search))
if (plus.size() + minus.size() + opt.size() == 0)
or.add(new SubjectTerm(search));
else
try {
or.add(construct(word, plus, minus, opt, SubjectTerm.class));
} catch (Throwable ex) {
Log.e(ex);
or.add(new SubjectTerm(search));
}
2022-10-09 06:03:02 +00:00
if (hasKeywords)
if (andKeyword.size() > 0) {
for (String term : andKeyword)
and.add(new FlagTerm(new Flags(term), true));
} else {
2022-10-11 14:02:10 +00:00
if (in_keywords && !TextUtils.isEmpty(search) &&
plus.size() + minus.size() + opt.size() == 0) {
2022-10-09 06:03:02 +00:00
String keyword = MessageHelper.sanitizeKeyword(search);
if (TextUtils.isEmpty(keyword))
Log.w("Keyword empty=" + search);
else
or.add(new FlagTerm(new Flags(keyword), true));
}
}
2022-10-09 06:03:02 +00:00
if (in_message && !TextUtils.isEmpty(search))
if (plus.size() + minus.size() + opt.size() == 0)
or.add(new BodyTerm(search));
else
try {
or.add(construct(word, plus, minus, opt, BodyTerm.class));
} catch (Throwable ex) {
Log.e(ex);
or.add(new BodyTerm(search));
}
2020-05-03 20:22:55 +00:00
}
if (with_unseen && flags.contains(Flags.Flag.SEEN))
and.add(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
if (with_flagged && flags.contains(Flags.Flag.FLAGGED))
and.add(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
2020-06-12 16:49:46 +00:00
if (with_size != null)
and.add(new SizeTerm(ComparisonTerm.GT, with_size));
2020-05-03 20:22:55 +00:00
if (after != null)
2020-05-27 06:17:55 +00:00
and.add(new ReceivedDateTerm(ComparisonTerm.GE, new Date(after)));
2020-05-03 20:22:55 +00:00
if (before != null)
2020-05-27 06:17:55 +00:00
and.add(new ReceivedDateTerm(ComparisonTerm.LE, new Date(before)));
2020-05-03 20:22:55 +00:00
SearchTerm term = null;
if (or.size() > 0)
term = new OrTerm(or.toArray(new SearchTerm[0]));
if (and.size() > 0)
if (term == null)
term = new AndTerm(and.toArray(new SearchTerm[0]));
else
term = new AndTerm(term, new AndTerm(and.toArray(new SearchTerm[0])));
return term;
}
private SearchTerm construct(
List<String> word,
List<String> plus,
List<String> minus,
List<String> opt,
Class<?> clazz) throws ReflectiveOperationException {
SearchTerm term = null;
Constructor<?> ctor = clazz.getConstructor(String.class);
if (word.size() > 0)
term = (SearchTerm) ctor.newInstance(TextUtils.join(" ", word));
for (String p : plus)
if (term == null)
term = (SearchTerm) ctor.newInstance(p);
else
term = new AndTerm(term, (SearchTerm) ctor.newInstance(p));
for (String m : minus)
if (term == null)
term = new NotTerm((SearchTerm) ctor.newInstance(m));
else
term = new AndTerm(term, new NotTerm((SearchTerm) ctor.newInstance(m)));
for (String o : opt)
if (term == null)
term = (SearchTerm) ctor.newInstance(o);
else
term = new OrTerm(term, (SearchTerm) ctor.newInstance(o));
return term;
}
2020-04-09 17:33:57 +00:00
String getTitle(Context context) {
List<String> flags = new ArrayList<>();
if (with_unseen)
flags.add(context.getString(R.string.title_search_flag_unseen));
if (with_flagged)
flags.add(context.getString(R.string.title_search_flag_flagged));
if (with_hidden)
flags.add(context.getString(R.string.title_search_flag_hidden));
if (with_encrypted)
flags.add(context.getString(R.string.title_search_flag_encrypted));
if (with_attachments)
flags.add(context.getString(R.string.title_search_flag_attachments));
if (with_notes)
flags.add(context.getString(R.string.title_search_flag_notes));
2020-06-19 13:18:43 +00:00
if (with_types != null)
if (with_types.length == 1 && "text/calendar".equals(with_types[0]))
flags.add(context.getString(R.string.title_search_flag_invite));
else
flags.add(TextUtils.join(", ", with_types));
2020-06-12 16:49:46 +00:00
if (with_size != null)
flags.add(context.getString(R.string.title_search_flag_size,
2020-07-02 08:19:01 +00:00
Helper.humanReadableByteCount(with_size)));
2021-09-25 11:02:56 +00:00
return (query == null ? "" : query + " ")
+ (flags.size() > 0 ? "+" : "")
2020-04-09 17:33:57 +00:00
+ TextUtils.join(",", flags);
2020-04-09 15:50:29 +00:00
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof SearchCriteria) {
SearchCriteria other = (SearchCriteria) obj;
return (Objects.equals(this.query, other.query) &&
2022-12-31 09:31:47 +00:00
this.fts == other.fts &&
2020-04-09 15:50:29 +00:00
this.in_senders == other.in_senders &&
2020-05-04 08:23:10 +00:00
this.in_recipients == other.in_recipients &&
2020-04-09 15:50:29 +00:00
this.in_subject == other.in_subject &&
this.in_keywords == other.in_keywords &&
this.in_message == other.in_message &&
2021-02-28 15:12:48 +00:00
this.in_notes == other.in_notes &&
this.in_filenames == other.in_filenames &&
this.in_headers == other.in_headers &&
this.in_html == other.in_html &&
2020-04-09 15:50:29 +00:00
this.with_unseen == other.with_unseen &&
this.with_flagged == other.with_flagged &&
this.with_hidden == other.with_hidden &&
this.with_encrypted == other.with_encrypted &&
2020-04-10 09:51:50 +00:00
this.with_attachments == other.with_attachments &&
this.with_notes == other.with_notes &&
2021-02-24 18:19:58 +00:00
Arrays.equals(this.with_types, other.with_types) &&
2020-06-12 16:49:46 +00:00
Objects.equals(this.with_size, other.with_size) &&
2021-06-17 15:15:52 +00:00
this.in_trash == other.in_trash &&
this.in_junk == other.in_junk &&
2020-04-10 10:26:23 +00:00
Objects.equals(this.after, other.after) &&
Objects.equals(this.before, other.before));
2020-04-09 15:50:29 +00:00
} else
return false;
}
JSONObject toJsonData() throws JSONException {
2021-09-24 11:36:06 +00:00
JSONObject json = new JSONObject();
json.put("query", query);
2022-11-04 06:46:18 +00:00
json.put("fts", fts);
2021-09-24 11:36:06 +00:00
json.put("in_senders", in_senders);
json.put("in_recipients", in_recipients);
json.put("in_subject", in_subject);
json.put("in_keywords", in_keywords);
json.put("in_message", in_message);
json.put("in_notes", in_notes);
json.put("in_filenames", in_filenames);
2021-09-24 11:36:06 +00:00
json.put("in_headers", in_headers);
json.put("in_html", in_html);
json.put("with_unseen", with_unseen);
json.put("with_flagged", with_flagged);
json.put("with_hidden", with_hidden);
json.put("with_encrypted", with_encrypted);
json.put("with_attachments", with_attachments);
json.put("with_notes", with_notes);
if (with_types != null) {
JSONArray jtypes = new JSONArray();
for (String type : with_types)
jtypes.put(type);
json.put("with_types", jtypes);
}
if (with_size != null)
json.put("with_size", with_size);
json.put("in_trash", in_trash);
json.put("in_junk", in_junk);
2022-03-18 18:46:23 +00:00
Calendar now = Calendar.getInstance();
now.set(Calendar.MILLISECOND, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MINUTE, 0);
2022-12-23 13:10:14 +00:00
now.set(Calendar.HOUR_OF_DAY, 0);
2022-03-18 18:46:23 +00:00
2021-09-24 11:36:06 +00:00
if (after != null)
2022-03-18 18:46:23 +00:00
json.put("after", after - now.getTimeInMillis());
2021-09-24 11:36:06 +00:00
if (before != null)
2022-03-18 18:46:23 +00:00
json.put("before", before - now.getTimeInMillis());
2021-09-24 11:36:06 +00:00
return json;
}
public static SearchCriteria fromJsonData(JSONObject json) throws JSONException {
2021-09-24 11:36:06 +00:00
SearchCriteria criteria = new SearchCriteria();
criteria.query = json.optString("query");
2022-11-04 06:46:18 +00:00
criteria.fts = json.optBoolean("fts");
2021-09-24 11:36:06 +00:00
criteria.in_senders = json.optBoolean("in_senders");
criteria.in_recipients = json.optBoolean("in_recipients");
criteria.in_subject = json.optBoolean("in_subject");
criteria.in_keywords = json.optBoolean("in_keywords");
criteria.in_message = json.optBoolean("in_message");
criteria.in_notes = json.optBoolean("in_notes");
criteria.in_filenames = json.optBoolean("in_filenames");
2021-09-24 11:36:06 +00:00
criteria.in_headers = json.optBoolean("in_headers");
criteria.in_html = json.optBoolean("in_html");
criteria.with_unseen = json.optBoolean("with_unseen");
criteria.with_flagged = json.optBoolean("with_flagged");
criteria.with_hidden = json.optBoolean("with_hidden");
criteria.with_encrypted = json.optBoolean("with_encrypted");
criteria.with_attachments = json.optBoolean("with_attachments");
criteria.with_notes = json.optBoolean("with_notes");
if (json.has("with_types")) {
JSONArray jtypes = json.getJSONArray("with_types");
criteria.with_types = new String[jtypes.length()];
for (int i = 0; i < jtypes.length(); i++)
criteria.with_types[i] = jtypes.getString(i);
}
if (json.has("with_size"))
criteria.with_size = json.getInt("with_size");
criteria.in_trash = json.optBoolean("in_trash");
criteria.in_junk = json.optBoolean("in_junk");
2022-03-18 18:46:23 +00:00
Calendar now = Calendar.getInstance();
now.set(Calendar.MILLISECOND, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MINUTE, 0);
2022-12-23 13:10:14 +00:00
now.set(Calendar.HOUR_OF_DAY, 0);
2022-03-18 18:46:23 +00:00
2021-09-24 11:36:06 +00:00
if (json.has("after"))
2022-03-18 18:46:23 +00:00
criteria.after = json.getLong("after") + now.getTimeInMillis();
2021-09-24 11:36:06 +00:00
if (json.has("before"))
2022-03-18 19:55:16 +00:00
criteria.before = json.getLong("before") + now.getTimeInMillis();
2021-09-24 11:36:06 +00:00
return criteria;
}
2020-04-09 15:50:29 +00:00
@NonNull
@Override
public String toString() {
return query +
2021-02-24 18:18:50 +00:00
" fts=" + fts +
2020-04-09 15:50:29 +00:00
" senders=" + in_senders +
2020-05-04 08:23:10 +00:00
" recipients=" + in_recipients +
2020-04-09 15:50:29 +00:00
" subject=" + in_subject +
" keywords=" + in_keywords +
" message=" + in_message +
2021-02-28 15:12:48 +00:00
" notes=" + in_notes +
" filenames=" + in_filenames +
" headers=" + in_headers +
" html=" + in_html +
2020-04-09 15:50:29 +00:00
" unseen=" + with_unseen +
" flagged=" + with_flagged +
" hidden=" + with_hidden +
" encrypted=" + with_encrypted +
" w/attachments=" + with_attachments +
" w/notes=" + with_notes +
2020-06-19 13:18:43 +00:00
" type=" + (with_types == null ? null : TextUtils.join(",", with_types)) +
2020-06-12 16:49:46 +00:00
" size=" + with_size +
2021-06-17 15:04:37 +00:00
" trash=" + in_trash +
" junk=" + in_junk +
2020-04-10 16:08:19 +00:00
" after=" + (after == null ? "" : new Date(after)) +
" before=" + (before == null ? "" : new Date(before));
2020-04-09 15:50:29 +00:00
}
}
2018-09-02 06:59:49 +00:00
}