diff --git a/app/src/main/java/eu/faircode/email/EntityAnswer.java b/app/src/main/java/eu/faircode/email/EntityAnswer.java index a3fd91915c..4432285ef9 100644 --- a/app/src/main/java/eu/faircode/email/EntityAnswer.java +++ b/app/src/main/java/eu/faircode/email/EntityAnswer.java @@ -24,6 +24,9 @@ import org.json.JSONObject; import java.io.Serializable; +import javax.mail.Address; +import javax.mail.internet.InternetAddress; + import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.PrimaryKey; @@ -47,6 +50,25 @@ public class EntityAnswer implements Serializable { @NonNull public String text; + static String getAnswerText(DB db, long id, Address[] from) { + EntityAnswer answer = db.answer().getAnswer(id); + if (answer == null) + return null; + + String name = null; + String email = null; + if (from != null && from.length > 0) { + name = ((InternetAddress) from[0]).getPersonal(); + email = ((InternetAddress) from[0]).getAddress(); + } + + String text = answer.text; + text = text.replace("$name$", name == null ? "" : name); + text = text.replace("$email$", email == null ? "" : email); + + return text; + } + public JSONObject toJSON() throws JSONException { JSONObject json = new JSONObject(); json.put("name", name); @@ -71,4 +93,10 @@ public class EntityAnswer implements Serializable { } return false; } + + @NonNull + @Override + public String toString() { + return name; + } } diff --git a/app/src/main/java/eu/faircode/email/EntityIdentity.java b/app/src/main/java/eu/faircode/email/EntityIdentity.java index e1079b5a94..3b906d91c7 100644 --- a/app/src/main/java/eu/faircode/email/EntityIdentity.java +++ b/app/src/main/java/eu/faircode/email/EntityIdentity.java @@ -198,4 +198,10 @@ public class EntityIdentity { String getDisplayName() { return (display == null ? name : display); } + + @NonNull + @Override + public String toString() { + return getDisplayName() + (primary ? " ★" : ""); + } } diff --git a/app/src/main/java/eu/faircode/email/EntityRule.java b/app/src/main/java/eu/faircode/email/EntityRule.java index b8654db2e6..fcc660691e 100644 --- a/app/src/main/java/eu/faircode/email/EntityRule.java +++ b/app/src/main/java/eu/faircode/email/EntityRule.java @@ -24,6 +24,8 @@ import android.content.Context; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; +import java.util.Date; import java.util.Enumeration; import java.util.regex.Pattern; @@ -74,6 +76,7 @@ public class EntityRule { static final int TYPE_SEEN = 1; static final int TYPE_UNSEEN = 2; static final int TYPE_MOVE = 3; + static final int TYPE_ANSWER = 4; boolean matches(Context context, EntityMessage message, Message imessage) throws MessagingException { try { @@ -152,7 +155,7 @@ public class EntityRule { return haystack.toLowerCase().contains(needle.toLowerCase()); } - void execute(Context context, DB db, EntityMessage message) { + void execute(Context context, DB db, EntityMessage message) throws IOException { try { JSONObject jargs = new JSONObject(action); int type = jargs.getInt("type"); @@ -168,6 +171,9 @@ public class EntityRule { case TYPE_MOVE: onActionMove(context, db, message, jargs); break; + case TYPE_ANSWER: + onActionAnswer(context, db, message, jargs); + break; } } catch (JSONException ex) { Log.e(ex); @@ -184,6 +190,38 @@ public class EntityRule { EntityOperation.queue(context, db, message, EntityOperation.MOVE, target, false); } + private void onActionAnswer(Context context, DB db, EntityMessage message, JSONObject jargs) throws JSONException, IOException { + long iid = jargs.getLong("identity"); + long aid = jargs.getLong("answer"); + + EntityIdentity identity = db.identity().getIdentity(iid); + if (identity == null) + throw new IllegalArgumentException("Rule identity not found"); + + String body = EntityAnswer.getAnswerText(db, aid, message.from); + if (body == null) + throw new IllegalArgumentException("Rule answer not found"); + + EntityMessage reply = new EntityMessage(); + reply.account = message.account; + reply.folder = db.folder().getOutbox().id; + reply.identity = identity.id; + reply.msgid = EntityMessage.generateMessageId(); + reply.thread = message.thread; + reply.replying = message.id; + reply.to = (message.reply == null || message.reply.length == 0 ? message.from : message.reply); + reply.from = new InternetAddress[]{new InternetAddress(identity.email, identity.name)}; + reply.subject = context.getString(R.string.title_subject_reply, message.subject == null ? "" : message.subject); + reply.sender = MessageHelper.getSortKey(reply.from); + reply.received = new Date().getTime(); + reply.setContactInfo(context); + reply.id = db.message().insertMessage(reply); + reply.write(context, body); + db.message().setMessageContent(reply.id, true, HtmlHelper.getPreview(body)); + + EntityOperation.queue(context, db, reply, EntityOperation.SEND); + } + @Override public boolean equals(Object obj) { if (obj instanceof EntityRule) { diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index f177e610a9..084300eff5 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -1471,12 +1471,8 @@ public class FragmentCompose extends FragmentBase { body = args.getString("body", ""); body = body.replaceAll("\\r?\\n", "
"); - if (answer > 0) { - String text = db.answer().getAnswer(answer).text; - text = text.replace("$name$", ""); - text = text.replace("$email$", ""); - body = text + body; - } + if (answer > 0) + body = EntityAnswer.getAnswerText(db, answer, null) + body; } else { result.draft.thread = ref.thread; @@ -1514,20 +1510,8 @@ public class FragmentCompose extends FragmentBase { result.draft.subject = context.getString(R.string.title_subject_forward, ref.subject == null ? "" : ref.subject); - if (answer > 0 && ("reply".equals(action) || "reply_all".equals(action))) { - String text = db.answer().getAnswer(answer).text; - - String name = null; - String email = null; - if (result.draft.to != null && result.draft.to.length > 0) { - name = ((InternetAddress) result.draft.to[0]).getPersonal(); - email = ((InternetAddress) result.draft.to[0]).getAddress(); - } - text = text.replace("$name$", name == null ? "" : name); - text = text.replace("$email$", email == null ? "" : email); - - body = text + body; - } + if (answer > 0 && ("reply".equals(action) || "reply_all".equals(action))) + body = EntityAnswer.getAnswerText(db, answer, result.draft.to) + body; } // Select identity matching from address diff --git a/app/src/main/java/eu/faircode/email/FragmentRule.java b/app/src/main/java/eu/faircode/email/FragmentRule.java index d3f20ebaea..bad31d0a87 100644 --- a/app/src/main/java/eu/faircode/email/FragmentRule.java +++ b/app/src/main/java/eu/faircode/email/FragmentRule.java @@ -65,13 +65,18 @@ public class FragmentRule extends FragmentBase { private CheckBox cbHeader; private Spinner spAction; private Spinner spTarget; + private Spinner spIdent; + private Spinner spAnswer; private BottomNavigationView bottom_navigation; private ContentLoadingProgressBar pbWait; private Group grpReady; private Group grpMove; + private Group grpAnswer; private ArrayAdapter adapterAction; private ArrayAdapter adapterTarget; + private ArrayAdapter adapterIdentity; + private ArrayAdapter adapterAnswer; private long id = -1; private long account = -1; @@ -108,10 +113,13 @@ public class FragmentRule extends FragmentBase { cbHeader = view.findViewById(R.id.cbHeader); spAction = view.findViewById(R.id.spAction); spTarget = view.findViewById(R.id.spTarget); + spIdent = view.findViewById(R.id.spIdent); + spAnswer = view.findViewById(R.id.spAnswer); bottom_navigation = view.findViewById(R.id.bottom_navigation); pbWait = view.findViewById(R.id.pbWait); grpReady = view.findViewById(R.id.grpReady); grpMove = view.findViewById(R.id.grpMove); + grpAnswer = view.findViewById(R.id.grpAnswer); adapterAction = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList()); adapterAction.setDropDownViewResource(R.layout.spinner_item1_dropdown); @@ -121,10 +129,19 @@ public class FragmentRule extends FragmentBase { adapterTarget.setDropDownViewResource(R.layout.spinner_item1_dropdown); spTarget.setAdapter(adapterTarget); + adapterIdentity = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList()); + adapterIdentity.setDropDownViewResource(R.layout.spinner_item1_dropdown); + spIdent.setAdapter(adapterIdentity); + + adapterAnswer = new ArrayAdapter<>(getContext(), R.layout.spinner_item1, android.R.id.text1, new ArrayList()); + adapterAnswer.setDropDownViewResource(R.layout.spinner_item1_dropdown); + spAnswer.setAdapter(adapterAnswer); + List actions = new ArrayList<>(); actions.add(new Action(EntityRule.TYPE_SEEN, getString(R.string.title_seen))); actions.add(new Action(EntityRule.TYPE_UNSEEN, getString(R.string.title_unseen))); actions.add(new Action(EntityRule.TYPE_MOVE, getString(R.string.title_move))); + actions.add(new Action(EntityRule.TYPE_ANSWER, getString(R.string.menu_answers))); adapterAction.addAll(actions); spAction.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -141,6 +158,7 @@ public class FragmentRule extends FragmentBase { private void onActionSelected(int type) { grpMove.setVisibility(type == EntityRule.TYPE_MOVE ? View.VISIBLE : View.GONE); + grpAnswer.setVisibility(type == EntityRule.TYPE_ANSWER ? View.VISIBLE : View.GONE); new Handler().post(new Runnable() { @Override @@ -171,6 +189,7 @@ public class FragmentRule extends FragmentBase { bottom_navigation.setVisibility(View.GONE); grpReady.setVisibility(View.GONE); grpMove.setVisibility(View.GONE); + grpAnswer.setVisibility(View.GONE); pbWait.setVisibility(View.VISIBLE); return view; @@ -183,30 +202,39 @@ public class FragmentRule extends FragmentBase { Bundle args = new Bundle(); args.putLong("account", account); - new SimpleTask>() { + new SimpleTask() { @Override - protected List onExecute(Context context, Bundle args) { + protected RefData onExecute(Context context, Bundle args) { long account = args.getLong("account"); + RefData data = new RefData(); + DB db = DB.getInstance(context); - List folders = db.folder().getFolders(account); + data.folders = db.folder().getFolders(account); - if (folders != null) { - for (EntityFolder folder : folders) - folder.display = folder.getDisplayName(context); - EntityFolder.sort(context, folders); - } + if (data.folders == null) + data.folders = new ArrayList<>(); + + for (EntityFolder folder : data.folders) + folder.display = folder.getDisplayName(context); + EntityFolder.sort(context, data.folders); + + data.identities = db.identity().getIdentities(account); + data.answers = db.answer().getAnswers(); - return folders; + return data; } @Override - protected void onExecuted(Bundle args, List folders) { - if (folders == null) - folders = new ArrayList<>(); - + protected void onExecuted(Bundle args, RefData data) { adapterTarget.clear(); - adapterTarget.addAll(folders); + adapterTarget.addAll(data.folders); + + adapterIdentity.clear(); + adapterIdentity.addAll(data.identities); + + adapterAnswer.clear(); + adapterAnswer.addAll(data.answers); Bundle rargs = new Bundle(); rargs.putLong("id", id); @@ -232,33 +260,47 @@ public class FragmentRule extends FragmentBase { etOrder.setText(rule == null ? null : Integer.toString(rule.order)); cbEnabled.setChecked(rule == null || rule.enabled); cbStop.setChecked(rule != null && rule.stop); - etSender.setText(jsender == null ? null : jsender.optString("value")); - cbSender.setChecked(jsender != null && jsender.optBoolean("regex", false)); - etSubject.setText(jsubject == null ? null : jsubject.optString("value")); - cbSubject.setChecked(jsubject != null && jsubject.optBoolean("regex", false)); - etHeader.setText(jheader == null ? null : jheader.optString("value")); - cbHeader.setChecked(jheader != null && jheader.optBoolean("regex", false)); - - int type = jaction.optInt("type", -1); - for (int pos = 0; pos < adapterAction.getCount(); pos++) - if (adapterAction.getItem(pos).type == type) { - spAction.setSelection(pos); - break; + etSender.setText(jsender == null ? null : jsender.getString("value")); + cbSender.setChecked(jsender != null && jsender.getBoolean("regex")); + etSubject.setText(jsubject == null ? null : jsubject.getString("value")); + cbSubject.setChecked(jsubject != null && jsubject.getBoolean("regex")); + etHeader.setText(jheader == null ? null : jheader.getString("value")); + cbHeader.setChecked(jheader != null && jheader.getBoolean("regex")); + + if (rule != null) { + int type = jaction.getInt("type"); + switch (type) { + case EntityRule.TYPE_MOVE: + long target = jaction.getLong("target"); + for (int pos = 0; pos < adapterTarget.getCount(); pos++) + if (adapterTarget.getItem(pos).id.equals(target)) { + spTarget.setSelection(pos); + break; + } + break; + + case EntityRule.TYPE_ANSWER: + long identity = jaction.getLong("identity"); + for (int pos = 0; pos < adapterIdentity.getCount(); pos++) + if (adapterIdentity.getItem(pos).id.equals(identity)) { + spIdent.setSelection(pos); + break; + } + + long answer = jaction.getLong("answer"); + for (int pos = 0; pos < adapterAnswer.getCount(); pos++) + if (adapterAnswer.getItem(pos).id.equals(answer)) { + spAnswer.setSelection(pos); + break; + } + break; } - if (rule == null) { - grpReady.setVisibility(View.VISIBLE); - bottom_navigation.setVisibility(View.VISIBLE); - pbWait.setVisibility(View.GONE); - } else { - if (type == EntityRule.TYPE_MOVE) { - long target = jaction.optLong("target", -1); - for (int pos = 0; pos < adapterTarget.getCount(); pos++) - if (adapterTarget.getItem(pos).id.equals(target)) { - spTarget.setSelection(pos); - break; - } - } + for (int pos = 0; pos < adapterAction.getCount(); pos++) + if (adapterAction.getItem(pos).type == type) { + spAction.setSelection(pos); + break; + } } grpReady.setVisibility(View.VISIBLE); @@ -362,9 +404,18 @@ public class FragmentRule extends FragmentBase { Action action = (Action) spAction.getSelectedItem(); if (action != null) { jaction.put("type", action.type); - if (action.type == EntityRule.TYPE_MOVE) { - EntityFolder target = (EntityFolder) spTarget.getSelectedItem(); - jaction.put("target", target.id); + switch (action.type) { + case EntityRule.TYPE_MOVE: + EntityFolder target = (EntityFolder) spTarget.getSelectedItem(); + jaction.put("target", target.id); + break; + + case EntityRule.TYPE_ANSWER: + EntityIdentity identity = (EntityIdentity) spIdent.getSelectedItem(); + EntityAnswer answer = (EntityAnswer) spAnswer.getSelectedItem(); + jaction.put("identity", identity.id); + jaction.put("answer", answer.id); + break; } } @@ -458,6 +509,12 @@ public class FragmentRule extends FragmentBase { } } + private class RefData { + List folders; + List identities; + List answers; + } + private class Action { int type; String name; diff --git a/app/src/main/res/layout/fragment_rule.xml b/app/src/main/res/layout/fragment_rule.xml index 5e03351682..08ed34970b 100644 --- a/app/src/main/res/layout/fragment_rule.xml +++ b/app/src/main/res/layout/fragment_rule.xml @@ -12,6 +12,7 @@ android:layout_height="0dp" android:layout_margin="12dp" android:orientation="vertical" + android:scrollbarStyle="outsideOverlay" app:layout_constraintBottom_toTopOf="@+id/bottom_navigation" app:layout_constraintTop_toTopOf="parent"> @@ -221,14 +222,49 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvAction" /> + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/spIdent" /> + app:constraint_referenced_ids="tvTargetArguments,spTarget" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fcfd8763cd..3cd6c1a0c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -390,6 +390,7 @@ Regex AND Action + Parameters Rule name missing Condition missing