From 47bae9a8e2defd23c1db64e75c222db00ded9b94 Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 5 Jun 2022 10:01:02 +0200 Subject: [PATCH] Block POP3 senders --- .../eu/faircode/email/AdapterMessage.java | 54 +++---- .../java/eu/faircode/email/EntityMessage.java | 16 ++ .../email/FragmentDialogBlockSender.java | 71 ++++++++ .../eu/faircode/email/FragmentDialogJunk.java | 19 +-- .../eu/faircode/email/FragmentMessages.java | 152 +++++++++++++----- .../java/eu/faircode/email/FragmentPop.java | 5 + .../main/res/layout/dialog_block_sender.xml | 53 ++++++ app/src/main/res/layout/dialog_junk.xml | 33 +--- app/src/main/res/values/strings.xml | 5 + 9 files changed, 300 insertions(+), 108 deletions(-) create mode 100644 app/src/main/java/eu/faircode/email/FragmentDialogBlockSender.java create mode 100644 app/src/main/res/layout/dialog_block_sender.xml diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 473a3aa60f..df26bec4ca 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -2032,25 +2032,6 @@ public class AdapterMessage extends RecyclerView.Adapter senders = new ArrayList<>(); - if (message.from != null) - senders.addAll(Arrays.asList(message.from)); - if (message.reply != null) - senders.addAll(Arrays.asList(message.reply)); - - List identities = db.identity().getComposableIdentities(null); - if (identities != null) { - for (TupleIdentityEx identity : identities) - for (Address sender : senders) - if (identity.self && identity.similarAddress(sender)) { - data.fromSelf = true; - break; - } - } - } - EntityAccount account = db.account().getAccount(aid); data.isGmail = (account != null && account.isGmail()); data.folders = db.folder().getSystemFolders(aid); @@ -2182,7 +2163,7 @@ public class AdapterMessage extends RecyclerView.Adapter folders; private List attachments; diff --git a/app/src/main/java/eu/faircode/email/EntityMessage.java b/app/src/main/java/eu/faircode/email/EntityMessage.java index f588e837c1..5162292ffa 100644 --- a/app/src/main/java/eu/faircode/email/EntityMessage.java +++ b/app/src/main/java/eu/faircode/email/EntityMessage.java @@ -267,6 +267,22 @@ public class EntityMessage implements Serializable { return (this.plain_only != null && (this.plain_only & 0x80) != 0); } + boolean fromSelf(List identities) { + List
senders = new ArrayList<>(); + if (from != null) + senders.addAll(Arrays.asList(from)); + if (reply != null) + senders.addAll(Arrays.asList(reply)); + + if (identities != null) + for (TupleIdentityEx identity : identities) + for (Address sender : senders) + if (identity.self && identity.similarAddress(sender)) + return true; + + return false; + } + boolean replySelf(List identities, long account) { Address[] senders = (reply == null || reply.length == 0 ? from : reply); if (identities != null && senders != null) diff --git a/app/src/main/java/eu/faircode/email/FragmentDialogBlockSender.java b/app/src/main/java/eu/faircode/email/FragmentDialogBlockSender.java new file mode 100644 index 0000000000..40dbe5c477 --- /dev/null +++ b/app/src/main/java/eu/faircode/email/FragmentDialogBlockSender.java @@ -0,0 +1,71 @@ +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-2022 by Marcel Bokhorst (M66B) +*/ + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; + +import java.text.NumberFormat; + +public class FragmentDialogBlockSender extends FragmentDialogBase { + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + long[] ids = getArguments().getLongArray("ids"); + + final Context context = getContext(); + final View view = LayoutInflater.from(context).inflate(R.layout.dialog_block_sender, null); + final TextView tvMessage = view.findViewById(R.id.tvMessage); + final TextView tvJunkHint = view.findViewById(R.id.tvJunkHint); + + tvJunkHint.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.viewFAQ(v.getContext(), 92); + } + }); + + NumberFormat NF = NumberFormat.getNumberInstance(); + String msg = context.getResources().getQuantityString(R.plurals.title_ask_block_sender, + ids.length, NF.format(ids.length)); + tvMessage.setText(msg); + + return new AlertDialog.Builder(context) + .setView(view) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + sendResult(Activity.RESULT_OK); + } + }) + .create(); + } +} diff --git a/app/src/main/java/eu/faircode/email/FragmentDialogJunk.java b/app/src/main/java/eu/faircode/email/FragmentDialogJunk.java index d0566a5c11..4c551c3610 100644 --- a/app/src/main/java/eu/faircode/email/FragmentDialogJunk.java +++ b/app/src/main/java/eu/faircode/email/FragmentDialogJunk.java @@ -77,13 +77,10 @@ public class FragmentDialogJunk extends FragmentDialogBase { final String type = args.getString("type"); final Address[] froms = DB.Converters.decodeAddresses(args.getString("from")); - boolean imap = (protocol == EntityAccount.TYPE_IMAP); - final Context context = getContext(); final View view = LayoutInflater.from(context).inflate(R.layout.dialog_junk, null); final TextView tvMessage = view.findViewById(R.id.tvMessage); - final ImageButton ibInfoProvider = view.findViewById(R.id.ibInfoProvider); - final TextView tvPopHint = view.findViewById(R.id.tvPopHint); + final TextView tvJunkHint = view.findViewById(R.id.tvJunkHint); final CheckBox cbBlockSender = view.findViewById(R.id.cbBlockSender); final CheckBox cbBlockDomain = view.findViewById(R.id.cbBlockDomain); final ImageButton ibMore = view.findViewById(R.id.ibMore); @@ -113,15 +110,13 @@ public class FragmentDialogJunk extends FragmentDialogBase { // Wire controls - ibInfoProvider.setOnClickListener(new View.OnClickListener() { + tvJunkHint.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Helper.viewFAQ(v.getContext(), 92); } }); - tvPopHint.setVisibility(imap ? View.GONE : View.VISIBLE); - cbBlockSender.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -138,7 +133,7 @@ public class FragmentDialogJunk extends FragmentDialogBase { grpManage.setVisibility(View.GONE); } else { ibMore.setImageLevel(0); - grpFilter.setVisibility(imap ? View.VISIBLE : View.GONE); + grpFilter.setVisibility(View.VISIBLE); grpManage.setVisibility(View.VISIBLE); } } @@ -387,8 +382,8 @@ public class FragmentDialogJunk extends FragmentDialogBase { cbBlocklist.setChecked(check_blocklist && use_blocklist); tvBlocklist.setText(TextUtils.join(", ", DnsBlockList.getNamesEnabled(context))); - cbBlockSender.setVisibility(imap ? View.VISIBLE : View.GONE); - grpBlockDomain.setVisibility(domains.size() > 0 && imap ? View.VISIBLE : View.GONE); + cbBlockSender.setVisibility(View.VISIBLE); + grpBlockDomain.setVisibility(domains.size() > 0 ? View.VISIBLE : View.GONE); grpFilter.setVisibility(View.GONE); grpManage.setVisibility(View.GONE); @@ -442,8 +437,8 @@ public class FragmentDialogJunk extends FragmentDialogBase { @Override public void onClick(DialogInterface dialog, int which) { prefs.edit().putBoolean("block_sender", cbBlockSender.isChecked()).apply(); - getArguments().putBoolean("block_sender", cbBlockSender.isChecked() || !imap); - getArguments().putBoolean("block_domain", cbBlockDomain.isChecked() && imap); + getArguments().putBoolean("block_sender", cbBlockSender.isChecked()); + getArguments().putBoolean("block_domain", cbBlockDomain.isChecked()); sendResult(Activity.RESULT_OK); } }); diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index ffdc80a9d8..d52ad468f2 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -425,6 +425,7 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. private static final int REQUEST_SAVE_SEARCH = 26; private static final int REQUEST_DELETE_SEARCH = 27; private static final int REQUEST_QUICK_ACTIONS = 28; + static final int REQUEST_BLOCK_SENDERS = 29; static final String ACTION_STORE_RAW = BuildConfig.APPLICATION_ID + ".STORE_RAW"; static final String ACTION_DECRYPT = BuildConfig.APPLICATION_ID + ".DECRYPT"; @@ -1378,7 +1379,14 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. ibJunk.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - onActionJunkSelection(); + MoreResult result = (MoreResult) cardMore.getTag(); + if (result == null) + return; + + if (result.hasPop && !result.hasImap) + onActionBlockSender(); + else if (!result.hasPop && result.hasImap) + onActionJunkSelection(); } }); @@ -2896,18 +2904,28 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. } private void onSwipeJunk(final @NonNull TupleMessageEx message) { - Bundle aargs = new Bundle(); - aargs.putLong("id", message.id); - aargs.putLong("account", message.account); - aargs.putInt("protocol", message.accountProtocol); - aargs.putLong("folder", message.folder); - aargs.putString("type", message.folderType); - aargs.putString("from", DB.Converters.encodeAddresses(message.from)); - - FragmentDialogJunk ask = new FragmentDialogJunk(); - ask.setArguments(aargs); - ask.setTargetFragment(FragmentMessages.this, REQUEST_MESSAGE_JUNK); - ask.show(getParentFragmentManager(), "swipe:junk"); + if (message.accountProtocol == EntityAccount.TYPE_POP) { + Bundle aargs = new Bundle(); + aargs.putLongArray("ids", new long[]{message.id}); + + FragmentDialogBlockSender ask = new FragmentDialogBlockSender(); + ask.setArguments(aargs); + ask.setTargetFragment(FragmentMessages.this, REQUEST_BLOCK_SENDERS); + ask.show(getParentFragmentManager(), "message:block"); + } else { + Bundle aargs = new Bundle(); + aargs.putLong("id", message.id); + aargs.putLong("account", message.account); + aargs.putInt("protocol", message.accountProtocol); + aargs.putLong("folder", message.folder); + aargs.putString("type", message.folderType); + aargs.putString("from", DB.Converters.encodeAddresses(message.from)); + + FragmentDialogJunk ask = new FragmentDialogJunk(); + ask.setArguments(aargs); + ask.setTargetFragment(FragmentMessages.this, REQUEST_MESSAGE_JUNK); + ask.show(getParentFragmentManager(), "swipe:junk"); + } } private void onSwipeDelete(@NonNull TupleMessageEx message, int pos) { @@ -3528,7 +3546,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. onActionMoveSelection(EntityFolder.ARCHIVE, false); return true; } else if (itemId == R.string.title_spam) { - onActionJunkSelection(); + if (result.hasPop && !result.hasImap) + onActionBlockSender(); + else if (!result.hasPop && result.hasImap) + onActionJunkSelection(); return true; } else if (itemId == R.string.title_trash) { onActionMoveSelection(EntityFolder.TRASH, false); @@ -3926,6 +3947,16 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. ask.show(getParentFragmentManager(), "messages:junk"); } + private void onActionBlockSender() { + Bundle args = new Bundle(); + args.putLongArray("ids", getSelection()); + + FragmentDialogBlockSender ask = new FragmentDialogBlockSender(); + ask.setArguments(args); + ask.setTargetFragment(FragmentMessages.this, REQUEST_BLOCK_SENDERS); + ask.show(getParentFragmentManager(), "messages:block"); + } + private void onActionMoveSelection(final String type, boolean block) { Bundle args = new Bundle(); args.putString("type", type); @@ -7584,6 +7615,10 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. if (resultCode == RESULT_OK) updateMore(); break; + case REQUEST_BLOCK_SENDERS: + if (resultCode == RESULT_OK) + onBlockSenders(data.getBundleExtra("args")); + break; } } catch (Throwable ex) { Log.e(ex); @@ -8840,8 +8875,12 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. if (message == null) return null; + List identities = db.identity().getComposableIdentities(null); + if (message.fromSelf(identities)) + return null; + EntityAccount account = db.account().getAccount(message.account); - if (account == null) + if (account == null || account.protocol != EntityAccount.TYPE_IMAP) return null; if (block_sender) @@ -8849,28 +8888,25 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. message.account, message.identity, message.from, EntityContact.TYPE_JUNK, message.received); - if (account.protocol == EntityAccount.TYPE_IMAP) { - EntityFolder junk = db.folder().getFolderByType(message.account, EntityFolder.JUNK); - if (junk == null) - throw new IllegalArgumentException(context.getString(R.string.title_no_junk_folder)); - - if (!message.folder.equals(junk.id)) - EntityOperation.queue(context, message, EntityOperation.MOVE, junk.id, null, null, true); - - if (block_domain) { - List rules = EntityRule.blockSender(context, message, junk, block_domain); - for (EntityRule rule : rules) { - if (message.folder.equals(junk.id)) { - EntityFolder inbox = db.folder().getFolderByType(message.account, EntityFolder.INBOX); - if (inbox == null) - continue; - rule.folder = inbox.id; - } - rule.id = db.rule().insertRule(rule); + EntityFolder junk = db.folder().getFolderByType(message.account, EntityFolder.JUNK); + if (junk == null) + throw new IllegalArgumentException(context.getString(R.string.title_no_junk_folder)); + + if (!message.folder.equals(junk.id)) + EntityOperation.queue(context, message, EntityOperation.MOVE, junk.id, null, null, true); + + if (block_domain) { + List rules = EntityRule.blockSender(context, message, junk, block_domain); + for (EntityRule rule : rules) { + if (message.folder.equals(junk.id)) { + EntityFolder inbox = db.folder().getFolderByType(message.account, EntityFolder.INBOX); + if (inbox == null) + continue; + rule.folder = inbox.id; } + rule.id = db.rule().insertRule(rule); } - } else - db.message().deleteMessage(message.id); + } db.setTransactionSuccessful(); } finally { @@ -8902,6 +8938,49 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. }.execute(this, args, "message:junk"); } + private void onBlockSenders(Bundle args) { + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + long[] ids = args.getLongArray("ids"); + + DB db = DB.getInstance(context); + try { + db.beginTransaction(); + + List identities = db.identity().getComposableIdentities(null); + + for (long id : ids) { + EntityMessage message = db.message().getMessage(id); + if (message == null || message.fromSelf(identities)) + continue; + + EntityAccount account = db.account().getAccount(message.account); + if (account == null || account.protocol != EntityAccount.TYPE_POP) + continue; + + EntityContact.update(context, + message.account, message.identity, message.from, + EntityContact.TYPE_JUNK, message.received); + + db.message().deleteMessage(message.id); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + return null; + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Log.unexpectedError(getParentFragmentManager(), ex); + } + }.execute(this, args, "messages:block"); + } + private void onMoveAskAcross(final ArrayList result) { boolean across = false; for (MessageTarget target : result) @@ -9665,7 +9744,8 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences. boolean canJunk() { if (read_only) return false; - return (hasJunk && !isJunk && !isDrafts); + return (hasJunk && !isJunk && !isDrafts) || + (hasPop && !hasImap); } boolean canTrash() { diff --git a/app/src/main/java/eu/faircode/email/FragmentPop.java b/app/src/main/java/eu/faircode/email/FragmentPop.java index 53ea2a5507..b3132ebb7a 100644 --- a/app/src/main/java/eu/faircode/email/FragmentPop.java +++ b/app/src/main/java/eu/faircode/email/FragmentPop.java @@ -955,6 +955,11 @@ public class FragmentPop extends FragmentBase { hide.name = getString(R.string.title_hide); folders.add(hide); + EntityFolder junk = new EntityFolder(); + junk.id = EntityMessage.SWIPE_ACTION_JUNK; + junk.name = getString(R.string.title_report_spam); + folders.add(junk); + EntityFolder delete = new EntityFolder(); delete.id = EntityMessage.SWIPE_ACTION_DELETE; delete.name = getString(R.string.title_delete_permanently); diff --git a/app/src/main/res/layout/dialog_block_sender.xml b/app/src/main/res/layout/dialog_block_sender.xml new file mode 100644 index 0000000000..75f25c4916 --- /dev/null +++ b/app/src/main/res/layout/dialog_block_sender.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_junk.xml b/app/src/main/res/layout/dialog_junk.xml index 26986dbf4d..67e041464f 100644 --- a/app/src/main/res/layout/dialog_junk.xml +++ b/app/src/main/res/layout/dialog_junk.xml @@ -27,39 +27,16 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="12dp" + android:drawableEnd="@drawable/twotone_info_24" + android:drawablePadding="6dp" + android:drawableTint="?attr/colorAccent" android:minHeight="45dp" android:text="@string/title_junk_hint" android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textStyle="italic" - app:layout_constraintEnd_toStartOf="@+id/ibInfoProvider" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/tvMessage" /> - - - - + app:layout_constraintTop_toBottomOf="@+id/tvMessage" /> + app:layout_constraintTop_toBottomOf="@id/tvJunkHint" /> Blocked senders + + Block sender of %1$s message? + Block sender of %1$s messages? + + Use local spam filter This can increase battery usage and incorrectly mark messages as spam Use spam block lists