diff --git a/FAQ.md b/FAQ.md
index 6ab419fb90..e632614985 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -276,7 +276,7 @@ Fonts, sizes, colors, etc should be material design whenever possible.
* [(151) Can you add backup/restore messages?](#user-content-faq151)
* [(152) How can I insert a contact group?](#user-content-faq152)
* [(153) Why does permanently deleting Gmail message not work?](#user-content-faq153)
-* [(154) Can you add favicons as contact photos?](#user-content-faq154)
+* [~~(154) Can you add favicons as contact photos?~~](#user-content-faq154)
* [(155) What is a winmail.dat file?](#user-content-faq155)
* [(156) How can I set up an Office365 account?](#user-content-faq156)
* [(157) How can I set up an Free.fr account?](#user-content-faq157)
@@ -3045,10 +3045,10 @@ Some background: Gmail seems to have an additional message view for IMAP, which
-**(154) Can you add favicons as contact photos?**
+**~~(154) Can you add favicons as contact photos?~~**
-Besides that a [favicon](https://en.wikipedia.org/wiki/Favicon) might be shared by many email addresses with the same domain name
-and therefore is not directly related to an email address, favicons can be used to track you.
+~~Besides that a [favicon](https://en.wikipedia.org/wiki/Favicon) might be shared by many email addresses with the same domain name~~
+~~and therefore is not directly related to an email address, favicons can be used to track you.~~
diff --git a/app/src/main/java/eu/faircode/email/ContactInfo.java b/app/src/main/java/eu/faircode/email/ContactInfo.java
index 62d28fdd84..b36f4c6933 100644
--- a/app/src/main/java/eu/faircode/email/ContactInfo.java
+++ b/app/src/main/java/eu/faircode/email/ContactInfo.java
@@ -36,14 +36,24 @@ import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -51,6 +61,7 @@ import java.util.concurrent.ExecutorService;
import javax.mail.Address;
import javax.mail.internet.InternetAddress;
+import javax.net.ssl.HttpsURLConnection;
public class ContactInfo {
private String email;
@@ -63,13 +74,20 @@ public class ContactInfo {
private static Map emailLookup = new ConcurrentHashMap<>();
private static final Map emailContactInfo = new HashMap<>();
private static final Map emailGravatar = new HashMap<>();
+ private static final List emailFaviconBlacklist = new ArrayList<>();
+
private static final ExecutorService executor =
Helper.getBackgroundExecutor(1, "contact");
private static final int GRAVATAR_TIMEOUT = 5 * 1000; // milliseconds
+ private static final int FAVICON_TIMEOUT = 15 * 1000; // milliseconds
private static final long CACHE_CONTACT_DURATION = 2 * 60 * 1000L; // milliseconds
private static final long CACHE_GRAVATAR_DURATION = 2 * 60 * 60 * 1000L; // milliseconds
+ static {
+ emailFaviconBlacklist.add("gmail.com");
+ }
+
private ContactInfo() {
}
@@ -151,10 +169,12 @@ public class ContactInfo {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean avatars = prefs.getBoolean("avatars", true);
boolean gravatars = prefs.getBoolean("gravatars", false);
+ boolean favicons = prefs.getBoolean("favicons", false);
boolean generated = prefs.getBoolean("generated_icons", true);
boolean identicons = prefs.getBoolean("identicons", false);
boolean circular = prefs.getBoolean("circular", true);
+ // Contact photo
if (!TextUtils.isEmpty(info.email) &&
Helper.hasPermission(context, Manifest.permission.READ_CONTACTS)) {
ContentResolver resolver = context.getContentResolver();
@@ -195,6 +215,7 @@ public class ContactInfo {
}
}
+ // Gravatar
if (info.bitmap == null) {
if (gravatars && !TextUtils.isEmpty(info.email)) {
String gkey = info.email.toLowerCase(Locale.ROOT);
@@ -243,6 +264,78 @@ public class ContactInfo {
}
}
+ // Favicon
+ if (info.bitmap == null) {
+ int at = (info.email == null ? -1 : info.email.indexOf('@'));
+ String domain = (at < 0 ? null : info.email.substring(at + 1).toLowerCase(Locale.ROOT));
+ synchronized (emailFaviconBlacklist) {
+ if (emailFaviconBlacklist.contains(domain)) {
+ Log.i("Favicon blacklisted domain=" + domain);
+ domain = null;
+ }
+ }
+
+ if (favicons && domain != null) {
+ try {
+ File dir = new File(context.getCacheDir(), "favicons");
+ if (!dir.exists())
+ dir.mkdir();
+ File file = new File(dir, domain);
+ if (file.exists())
+ info.bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
+ else {
+ URL base = new URL("https://" + domain);
+
+ info.bitmap = getFavicon(new URL(base, "favicon.ico"));
+ if (info.bitmap == null) {
+ Log.i("GET " + base);
+ HttpsURLConnection connection = (HttpsURLConnection) base.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setReadTimeout(FAVICON_TIMEOUT);
+ connection.setConnectTimeout(FAVICON_TIMEOUT);
+ connection.connect();
+
+ String response;
+ try {
+ response = Helper.readStream(connection.getInputStream(), StandardCharsets.UTF_8.name());
+ } finally {
+ connection.disconnect();
+ }
+
+ Document doc = JsoupEx.parse(response);
+
+ Element link = doc.head().select("link[href~=.*\\.(ico|png)]").first();
+ String favicon = (link == null ? null : link.attr("href"));
+
+ if (TextUtils.isEmpty(favicon)) {
+ Element meta = doc.head().select("meta[itemprop=image]").first();
+ favicon = (meta == null ? null : meta.attr("content"));
+ }
+
+ if (!TextUtils.isEmpty(favicon)) {
+ URL url = new URL(base, favicon);
+ if ("https".equals(url.getProtocol()))
+ info.bitmap = getFavicon(url);
+ }
+ }
+
+ if (info.bitmap != null)
+ try (OutputStream os = new BufferedOutputStream(new FileOutputStream(file))) {
+ info.bitmap.compress(Bitmap.CompressFormat.PNG, 90, os);
+ }
+ }
+ } catch (Throwable ex) {
+ Log.w(ex);
+ } finally {
+ if (info.bitmap == null)
+ synchronized (emailFaviconBlacklist) {
+ emailFaviconBlacklist.add(domain);
+ }
+ }
+ }
+ }
+
+ // Generated
boolean identicon = false;
if (info.bitmap == null) {
int dp = Helper.dp2pixels(context, 96);
@@ -277,6 +370,29 @@ public class ContactInfo {
return info;
}
+ private static Bitmap getFavicon(URL url) throws IOException {
+ try {
+ Log.i("GET favicon " + url);
+
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setReadTimeout(FAVICON_TIMEOUT);
+ connection.setConnectTimeout(FAVICON_TIMEOUT);
+ connection.connect();
+
+ try {
+ return BitmapFactory.decodeStream(connection.getInputStream());
+ } finally {
+ connection.disconnect();
+ }
+ } catch (IOException ex) {
+ Log.w(ex);
+ if (ex instanceof SocketTimeoutException)
+ throw ex;
+ return null;
+ }
+ }
+
static void init(final Context context) {
if (Helper.hasPermission(context, Manifest.permission.READ_CONTACTS)) {
Handler handler = new Handler(Looper.getMainLooper());
diff --git a/app/src/main/java/eu/faircode/email/FragmentOptions.java b/app/src/main/java/eu/faircode/email/FragmentOptions.java
index 3dcb11a60b..3d017d2234 100644
--- a/app/src/main/java/eu/faircode/email/FragmentOptions.java
+++ b/app/src/main/java/eu/faircode/email/FragmentOptions.java
@@ -80,7 +80,7 @@ public class FragmentOptions extends FragmentBase {
"subscriptions",
"landscape", "landscape3", "startup", "cards", "indentation", "date", "threading", "threading_unread",
"highlight_unread", "color_stripe",
- "avatars", "gravatars", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold",
+ "avatars", "gravatars", "favicons", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold",
"name_email", "prefer_contact", "distinguish_contacts", "show_recipients", "authentication",
"subject_top", "font_size_sender", "font_size_subject", "subject_italic", "highlight_subject", "subject_ellipsize",
"keywords_header", "labels_header", "flags", "flags_background", "preview", "preview_italic", "preview_lines",
diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java b/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java
index 3a5c2787b3..c3739655cc 100644
--- a/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java
+++ b/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java
@@ -75,6 +75,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
private SwitchCompat swAvatars;
private TextView tvGravatarsHint;
private SwitchCompat swGravatars;
+ private SwitchCompat swFavicons;
private SwitchCompat swGeneratedIcons;
private SwitchCompat swIdenticons;
private SwitchCompat swCircular;
@@ -122,7 +123,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
"theme", "startup", "cards", "date", "navbar_colorize", "landscape", "landscape3",
"threading", "threading_unread", "indentation", "seekbar", "actionbar", "actionbar_color",
"highlight_unread", "color_stripe",
- "avatars", "gravatars", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold",
+ "avatars", "gravatars", "favicons", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold",
"name_email", "prefer_contact", "distinguish_contacts", "show_recipients",
"subject_top", "font_size_sender", "font_size_subject", "subject_italic", "highlight_subject", "subject_ellipsize",
"keywords_header", "labels_header", "flags", "flags_background",
@@ -163,6 +164,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
swAvatars = view.findViewById(R.id.swAvatars);
swGravatars = view.findViewById(R.id.swGravatars);
tvGravatarsHint = view.findViewById(R.id.tvGravatarsHint);
+ swFavicons = view.findViewById(R.id.swFavicons);
swGeneratedIcons = view.findViewById(R.id.swGeneratedIcons);
swIdenticons = view.findViewById(R.id.swIdenticons);
swCircular = view.findViewById(R.id.swCircular);
@@ -344,6 +346,14 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
}
});
+ swFavicons.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
+ prefs.edit().putBoolean("favicons", checked).apply();
+ ContactInfo.clearCache();
+ }
+ });
+
tvGravatarsHint.getPaint().setUnderlineText(true);
tvGravatarsHint.setOnClickListener(new View.OnClickListener() {
@Override
@@ -757,6 +767,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer
swColorStripe.setChecked(prefs.getBoolean("color_stripe", true));
swAvatars.setChecked(prefs.getBoolean("avatars", true));
swGravatars.setChecked(prefs.getBoolean("gravatars", false));
+ swFavicons.setChecked(prefs.getBoolean("favicons", false));
swGeneratedIcons.setChecked(prefs.getBoolean("generated_icons", true));
swIdenticons.setChecked(prefs.getBoolean("identicons", false));
swIdenticons.setEnabled(swGeneratedIcons.isChecked());
diff --git a/app/src/main/res/layout/fragment_options_display.xml b/app/src/main/res/layout/fragment_options_display.xml
index 50f102e114..4be2fc4e00 100644
--- a/app/src/main/res/layout/fragment_options_display.xml
+++ b/app/src/main/res/layout/fragment_options_display.xml
@@ -349,6 +349,29 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/swGravatars" />
+
+
+
+
Show color stripe
Show contact photos
Show Gravatars
+ Show favicons
Show generated icons
Show identicons
Show round icons