Added DNS blocklist check

pull/199/head
M66B 4 years ago
parent fd8cb3f68a
commit eae08d5e29

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 avatars;
private boolean color_stripe; private boolean color_stripe;
private boolean check_authentication; private boolean check_authentication;
private boolean check_mx;
private boolean check_blocklist;
private boolean check_reply_domain; private boolean check_reply_domain;
private MessageHelper.AddressFormat email_format; 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.dkim) && check_authentication) ||
(Boolean.FALSE.equals(message.spf) && check_authentication) || (Boolean.FALSE.equals(message.spf) && check_authentication) ||
(Boolean.FALSE.equals(message.dmarc) && 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.FALSE.equals(message.reply_domain) && check_reply_domain));
boolean expanded = (viewType == ViewType.THREAD && properties.getValue("expanded", message.id)); 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) if (result.size() > 0)
sb.append(context.getString(R.string.title_authentication_failed, TextUtils.join(", ", result))); 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 (Boolean.FALSE.equals(message.reply_domain)) {
if (sb.length() > 0) if (sb.length() > 0)
sb.append('\n'); sb.append('\n');
@ -5605,6 +5614,8 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
this.avatars = (contacts && avatars) || (gravatars || favicons || generated); this.avatars = (contacts && avatars) || (gravatars || favicons || generated);
this.color_stripe = prefs.getBoolean("color_stripe", true); this.color_stripe = prefs.getBoolean("color_stripe", true);
this.check_authentication = prefs.getBoolean("check_authentication", 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.check_reply_domain = prefs.getBoolean("check_reply_domain", true);
this.email_format = MessageHelper.getAddressFormat(context); this.email_format = MessageHelper.getAddressFormat(context);
@ -5727,6 +5738,10 @@ public class AdapterMessage extends RecyclerView.Adapter<AdapterMessage.ViewHold
same = false; same = false;
log("mx changed", next.id); 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)) { if (!Objects.equals(prev.reply_domain, next.reply_domain)) {
same = false; same = false;
log("reply_domain changed", next.id); log("reply_domain changed", next.id);

@ -3332,6 +3332,24 @@ class Core {
message.warning = Log.formatThrowable(ex, false); 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 { try {
db.beginTransaction(); db.beginTransaction();

@ -65,7 +65,7 @@ import static eu.faircode.email.ServiceAuthenticator.AUTH_TYPE_PASSWORD;
// https://developer.android.com/topic/libraries/architecture/room.html // https://developer.android.com/topic/libraries/architecture/room.html
@Database( @Database(
version = 199, version = 200,
entities = { entities = {
EntityIdentity.class, EntityIdentity.class,
EntityAccount.class, EntityAccount.class,
@ -2035,6 +2035,12 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `account` ADD COLUMN `capability_utf8` INTEGER"); db.execSQL("ALTER TABLE `account` ADD COLUMN `capability_utf8` INTEGER");
} }
}).addMigrations(new Migration(199, 200) { }).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 @Override
public void migrate(@NonNull SupportSQLiteDatabase db) { public void migrate(@NonNull SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion); Log.i("DB migration from version " + startVersion + " to " + endVersion);

@ -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 spf;
public Boolean dmarc; public Boolean dmarc;
public Boolean mx; public Boolean mx;
public Boolean blocklist;
public Boolean reply_domain; // differs from 'from' public Boolean reply_domain; // differs from 'from'
public String avatar; // lookup URI from sender public String avatar; // lookup URI from sender
public String sender; // sort key: from email address 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.spf, other.spf) &&
Objects.equals(this.dmarc, other.dmarc) && Objects.equals(this.dmarc, other.dmarc) &&
Objects.equals(this.mx, other.mx) && Objects.equals(this.mx, other.mx) &&
Objects.equals(this.blocklist, other.blocklist) &&
Objects.equals(this.reply_domain, other.reply_domain) && Objects.equals(this.reply_domain, other.reply_domain) &&
Objects.equals(this.avatar, other.avatar) && Objects.equals(this.avatar, other.avatar) &&
Objects.equals(this.sender, other.sender) && Objects.equals(this.sender, other.sender) &&

@ -24,6 +24,7 @@ import android.app.TimePickerDialog;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
@ -84,6 +85,8 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
private SwitchCompat swCheckAuthentication; private SwitchCompat swCheckAuthentication;
private SwitchCompat swCheckReply; private SwitchCompat swCheckReply;
private SwitchCompat swCheckMx; private SwitchCompat swCheckMx;
private SwitchCompat swCheckBlocklist;
private TextView tvCheckBlocklistHint;
private SwitchCompat swTuneKeepAlive; private SwitchCompat swTuneKeepAlive;
private Group grpExempted; private Group grpExempted;
@ -93,7 +96,7 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
"enabled", "poll_interval", "auto_optimize", "schedule", "schedule_start", "schedule_end", "enabled", "poll_interval", "auto_optimize", "schedule", "schedule_start", "schedule_end",
"sync_nodate", "sync_unseen", "sync_flagged", "delete_unseen", "sync_kept", "gmail_thread_id", "sync_nodate", "sync_unseen", "sync_flagged", "delete_unseen", "sync_kept", "gmail_thread_id",
"sync_folders", "sync_shared_folders", "subscriptions", "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 @Override
@ -139,6 +142,8 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
swCheckAuthentication = view.findViewById(R.id.swCheckAuthentication); swCheckAuthentication = view.findViewById(R.id.swCheckAuthentication);
swCheckReply = view.findViewById(R.id.swCheckReply); swCheckReply = view.findViewById(R.id.swCheckReply);
swCheckMx = view.findViewById(R.id.swCheckMx); swCheckMx = view.findViewById(R.id.swCheckMx);
swCheckBlocklist = view.findViewById(R.id.swCheckBlocklist);
tvCheckBlocklistHint = view.findViewById(R.id.tvCheckBlocklistHint);
swTuneKeepAlive = view.findViewById(R.id.swTuneKeepAlive); swTuneKeepAlive = view.findViewById(R.id.swTuneKeepAlive);
grpExempted = view.findViewById(R.id.grpExempted); 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() { swTuneKeepAlive.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override @Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { 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); PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this);
return view; return view;
@ -429,6 +442,7 @@ public class FragmentOptionsSynchronize extends FragmentBase implements SharedPr
swCheckAuthentication.setChecked(prefs.getBoolean("check_authentication", true)); swCheckAuthentication.setChecked(prefs.getBoolean("check_authentication", true));
swCheckReply.setChecked(prefs.getBoolean("check_reply_domain", true)); swCheckReply.setChecked(prefs.getBoolean("check_reply_domain", true));
swCheckMx.setChecked(prefs.getBoolean("check_mx", false)); swCheckMx.setChecked(prefs.getBoolean("check_mx", false));
swCheckBlocklist.setChecked(prefs.getBoolean("check_blocklist", false));
swTuneKeepAlive.setChecked(prefs.getBoolean("tune_keep_alive", true)); swTuneKeepAlive.setChecked(prefs.getBoolean("tune_keep_alive", true));
} }

@ -630,7 +630,7 @@
app:layout_constraintTop_toBottomOf="@id/swCheckMx" /> app:layout_constraintTop_toBottomOf="@id/swCheckMx" />
<eu.faircode.email.FixedTextView <eu.faircode.email.FixedTextView
android:id="@+id/tvDelayHint" android:id="@+id/tvCheckMxWarning"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="48dp" android:layout_marginEnd="48dp"
@ -642,6 +642,42 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvCheckMxHint" /> 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 <androidx.appcompat.widget.SwitchCompat
android:id="@+id/swTuneKeepAlive" android:id="@+id/swTuneKeepAlive"
android:layout_width="0dp" android:layout_width="0dp"
@ -651,7 +687,7 @@
android:text="@string/title_advanced_tune_keep_alive" android:text="@string/title_advanced_tune_keep_alive"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvDelayHint" app:layout_constraintTop_toBottomOf="@id/tvCheckBlocklistWarning"
app:switchPadding="12dp" /> app:switchPadding="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>

@ -308,6 +308,7 @@
<string name="title_advanced_check_authentication">Check message authentication</string> <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_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_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_tune_keep_alive">Automatically tune the keep-alive interval</string>
<string name="title_advanced_keyboard">Show keyboard by default</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_move_undo">Moving to %1$s (%2$d)</string>
<string name="title_open_with">Open with</string> <string name="title_open_with">Open with</string>
<string name="title_authentication_failed">%1$s authentication failed</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_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> <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…
Cancel
Save