mirror of
https://github.com/M66B/FairEmail.git
synced 2025-03-19 02:15:28 +00:00
Added DNS blocklist check
This commit is contained in:
parent
fd8cb3f68a
commit
eae08d5e29
9 changed files with 2732 additions and 5 deletions
2478
app/schemas/eu.faircode.email.DB/200.json
Normal file
2478
app/schemas/eu.faircode.email.DB/200.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -246,6 +246,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
private boolean avatars;
|
||||
private boolean color_stripe;
|
||||
private boolean check_authentication;
|
||||
private boolean check_mx;
|
||||
private boolean check_blocklist;
|
||||
private boolean check_reply_domain;
|
||||
|
||||
private MessageHelper.AddressFormat email_format;
|
||||
|
@ -958,7 +960,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
!((Boolean.FALSE.equals(message.dkim) && check_authentication) ||
|
||||
(Boolean.FALSE.equals(message.spf) && check_authentication) ||
|
||||
(Boolean.FALSE.equals(message.dmarc) && check_authentication) ||
|
||||
Boolean.FALSE.equals(message.mx) ||
|
||||
(Boolean.FALSE.equals(message.mx) && check_mx) ||
|
||||
(Boolean.TRUE.equals(message.blocklist) && check_blocklist) ||
|
||||
(Boolean.FALSE.equals(message.reply_domain) && check_reply_domain));
|
||||
boolean expanded = (viewType == ViewType.THREAD && properties.getValue("expanded", message.id));
|
||||
|
||||
|
@ -3436,6 +3439,12 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
if (result.size() > 0)
|
||||
sb.append(context.getString(R.string.title_authentication_failed, TextUtils.join(", ", result)));
|
||||
|
||||
if (Boolean.TRUE.equals(message.blocklist)) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
sb.append(context.getString(R.string.title_on_blocklist));
|
||||
}
|
||||
|
||||
if (Boolean.FALSE.equals(message.reply_domain)) {
|
||||
if (sb.length() > 0)
|
||||
sb.append('\n');
|
||||
|
@ -5605,6 +5614,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
this.avatars = (contacts && avatars) || (gravatars || favicons || generated);
|
||||
this.color_stripe = prefs.getBoolean("color_stripe", true);
|
||||
this.check_authentication = prefs.getBoolean("check_authentication", true);
|
||||
this.check_mx = prefs.getBoolean("check_mx", false);
|
||||
this.check_blocklist = prefs.getBoolean("check_blocklist", false);
|
||||
this.check_reply_domain = prefs.getBoolean("check_reply_domain", true);
|
||||
|
||||
this.email_format = MessageHelper.getAddressFormat(context);
|
||||
|
@ -5727,6 +5738,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
|
|||
same = false;
|
||||
log("mx changed", next.id);
|
||||
}
|
||||
if (!Objects.equals(prev.blocklist, next.blocklist)) {
|
||||
same = false;
|
||||
log("blocklist changed", next.id);
|
||||
}
|
||||
if (!Objects.equals(prev.reply_domain, next.reply_domain)) {
|
||||
same = false;
|
||||
log("reply_domain changed", next.id);
|
||||
|
|
|
@ -3332,6 +3332,24 @@ class Core {
|
|||
message.warning = Log.formatThrowable(ex, false);
|
||||
}
|
||||
|
||||
boolean check_blocklist = prefs.getBoolean("check_blocklist", false);
|
||||
if (check_blocklist) {
|
||||
List<Address> senders = new ArrayList<>();
|
||||
if (message.from != null)
|
||||
senders.addAll(Arrays.asList(message.from));
|
||||
if (message.reply != null)
|
||||
senders.addAll(Arrays.asList(message.reply));
|
||||
boolean blocklist = false;
|
||||
for (Address sender : senders) {
|
||||
String email = ((InternetAddress) sender).getAddress();
|
||||
if (DnsBlockList.isJunk(email)) {
|
||||
blocklist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
message.blocklist = blocklist;
|
||||
}
|
||||
|
||||
try {
|
||||
db.beginTransaction();
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
|
|||
// https://developer.android.com/topic/libraries/architecture/room.html
|
||||
|
||||
@Database(
|
||||
version = 199,
|
||||
version = 200,
|
||||
entities = {
|
||||
EntityIdentity.class,
|
||||
EntityAccount.class,
|
||||
|
@ -2035,6 +2035,12 @@ public abstract class DB extends RoomDatabase {
|
|||
db.execSQL("ALTER TABLE `account` ADD COLUMN `capability_utf8` INTEGER");
|
||||
}
|
||||
}).addMigrations(new Migration(199, 200) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
Log.i("DB migration from version " + startVersion + " to " + endVersion);
|
||||
db.execSQL("ALTER TABLE `message` ADD COLUMN `blocklist` INTEGER");
|
||||
}
|
||||
}).addMigrations(new Migration(200, 201) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase db) {
|
||||
Log.i("DB migration from version " + startVersion + " to " + endVersion);
|
||||
|
|
156
app/src/main/java/eu/faircode/email/DnsBlockList.java
Normal file
156
app/src/main/java/eu/faircode/email/DnsBlockList.java
Normal file
|
@ -0,0 +1,156 @@
|
|||
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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
Copyright 2018-2021 by Marcel Bokhorst (M66B)
|
||||
*/
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Date;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
|
||||
public class DnsBlockList {
|
||||
// https://www.spamhaus.org/zen/
|
||||
static String[] DEFAULT_BLOCKLISTS = new String[]{"zen.spamhaus.org"};
|
||||
private static final long CACHE_EXPIRY_AFTER = 3600 * 1000L; // milliseconds
|
||||
private static final Map<InetAddress, CacheEntry> cache = new Hashtable<>();
|
||||
|
||||
static boolean isJunk(String email) {
|
||||
return isJunk(email, DEFAULT_BLOCKLISTS);
|
||||
}
|
||||
|
||||
static boolean isJunk(String email, String[] blocklists) {
|
||||
if (TextUtils.isEmpty(email))
|
||||
return false;
|
||||
int at = email.indexOf('@');
|
||||
if (at < 0)
|
||||
return false;
|
||||
String domain = email.substring(at + 1);
|
||||
for (String blocklist : blocklists)
|
||||
if (isJunk(domain, blocklist))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isJunk(String domain, String blocklist) {
|
||||
boolean blocked = false;
|
||||
try {
|
||||
for (InetAddress addr : InetAddress.getAllByName(domain))
|
||||
try {
|
||||
synchronized (cache) {
|
||||
CacheEntry cached = cache.get(addr);
|
||||
if (cached != null && !cached.isExpired())
|
||||
return cached.isJunk();
|
||||
}
|
||||
|
||||
StringBuilder lookup = new StringBuilder();
|
||||
if (addr instanceof Inet4Address) {
|
||||
byte[] a = addr.getAddress();
|
||||
for (int i = 3; i >= 0; i--)
|
||||
lookup.append(a[i] & 0xff).append('.');
|
||||
} else if (addr instanceof Inet6Address) {
|
||||
byte[] a = addr.getAddress();
|
||||
for (int i = 15; i >= 0; i--) {
|
||||
int b = a[i] & 0xff;
|
||||
lookup.append(String.format("%01x", b & 0xf)).append('.');
|
||||
lookup.append(String.format("%01x", b >> 4)).append('.');
|
||||
}
|
||||
}
|
||||
|
||||
lookup.append(blocklist);
|
||||
|
||||
InetAddress result;
|
||||
try {
|
||||
result = InetAddress.getByName(lookup.toString());
|
||||
if (result instanceof Inet4Address) {
|
||||
/*
|
||||
https://www.spamhaus.org/faq/section/DNSBL%20Usage#200
|
||||
|
||||
127.0.0.2 SBL Spamhaus SBL Data
|
||||
127.0.0.3 SBL Spamhaus SBL CSS Data
|
||||
127.0.0.4 XBL CBL Data
|
||||
127.0.0.9 SBL Spamhaus DROP/EDROP Data (in addition to 127.0.0.2, since 01-Jun-2016)
|
||||
127.0.0.10 PBL ISP Maintained
|
||||
127.0.0.11 PBL Spamhaus Maintained
|
||||
*/
|
||||
|
||||
byte[] a = result.getAddress();
|
||||
int statusClass = a[1] & 0xFF;
|
||||
int statusCode = a[3] & 0xFF;
|
||||
if (statusClass != 0 ||
|
||||
(statusCode != 2 &&
|
||||
statusCode != 3 &&
|
||||
statusCode != 4 &&
|
||||
statusCode != 9)) {
|
||||
Log.w("isJunk" +
|
||||
" addr=" + addr +
|
||||
" lookup=" + lookup +
|
||||
" result=" + result +
|
||||
" status=" + statusClass + "/" + statusCode);
|
||||
result = null;
|
||||
}
|
||||
} else {
|
||||
Log.w("isJunk result=" + result);
|
||||
result = null;
|
||||
}
|
||||
} catch (UnknownHostException ignored) {
|
||||
// Not blocked
|
||||
result = null;
|
||||
}
|
||||
|
||||
Log.i("isJunk " + addr + " " + lookup + "=" + (result == null ? "false" : result));
|
||||
|
||||
synchronized (cache) {
|
||||
cache.put(addr, new CacheEntry(result));
|
||||
}
|
||||
|
||||
if (result != null)
|
||||
blocked = true;
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
}
|
||||
} catch (Throwable ex) {
|
||||
Log.w(ex);
|
||||
}
|
||||
|
||||
return blocked;
|
||||
}
|
||||
|
||||
private static class CacheEntry {
|
||||
private final long time;
|
||||
private final InetAddress result;
|
||||
|
||||
CacheEntry(InetAddress result) {
|
||||
this.time = new Date().getTime();
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return (new Date().getTime() - this.time) > CACHE_EXPIRY_AFTER;
|
||||
}
|
||||
|
||||
boolean isJunk() {
|
||||
return (this.result != null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -144,6 +144,7 @@ public class EntityMessage implements Serializable {
|
|||
public Boolean spf;
|
||||
public Boolean dmarc;
|
||||
public Boolean mx;
|
||||
public Boolean blocklist;
|
||||
public Boolean reply_domain; // differs from 'from'
|
||||
public String avatar; // lookup URI from sender
|
||||
public String sender; // sort key: from email address
|
||||
|
@ -542,6 +543,7 @@ public class EntityMessage implements Serializable {
|
|||
Objects.equals(this.spf, other.spf) &&
|
||||
Objects.equals(this.dmarc, other.dmarc) &&
|
||||
Objects.equals(this.mx, other.mx) &&
|
||||
Objects.equals(this.blocklist, other.blocklist) &&
|
||||
Objects.equals(this.reply_domain, other.reply_domain) &&
|
||||
Objects.equals(this.avatar, other.avatar) &&
|
||||
Objects.equals(this.sender, other.sender) &&
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.app.TimePickerDialog;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
|
@ -84,6 +85,8 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
|
|||
private SwitchCompat swCheckAuthentication;
|
||||
private SwitchCompat swCheckReply;
|
||||
private SwitchCompat swCheckMx;
|
||||
private SwitchCompat swCheckBlocklist;
|
||||
private TextView tvCheckBlocklistHint;
|
||||
private SwitchCompat swTuneKeepAlive;
|
||||
private Group grpExempted;
|
||||
|
||||
|
@ -93,7 +96,7 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
|
|||
"enabled", "poll_interval", "auto_optimize", "schedule", "schedule_start", "schedule_end",
|
||||
"sync_nodate", "sync_unseen", "sync_flagged", "delete_unseen", "sync_kept", "gmail_thread_id",
|
||||
"sync_folders", "sync_shared_folders", "subscriptions",
|
||||
"check_authentication", "check_reply_domain", "check_mx", "tune_keep_alive"
|
||||
"check_authentication", "check_reply_domain", "check_mx", "check_blocklist", "tune_keep_alive"
|
||||
};
|
||||
|
||||
@Override
|
||||
|
@ -139,6 +142,8 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
|
|||
swCheckAuthentication = view.findViewById(R.id.swCheckAuthentication);
|
||||
swCheckReply = view.findViewById(R.id.swCheckReply);
|
||||
swCheckMx = view.findViewById(R.id.swCheckMx);
|
||||
swCheckBlocklist = view.findViewById(R.id.swCheckBlocklist);
|
||||
tvCheckBlocklistHint = view.findViewById(R.id.tvCheckBlocklistHint);
|
||||
swTuneKeepAlive = view.findViewById(R.id.swTuneKeepAlive);
|
||||
grpExempted = view.findViewById(R.id.grpExempted);
|
||||
|
||||
|
@ -334,6 +339,13 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
|
|||
}
|
||||
});
|
||||
|
||||
swCheckBlocklist.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
prefs.edit().putBoolean("check_blocklist", checked).apply();
|
||||
}
|
||||
});
|
||||
|
||||
swTuneKeepAlive.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
|
||||
|
@ -359,6 +371,7 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
|
|||
}
|
||||
});
|
||||
|
||||
tvCheckBlocklistHint.setText(TextUtils.join(",", DnsBlockList.DEFAULT_BLOCKLISTS));
|
||||
PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
return view;
|
||||
|
@ -429,6 +442,7 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
|
|||
swCheckAuthentication.setChecked(prefs.getBoolean("check_authentication", true));
|
||||
swCheckReply.setChecked(prefs.getBoolean("check_reply_domain", true));
|
||||
swCheckMx.setChecked(prefs.getBoolean("check_mx", false));
|
||||
swCheckBlocklist.setChecked(prefs.getBoolean("check_blocklist", false));
|
||||
swTuneKeepAlive.setChecked(prefs.getBoolean("tune_keep_alive", true));
|
||||
}
|
||||
|
||||
|
|
|
@ -630,7 +630,7 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/swCheckMx" />
|
||||
|
||||
<eu.faircode.email.FixedTextView
|
||||
android:id="@+id/tvDelayHint"
|
||||
android:id="@+id/tvCheckMxWarning"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="48dp"
|
||||
|
@ -642,6 +642,42 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCheckMxHint" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/swCheckBlocklist"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/title_advanced_check_blocklist"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCheckMxWarning"
|
||||
app:switchPadding="12dp" />
|
||||
|
||||
<eu.faircode.email.FixedTextView
|
||||
android:id="@+id/tvCheckBlocklistHint"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:text="blocklists"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/swCheckBlocklist" />
|
||||
|
||||
<eu.faircode.email.FixedTextView
|
||||
android:id="@+id/tvCheckBlocklistWarning"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="48dp"
|
||||
android:text="@string/title_advanced_sync_delay_hint"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="?attr/colorWarning"
|
||||
android:textStyle="italic"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCheckBlocklistHint" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/swTuneKeepAlive"
|
||||
android:layout_width="0dp"
|
||||
|
@ -651,7 +687,7 @@
|
|||
android:text="@string/title_advanced_tune_keep_alive"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvDelayHint"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvCheckBlocklistWarning"
|
||||
app:switchPadding="12dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
|
@ -308,6 +308,7 @@
|
|||
<string name="title_advanced_check_authentication">Check message authentication</string>
|
||||
<string name="title_advanced_check_reply_domain">Check reply address on synchronizing messages</string>
|
||||
<string name="title_advanced_check_mx">Check sender email addresses on synchronizing messages</string>
|
||||
<string name="title_advanced_check_blocklist">Check if the sender\'s domain name is on a spam block list</string>
|
||||
<string name="title_advanced_tune_keep_alive">Automatically tune the keep-alive interval</string>
|
||||
|
||||
<string name="title_advanced_keyboard">Show keyboard by default</string>
|
||||
|
@ -970,6 +971,7 @@
|
|||
<string name="title_move_undo">Moving to %1$s (%2$d)</string>
|
||||
<string name="title_open_with">Open with</string>
|
||||
<string name="title_authentication_failed">%1$s authentication failed</string>
|
||||
<string name="title_on_blocklist">On blocklist</string>
|
||||
|
||||
<string name="title_receipt_subject">Read receipt: %1$s</string>
|
||||
<string name="title_receipt_text">This read receipt only acknowledges that the message was displayed. There is no guarantee that the recipient has read the message contents.</string>
|
||||
|
|
Loading…
Add table
Reference in a new issue