Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e80aa401 authored by Philip's avatar Philip Committed by cketti
Browse files

Add privacy option to hide hostname when connecting to SMTP servers (#3078)

parent 63010dd4
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -32,4 +32,6 @@ public interface StoreConfig {
    int getDisplayCount();

    int getIdleRefreshMinutes();

    boolean shouldHideHostname();
}
+36 −20
Original line number Diff line number Diff line
@@ -81,6 +81,7 @@ public class SmtpTransport extends Transport {
    private int largestAcceptableMessage;
    private boolean retryXoauthWithNewToken;
    private boolean isPipeliningSupported;
    private boolean shouldHideHostname;


    public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
@@ -108,6 +109,7 @@ public class SmtpTransport extends Transport {

        this.trustedSocketFactory = trustedSocketFactory;
        this.oauthTokenProvider = oauthTokenProvider;
        this.shouldHideHostname = storeConfig.shouldHideHostname();
    }

    @Override
@@ -145,26 +147,9 @@ public class SmtpTransport extends Transport {
            // Eat the banner
            executeCommand(null);

            InetAddress localAddress = socket.getLocalAddress();
            String localHost = getCanonicalHostName(localAddress);
            String ipAddr = localAddress.getHostAddress();
            String hostnameToReportInHelo = buildHostnameToReport();

            if (localHost.equals("") || localHost.equals(ipAddr) || localHost.contains("_")) {
                // We don't have a FQDN or the hostname contains invalid
                // characters (see issue 2143), so use IP address.
                if (!ipAddr.equals("")) {
                    if (localAddress instanceof Inet6Address) {
                        localHost = "[IPv6:" + ipAddr + "]";
                    } else {
                        localHost = "[" + ipAddr + "]";
                    }
                } else {
                    // If the IP address is no good, set a sane default (see issue 2750).
                    localHost = "android";
                }
            }

            Map<String, String> extensions = sendHello(localHost);
            Map<String, String> extensions = sendHello(hostnameToReportInHelo);

            is8bitEncodingAllowed = extensions.containsKey("8BITMIME");
            isEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES");
@@ -187,7 +172,7 @@ public class SmtpTransport extends Transport {
                     * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
                     * Exim.
                     */
                    extensions = sendHello(localHost);
                    extensions = sendHello(hostnameToReportInHelo);
                    secureConnection = true;
                } else {
                    /*
@@ -328,6 +313,32 @@ public class SmtpTransport extends Transport {
        }
    }

    private String buildHostnameToReport() {
        if (shouldHideHostname) {
            return "localhost";
        }
        InetAddress localAddress = socket.getLocalAddress();
        String localHostname = getCanonicalHostName(localAddress);
        String ipAddr = getHostAddress(localAddress);

        if (localHostname.equals("") || localHostname.equals(ipAddr) || localHostname.contains("_")) {
            // We don't have a FQDN or the hostname contains invalid
            // characters (see issue 2143), so use IP address.
            if (!ipAddr.equals("")) {
                if (localAddress instanceof Inet6Address) {
                    return "[IPv6:" + ipAddr + "]";
                } else {
                    return "[" + ipAddr + "]";
                }
            } else {
                // If the IP address is no good, set a sane default
                return "android";
            }
        } else {
            return localHostname;
        }
    }

    private void parseOptionalSizeValue(Map<String, String> extensions) {
        if (extensions.containsKey("SIZE")) {
            String optionalsizeValue = extensions.get("SIZE");
@@ -828,4 +839,9 @@ public class SmtpTransport extends Transport {
    protected String getCanonicalHostName(InetAddress localAddress) {
        return localAddress.getCanonicalHostName();
    }

    @VisibleForTesting
    protected String getHostAddress(InetAddress localAddress) {
        return localAddress.getHostAddress();
    }
}
+92 −13
Original line number Diff line number Diff line
@@ -41,7 +41,6 @@ import static org.mockito.Mockito.when;

@RunWith(K9LibRobolectricTestRunner.class)
public class SmtpTransportTest {
    private static final String LOCALHOST_NAME = "localhost";
    private static final String USERNAME = "user";
    private static final String PASSWORD = "password";
    private static final String CLIENT_CERTIFICATE_ALIAS = null;
@@ -49,6 +48,7 @@ public class SmtpTransportTest {
    
    private TrustedSocketFactory socketFactory;
    private OAuth2TokenProvider oAuth2TokenProvider;
    private StoreConfig storeConfig = mock(StoreConfig.class);


    @Before
@@ -61,18 +61,83 @@ public class SmtpTransportTest {

    @Test
    public void SmtpTransport_withValidTransportUri() throws Exception {
        StoreConfig storeConfig = createStoreConfigWithTransportUri("smtp://user:password:CRAM_MD5@server:123456");
        StoreConfig storeConfig = setupStoreConfigWithTransportUri("smtp://user:password:CRAM_MD5@server:123456");

        new SmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
    }

    @Test(expected = MessagingException.class)
    public void SmtpTransport_withInvalidTransportUri_shouldThrow() throws Exception {
        StoreConfig storeConfig = createStoreConfigWithTransportUri("smpt://");
        StoreConfig storeConfig = setupStoreConfigWithTransportUri("smpt://");

        new SmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
    }

    @Test
    public void open_withShouldHideHostnameTrue_shouldProvideLocalhost() throws Exception {
        MockSmtpServer server = new MockSmtpServer();
        server.output("220 localhost Simple Mail Transfer Service Ready");
        server.expect("EHLO localhost");
        server.output("250-localhost Hello client.localhost");
        server.output("250 OK");
        when(storeConfig.shouldHideHostname()).thenReturn(true);
        SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null,
                "private.host.org", "127.0.0.1");

        transport.open();

        server.verifyConnectionStillOpen();
        server.verifyInteractionCompleted();
    }

    @Test
    public void open_withShouldHideHostnameFalse_shouldProvideHostname() throws Exception {
        MockSmtpServer server = new MockSmtpServer();
        server.output("220 localhost Simple Mail Transfer Service Ready");
        server.expect("EHLO visible.host.org");
        server.output("250-localhost Hello client.localhost");
        server.output("250 OK");
        SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null,
                "visible.host.org", "127.0.0.1");

        transport.open();

        server.verifyConnectionStillOpen();
        server.verifyInteractionCompleted();
    }

    @Test
    public void open_withEmptyHostname_shouldProvideIPAddress() throws Exception {
        MockSmtpServer server = new MockSmtpServer();
        server.output("220 localhost Simple Mail Transfer Service Ready");
        server.expect("EHLO [127.0.0.1]");
        server.output("250-localhost Hello client.localhost");
        server.output("250 OK");
        SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null,
                "", "127.0.0.1");

        transport.open();

        server.verifyConnectionStillOpen();
        server.verifyInteractionCompleted();
    }

    @Test
    public void open_withEmptyHostnameAndIP_shouldProvideSensibleDefault() throws Exception {
        MockSmtpServer server = new MockSmtpServer();
        server.output("220 localhost Simple Mail Transfer Service Ready");
        server.expect("EHLO android");
        server.output("250-localhost Hello client.localhost");
        server.output("250 OK");
        SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null,
                "", "");

        transport.open();

        server.verifyConnectionStillOpen();
        server.verifyInteractionCompleted();
    }

    @Test
    public void open_withoutAuthLoginExtension_shouldConnectWithoutAuthentication() throws Exception {
        MockSmtpServer server = new MockSmtpServer();
@@ -865,16 +930,19 @@ public class SmtpTransportTest {

    private SmtpTransport startServerAndCreateSmtpTransportWithoutPassword(MockSmtpServer server) throws IOException,
            MessagingException {
        return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null);
        return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null,
                "localhost", "127.0.0.1");
    }

    private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType,
            ConnectionSecurity connectionSecurity) throws IOException, MessagingException {
        return startServerAndCreateSmtpTransport(server, authenticationType, connectionSecurity, PASSWORD);
        return startServerAndCreateSmtpTransport(server, authenticationType, connectionSecurity, PASSWORD,
                "localhost", "127.0.0.1");
    }

    private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType,
            ConnectionSecurity connectionSecurity, String password) throws IOException, MessagingException {
            ConnectionSecurity connectionSecurity, String password,
            String injectedHostname, String injectedIP) throws IOException, MessagingException {
        server.start();

        String host = server.getHost();
@@ -889,13 +957,12 @@ public class SmtpTransportTest {
                password,
                CLIENT_CERTIFICATE_ALIAS);
        String uri = TransportUris.createTransportUri(serverSettings);
        StoreConfig storeConfig = createStoreConfigWithTransportUri(uri);
        StoreConfig storeConfig = setupStoreConfigWithTransportUri(uri);

        return new TestSmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider);
        return new TestSmtpTransport(storeConfig, socketFactory, oAuth2TokenProvider, injectedHostname, injectedIP);
    }

    private StoreConfig createStoreConfigWithTransportUri(String value) {
        StoreConfig storeConfig = mock(StoreConfig.class);
    private StoreConfig setupStoreConfigWithTransportUri(String value) {
        when(storeConfig.getTransportUri()).thenReturn(value);
        return storeConfig;
    }
@@ -937,14 +1004,26 @@ public class SmtpTransportTest {
    
    
    static class TestSmtpTransport extends SmtpTransport {
        TestSmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory, OAuth2TokenProvider oAuth2TokenProvider)
        private final String injectedHostname;
        private final String injectedIP;

        TestSmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
                OAuth2TokenProvider oAuth2TokenProvider,
                String injectedHostname, String injectedIP)
                throws MessagingException {
            super(storeConfig, trustedSocketFactory, oAuth2TokenProvider);
            this.injectedHostname = injectedHostname;
            this.injectedIP = injectedIP;
        }

        @Override
        protected String getCanonicalHostName(InetAddress localAddress) {
            return LOCALHOST_NAME;
            return injectedHostname;
        }

        @Override
        protected String getHostAddress(InetAddress localAddress) {
            return injectedIP;
        }
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -1481,6 +1481,11 @@ public class Account implements BaseAccount, StoreConfig {
        return idleRefreshMinutes;
    }

    @Override
    public boolean shouldHideHostname() {
        return K9.hideHostnameWhenConnecting();
    }

    public synchronized void setIdleRefreshMinutes(int idleRefreshMinutes) {
        this.idleRefreshMinutes = idleRefreshMinutes;
    }
+11 −0
Original line number Diff line number Diff line
@@ -237,6 +237,7 @@ public class K9 extends Application {
    private static boolean wrapFolderNames = false;
    private static boolean hideUserAgent = false;
    private static boolean hideTimeZone = false;
    private static boolean hideHostnameWhenConnecting = false;

    private static String openPgpProvider = "";
    private static boolean openPgpSupportSignOnly = false;
@@ -476,6 +477,7 @@ public class K9 extends Application {
        editor.putBoolean("wrapFolderNames", wrapFolderNames);
        editor.putBoolean("hideUserAgent", hideUserAgent);
        editor.putBoolean("hideTimeZone", hideTimeZone);
        editor.putBoolean("hideHostnameWhenConnecting", hideHostnameWhenConnecting);

        editor.putString("openPgpProvider", openPgpProvider);
        editor.putBoolean("openPgpSupportSignOnly", openPgpSupportSignOnly);
@@ -711,6 +713,7 @@ public class K9 extends Application {
        wrapFolderNames = storage.getBoolean("wrapFolderNames", false);
        hideUserAgent = storage.getBoolean("hideUserAgent", false);
        hideTimeZone = storage.getBoolean("hideTimeZone", false);
        hideHostnameWhenConnecting = storage.getBoolean("hideHostnameWhenConnecting", false);

        openPgpProvider = storage.getString("openPgpProvider", NO_OPENPGP_PROVIDER);
        openPgpSupportSignOnly = storage.getBoolean("openPgpSupportSignOnly", false);
@@ -1233,6 +1236,14 @@ public class K9 extends Application {
        hideTimeZone = state;
    }

    public static boolean hideHostnameWhenConnecting() {
        return hideHostnameWhenConnecting;
    }

    public static void setHideHostnameWhenConnecting(final boolean state) {
        hideHostnameWhenConnecting = state;
    }

    public static boolean isOpenPgpProviderConfigured() {
        return !NO_OPENPGP_PROVIDER.equals(openPgpProvider);
    }
Loading