Draft new search

This commit is contained in:
M66B
2020-04-09 17:50:29 +02:00
parent b0ae99b216
commit adb6d9658e
13 changed files with 637 additions and 476 deletions

View File

@@ -25,10 +25,12 @@ import android.os.Handler;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagedList;
import androidx.preference.PreferenceManager;
import com.sun.mail.iap.Argument;
import com.sun.mail.iap.ProtocolException;
import com.sun.mail.iap.Response;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPMessage;
@@ -38,11 +40,12 @@ import com.sun.mail.imap.protocol.IMAPResponse;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.io.Serializable;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import javax.mail.FetchProfile;
@@ -61,6 +64,7 @@ import javax.mail.search.FlagTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.OrTerm;
import javax.mail.search.RecipientStringTerm;
import javax.mail.search.SearchException;
import javax.mail.search.SearchTerm;
import javax.mail.search.SubjectTerm;
@@ -71,7 +75,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private Long account;
private Long folder;
private boolean server;
private String query;
private SearchCriteria criteria;
private int pageSize;
private IBoundaryCallbackMessages intf;
@@ -91,12 +95,12 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
void onException(@NonNull Throwable ex);
}
BoundaryCallbackMessages(Context context, long account, long folder, boolean server, String query, int pageSize) {
BoundaryCallbackMessages(Context context, long account, long folder, boolean server, SearchCriteria criteria, int pageSize) {
this.context = context.getApplicationContext();
this.account = (account < 0 ? null : account);
this.folder = (folder < 0 ? null : folder);
this.server = server;
this.query = query;
this.criteria = criteria;
this.pageSize = pageSize;
}
@@ -182,35 +186,15 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
private int load_device(State state) {
DB db = DB.getInstance(context);
Boolean seen = null;
Boolean flagged = null;
Boolean snoozed = null;
Boolean encrypted = null;
Boolean attachments = null;
String find = (TextUtils.isEmpty(query) ? null : query.toLowerCase());
if (find != null && find.startsWith(context.getString(R.string.title_search_special_prefix) + ":")) {
String special = find.split(":")[1];
if (context.getString(R.string.title_search_special_unseen).equals(special))
seen = false;
else if (context.getString(R.string.title_search_special_flagged).equals(special))
flagged = true;
else if (context.getString(R.string.title_search_special_snoozed).equals(special))
snoozed = true;
else if (context.getString(R.string.title_search_special_encrypted).equals(special))
encrypted = true;
else if (context.getString(R.string.title_search_special_attachments).equals(special))
attachments = true;
}
int found = 0;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean fts = prefs.getBoolean("fts", false);
boolean pro = ActivityBilling.isPro(context);
if (fts && pro && seen == null && flagged == null && snoozed == null && encrypted == null && attachments == null) {
if (fts && pro && criteria.isQueryOnly()) {
if (state.ids == null) {
SQLiteDatabase sdb = FtsDbHelper.getInstance(context);
state.ids = FtsDbHelper.match(sdb, account, folder, query);
state.ids = FtsDbHelper.match(sdb, account, folder, criteria.query);
}
try {
@@ -237,16 +221,15 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
(state.matches.size() > 0 && state.index >= state.matches.size())) {
state.matches = db.message().matchMessages(
account, folder,
"%" + find + "%",
seen, flagged, snoozed, encrypted, attachments,
criteria.query == null ? null : "%" + criteria.query + "%",
criteria.with_unseen,
criteria.with_flagged,
criteria.with_hidden,
criteria.with_encrypted,
criteria.with_attachments,
SEARCH_LIMIT, state.offset);
Log.i("Boundary device folder=" + folder +
" query=" + query +
" seen=" + seen +
" flagged=" + flagged +
" snoozed=" + snoozed +
" encrypted=" + encrypted +
" attachments=" + attachments +
" criteria=" + criteria +
" offset=" + state.offset +
" size=" + state.matches.size());
state.offset += Math.min(state.matches.size(), SEARCH_LIMIT);
@@ -260,25 +243,20 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
state.index = i + 1;
TupleMatch match = state.matches.get(i);
if (find == null || seen != null || flagged != null || snoozed != null || encrypted != null || attachments != null)
match.matched = true;
else {
if (match.matched == null || !match.matched)
try {
File file = EntityMessage.getFile(context, match.id);
if (file.exists()) {
String html = Helper.readText(file);
if (html.toLowerCase().contains(find)) {
String text = HtmlHelper.getFullText(html);
if (text.toLowerCase().contains(find))
match.matched = true;
}
if (criteria.query != null && (match.matched == null || !match.matched))
try {
File file = EntityMessage.getFile(context, match.id);
if (file.exists()) {
String html = Helper.readText(file);
if (html.toLowerCase().contains(criteria.query)) {
String text = HtmlHelper.getFullText(html);
if (text.toLowerCase().contains(criteria.query))
match.matched = true;
}
} catch (IOException ex) {
Log.e(ex);
}
}
} catch (IOException ex) {
Log.e(ex);
}
if (match.matched != null && match.matched) {
found++;
@@ -303,10 +281,9 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
DB db = DB.getInstance(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final boolean search_text = prefs.getBoolean("search_text", false);
final boolean debug = (prefs.getBoolean("debug", false) || BuildConfig.BETA_RELEASE);
final EntityFolder browsable = db.folder().getBrowsableFolder(folder, query != null);
final EntityFolder browsable = db.folder().getBrowsableFolder(folder, criteria != null);
if (browsable == null || !browsable.selectable) {
Log.w("Boundary not browsable=" + (folder != null));
return 0;
@@ -344,8 +321,8 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
int count = state.ifolder.getMessageCount();
db.folder().setFolderTotal(browsable.id, count < 0 ? null : count);
Log.i("Boundary server query=" + query);
if (query == null) {
Log.i("Boundary server query=" + criteria.query);
if (criteria == null) {
boolean filter_seen = prefs.getBoolean("filter_seen", false);
boolean filter_unflagged = prefs.getBoolean("filter_unflagged", false);
Log.i("Boundary filter seen=" + filter_seen + " unflagged=" + filter_unflagged);
@@ -366,67 +343,28 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
state.imessages = state.ifolder.search(searchFlagged);
else
state.imessages = state.ifolder.getMessages();
} else if (query.startsWith(context.getString(R.string.title_search_special_prefix) + ":")) {
String special = query.split(":")[1];
if (context.getString(R.string.title_search_special_unseen).equals(special))
state.imessages = state.ifolder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
else if (context.getString(R.string.title_search_special_flagged).equals(special))
state.imessages = state.ifolder.search(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
else
state.imessages = new Message[0];
} else {
Object result = state.ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol protocol) {
// Yahoo! does not support keyword search, but uses the flags $Forwarded $Junk $NotJunk
boolean keywords = false;
for (String keyword : browsable.keywords)
if (!keyword.startsWith("$")) {
keywords = true;
break;
}
public Object doCommand(IMAPProtocol protocol) throws ProtocolException {
try {
// https://tools.ietf.org/html/rfc3501#section-6.4.4
Argument arg = new Argument();
if (query.startsWith("raw:") && state.iservice.hasCapability("X-GM-EXT-1")) {
if (criteria.query != null &&
criteria.query.startsWith("raw:") &&
state.iservice.hasCapability("X-GM-EXT-1")) {
// https://support.google.com/mail/answer/7190
// https://developers.google.com/gmail/imap/imap-extensions#extension_of_the_search_command_x-gm-raw
arg.writeAtom("X-GM-RAW");
arg.writeString(query.substring(4));
} else {
if (!protocol.supportsUtf8()) {
arg.writeAtom("CHARSET");
arg.writeAtom(StandardCharsets.UTF_8.name());
}
arg.writeAtom("OR");
arg.writeAtom("OR");
arg.writeAtom("OR");
if (search_text)
arg.writeAtom("OR");
if (keywords)
arg.writeAtom("OR");
arg.writeAtom("FROM");
arg.writeBytes(query.getBytes());
arg.writeAtom("TO");
arg.writeBytes(query.getBytes());
arg.writeAtom("CC");
arg.writeBytes(query.getBytes());
arg.writeAtom("SUBJECT");
arg.writeBytes(query.getBytes());
if (search_text) {
arg.writeAtom("BODY");
arg.writeBytes(query.getBytes());
}
if (keywords) {
arg.writeAtom("KEYWORD");
arg.writeBytes(query.getBytes());
}
}
arg.writeString(criteria.query.substring(4));
Response[] responses = protocol.command("SEARCH", arg);
if (responses.length > 0 && responses[responses.length - 1].isOK()) {
Log.i("Boundary UTF8 search=" + query);
Response[] responses = protocol.command("SEARCH", arg);
if (responses.length == 0)
throw new ProtocolException("No response");
if (!responses[responses.length - 1].isOK())
throw new ProtocolException(responses[responses.length - 1]);
Log.i("Boundary raw search=" + criteria.query);
List<Integer> msgnums = new ArrayList<>();
for (Response response : responses)
@@ -442,37 +380,68 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
return imessages;
} else {
if (responses.length > 0)
Log.e("Search response=" + responses[responses.length - 1]);
Log.i("Boundary search=" + criteria);
// Assume no UTF-8 support
String search = query.replace("ß", "ss"); // Eszett
search = Normalizer.normalize(search, Normalizer.Form.NFD)
.replaceAll("[^\\p{ASCII}]", "");
List<SearchTerm> or = new ArrayList<>();
List<SearchTerm> and = new ArrayList<>();
if (criteria.query != null) {
String search = criteria.query;
Log.i("Boundary ASCII search=" + search);
SearchTerm term = new FromStringTerm(search);
term = new OrTerm(term, new RecipientStringTerm(Message.RecipientType.TO, search));
term = new OrTerm(term, new RecipientStringTerm(Message.RecipientType.CC, search));
term = new OrTerm(term, new SubjectTerm(search));
if (search_text)
term = new OrTerm(term, new BodyTerm(search));
if (keywords)
term = new OrTerm(term, new FlagTerm(
new Flags(MessageHelper.sanitizeKeyword(search)), true));
if (!protocol.supportsUtf8()) {
search = search.replace("ß", "ss"); // Eszett
search = Normalizer.normalize(search, Normalizer.Form.NFD)
.replaceAll("[^\\p{ASCII}]", "");
}
// Yahoo! does not support keyword search, but uses the flags $Forwarded $Junk $NotJunk
boolean keywords = false;
for (String keyword : browsable.keywords)
if (!keyword.startsWith("$")) {
keywords = true;
break;
}
if (criteria.in_senders)
or.add(new FromStringTerm(search));
if (criteria.in_receipients) {
or.add(new RecipientStringTerm(Message.RecipientType.TO, search));
or.add(new RecipientStringTerm(Message.RecipientType.CC, search));
}
if (criteria.in_subject)
or.add(new SubjectTerm(search));
if (criteria.in_keywords && keywords)
or.add(new FlagTerm(new Flags(MessageHelper.sanitizeKeyword(search)), true));
if (criteria.in_message)
or.add(new BodyTerm(search));
}
if (criteria.with_unseen)
and.add(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
if (criteria.with_flagged)
and.add(new FlagTerm(new Flags(Flags.Flag.FLAGGED), true));
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])));
if (term == null)
throw new SearchException();
return state.ifolder.search(term);
}
} catch (MessagingException ex) {
Log.e(ex);
return ex;
throw new ProtocolException("Search", ex);
}
}
});
if (result instanceof MessagingException)
throw (MessagingException) result;
state.imessages = (Message[]) result;
}
Log.i("Boundary server found messages=" + state.imessages.length);
@@ -541,7 +510,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
rules, astate);
found++;
}
if (message != null && query != null /* browsed */)
if (message != null && criteria != null /* browsed */)
db.message().setMessageFound(message.id);
} catch (MessageRemovedException ex) {
Log.w(browsable.name + " boundary server", ex);
@@ -623,4 +592,87 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback<TupleMe
imessages = null;
}
}
static class SearchCriteria implements Serializable {
String query;
boolean in_senders = true;
boolean in_receipients = true;
boolean in_subject = true;
boolean in_keywords = true;
boolean in_message = true;
boolean with_unseen;
boolean with_flagged;
boolean with_hidden;
boolean with_encrypted;
boolean with_attachments;
SearchCriteria() {
}
SearchCriteria(String query) {
this.query = query;
}
boolean isQueryOnly() {
return (!TextUtils.isEmpty(query) &&
in_senders &&
in_receipients &&
in_subject &&
in_keywords &&
in_message &&
isWithout());
}
boolean isWithout() {
return !(with_unseen ||
with_flagged ||
with_hidden ||
with_encrypted ||
with_attachments);
}
String getTitle() {
return query +
(with_unseen ? " ?" : "") +
(with_flagged ? "" : "") +
(with_hidden ? "" : "") +
(with_encrypted ? "" : "") +
(with_attachments ? " &" : "");
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof SearchCriteria) {
SearchCriteria other = (SearchCriteria) obj;
return (Objects.equals(this.query, other.query) &&
this.in_senders == other.in_senders &&
this.in_receipients == other.in_receipients &&
this.in_subject == other.in_subject &&
this.in_keywords == other.in_keywords &&
this.in_message == other.in_message &&
this.with_unseen == other.with_unseen &&
this.with_flagged == other.with_flagged &&
this.with_hidden == other.with_hidden &&
this.with_encrypted == other.with_encrypted &&
this.with_attachments == other.with_attachments);
} else
return false;
}
@NonNull
@Override
public String toString() {
return query +
" senders=" + in_senders +
" receipients=" + in_receipients +
" subject=" + in_subject +
" keywords=" + in_keywords +
" message=" + in_message +
" unseen=" + with_unseen +
" flagged=" + with_flagged +
" hidden=" + with_hidden +
" encrypted=" + with_encrypted +
" attachments=" + with_attachments;
}
}
}