diff --git a/app/src/dummy/java/eu/faircode/email/Extra.java b/app/src/dummy/java/eu/faircode/email/Extra.java deleted file mode 100644 index 67ff455d1f..0000000000 --- a/app/src/dummy/java/eu/faircode/email/Extra.java +++ /dev/null @@ -1,45 +0,0 @@ -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.content.Context; -import android.os.OperationCanceledException; - -import java.util.concurrent.Callable; - -public class Extra { - static Callable getG(String email, int scaleToPixels, Context context) { - return new Callable() { - @Override - public ContactInfo.Favicon call() throws Exception { - throw new OperationCanceledException(); - } - }; - } - - static Callable getL(String email, int scaleToPixels, Context context) { - return new Callable() { - @Override - public ContactInfo.Favicon call() throws Exception { - throw new OperationCanceledException(); - } - }; - } -} \ No newline at end of file diff --git a/app/src/extra/java/eu/faircode/email/Extra.java b/app/src/extra/java/eu/faircode/email/Extra.java deleted file mode 100644 index ac669af3e0..0000000000 --- a/app/src/extra/java/eu/faircode/email/Extra.java +++ /dev/null @@ -1,117 +0,0 @@ -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-2021 by Marcel Bokhorst (M66B) -*/ - -import android.content.Context; -import android.graphics.Bitmap; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.concurrent.Callable; - -public class Extra { - private static final String GRAVATAR_URI = "https://www.gravatar.com/avatar/"; - private static final int GRAVATAR_CONNECT_TIMEOUT = 5 * 1000; // milliseconds - private static final int GRAVATAR_READ_TIMEOUT = 10 * 1000; // milliseconds - private static final int LIBRAVATAR_CONNECT_TIMEOUT = 5 * 1000; // milliseconds - private static final int LIBRAVATAR_READ_TIMEOUT = 10 * 1000; // milliseconds - private static final String LIBRAVATAR_DNS = "_avatars-sec._tcp,_avatars._tcp"; - private static final String LIBRAVATAR_URI = "https://seccdn.libravatar.org/avatar/"; - - static Callable getG(String email, int scaleToPixels, Context context) { - return new Callable() { - @Override - public ContactInfo.Favicon call() throws Exception { - String hash = Helper.md5(email.getBytes()); - URL url = new URL(GRAVATAR_URI + hash + "?d=404"); - Log.i("Gravatar key=" + email + " url=" + url); - - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setRequestMethod("GET"); - urlConnection.setReadTimeout(GRAVATAR_READ_TIMEOUT); - urlConnection.setConnectTimeout(GRAVATAR_CONNECT_TIMEOUT); - urlConnection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context)); - urlConnection.connect(); - - try { - int status = urlConnection.getResponseCode(); - if (status == HttpURLConnection.HTTP_OK) { - // Positive reply - Bitmap bitmap = ImageHelper.getScaledBitmap(urlConnection.getInputStream(), url.toString(), null, scaleToPixels); - return (bitmap == null ? null : new ContactInfo.Favicon(bitmap, "extra", false)); - } else if (status == HttpURLConnection.HTTP_NOT_FOUND) { - // Negative reply - return null; - } else - throw new IOException("Error " + status + ": " + urlConnection.getResponseMessage()); - } finally { - urlConnection.disconnect(); - } - } - }; - } - - static Callable getL(String email, int scaleToPixels, Context context) { - return new Callable() { - @Override - public ContactInfo.Favicon call() throws Exception { - String domain = UriHelper.getEmailDomain(email); - - // https://wiki.libravatar.org/api/ - String baseUrl = LIBRAVATAR_URI; - for (String dns : LIBRAVATAR_DNS.split(",")) { - DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns + "." + domain, "srv"); - if (records.length > 0) { - baseUrl = (records[0].port == 443 ? "https" : "http") + "://" + records[0].name + "/avatar/"; - break; - } - } - - String hash = Helper.md5(email.getBytes()); - - URL url = new URL(baseUrl + hash + "?d=404"); - Log.i("Libravatar key=" + email + " url=" + url); - - HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setRequestMethod("GET"); - urlConnection.setReadTimeout(LIBRAVATAR_READ_TIMEOUT); - urlConnection.setConnectTimeout(LIBRAVATAR_CONNECT_TIMEOUT); - urlConnection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context)); - urlConnection.connect(); - - try { - int status = urlConnection.getResponseCode(); - if (status == HttpURLConnection.HTTP_OK) { - // Positive reply - Bitmap bitmap = ImageHelper.getScaledBitmap(urlConnection.getInputStream(), url.toString(), null, scaleToPixels); - return (bitmap == null ? null : new ContactInfo.Favicon(bitmap, "extra", false)); - } else if (status == HttpURLConnection.HTTP_NOT_FOUND) { - // Negative reply - return null; - } else - throw new IOException("Error " + status + ": " + urlConnection.getResponseMessage()); - } finally { - urlConnection.disconnect(); - } - } - }; - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 8d53023f05..ab14191593 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -6880,9 +6880,10 @@ public class AdapterMessage extends RecyclerView.Adapter REL_EXCLUDE = Collections.unmodifiableList(Arrays.asList( @@ -133,6 +141,10 @@ public class ContactInfo { return type; } + boolean isEmailBased() { + return ("gravatar".equals(type) || "libravatar".equals(type)); + } + boolean isVerified() { return (bitmap != null && verified); } @@ -254,9 +266,10 @@ public class ContactInfo { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean avatars = prefs.getBoolean("avatars", true); - boolean bimi = (prefs.getBoolean("bimi", false) && !BuildConfig.PLAY_STORE_RELEASE); - boolean efavicons = (prefs.getBoolean("efavicons", false) && !BuildConfig.PLAY_STORE_RELEASE); - boolean favicons = (prefs.getBoolean("favicons", false) && !BuildConfig.PLAY_STORE_RELEASE); + boolean bimi = prefs.getBoolean("bimi", false); + boolean gravatars = prefs.getBoolean("gravatars", false); + boolean libravatars = prefs.getBoolean("libravatars", 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); @@ -305,7 +318,7 @@ public class ContactInfo { // Favicon if (info.bitmap == null && - !EntityFolder.JUNK.equals(folderType) && (bimi || efavicons || favicons)) { + !EntityFolder.JUNK.equals(folderType) && (bimi || gravatars || libravatars || favicons)) { String d = UriHelper.getEmailDomain(info.email); if (d != null) { // Prevent using Doodles @@ -331,8 +344,13 @@ public class ContactInfo { try { // check cache File[] files = null; - if (efavicons) { - File f = new File(dir, email + ".extra"); + if (gravatars) { + File f = new File(dir, email + ".gravatar"); + if (f.exists()) + files = new File[]{f}; + } + if (files == null && libravatars) { + File f = new File(dir, email + ".libravatar"); if (f.exists()) files = new File[]{f}; } @@ -371,10 +389,10 @@ public class ContactInfo { } })); - if (efavicons) { - futures.add(executorFavicon.submit(Extra.getG(email, scaleToPixels, context))); - futures.add(executorFavicon.submit(Extra.getL(email, scaleToPixels, context))); - } + if (gravatars) + futures.add(executorFavicon.submit(getGravatar(email, scaleToPixels, context))); + if (libravatars) + futures.add(executorFavicon.submit(getLibravatar(email, scaleToPixels, context))); if (favicons) { String host = domain; @@ -464,7 +482,7 @@ public class ContactInfo { // Add to cache File output = new File(dir, - ("extra".equals(info.type) ? email : domain) + + (info.isEmailBased() ? email : domain) + "." + info.type + (info.verified ? "_verified" : "")); try (OutputStream os = new BufferedOutputStream(new FileOutputStream(output))) { @@ -860,6 +878,85 @@ public class ContactInfo { } } + static Callable getGravatar(String email, int scaleToPixels, Context context) { + return new Callable() { + @Override + public ContactInfo.Favicon call() throws Exception { + String hash = Helper.md5(email.getBytes()); + URL url = new URL(GRAVATAR_URI + hash + "?d=404"); + Log.i("Gravatar key=" + email + " url=" + url); + + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.setReadTimeout(GRAVATAR_READ_TIMEOUT); + urlConnection.setConnectTimeout(GRAVATAR_CONNECT_TIMEOUT); + urlConnection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context)); + urlConnection.connect(); + + try { + int status = urlConnection.getResponseCode(); + if (status == HttpURLConnection.HTTP_OK) { + // Positive reply + Bitmap bitmap = ImageHelper.getScaledBitmap(urlConnection.getInputStream(), url.toString(), null, scaleToPixels); + return (bitmap == null ? null : new ContactInfo.Favicon(bitmap, "gravatar", false)); + } else if (status == HttpURLConnection.HTTP_NOT_FOUND) { + // Negative reply + return null; + } else + throw new IOException("Error " + status + ": " + urlConnection.getResponseMessage()); + } finally { + urlConnection.disconnect(); + } + } + }; + } + + static Callable getLibravatar(String email, int scaleToPixels, Context context) { + return new Callable() { + @Override + public ContactInfo.Favicon call() throws Exception { + String domain = UriHelper.getEmailDomain(email); + + // https://wiki.libravatar.org/api/ + String baseUrl = LIBRAVATAR_URI; + for (String dns : LIBRAVATAR_DNS.split(",")) { + DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns + "." + domain, "srv"); + if (records.length > 0) { + baseUrl = (records[0].port == 443 ? "https" : "http") + "://" + records[0].name + "/avatar/"; + break; + } + } + + String hash = Helper.md5(email.getBytes()); + + URL url = new URL(baseUrl + hash + "?d=404"); + Log.i("Libravatar key=" + email + " url=" + url); + + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod("GET"); + urlConnection.setReadTimeout(LIBRAVATAR_READ_TIMEOUT); + urlConnection.setConnectTimeout(LIBRAVATAR_CONNECT_TIMEOUT); + urlConnection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context)); + urlConnection.connect(); + + try { + int status = urlConnection.getResponseCode(); + if (status == HttpURLConnection.HTTP_OK) { + // Positive reply + Bitmap bitmap = ImageHelper.getScaledBitmap(urlConnection.getInputStream(), url.toString(), null, scaleToPixels); + return (bitmap == null ? null : new ContactInfo.Favicon(bitmap, "libravatar", false)); + } else if (status == HttpURLConnection.HTTP_NOT_FOUND) { + // Negative reply + return null; + } else + throw new IOException("Error " + status + ": " + urlConnection.getResponseMessage()); + } finally { + urlConnection.disconnect(); + } + } + }; + } + private static boolean isRecoverable(Throwable ex, Context context) { if (ex instanceof SocketTimeoutException) { ConnectivityManager cm = Helper.getSystemService(context, ConnectivityManager.class); diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java b/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java index ea183d8533..9de7e54a13 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsDisplay.java @@ -108,7 +108,10 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer private TextView tvBimiHint; private TextView tvBimiUnverified; private SwitchCompat swBimi; - private SwitchCompat swEFavicons; + private SwitchCompat swGravatars; + private TextView tvGravatarPrivacy; + private SwitchCompat swLibravatars; + private TextView tvLibravatarPrivacy; private SwitchCompat swFavicons; private SwitchCompat swFaviconsPartial; private TextView tvFaviconsHint; @@ -177,7 +180,6 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer private SwitchCompat swAuthentication; private SwitchCompat swAuthenticationIndicator; - private Group grpPlay; private Group grpUnzip; private NumberFormat NF = NumberFormat.getNumberInstance(); @@ -190,7 +192,7 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer "nav_options", "nav_categories", "nav_count", "nav_unseen_drafts", "nav_count_pinned", "navbar_colorize", "threading", "threading_unread", "indentation", "seekbar", "actionbar", "actionbar_color", "highlight_unread", "highlight_color", "color_stripe", "color_stripe_wide", - "avatars", "bimi", "efavicons", "favicons", "favicons_partial", "generated_icons", "identicons", + "avatars", "bimi", "gravatars", "libravatars", "favicons", "favicons_partial", "generated_icons", "identicons", "circular", "saturation", "brightness", "threshold", "email_format", "prefer_contact", "only_contact", "distinguish_contacts", "show_recipients", "font_size_sender", "sender_ellipsize", @@ -262,7 +264,10 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer tvBimiHint = view.findViewById(R.id.tvBimiHint); tvBimiUnverified = view.findViewById(R.id.tvBimiUnverified); ibBimi = view.findViewById(R.id.ibBimi); - swEFavicons = view.findViewById(R.id.swEFavicons); + swGravatars = view.findViewById(R.id.swGravatars); + tvGravatarPrivacy = view.findViewById(R.id.tvGravatarPrivacy); + swLibravatars = view.findViewById(R.id.swLibravatars); + tvLibravatarPrivacy = view.findViewById(R.id.tvLibravatarPrivacy); swFavicons = view.findViewById(R.id.swFavicons); swFaviconsPartial = view.findViewById(R.id.swFaviconsPartial); tvFaviconsHint = view.findViewById(R.id.tvFaviconsHint); @@ -329,7 +334,6 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer swAuthentication = view.findViewById(R.id.swAuthentication); swAuthenticationIndicator = view.findViewById(R.id.swAuthenticationIndicator); - grpPlay = view.findViewById(R.id.grpPlay); grpUnzip = view.findViewById(R.id.grpUnzip); List fonts = StyleHelper.getFonts(getContext()); @@ -728,14 +732,38 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer } }); - swEFavicons.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + swGravatars.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { - prefs.edit().putBoolean("efavicons", checked).apply(); + prefs.edit().putBoolean("gravatars", checked).apply(); ContactInfo.clearCache(compoundButton.getContext()); } }); + tvGravatarPrivacy.getPaint().setUnderlineText(true); + tvGravatarPrivacy.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.view(v.getContext(), Uri.parse(Helper.GRAVATAR_PRIVACY_URI), true); + } + }); + + swLibravatars.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("libravatars", checked).apply(); + ContactInfo.clearCache(compoundButton.getContext()); + } + }); + + tvLibravatarPrivacy.getPaint().setUnderlineText(true); + tvLibravatarPrivacy.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.view(v.getContext(), Uri.parse(Helper.LIBRAVATAR_PRIVACY_URI), true); + } + }); + swFavicons.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { @@ -1227,7 +1255,6 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer // Initialize FragmentDialogTheme.setBackground(getContext(), view, false); - grpPlay.setVisibility(BuildConfig.PLAY_STORE_RELEASE || BuildConfig.DEBUG ? View.VISIBLE : View.GONE); swFaviconsPartial.setText(getString(R.string.title_advanced_favicons_partial, Helper.humanReadableByteCount(ContactInfo.FAVICON_READ_BYTES, false))); grpUnzip.setVisibility(Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? View.GONE : View.VISIBLE); @@ -1338,12 +1365,10 @@ public class FragmentOptionsDisplay extends FragmentBase implements SharedPrefer swColorStripeWide.setChecked(prefs.getBoolean("color_stripe_wide", false)); //swColorStripeWide.setEnabled(swColorStripe.isChecked()); swAvatars.setChecked(prefs.getBoolean("avatars", true)); - swBimi.setChecked(prefs.getBoolean("bimi", false) && !BuildConfig.PLAY_STORE_RELEASE); - swBimi.setEnabled(!BuildConfig.PLAY_STORE_RELEASE); - swEFavicons.setChecked(prefs.getBoolean("efavicons", false) && !BuildConfig.PLAY_STORE_RELEASE); - swEFavicons.setEnabled(!BuildConfig.PLAY_STORE_RELEASE); - swFavicons.setChecked(prefs.getBoolean("favicons", false) && !BuildConfig.PLAY_STORE_RELEASE); - swFavicons.setEnabled(!BuildConfig.PLAY_STORE_RELEASE); + swBimi.setChecked(prefs.getBoolean("bimi", false)); + swGravatars.setChecked(prefs.getBoolean("gravatars", false)); + swLibravatars.setChecked(prefs.getBoolean("libravatars", false)); + swFavicons.setChecked(prefs.getBoolean("favicons", false)); swFaviconsPartial.setChecked(prefs.getBoolean("favicons_partial", true)); swFaviconsPartial.setEnabled(swFavicons.isChecked()); swGeneratedIcons.setChecked(prefs.getBoolean("generated_icons", true)); diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index 59ef5aa90b..23d316a29d 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -184,6 +184,8 @@ public class Helper { static final String SUPPORT_URI = "https://contact.faircode.eu/"; static final String TEST_URI = "https://play.google.com/apps/testing/" + BuildConfig.APPLICATION_ID; static final String BIMI_PRIVACY_URI = "https://datatracker.ietf.org/doc/html/draft-brotman-ietf-bimi-guidance-03#section-7.4"; + static final String GRAVATAR_PRIVACY_URI = "https://automattic.com/privacy/"; + static final String LIBRAVATAR_PRIVACY_URI = "https://www.libravatar.org/privacy/"; static final String ID_COMMAND_URI = "https://datatracker.ietf.org/doc/html/rfc2971#section-3.1"; static final String AUTH_RESULTS_URI = "https://datatracker.ietf.org/doc/html/rfc7601"; static final String FAVICON_PRIVACY_URI = "https://en.wikipedia.org/wiki/Favicon"; diff --git a/app/src/main/res/layout/fragment_options_display.xml b/app/src/main/res/layout/fragment_options_display.xml index 2f5f18fdf2..d7cf8d452d 100644 --- a/app/src/main/res/layout/fragment_options_display.xml +++ b/app/src/main/res/layout/fragment_options_display.xml @@ -808,23 +808,6 @@ app:layout_constraintTop_toBottomOf="@id/vSeparatorAvatar" app:switchPadding="12dp" /> - - - - @@ -840,6 +823,7 @@ android:id="@+id/tvBimiHint" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="12dp" android:layout_marginTop="6dp" android:drawableEnd="@drawable/twotone_open_in_new_12" android:drawablePadding="6dp" @@ -847,13 +831,14 @@ android:text="@string/title_advanced_privacy_risk" android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textColor="?attr/colorWarning" - app:layout_constraintStart_toEndOf="@id/vwFaviconPadding" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/swBimi" /> + + + + + + + + + + - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91a88aa0e0..100c9e4f88 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -495,10 +495,10 @@ Show Brand Indicators for Message Identification (BIMI) Unverified sender Verified sender - Show avatars + Show Gravatars + Show Libravatars Show favicons Scan only the first %1$s of the web page - Google presumably doesn\'t allow online icons for privacy reasons Show generated icons Show identicons Show round icons