diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
index 148e645934..a21b07872f 100644
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -57,3 +57,4 @@ FairEmail uses parts or all of:
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
+* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).
diff --git a/app/build.gradle b/app/build.gradle
index 7ed5862b75..4c90f06d6b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -587,6 +587,7 @@ dependencies {
def ws_version = "2.14"
def tinylog_version = "2.6.2"
def zxing_version = "3.5.3"
+ def evalex_version = "3.2.0"
// https://mvnrepository.com/artifact/com.android.tools/desugar_jdk_libs?repo=google
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$desugar_version"
@@ -857,4 +858,7 @@ dependencies {
// https://github.com/zxing/zxing
// https://mvnrepository.com/artifact/com.google.zxing/core
implementation "com.google.zxing:core:$zxing_version"
+
+ // https://github.com/ezylang/EvalEx
+ implementation "com.ezylang:EvalEx:$evalex_version"
}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index b1a4226b20..d3e91784e9 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -161,3 +161,6 @@
-dontwarn java.lang.**
-dontwarn javax.naming.**
-dontwarn sun.reflect.Reflection
+
+#EvalEx
+-dontwarn lombok.Generated
\ No newline at end of file
diff --git a/app/src/main/assets/ATTRIBUTION.md b/app/src/main/assets/ATTRIBUTION.md
index 148e645934..a21b07872f 100644
--- a/app/src/main/assets/ATTRIBUTION.md
+++ b/app/src/main/assets/ATTRIBUTION.md
@@ -57,3 +57,4 @@ FairEmail uses parts or all of:
* [ZXing](https://github.com/zxing/zxing). Copyright (C) 2014 ZXing authors. [Apache License 2.0](https://github.com/zxing/zxing/blob/master/LICENSE).
* [commonmark-java](https://github.com/commonmark/commonmark-java). Copyright (c) 2015, Atlassian Pty Ltd. All rights reserved. [BSD-2-Clause license](https://github.com/commonmark/commonmark-java/blob/main/LICENSE.txt).
* [flexmark-java](https://github.com/vsch/flexmark-java). Copyright (c) 2016-2018, Vladimir Schneider. All rights reserved. [BSD-2-Clause license](https://github.com/vsch/flexmark-java/blob/master/LICENSE.txt).
+* [EvalEx](https://github.com/ezylang/EvalEx). Copyright 2012-2022 Udo Klimaschewski. [Apache License 2.0](https://github.com/ezylang/EvalEx/blob/main/LICENSE).
diff --git a/app/src/main/java/eu/faircode/email/EntityRule.java b/app/src/main/java/eu/faircode/email/EntityRule.java
index dbd4baea74..263e4b07eb 100644
--- a/app/src/main/java/eu/faircode/email/EntityRule.java
+++ b/app/src/main/java/eu/faircode/email/EntityRule.java
@@ -21,6 +21,8 @@ package eu.faircode.email;
import static androidx.room.ForeignKey.CASCADE;
+import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON;
+
import android.Manifest;
import android.content.ContentResolver;
import android.content.Context;
@@ -41,6 +43,15 @@ import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
+import com.ezylang.evalex.EvaluationException;
+import com.ezylang.evalex.Expression;
+import com.ezylang.evalex.config.ExpressionConfiguration;
+import com.ezylang.evalex.data.EvaluationValue;
+import com.ezylang.evalex.operators.AbstractOperator;
+import com.ezylang.evalex.operators.InfixOperator;
+import com.ezylang.evalex.parser.ParseException;
+import com.ezylang.evalex.parser.Token;
+
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.nodes.Document;
@@ -62,6 +73,7 @@ import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Matcher;
@@ -151,6 +163,10 @@ public class EntityRule {
private static final int MAX_NOTES_LENGTH = 512; // characters
private static final int URL_TIMEOUT = 15 * 1000; // milliseconds
+ private static final List EXPR_VARIABLES = Collections.unmodifiableList(Arrays.asList(
+ "sender", "subject"
+ ));
+
static boolean needsHeaders(EntityMessage message, List rules) {
return needsHeaders(rules);
}
@@ -183,6 +199,16 @@ public class EntityRule {
}
return true;
}
+ if (jcondition.has("expression")) {
+ Expression expression = getExpression(rule, null);
+ if (expression != null)
+ for (String variable : expression.getUsedVariables())
+ if ("body".equals(what) && "body".equalsIgnoreCase(variable))
+ return true;
+ else if ("header".equals(what) && "header".equalsIgnoreCase(variable))
+ return true;
+ return false;
+ }
} catch (Throwable ex) {
Log.e(ex);
}
@@ -463,6 +489,19 @@ public class EntityRule {
return false;
}
+ Expression expression = getExpression(this, message);
+ if (expression != null) {
+ for (String variable : expression.getUsedVariables())
+ if ("header".equalsIgnoreCase(variable) && message.headers == null)
+ throw new IllegalArgumentException(context.getString(R.string.title_rule_no_headers));
+
+ Log.i("EXPR evaluating " + jcondition.getString("expression"));
+ Boolean result = expression.evaluate().getBooleanValue();
+ Log.i("EXPR evaluated=" + result);
+ if (!Boolean.TRUE.equals(result))
+ return false;
+ }
+
// Safeguard
if (jsender == null &&
jrecipient == null &&
@@ -472,9 +511,10 @@ public class EntityRule {
jbody == null &&
jdate == null &&
jschedule == null &&
- !jcondition.has("younger"))
+ !jcondition.has("younger") &&
+ !jcondition.has("expression"))
return false;
- } catch (JSONException ex) {
+ } catch (JSONException | ParseException | EvaluationException ex) {
Log.e(ex);
return false;
}
@@ -593,6 +633,58 @@ public class EntityRule {
return matched;
}
+ @InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON)
+ public static class ContainsOperator extends AbstractOperator {
+ @Override
+ public EvaluationValue evaluate(
+ Expression expression, Token operatorToken, EvaluationValue... operands) {
+ String op1 = operands[0].getStringValue();
+ String op2 = operands[1].getStringValue();
+ Log.i("EXPR " + op1 + " CONTAINS " + op2);
+ return expression.convertValue(
+ !TextUtils.isEmpty(op1) && !TextUtils.isEmpty(op2) &&
+ op1.toLowerCase().contains(op2.toLowerCase()));
+ }
+ }
+
+ @InfixOperator(precedence = OPERATOR_PRECEDENCE_COMPARISON)
+ public static class MatchesOperator extends AbstractOperator {
+ @Override
+ public EvaluationValue evaluate(
+ Expression expression, Token operatorToken, EvaluationValue... operands) {
+ String op1 = operands[0].getStringValue();
+ String op2 = operands[1].getStringValue();
+ Log.i("EXPR " + op1 + " MATCHES " + op2);
+ return expression.convertValue(
+ !TextUtils.isEmpty(op1) && !TextUtils.isEmpty(op2) &&
+ Pattern.compile(op2, Pattern.DOTALL).matcher(op1).matches());
+ }
+ }
+
+ static Expression getExpression(EntityRule rule, EntityMessage message) throws JSONException {
+ // https://ezylang.github.io/EvalEx/
+
+ JSONObject jcondition = new JSONObject(rule.condition);
+ if (!jcondition.has("expression"))
+ return null;
+
+ String sender = null;
+ String subject = null;
+ if (message != null) {
+ if (message.from != null && message.from.length == 1)
+ sender = MessageHelper.formatAddresses(message.from);
+ subject = message.subject;
+ }
+
+ ExpressionConfiguration configuration = ExpressionConfiguration.defaultConfiguration();
+ configuration.getOperatorDictionary().addOperator("CONTAINS", new ContainsOperator());
+ configuration.getOperatorDictionary().addOperator("MATCHES", new MatchesOperator());
+
+ return new Expression(jcondition.getString("expression"), configuration)
+ .with("sender", sender)
+ .with("subject", subject);
+ }
+
boolean execute(Context context, EntityMessage message, String html) throws JSONException, IOException {
boolean executed = _execute(context, message, html);
if (this.id != null && executed) {
@@ -655,6 +747,25 @@ public class EntityRule {
}
void validate(Context context) throws JSONException, IllegalArgumentException {
+ Expression expression = getExpression(this, null);
+ if (expression != null)
+ try {
+ for (String variable : expression.getUsedVariables()) {
+ Log.i("EXPR variable=" + variable);
+ if (!EXPR_VARIABLES.contains(variable))
+ throw new IllegalArgumentException("Unknown variable '" + variable + "'");
+ }
+ Log.i("EXPR validating");
+ expression.validate();
+ Log.i("EXPR validated");
+ } catch (ParseException ex) {
+ Log.w("EXPR", ex);
+ String message = ex.getMessage();
+ if (TextUtils.isEmpty(message))
+ message = "Invalid expression";
+ throw new IllegalArgumentException(message, ex);
+ }
+
JSONObject jargs = new JSONObject(action);
int type = jargs.getInt("type");
diff --git a/app/src/main/java/eu/faircode/email/FragmentRule.java b/app/src/main/java/eu/faircode/email/FragmentRule.java
index 7393f8e578..bf86061efc 100644
--- a/app/src/main/java/eu/faircode/email/FragmentRule.java
+++ b/app/src/main/java/eu/faircode/email/FragmentRule.java
@@ -133,6 +133,8 @@ public class FragmentRule extends FragmentBase {
private CheckBox cbEveryDay;
private EditText etYounger;
+ private EditText etExpression;
+
private Spinner spAction;
private TextView tvActionRemark;
@@ -184,6 +186,7 @@ public class FragmentRule extends FragmentBase {
private ContentLoadingProgressBar pbWait;
private Group grpReady;
+ private Group grpExpression;
private Group grpAge;
private Group grpSnooze;
private Group grpFlag;
@@ -333,6 +336,8 @@ public class FragmentRule extends FragmentBase {
cbEveryDay = view.findViewById(R.id.cbEveryDay);
etYounger = view.findViewById(R.id.etYounger);
+ etExpression = view.findViewById(R.id.etExpression);
+
spAction = view.findViewById(R.id.spAction);
tvActionRemark = view.findViewById(R.id.tvActionRemark);
@@ -385,6 +390,7 @@ public class FragmentRule extends FragmentBase {
pbWait = view.findViewById(R.id.pbWait);
grpReady = view.findViewById(R.id.grpReady);
+ grpExpression = view.findViewById(R.id.grpExpression);
grpAge = view.findViewById(R.id.grpAge);
grpSnooze = view.findViewById(R.id.grpSnooze);
grpFlag = view.findViewById(R.id.grpFlag);
@@ -854,6 +860,7 @@ public class FragmentRule extends FragmentBase {
tvFolder.setText(null);
bottom_navigation.setVisibility(View.GONE);
grpReady.setVisibility(View.GONE);
+ grpExpression.setVisibility(View.GONE);
grpAge.setVisibility(View.GONE);
grpSnooze.setVisibility(View.GONE);
grpFlag.setVisibility(View.GONE);
@@ -1286,6 +1293,8 @@ public class FragmentRule extends FragmentBase {
etYounger.setText(jcondition.has("younger")
? Integer.toString(jcondition.optInt("younger")) : null);
+ etExpression.setText(jcondition.optString("expression"));
+
spScheduleDayStart.setSelection(start / (24 * 60));
spScheduleDayEnd.setSelection(end / (24 * 60));
@@ -1423,6 +1432,7 @@ public class FragmentRule extends FragmentBase {
Log.e(ex);
} finally {
grpReady.setVisibility(View.VISIBLE);
+ grpExpression.setVisibility(BuildConfig.DEBUG ? View.VISIBLE : View.GONE);
grpAge.setVisibility(cbDaily.isChecked() ? View.VISIBLE : View.GONE);
if (id < 0)
bottom_navigation.getMenu().removeItem(R.id.action_delete);
@@ -1561,7 +1571,9 @@ public class FragmentRule extends FragmentBase {
jheader == null &&
jbody == null &&
jdate == null &&
- jschedule == null)
+ jschedule == null &&
+ !jcondition.has("younger") &&
+ !jcondition.has("expression"))
throw new IllegalArgumentException(context.getString(R.string.title_rule_condition_missing));
if (TextUtils.isEmpty(order))
@@ -1729,6 +1741,10 @@ public class FragmentRule extends FragmentBase {
Log.e(ex);
}
+ String expression = etExpression.getText().toString().trim();
+ if (!TextUtils.isEmpty(expression))
+ jcondition.put("expression", expression);
+
return jcondition;
}
diff --git a/app/src/main/res/layout/fragment_rule.xml b/app/src/main/res/layout/fragment_rule.xml
index 86517b47b8..48b8588637 100644
--- a/app/src/main/res/layout/fragment_rule.xml
+++ b/app/src/main/res/layout/fragment_rule.xml
@@ -818,6 +818,54 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvYounger" />
+
+
+
+
+
+
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@+id/etExpression" />
Relative time (received) between
Every day
Messages younger than (hours)
+ Expression
Regex
AND
NOT