From b48bb38b5d198230bc4f7b2b1e71e8ed106f5575 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 8 Jul 2026 18:30:59 +0200 Subject: [PATCH] Added warning about local network access --- FAQ.md | 19 ++++++++ .../eu/faircode/email/FragmentAccount.java | 36 ++++++++++++++++ .../eu/faircode/email/FragmentIdentity.java | 43 ++++++++++++++++++- .../email/FragmentOptionsConnection.java | 7 ++- .../java/eu/faircode/email/FragmentPop.java | 34 +++++++++++++++ app/src/main/res/values/strings.xml | 1 + index.html | 9 ++++ 7 files changed, 145 insertions(+), 4 deletions(-) diff --git a/FAQ.md b/FAQ.md index 67f97c571b..d38b89b075 100644 --- a/FAQ.md +++ b/FAQ.md @@ -455,6 +455,7 @@ Anything on this list is in random order and *might* be added in the near future * [(207) What does 'Authentication failed' mean?](#faq207) * [(208) What does 'about:blank#blocked' mean when I click on a link?](#faq208) * [(209) Why is using a VPN often problematic?](#faq209) +* [(210) Why are local network permissions needed?](#faq210) [I have another question.](#get-support) @@ -6508,6 +6509,24 @@ This is also why the app warns against using a VPN.
+ +**(210) Why are local network permissions needed?** + +🌎 [Google Translate](https://translate.google.com/translate?sl=en&u=https%3A%2F%2Fm66b.github.io%2FFairEmail%2F%23faq210) + +Since Android 17, Android requires [local network permission](https://developer.android.com/privacy-and-security/local-network-permission) for local network access (LAN access). + +This permission can be granted via a button on the connection settings page. + +Very few people will need LAN access, and to avoid questions regarding a new permission, this permission is available only in the [GitHub version](https://github.com/M66B/FairEmail/releases). + +If the app navigated to this FAQ after you clicked the button to grant local network permissions, then the GitHub version is not (or is no longer) installed. + +Added in version 1.2325. + +
+ +

Get support

🌎 [Google Translate](https://translate.google.com/translate?sl=en&u=https%3A%2F%2Fm66b.github.io%2FFairEmail%2F%23get-support) diff --git a/app/src/main/java/eu/faircode/email/FragmentAccount.java b/app/src/main/java/eu/faircode/email/FragmentAccount.java index 06669b7077..996b2e91b6 100644 --- a/app/src/main/java/eu/faircode/email/FragmentAccount.java +++ b/app/src/main/java/eu/faircode/email/FragmentAccount.java @@ -74,6 +74,7 @@ import com.google.android.material.textfield.TextInputLayout; import org.json.JSONObject; import java.io.FileNotFoundException; +import java.net.InetAddress; import java.net.UnknownHostException; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -335,6 +336,7 @@ public class FragmentAccount extends FragmentBase { adapterView.setTag(position); etHost.setText(provider.imap.host); + checkLan(provider.imap.host); etPort.setText(provider.imap.host == null ? null : Integer.toString(provider.imap.port)); rgEncryption.check(provider.imap.starttls ? R.id.radio_starttls : R.id.radio_ssl); @@ -372,6 +374,23 @@ public class FragmentAccount extends FragmentBase { } }); + etHost.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + checkLan(s == null ? null : s.toString()); + } + }); + rgEncryption.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int id) { @@ -749,6 +768,7 @@ public class FragmentAccount extends FragmentBase { @Override protected void onExecuted(Bundle args, EmailProvider provider) { etHost.setText(provider.imap.host); + checkLan(provider.imap.host); etPort.setText(Integer.toString(provider.imap.port)); rgEncryption.check(provider.imap.starttls ? R.id.radio_starttls : R.id.radio_ssl); } @@ -1749,6 +1769,7 @@ public class FragmentAccount extends FragmentBase { spProvider.setSelection(1); } etHost.setText(account.host); + checkLan(account.host); etPort.setText(Long.toString(account.port)); } @@ -2146,6 +2167,21 @@ public class FragmentAccount extends FragmentBase { }.execute(this, args, "account:delete"); } + private void checkLan(String host) { + String msg = null; + if (!TextUtils.isEmpty(host)) + try { + Context context = getContext(); + InetAddress addr = InetAddress.getByName(host); + if ((addr.isSiteLocalAddress() || addr.isLinkLocalAddress() || addr.isLoopbackAddress()) && + !Helper.hasPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK)) + msg = context.getString(R.string.title_lan); + } catch (Throwable ignored) { + } + + etHost.setError(msg); + } + private void setFolders(List _folders, EntityAccount account) { { List folders = new ArrayList<>(); diff --git a/app/src/main/java/eu/faircode/email/FragmentIdentity.java b/app/src/main/java/eu/faircode/email/FragmentIdentity.java index e411a4e985..095e72126c 100644 --- a/app/src/main/java/eu/faircode/email/FragmentIdentity.java +++ b/app/src/main/java/eu/faircode/email/FragmentIdentity.java @@ -69,6 +69,7 @@ import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import java.io.FileNotFoundException; +import java.net.InetAddress; import java.net.UnknownHostException; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -310,10 +311,13 @@ public class FragmentIdentity extends FragmentBase { spProvider.setTag(0); spProvider.setSelection(0); setProvider((EmailProvider) spProvider.getItemAtPosition(0)); - if (account.host == null || account.host.startsWith("imap")) + if (account.host == null || account.host.startsWith("imap")) { etHost.setText(null); - else + checkLan(null); + } else { etHost.setText(account.host); + checkLan(account.host); + } grpAdvanced.setVisibility(View.VISIBLE); } @@ -463,6 +467,23 @@ public class FragmentIdentity extends FragmentBase { } }); + etHost.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + checkLan(s == null ? null : s.toString()); + } + }); + rgEncryption.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int id) { @@ -691,6 +712,7 @@ public class FragmentIdentity extends FragmentBase { private void setProvider(EmailProvider provider) { etHost.setText(provider.smtp.host); + checkLan(provider.smtp.host); etPort.setText(provider.smtp.port == 0 ? null : Integer.toString(provider.smtp.port)); rgEncryption.check(provider.smtp.starttls ? R.id.radio_starttls : R.id.radio_ssl); cbUseIp.setChecked(provider.useip); @@ -730,6 +752,7 @@ public class FragmentIdentity extends FragmentBase { @Override protected void onExecuted(Bundle args, EmailProvider provider) { etHost.setText(provider.smtp.host); + checkLan(provider.smtp.host); etPort.setText(Integer.toString(provider.smtp.port)); rgEncryption.check(provider.smtp.starttls ? R.id.radio_starttls : R.id.radio_ssl); cbUseIp.setChecked(provider.useip); @@ -1353,6 +1376,7 @@ public class FragmentIdentity extends FragmentBase { cbDnsSec.setChecked(identity == null ? false : identity.dnssec); etHost.setText(identity == null ? null : identity.host); + checkLan(identity == null ? null : identity.host); if (identity != null && identity.encryption == EmailService.ENCRYPTION_STARTTLS) rgEncryption.check(R.id.radio_starttls); @@ -1684,6 +1708,21 @@ public class FragmentIdentity extends FragmentBase { }.execute(this, args, "identity:signature"); } + private void checkLan(String host) { + String msg = null; + if (!TextUtils.isEmpty(host)) + try { + Context context = getContext(); + InetAddress addr = InetAddress.getByName(host); + if ((addr.isSiteLocalAddress() || addr.isLinkLocalAddress() || addr.isLoopbackAddress()) && + !Helper.hasPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK)) + msg = context.getString(R.string.title_lan); + } catch (Throwable ignored) { + } + + etHost.setError(msg); + } + private void onPickUri(Intent intent) { Uri uri = (intent == null ? null : intent.getData()); btnUri.setTag(uri == null ? null : uri.toString()); diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java index b2310d0a34..bf3e4acebb 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java @@ -513,13 +513,16 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre } }); - btnLocalNetwork.setVisibility(BuildConfig.PLAY_STORE_RELEASE || Build.VERSION.SDK_INT < Build.VERSION_CODES.CINNAMON_BUN + btnLocalNetwork.setVisibility(Build.VERSION.SDK_INT < Build.VERSION_CODES.CINNAMON_BUN ? View.GONE : View.VISIBLE); btnLocalNetwork.setEnabled(!Helper.hasPermission(getContext(), Manifest.permission.ACCESS_LOCAL_NETWORK)); btnLocalNetwork.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - requestPermissions(new String[]{Manifest.permission.ACCESS_LOCAL_NETWORK}, REQUEST_PERMISSIONS); + if (BuildConfig.PLAY_STORE_RELEASE) + Helper.viewFAQ(v.getContext(), 210); + else + requestPermissions(new String[]{Manifest.permission.ACCESS_LOCAL_NETWORK}, REQUEST_PERMISSIONS); } }); diff --git a/app/src/main/java/eu/faircode/email/FragmentPop.java b/app/src/main/java/eu/faircode/email/FragmentPop.java index b4071bbf0e..597ba7b1ed 100644 --- a/app/src/main/java/eu/faircode/email/FragmentPop.java +++ b/app/src/main/java/eu/faircode/email/FragmentPop.java @@ -65,6 +65,7 @@ import com.google.android.material.textfield.TextInputLayout; import org.json.JSONObject; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -224,6 +225,23 @@ public class FragmentPop extends FragmentBase { this.colorWarning = Helper.resolveColor(getContext(), R.attr.colorWarning); this.textColorSecondary = Helper.resolveColor(getContext(), android.R.attr.textColorSecondary); + etHost.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + checkLan(s == null ? null : s.toString()); + } + }); + rgEncryption.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int id) { @@ -938,6 +956,7 @@ public class FragmentPop extends FragmentBase { cbDnsSec.setChecked(account == null ? false : account.dnssec); etHost.setText(account == null ? null : account.host); + checkLan(account == null ? null : account.host); etPort.setText(account == null ? null : Long.toString(account.port)); if (account != null && account.encryption == EmailService.ENCRYPTION_STARTTLS) @@ -1206,6 +1225,21 @@ public class FragmentPop extends FragmentBase { }.execute(this, args, "account:delete"); } + private void checkLan(String host) { + String msg = null; + if (!TextUtils.isEmpty(host)) + try { + Context context = getContext(); + InetAddress addr = InetAddress.getByName(host); + if ((addr.isSiteLocalAddress() || addr.isLinkLocalAddress() || addr.isLoopbackAddress()) && + !Helper.hasPermission(context, Manifest.permission.ACCESS_LOCAL_NETWORK)) + msg = context.getString(R.string.title_lan); + } catch (Throwable ignored) { + } + + etHost.setError(msg); + } + private List getSwipeActions(Context context) { List folders = new ArrayList<>(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06903e8d6e..75009f1390 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1302,6 +1302,7 @@ IMAP POP3 SMTP + Local network permissions must be granted on the connection settings tab page Aliases Provider Custom diff --git a/index.html b/index.html index 94d5c466ca..ca7375cdcd 100644 --- a/index.html +++ b/index.html @@ -443,6 +443,7 @@
  • (207) What does ‘Authentication failed’ mean?
  • (208) What does ‘about:blank#blocked’ mean when I click on a link?
  • (209) Why is using a VPN often problematic?
  • +
  • (210) Why are local network permissions needed?
  • I have another question.

    (1) Which permissions are needed and why?

    @@ -3246,6 +3247,14 @@ adb install /path/to/FairEmail-xxx.apk

    🌎 Google Translate

    When you use a VPN, you share one network address with many people and not all those people will always behave nicely. Email servers often automatically block network addresses (IP addresses) when abuse is detected, for example when someone tries to send spam messages. The result is that logins are blocked for everyone using the same network address and the email server issues an error message such as “Authentication failed”. This is also why the app warns against using a VPN.


    +

    (210) Why are local network permissions needed?

    +

    🌎 Google Translate

    +

    Since Android 17, Android requires local network permission for local network access (LAN access).

    +

    This permission can be granted via a button on the connection settings page.

    +

    Very few people will need LAN access, and to avoid questions regarding a new permission, this permission is available only in the GitHub version.

    +

    If the app navigated to this FAQ after you clicked the button to grant local network permissions, then the GitHub version is not (or is no longer) installed.

    +

    Added in version 1.2325.

    +


    Get support