Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Connect to GMail with JavaMail API without enabling less secure access

Is there any way to connect to GMail using the JavaMail API without enabling "Less secure access"?

like image 639
Charlie Avatar asked Dec 09 '25 08:12

Charlie


1 Answers

You must authenticate using OAuth2 to avoid turning on Less secure access.

Per very-hard-to-find docs

  • https://developers.google.com/gmail/imap/xoauth2-protocol
  • https://javaee.github.io/javamail/OAuth2

Example code:

Properties props = (Properties) System.getProperties().clone();
props.put("mail.imaps.ssl.enable", "true");
props.put("mail.imaps.auth.mechanisms", "XOAUTH2");
// props.put("mail.debug.auth", "true");

Session session = Session.getDefaultInstance(props);
// session.setDebug(true);

store = session.getStore("imaps");
String accessToken = getAccessToken(user, clientId, clientSecret);
store.connect(hostname, user, accessToken);

The next trick is getting that access token.

  • https://developers.google.com/identity/protocols/oauth2/native-app#step-2:-send-a-request-to-googles-oauth-2.0-server

Here's one you can use locally:

You'll first need to create an app at https://console.developers.google.com/apis/credentials and grab the client id and secret JSON

You can start up a local web server on any port and use the http://localhost redirect URL on any port (which needs to be added on the previously mentioned screen).

private static String getAccessToken(String emailAddress, String clientId, String clientSecret) throws Exception {
    NetHttpTransport transport = GoogleNetHttpTransport.newTrustedTransport();
    JsonFactory jsonFactory = Utils.getDefaultJsonFactory();

    GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(jsonFactory, new StringReader("{\n" +
            "  \"installed\": {\n" +
            "    \"client_id\": \"" + clientId + "\",\n" +
            "    \"client_secret\": \"" + clientSecret + "\"\n" +
            "  }\n" +
            "}"));
    GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(transport, jsonFactory, clientSecrets, Collections.singleton(GMAIL_SCOPE)).setAccessType("offline").build();

    AtomicReference<String> redirectUri = new AtomicReference<>();

    String authorizationCode = doRedirect("code=([^&$]+)", RE.wrapConsumer(p -> {
        redirectUri.set("http://localhost:" + p);
        Desktop.getDesktop().browse(flow.newAuthorizationUrl().setRedirectUri(redirectUri.get()).toURI());
    }));

    // second redirect URL needs to be set and match the one on the newAuthorization flow but isn't actually used
    GoogleTokenResponse execute = flow.newTokenRequest(authorizationCode).setRedirectUri(redirectUri.get()).execute();

    String refreshToken = execute.getRefreshToken();
    String accessToken = execute.getAccessToken();

    return accessToken;
}

private static String doRedirect(String pattern, Consumer<Integer> portConsumer) {
    try {
        ServerSocket socket = new ServerSocket(0);
        portConsumer.accept(socket.getLocalPort());
        Socket connection = socket.accept();
        BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        OutputStream out = new BufferedOutputStream(connection.getOutputStream());
        PrintStream pout = new PrintStream(out);

        try {
            String request = in.readLine();
            Matcher matcher = Pattern.compile(pattern).matcher(request);
            String response = "<html><body>Window can be closed now.</body></html>";
            pout.println("HTTP/1.1 200 OK");
            pout.println("Server: MyApp");
            pout.println("Content-Type: text/html");
            pout.println("Content-Length: " + response.length());
            pout.println();
            pout.println(response);
            pout.flush();
            if (matcher.find())
                return matcher.group(1);
            else
                throw new RuntimeException("Could not find match");
        } finally {
            in.close();
        }
    } catch (Exception ex) {
        throw new RuntimeException("Error while listening for local redirect", ex);
    }
}

Getting a refresh token and saving it for later is the best bet. Per Getting null Refresh token:

GoogleAuthorizationCodeFlow flow = 
    new GoogleAuthorizationCodeFlow.Builder(transport, jsonFactory, clientSecrets, Collections.singleton(GMAIL_SCOPE))
        .setApprovalPrompt("force") // Needed only if you users didn't accept this earlier
        .setAccessType("offline")
        .build();
like image 63
Charlie Avatar answered Dec 10 '25 22:12

Charlie



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!