In response to Google's recent change to disallow username/password access to Google Workspace accounts from Google Cloud VM-instances, I am attempted to replace sendmail calls with Google API calls to Gmail API using Java with OAuth2 authentication using service accounts. The error seems to be fairly standard with what many others are seeing, with no details to help troubleshoot:
com.google.api.client.googleapis.json.GoogleJsonResponseException: 400 Bad Request
POST https://gmail.googleapis.com/gmail/v1/users/*****@*****.com/messages/send
{
"code": 400,
"errors": [
{
"domain": "global",
"message": "Precondition check failed.",
"reason": "failedPrecondition"
}
],
"message": "Precondition check failed.",
"status": "FAILED_PRECONDITION"
}
at com.google.api.client.googleapis.json.GoogleJsonResponseException.from(GoogleJsonResponseException.java:146)
at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:118)
at com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:37)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest$1.interceptResponse(AbstractGoogleClientRequest.java:439)
at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1111)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:525)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.executeUnparsed(AbstractGoogleClientRequest.java:466)
at com.google.api.client.googleapis.services.AbstractGoogleClientRequest.execute(AbstractGoogleClientRequest.java:576)
After researching using Google's documentation pages and stackoverflow articles I have performed all of the following steps:
- Created a new Service Account under my Google Cloud production project and granted it
Basic/Editorrole. - Updated my VM-instance to use this new service account (maybe unnecessary).
- Downloaded the Service Account's key file as JSON.
- In my Java application code, copied and modified Google's sample code from Sending Mail.
- In Google Workspace, enabled domain-wide delegation by adding a new client using my Service Account's client id, and setting the scope value to
https://www.googleapis.com/auth/gmail.send - Deployed my code to the VM-instance and tested.
Here is the Java email-sending code:
public void send(String toEmailAddress, String[] ccEmailAddress, String[] bccEmailAddress, String replyToAddress,
String subject, String htmlBodyText) {
GoogleCredentials credentials;
try {
String json = /** fetch the service account JSON file **/
InputStream stream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
credentials = ServiceAccountCredentials.fromStream(stream).createScoped(GmailScopes.GMAIL_SEND);
HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(credentials);
// Create the gmail API client
Gmail service = new Gmail.Builder(new NetHttpTransport(), GsonFactory.getDefaultInstance(),
requestInitializer).setApplicationName(MyAppName).build();
// Encode as MIME message
Properties props = new Properties();
Session session = Session.getDefaultInstance(props, null);
MimeMessage email = new MimeMessage(session);
email.setFrom(new InternetAddress(fromEmailAddress));
email.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(toEmailAddress));
if (ccEmailAddress != null) {
for (String addr : ccEmailAddress) {
email.addRecipient(javax.mail.Message.RecipientType.CC, new InternetAddress(addr));
}
}
if (bccEmailAddress != null) {
for (String addr : bccEmailAddress) {
email.addRecipient(javax.mail.Message.RecipientType.BCC, new InternetAddress(addr));
}
}
email.setSubject(subject);
email.setText(htmlBodyText);
// Encode and wrap the MIME message into a gmail message
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
email.writeTo(buffer);
byte[] rawMessageBytes = buffer.toByteArray();
String encodedEmail = Base64.encodeBase64URLSafeString(rawMessageBytes);
Message message = new Message();
message.setRaw(encodedEmail);
try {
// Create send message
message = service.users().messages().send(workspaceAccountEmail, message).execute();
LOGGER.debug("\"Message id: {}, {}", message.getId(),message.toPrettyString());
// return message;
} catch (GoogleJsonResponseException e) {
LOGGER.warn("Caught an {} exception when trying to send mail", e.getClass().getName(), e);
}
// return null;
} catch (IOException | MessagingException e) {
LOGGER.warn("Caught an {} exception when trying to prepare sendmail", e.getClass().getName(), e);
}
}
Google Workspace account impersonation is happening in the code when calling the Google API service send() function, and passing in the workspace account email address.
I have verified the contents of the service account JSON file by changing it manually, and seeing different error messages based on missing attributes etc., so I know that it's using the correct file contents.