package eu.faircode.email;
/*
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.
FairEmail is distributed in the hope that it will be useful,
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
along with FairEmail. If not, see .
Copyright 2018-2024 by Marcel Bokhorst (M66B)
*/
import static com.ezylang.evalex.operators.OperatorIfc.OPERATOR_PRECEDENCE_COMPARISON;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import androidx.preference.PreferenceManager;
import com.ezylang.evalex.Expression;
import com.ezylang.evalex.config.ExpressionConfiguration;
import com.ezylang.evalex.data.EvaluationValue;
import com.ezylang.evalex.functions.AbstractFunction;
import com.ezylang.evalex.functions.FunctionParameter;
import com.ezylang.evalex.operators.AbstractOperator;
import com.ezylang.evalex.operators.InfixOperator;
import com.ezylang.evalex.parser.ASTNode;
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;
import org.jsoup.nodes.Element;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import javax.mail.Address;
import javax.mail.Header;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.InternetHeaders;
public class ExpressionHelper {
static Expression getExpression(EntityRule rule, EntityMessage message, List headers, String html, Context context) throws JSONException, ParseException, MessagingException {
// https://ezylang.github.io/EvalEx/
JSONObject jcondition = new JSONObject(rule.condition);
if (!jcondition.has("expression"))
return null;
String eval = jcondition.getString("expression");
List to = new ArrayList<>();
if (message != null && message.to != null)
for (Address a : message.to)
to.add(MessageHelper.formatAddresses(new Address[]{a}));
List from = new ArrayList<>();
if (message != null && message.from != null)
for (Address a : message.from)
from.add(MessageHelper.formatAddresses(new Address[]{a}));
if (html == null && message != null && message.content)
try {
html = Helper.readText(message.getFile(context));
} catch (IOException ex) {
Log.e(ex);
}
Document doc = (html == null ? null : JsoupEx.parse(html));
if (headers == null && message != null && message.headers != null) {
ByteArrayInputStream bis = new ByteArrayInputStream(message.headers.getBytes());
headers = Collections.list(new InternetHeaders(bis, true).getAllHeaders());
}
HeaderFunction fHeader = new HeaderFunction(headers);
MessageFunction fMessage = new MessageFunction(message);
BlocklistFunction fBlocklist = new BlocklistFunction(context, message, headers);
MxFunction fMx = new MxFunction(context, message);
AttachmentsFunction fAttachments = new AttachmentsFunction(context, message);
JsoupFunction fJsoup = new JsoupFunction(context, message);
SizeFunction fSize = new SizeFunction();
KnownFunction fKnown = new KnownFunction(context, message);
ContainsOperator oContains = new ContainsOperator(false);
ContainsOperator oMatches = new ContainsOperator(true);
ExpressionConfiguration configuration = ExpressionConfiguration.defaultConfiguration();
configuration.getFunctionDictionary().addFunction("Header", fHeader);
configuration.getFunctionDictionary().addFunction("Message", fMessage);
configuration.getFunctionDictionary().addFunction("Blocklist", fBlocklist);
configuration.getFunctionDictionary().addFunction("onBlocklist", fBlocklist);
configuration.getFunctionDictionary().addFunction("hasMx", fMx);
configuration.getFunctionDictionary().addFunction("attachments", fAttachments);
configuration.getFunctionDictionary().addFunction("Jsoup", fJsoup);
configuration.getFunctionDictionary().addFunction("Size", fSize);
configuration.getFunctionDictionary().addFunction("knownContact", fKnown);
configuration.getOperatorDictionary().addOperator("Contains", oContains);
configuration.getOperatorDictionary().addOperator("Matches", oMatches);
Expression expression = new Expression(eval, configuration)
.with("to", to)
.with("from", from)
.with("subject", message == null ? null : message.subject)
.with("text", doc == null ? null : doc.text());
if (message != null) {
boolean hasAttachments = false;
for (String variable : expression.getUsedVariables())
if (!hasAttachments && "hasAttachments".equals(variable)) {
hasAttachments = true;
DB db = DB.getInstance(context);
List attachments = db.attachment().getAttachments(message.id);
expression.with("hasAttachments", attachments != null && !attachments.isEmpty());
}
}
return expression;
}
static boolean needsHeaders(Expression expression) {
try {
expression.validate();
for (ASTNode node : expression.getAllASTNodes()) {
Token token = node.getToken();
Log.i("EXPR token=" + token.getType() + ":" + token.getValue());
if (token.getType() == Token.TokenType.FUNCTION &&
("header".equalsIgnoreCase(token.getValue()) ||
"blocklist".equalsIgnoreCase(token.getValue()))) {
Log.i("EXPR needs headers");
return true;
}
}
} catch (Throwable ex) {
Log.e("EXPR", ex);
}
return false;
}
static boolean needsBody(Expression expression) {
try {
for (String variable : expression.getUsedVariables())
if ("text".equalsIgnoreCase(variable))
return true;
} catch (Throwable ex) {
Log.e("EXPR", ex);
}
return false;
}
@FunctionParameter(name = "value")
public static class HeaderFunction extends AbstractFunction {
private final List headers;
HeaderFunction(List headers) {
this.headers = headers;
}
@Override
public EvaluationValue evaluate(
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
List result = new ArrayList<>();
try {
if (parameterValues.length == 1) {
String name = parameterValues[0].getStringValue();
if (name != null && headers != null)
for (Header header : headers)
if (name.equalsIgnoreCase(header.getName()))
result.add(header.getValue());
}
} catch (Throwable ex) {
Log.e("EXPR", ex);
}
Log.i("EXPR header(" + parameterValues[0] + ")=" + TextUtils.join(", ", result));
return new EvaluationValue(result, ExpressionConfiguration.defaultConfiguration());
}
}
@FunctionParameter(name = "value")
public static class MessageFunction extends AbstractFunction {
private final EntityMessage message;
MessageFunction(EntityMessage message) {
this.message = message;
}
@Override
public EvaluationValue evaluate(
Expression expression, Token functionToken, EvaluationValue... parameterValues) {
List