Is there any way to connect to GMail using the JavaMail API without enabling "Less secure access"?
Is there any way to connect to GMail using the JavaMail API without enabling "Less secure access"?
You must authenticate using OAuth2 to avoid turning on Less secure access.
Per very-hard-to-find docs
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.
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();