mirror of
https://github.com/M66B/FairEmail.git
synced 2026-04-03 15:46:34 +02:00
Draft new search
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user