javagmail-apigoogle-workspaceservice-accountsgoogle-api-java-client

Java Google Mail API send "Precondition check failed" - 400 Bad Request for Service Account server-to-server no user


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:

  1. Created a new Service Account under my Google Cloud production project and granted it Basic/Editor role.
  2. Updated my VM-instance to use this new service account (maybe unnecessary).
  3. Downloaded the Service Account's key file as JSON.
  4. In my Java application code, copied and modified Google's sample code from Sending Mail.
  5. 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
  6. 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.


Solution

  • I stumbled across a couple of posts FAILED_PRECONDITION - Gmail API which provided an answer that worked for my case:

    This line from the google code sample:

    credentials = ServiceAccountCredentials.fromStream(stream).createScoped(GmailScopes.GMAIL_SEND);
    

    needs to change to this:

    ServiceAccountCredentials.fromStream(stream).createScoped(GmailScopes.GMAIL_SEND).createDelegated(workspaceEmailAddress);
    

    The .createDelegate(workspaceEmailAddress) apparently ensures that the impersonation is happening at the right place.

    Also for a bonus, to send an HTML formatted email, we need to change this line from the google sample code:

    email.setText(htmlBodyText);
    

    to this:

    email.setContent(htmlBodyText, "text/html");