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"
}
if (criteria.in_headers) {
if (message.headers != null && message.headers.contains(criteria.query))
return true;
}
if (criteria.in_html || criteria.in_message)
try {
File file = EntityMessage.getFile(context, message.id);
if (file.exists()) {
String selector = criteria.getJsoup();
if (selector != null) {
Document d = JsoupEx.parse(file);
return (d.select(selector).size() > 0);
}
String html = Helper.readText(file);
if (criteria.in_html) {
if (html.contains(criteria.query))
return true;
}
if (criteria.in_message) {
// This won't match An example
when searching for "An example"
if (contains(html, criteria.query, partial, true)) {
String text = HtmlHelper.getFullText(html);
if (contains(text, criteria.query, partial, false))
return true;
}
}
}
} catch (IOException ex) {
Log.e(ex);
}
return false;
}
private static boolean contains(Address[] addresses, String query, boolean partial) {
if (addresses == null)
return false;
for (Address address : addresses)
if (contains(address.toString(), query, partial, false))
return true;
return false;
}
private static boolean contains(String text, String query, boolean partial, boolean html) {
if (TextUtils.isEmpty(text))
return false;
text = Fts4DbHelper.processBreakText(text);
boolean or = false;
List 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 if (w.length() > 1 && w.startsWith("?")) {
or = true;
if (text.contains(Fts4DbHelper.preprocessText(w.substring(1))))
return true;
} else
word.addAll(Arrays.asList(Fts4DbHelper.processBreakText(w).split("\\s+")));
if (word.size() == 0)
return !or;
// \b is limited to [0-9A-Za-z_]
String b = "(^|\\s+)";
String a = "($|\\s+)";
StringBuilder sb = new StringBuilder();
sb.append(partial ? ".*(" : ".*?" + b + "(");
for (int i = 0; i < word.size(); i++) {
if (i > 0)
sb.append("\\s+");
sb.append(Pattern.quote(word.get(i)));
}
sb.append(partial ? ").*" : ")" + a + ".*?");
Pattern pat = Pattern.compile(sb.toString(), Pattern.DOTALL);
return pat.matcher(text).matches();
}
State getState() {
return this.state;
}
void destroy(State state) {
state.destroyed = true;
this.intf = null;
Log.i("Boundary destroy");
executor.submit(new Runnable() {
@Override
public void run() {
close(state, true);
}
});
}
private void close(State state, boolean reset) {
Log.i("Boundary close");
try {
if (state.ifolder != null && state.ifolder.isOpen())
state.ifolder.close(false);
} catch (Throwable ex) {
Log.e("Boundary", ex);
}
try {
if (state.iservice != null && state.iservice.isOpen())
state.iservice.close();
} catch (Throwable ex) {
Log.e("Boundary", ex);
}
if (reset)
state.reset();
}
static class State {
final AtomicInteger queued = new AtomicInteger(0);
boolean destroyed = false;
boolean error = false;
int index = 0;
int offset = 0;
List ids = null;
List matches = null;
EmailService iservice = null;
IMAPFolder ifolder = null;
Message[] imessages = null;
void getMessages(int max) throws MessagingException {
int total = Math.min(ifolder.getMessageCount(), max);
imessages = new Message[total];
for (int i = 1; i <= total; i++)
imessages[i - 1] = ifolder.getMessage(i);
}
void reset() {
Log.i("Boundary reset");
queued.set(0);
destroyed = false;
error = false;
index = 0;
offset = 0;
ids = null;
matches = null;
iservice = null;
ifolder = null;
imessages = null;
Helper.gc("Boundary reset");
}
}
static class SearchCriteria extends EntitySearch implements Serializable {
String query;
boolean fts = false;
boolean in_senders = true;
boolean in_recipients = true;
boolean in_subject = true;
boolean in_keywords = true;
boolean in_message = true;
boolean in_notes = true;
boolean in_filenames = false;
boolean in_headers = false;
boolean in_html = false;
boolean with_unseen;
boolean with_flagged;
boolean with_hidden;
boolean with_encrypted;
boolean with_attachments;
boolean with_notes;
String[] with_types;
Integer with_size = null;
boolean in_trash = true;
boolean in_junk = true;
Long after = null;
Long before = null;
private static final String FROM = "from:";
private static final String TO = "to:";
private static final String CC = "cc:";
private static final String BCC = "bcc:";
private static final String KEYWORD = "keyword:";
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());
}
boolean onServer() {
if (query == null)
return false;
for (String w : query.trim().split("\\s+"))
if (w.length() > FROM.length() && w.startsWith(FROM))
return true;
else if (w.length() > TO.length() && w.startsWith(TO))
return true;
else if (w.length() > CC.length() && w.startsWith(CC))
return true;
else if (w.length() > BCC.length() && w.startsWith(BCC))
return true;
else if (w.length() > KEYWORD.length() && w.startsWith(KEYWORD))
return true;
return false;
}
SearchTerm getTerms(boolean utf8, Flags flags, String[] keywords) {
List or = new ArrayList<>();
List and = new ArrayList<>();
if (query != null) {
String search = query;
if (!utf8) {
// Perhaps: Transliterator.getInstance("de-ASCII");
search = search
.replace("ß", "ss") // Eszett
.replace("ij", "ij")
.replace("ø", "o");
search = Normalizer
.normalize(search, Normalizer.Form.NFKD)
.replaceAll("[^\\p{ASCII}]", "");
if (TextUtils.isEmpty(search)) {
String msg = "Cannot convert to ASCII: " + query;
Log.e(msg);
throw new IllegalArgumentException(msg);
}
}
List word = new ArrayList<>();
List plus = new ArrayList<>();
List minus = new ArrayList<>();
List opt = new ArrayList<>();
List andFrom = new ArrayList<>();
List andTo = new ArrayList<>();
List andCc = new ArrayList<>();
List andBcc = new ArrayList<>();
List andKeyword = new ArrayList<>();
StringBuilder all = new StringBuilder();
for (String w : search.trim().split("\\s+")) {
if (all.length() > 0)
all.append(' ');
if (w.length() > 1 && w.startsWith("+")) {
plus.add(w.substring(1));
all.append(w.substring(1));
} else if (w.length() > 1 && w.startsWith("-")) {
minus.add(w.substring(1));
all.append(w.substring(1));
} else if (w.length() > 1 && w.startsWith("?")) {
opt.add(w.substring(1));
all.append(w.substring(1));
} 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()));
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()));
else if (w.length() > KEYWORD.length() && w.startsWith(KEYWORD))
andKeyword.add(w.substring(KEYWORD.length()));
else {
word.add(w);
all.append(w);
}
}
if (plus.size() + minus.size() + opt.size() +
andFrom.size() + andTo.size() + andCc.size() + andBcc.size() + andKeyword.size() > 0)
search = all.toString();
// 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;
}
if (andFrom.size() > 0) {
for (String term : andFrom)
and.add(new FromStringTerm(term));
} else {
if (in_senders && !TextUtils.isEmpty(search) &&
plus.size() + minus.size() + opt.size() == 0)
or.add(new FromStringTerm(search));
}
if (andTo.size() + andCc.size() + andBcc.size() > 0) {
for (String term : andTo)
and.add(new RecipientStringTerm(Message.RecipientType.TO, term));
for (String term : andCc)
and.add(new RecipientStringTerm(Message.RecipientType.CC, term));
for (String term : andBcc)
and.add(new RecipientStringTerm(Message.RecipientType.BCC, term));
} else {
if (in_recipients && !TextUtils.isEmpty(search) &&
plus.size() + minus.size() + opt.size() == 0) {
or.add(new RecipientStringTerm(Message.RecipientType.TO, search));
or.add(new RecipientStringTerm(Message.RecipientType.CC, search));
or.add(new RecipientStringTerm(Message.RecipientType.BCC, search));
}
}
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));
}
if (hasKeywords)
if (andKeyword.size() > 0) {
for (String term : andKeyword)
and.add(new FlagTerm(new Flags(term), true));
} else {
if (in_keywords && !TextUtils.isEmpty(search) &&
plus.size() + minus.size() + opt.size() == 0) {
String keyword = MessageHelper.sanitizeKeyword(search);
if (TextUtils.isEmpty(keyword))
Log.w("Keyword empty=" + search);
else
or.add(new FlagTerm(new Flags(keyword), true));
}
}
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));
}
}
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));
if (with_size != null)
and.add(new SizeTerm(ComparisonTerm.GT, with_size));
if (after != null)
and.add(new ReceivedDateTerm(ComparisonTerm.GE, new Date(after)));
if (before != null)
and.add(new ReceivedDateTerm(ComparisonTerm.LE, new Date(before)));
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 word,
List plus,
List minus,
List 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;
}
String getTitle(Context context) {
List 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));
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));
if (with_size != null)
flags.add(context.getString(R.string.title_search_flag_size,
Helper.humanReadableByteCount(with_size)));
return (query == null ? "" : query + " ")
+ (flags.size() > 0 ? "+" : "")
+ TextUtils.join(",", flags);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof SearchCriteria) {
SearchCriteria other = (SearchCriteria) obj;
return (Objects.equals(this.query, other.query) &&
this.fts == other.fts &&
this.in_senders == other.in_senders &&
this.in_recipients == other.in_recipients &&
this.in_subject == other.in_subject &&
this.in_keywords == other.in_keywords &&
this.in_message == other.in_message &&
this.in_notes == other.in_notes &&
this.in_filenames == other.in_filenames &&
this.in_headers == other.in_headers &&
this.in_html == other.in_html &&
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 &&
this.with_notes == other.with_notes &&
Arrays.equals(this.with_types, other.with_types) &&
Objects.equals(this.with_size, other.with_size) &&
this.in_trash == other.in_trash &&
this.in_junk == other.in_junk &&
Objects.equals(this.after, other.after) &&
Objects.equals(this.before, other.before));
} else
return false;
}
JSONObject toJsonData() throws JSONException {
JSONObject json = new JSONObject();
json.put("query", query);
json.put("fts", fts);
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);
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);
Calendar now = Calendar.getInstance();
now.set(Calendar.MILLISECOND, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MINUTE, 0);
now.set(Calendar.HOUR_OF_DAY, 0);
if (after != null)
json.put("after", after - now.getTimeInMillis());
if (before != null)
json.put("before", before - now.getTimeInMillis());
return json;
}
public static SearchCriteria fromJsonData(JSONObject json) throws JSONException {
SearchCriteria criteria = new SearchCriteria();
criteria.query = json.optString("query");
criteria.fts = json.optBoolean("fts");
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");
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");
Calendar now = Calendar.getInstance();
now.set(Calendar.MILLISECOND, 0);
now.set(Calendar.SECOND, 0);
now.set(Calendar.MINUTE, 0);
now.set(Calendar.HOUR_OF_DAY, 0);
if (json.has("after"))
criteria.after = json.getLong("after") + now.getTimeInMillis();
if (json.has("before"))
criteria.before = json.getLong("before") + now.getTimeInMillis();
return criteria;
}
@NonNull
@Override
public String toString() {
return query +
" fts=" + fts +
" senders=" + in_senders +
" recipients=" + in_recipients +
" subject=" + in_subject +
" keywords=" + in_keywords +
" message=" + in_message +
" notes=" + in_notes +
" filenames=" + in_filenames +
" headers=" + in_headers +
" html=" + in_html +
" unseen=" + with_unseen +
" flagged=" + with_flagged +
" hidden=" + with_hidden +
" encrypted=" + with_encrypted +
" w/attachments=" + with_attachments +
" w/notes=" + with_notes +
" type=" + (with_types == null ? null : TextUtils.join(",", with_types)) +
" size=" + with_size +
" trash=" + in_trash +
" junk=" + in_junk +
" after=" + (after == null ? "" : new Date(after)) +
" before=" + (before == null ? "" : new Date(before));
}
}
}