OAuth2 Library Yields Property Storage Quota Error - google-apps-script

I have a script using Google's OAuth2 library, which recently started to fail with the error
You have exceeded the property storage quota. Please remove some properties and try again.
I inspected the properties and did not find any unexpected differences from my original configuration. The full stringified text length of my properties is around 5000 characters, which is well below the 500kB/property store quota and the longest individual property ~2500 characters (below the 9kB/value quota).
I've discovered that this error only occurs when I use Google's OAuth2 library with a service account. However, if I include the library dist directly in my project, the error disappears.
Why is there a difference in how the properties service behaves between the library and the local copy if they appear to be the same versions?
(In other scripts where I use the OAuth2 library with a standard authorization code grant flow, I have no issues.)
This script will replicate the error, but requires that you create a service account and set the correct script properties as defined in getAdminDirectory_(). (Using Rhino interpreter.)
/**
* Get a GSuite User.
*/
function getUser() {
var email = "name#exampledomain.com";
var user = AdminDirectory_getUser_(email);
Logger.log(user);
}
/**
* Gets a user from the GSuite organization by their email address.
* #returns {User} - https://developers.google.com/admin-sdk/directory/v1/reference/users#resource
*/
function AdminDirectory_getUser_(email) {
var service = getAdminDirectory_();
if (service.hasAccess()) {
var url = "https://www.googleapis.com/admin/directory/v1/users/" + email;
var options = {
method: "get",
headers: {
Authorization: "Bearer " + service.getAccessToken()
}
};
var response = UrlFetchApp.fetch(url, options);
var result = JSON.parse(response.getContentText());
return result;
} else {
throw service.getLastError();
}
}
/**
* Configures the service.
*/
function getAdminDirectory_() {
// Get service account from properties
var scriptProperties = PropertiesService.getScriptProperties();
var serviceAccount = JSON.parse(scriptProperties.getProperty("service_account"));
// Email address of the user to impersonate.
var user_email = scriptProperties.getProperty("service_account_user");
return OAuth2.createService("AdminDirectory:" + user_email)
// Set the endpoint URL.
.setTokenUrl("https://oauth2.googleapis.com/token")
// Set the private key and issuer.
.setPrivateKey(serviceAccount.private_key)
.setIssuer(serviceAccount.client_email)
// Set the name of the user to impersonate. This will only work for
// Google Apps for Work/EDU accounts whose admin has setup domain-wide
// delegation:
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
.setSubject(user_email)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(scriptProperties)
// Set the scope. This must match one of the scopes configured during the
// setup of domain-wide delegation.
.setScope("https://www.googleapis.com/auth/admin.directory.user");
}

This seems to be a known issue. Consider adding a star(on top left) to this issue to let Google developers know that you're affected. Consider adding a comment to the tracker with details requested from #7
Possible solutions:
As said in the question, directly use the library code by copy pasting instead of using the buggy library feature.
Switch to v8-#21
Wait for random/Auto resolution -#14

Related

Event Creation impersonation

Trying to create events using Sheets/Calendar for users in our Workspace
The events must be owned by the user so that they have ownership of the Meet to control access to invitees outside of Google. The user doesn't manage the creation of the event though - this is done by another team
The events can be created easily enough, but not with the ownership. Any way around this?
Using an installable trigger allows the function creating the event to be executed by the user. The problem is two-fold:
the trigger needs to be owned by the user, so onOpen or onEdit, so
the call to the CalendarApp to get the user's calendar fails (see error below)
Exception: The script doesn't have permission to perform that action. Required permission: (https://www.googleapis.com/auth/calendar || https://www.googleapis.com/auth/calendar.readonly || https://www.google.com/calendar/feeds)
minimal reproducible example:
const USER = 'user#domain.co.uk';
const CAL = CalendarApp.getCalendarById(USER);
function createSlots() { // data comes from Sheet
var event = CAL.createEvent(
'slot',
new Date('2022-12-05T12:00Z'),
new Date('2022-12-05T13:00Z'),
{
guests: USER // includes user as a guest to create the Meet code
}
);
}
function makeTrigger() {
var trigger = ScriptApp.newTrigger('createSlots')
.forSpreadsheet(SHEET)
.onOpen()
.create();
}
Strangely - in doing the minimal reproducible example I thought about triggering the function createSlots directly instead of inside another function. This works!
So having the code as the post works, but my original effectively was
const USER = 'user#domain.co.uk';
const CAL = CalendarApp.getCalendarById(USER);
function createSlots() { // data comes from Sheet
var event = CAL.createEvent(
'slot',
new Date('2022-12-05T12:00Z'),
new Date('2022-12-05T13:00Z'),
{
guests: USER // includes user as a guest to create the Meet code
}
);
}
function onOpen() {
createSlots;
}
function makeTrigger() {
var trigger = ScriptApp.newTrigger('onOpen')
.forSpreadsheet(SHEET)
.onOpen()
.create();
}
Yes you can do this. However you will need to use a service account and configure domain wide delegation. This needs to be configured by the domain admin.
Once properly configured the service account will be able to login and impersonate the users on your domain.
setup
In app script under library you will need to include Oauth the script id is 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF.
The service account you created open the credetinals.json file and find the private key and the client email you will needs those in this script.
Note this sample just lists a users primary calendar but i can see that it is listing for different users when i change the delegated user. I assume you can get the rest working you just need the authorization part.
// Oauth script id 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
function run() {
var email = 'XXXX#daimto.com';
var service = getDomainWideDelegationService('Calendar: ', 'https://www.googleapis.com/auth/calendar', email);
if (!service.hasAccess()) {
Logger.log('failed to authenticate as user ' + email);
} else Logger.log('successfully authenticated as user ' + email);
Logger.log('access token ' + service.getAccessToken())
getCalendar(service)
}
function getCalendar(service){
var requestBody = {};
requestBody.headers = {'Authorization': 'Bearer ' + service.getAccessToken()};
requestBody.contentType = "application/json";
requestBody.method = "GET";
requestBody.muteHttpExceptions = false;
var url = 'https://www.googleapis.com/calendar/v3/users/me/calendarList/primary';
var calendarGetResponse = UrlFetchApp.fetch(url, requestBody);
Logger.log('response ' + calendarGetResponse)
}
// these two things are included in the .JSON file that you download when creating the service account and service account key
var OAUTH2_SERVICE_ACCOUNT_PRIVATE_KEY = '[REDACTED]';
var OAUTH2_SERVICE_ACCOUNT_CLIENT_EMAIL = '[REDACTED]';
function getDomainWideDelegationService(serviceName, scope, email) {
Logger.log('starting getDomainWideDelegationService for email: ' + email);
return OAuth2.createService(serviceName + email)
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(OAUTH2_SERVICE_ACCOUNT_PRIVATE_KEY)
.setIssuer(OAUTH2_SERVICE_ACCOUNT_CLIENT_EMAIL)
// Set the name of the user to impersonate. This will only work for
// Google Apps for Work/EDU accounts whose admin has setup domain-wide
// delegation:
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
.setSubject(email)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope. This must match one of the scopes configured during the
// setup of domain-wide delegation.
.setScope(scope);
}
The key to the above code is the line .setSubject(email) this is where you define which user the service account is to delegate as.

How can I tweet from Google App Script with OAuth2 library?

I've successfully made my GAS for tweet with OAuth1. It's known that OAuth1 GAS library is deprecated, so I'm trying to migrate to OAuth2 library.
I saw a few changes, but I don't get the correct way to authorize my request with this.
The main questions I have right now are:
Bearer Token replace in OAuth2 to key&access tokens in OAuth1?
I don't need key&access to authorize rquest? I'm base on example of the Google developers's site itself
For more clarity, I put the code, extracted from Google developers's site, adapted for my propouses:
// Call this function just once, to initialize the OAuth client.
function initializeOAuthClient() {
if (typeof OAuth2 === 'undefined') {
var libUrl = 'https://developers.google.com/google-ads/scripts/docs/examples/oauth20-library';
throw Error('OAuth2 library not found. Please take a copy of the OAuth2 ' +
'library from ' + libUrl + ' and append to the bottom of this script.');
}
var tokenUrl = 'https://api.twitter.com/oauth2/token';
authUrlFetch = OAuth2.withClientCredentials(
tokenUrl, CONSUMER_KEY, CONSUMER_SECRET);
}
function sendTweet(status) {
var service = accessProtectedResource(SERVICE_UPDATE_URL, "post");
if (service.hasAccess()) {
var url = 'https://api.twitter.com/1.1/statuses/update.json?include_entities=true&status=' + percentEncode(status);
var response = service.fetch(url);
//var result = JSON.parse(response.getContentText());
return response;
}
}
/**
* Attempts to access a non-Google API using a constructed service
* object.
*
* If your add-on needs access to non-Google APIs that require OAuth,
* you need to implement this method. You can use the OAuth1 and
* OAuth2 Apps Script libraries to help implement it.
*
* #param {String} url The URL to access.
* #param {String} method_opt The HTTP method. Defaults to GET.
* #param {Object} headers_opt The HTTP headers. Defaults to an empty
* object. The Authorization field is added
* to the headers in this method.
* #return {HttpResponse} the result from the UrlFetchApp.fetch() call.
*/
function accessProtectedResource(url, method_opt, headers_opt) {
var service = getOAuthService();
var maybeAuthorized = service.hasAccess();
if (maybeAuthorized) {
// A token is present, but it may be expired or invalid. Make a
// request and check the response code to be sure.
// Make the UrlFetch request and return the result.
var accessToken = service.getAccessToken();
var method = method_opt || 'post';
var headers = headers_opt || {};
headers['Authorization'] =
Utilities.formatString('Bearer %s', accessToken);
var resp = UrlFetchApp.fetch(url, {
'headers': headers,
'method' : method,
'muteHttpExceptions': true, // Prevents thrown HTTP exceptions.
});
var code = resp.getResponseCode();
if (code >= 200 && code < 300) {
return resp.getContentText("utf-8"); // Success
} else if (code == 401 || code == 403) {
// Not fully authorized for this action.
maybeAuthorized = false;
} else {
// Handle other response codes by logging them and throwing an
// exception.
console.error("Backend server error (%s): %s", code.toString(),
resp.getContentText("utf-8"));
throw ("Backend server error: " + code);
}
}
if (!maybeAuthorized) {
// Invoke the authorization flow using the default authorization
// prompt card.
CardService.newAuthorizationException()
.setAuthorizationUrl(service.getAuthorizationUrl())
.setResourceDisplayName("Display name to show to the user")
.throwException();
}
}
/**
* Create a new OAuth service to facilitate accessing an API.
* This example assumes there is a single service that the add-on needs to
* access. Its name is used when persisting the authorized token, so ensure
* it is unique within the scope of the property store. You must set the
* client secret and client ID, which are obtained when registering your
* add-on with the API.
*
* See the Apps Script OAuth2 Library documentation for more
* information:
* https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
*
* #return A configured OAuth2 service object.
*/
function getOAuthService() {
return OAuth2.createService('SERVICE_NAME')
.setAuthorizationBaseUrl('SERVICE_AUTH_URL')
.setTokenUrl('SERVICE_AUTH_TOKEN_URL')
.setClientId('CLIENT_ID')
.setClientSecret('CLIENT_SECRET')
.setScope('SERVICE_SCOPE_REQUESTS')
.setCallbackFunction('authCallback')
.setCache(CacheService.getUserCache())
.setPropertyStore(PropertiesService.getScriptProperties())
}
/**
* Boilerplate code to determine if a request is authorized and returns
* a corresponding HTML message. When the user completes the OAuth2 flow
* on the service provider's website, this function is invoked from the
* service. In order for authorization to succeed you must make sure that
* the service knows how to call this function by setting the correct
* redirect URL.
*
* The redirect URL to enter is:
* https://script.google.com/macros/d/<Apps Script ID>/usercallback
*
* See the Apps Script OAuth2 Library documentation for more
* information:
* https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
*
* #param {Object} callbackRequest The request data received from the
* callback function. Pass it to the service's
* handleCallback() method to complete the
* authorization process.
* #return {HtmlOutput} a success or denied HTML message to display to
* the user. Also sets a timer to close the window
* automatically.
*/
function authCallback(callbackRequest) {
var authorized = getOAuthService().handleCallback(callbackRequest);
if (authorized) {
return HtmlService.createHtmlOutput(
'Success! <script>setTimeout(function() { top.window.close() }, 1);</script>');
} else {
return HtmlService.createHtmlOutput('Denied');
}
}
/**
* Unauthorizes the non-Google service. This is useful for OAuth
* development/testing. Run this method (Run > resetOAuth in the script
* editor) to reset OAuth to re-prompt the user for OAuth.
*/
function resetOAuth() {
getOAuthService().reset();
}
function main() {
try {
let result = sendTweet("Este va a ser un gran día!\n https://www.instagram.com/amos_oficialba/");
Logger.log("Resultado: " + result);
}
catch(err) {
console.log(err["stack"]);
}
}
Native support was removed from OAuthConfig, but that does not prevent your app to make an OAuth 1 request to external APIs. The open source library OAuth1 for Apps Script was created as a replacement in case you were using OAuthConfig before.
To Tweet from Google Apps Script with the OAuth1 for Apps Script library:
You need to setup the callback URL in your Twitter Developer portal. When using this library, the callback URL will always be in the format https://script.google.com/macros/s/YOUR_SCRIPT_ID/usercallback. You will need to replace YOUR_SCRIPT_ID with, well, your script's ID.
In Google Apps Script, go to the File menu and select Project properties. Take a note of your script ID.
In the Twitter Developer portal, select your app, then click Edit under Authentication settings.
Add the callback URL, then click Save when done:
Back in Google Apps Script, select the Resources menu, then click Libraries.
In the Libraries window, import the OAuth1 library by typing its ID 1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s, then click Add.
Select the latest version (18 at the time of writing)
Once done, use this script to setup a valid Tweet request. Replace CONSUMER_KEY and CONSUMER_SECRET with the API key and secret for your app; replace TOKEN, and TOKEN_SECRET with your user's access token and access token secret.
var CONSUMER_KEY = 'your consumer key';
var CONSUMER_SECRET = 'your consumer secret';
var TOKEN = 'your access token';
var TOKEN_SECRET = 'your access token secret';
/**
* Authorizes and makes a request to the Twitter API.
*/
function run() {
var service = getService();
Logger.log(service.getCallbackUrl())
if (service.hasAccess()) {
var url = 'https://api.twitter.com/1.1/statuses/update.json';
var payload = {
status: 'just setting up my google apps script'
};
var response = service.fetch(url, {
method: 'post',
payload: payload
});
var result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
} else {
var authorizationUrl = service.authorize();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function doGet() {
return HtmlService.createHtmlOutput(ScriptApp.getService().getUrl());
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset() {
var service = getService();
service.reset();
}
/**
* Configures the service.
*/
function getService() {
return OAuth1.createService('Twitter')
// Set the endpoint URLs.
.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
.setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
// Set the consumer key and secret.
.setConsumerKey(CONSUMER_KEY)
.setConsumerSecret(CONSUMER_SECRET)
// Set your user's access token key and secret
.setAccessToken(TOKEN, TOKEN_SECRET)
.setCallbackFunction('authCallback')
}
/**
* Handles the OAuth callback.
*/
function authCallback(request) {
var service = getService();
var authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied');
}
}
Alternatively, you can use Google's own OAuth 1 replacement script to sign OAuth 1 requests. You can find an example of usage in the Google Ads script page.

Google Docs API in Google Apps Script as an external API (NOT as extended service)

I am trying to use the new Google Docs API using Google Apps Script. Since new API is not yet available as an extended service, I am trying to do it using UrlFetchApp() but failing.
Apologies for my naive attempt here:
function apiCall(){
var API_KEY = 'YOUR_API_KEY';
var username = 'YOUR_USERNAME';
var password = 'YOU_PASSWORD';
var DOC_ID = 'YOUR_DOC_ID';
var root = 'https://docs.googleapis.com/v1/documents/';
var endpoint = DOC_ID;
var query = '?key=' + API_KEY;
var params = {
'method': 'GET',
'muteHttpExceptions': true,
'headers': {
'Authorization': 'Basic ' + Utilities.base64Encode(username + ':' + password)
}
};
var response = UrlFetchApp.fetch(root + endpoint + query, params);
var data = response.getContentText();
var json = JSON.parse(data);
Logger.log(json);
}
I get the following response:
{error={code=401, message=Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or another valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project., status=UNAUTHENTICATED}}
Can someone point to the right direction, where I can find some documentation how to use Google Docs API in Google Apps Script.
If you own the document then you don't need to leverage an API key. Also, instead of using Basic authentication you can leverage the built-in Bearer OAuth token as follows:
/**
* Get `Document` resource object from Google Docs REST API.
*
* #param {String} docId - A Google Document Id
*
* #return {Document} A Document resource object.
*/
function getDocumentResouce(docId) {
return JSON.parse(UrlFetchApp.fetch(
"https://docs.googleapis.com/v1/documents/" + docId,
{
"headers": {
"Authorization":"Bearer " + ScriptApp.getOAuthToken()
}
}
)
);
}
Note: GET is the default HTTP request method used by UrlFetchApp.fetch() so you don't need to define it in the options object.
ADDENDUM
As Tanaike stated in the comments you'll need to manually add the relevant scopes (in addition to the ones you already have enabled) to your manifest JSON.
First check your project properties to get the list of existing scopes via the menu
File > Project Properties > Scopes. You need to add those scopes, as well as one of the relevant document scopes (listed in the documentation) to your manifest.
The following links provide the information you'll need to manage your manifest and scopes:
https://developers.google.com/apps-script/concepts/manifests
https://developers.google.com/apps-script/concepts/scopes

Client ID no longer exists

When I try to run an Appscript I get error ..
[17-02-08 01:00:35:160 PST] Open the following URL and re-run the script: https://accounts.google.com/o/oauth2/auth?client_id=317559754348->0p1ti3fmjae175i06hn07jrbia6701q6.apps.googleusercontent.com&response_type=code&redirect_uri=https%3A%2F%2Fscript.google.com%2Fmacros%2Fd>%2F1pgAT7ZCwiKrHx_7Iys770hJNRqTYwn9zioe9Qjvmhzc9rfIxO04P8Uum%2Fusercallback&state=ADEpC8xV9BMj3kqytygKLnjEYT7PX918NJg0i1oSCAsUTRwXcOgdzDStZA3lzGDK98CJ6OOhlDnlYEyyji5rx6P8haao8oDop->PMQBZsMMjk2Jl_GtsPnsifFDt1XjqSXtCS2Wx6X3fdLDHTlBzfwqvqrinfkHhW1dVw0oNv6-MaqDhimE912Po&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth>%2Fdevstorage.read_write&access_type=offline&approval_prompt=force&login_hint=imerrywe%40jaguarlandrover.com
Trouble is Client ID 317559754348-0p1ti3fmjae175i06hn07jrbia6701q6.apps.googleusercontent.com
no longer exists. Looks like it has accidentaly been deleted.
I have tried created new Oauth2 credentials but my appscript wants to use the old one.
How do I get my appscripts to use new credentials. ?
Regards,
Ian
// Global Project variables
var CLIENT_ID = 'xxxxxxxxxx-
leuap166eur7gi5ufr6kiau2nqefknci.apps.googleusercontent.com';
var CLIENT_SECRET = 'xxxxxxxxxx';
// OAuth2.0 Access token
var token;
function oAuth() {
// Check we have access to the service
Logger.log(Session.getActiveUser().getEmail());
Logger.log( Session.getEffectiveUser().getEmail());
var service = getService();
Logger.log('Access '+service.hasAccess());
var authInfo = ScriptApp.getAuthorizationInfo(ScriptApp.AuthMode.FULL);
Logger.log(authInfo.getAuthorizationStatus());
if (!service.hasAccess()) {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
return;
}
}
function getService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('xxxxxxxxxx')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
// Set the scopes to request (space-separated for Google services).
// this is admin access for the sqlservice and access to the cloud- platform:
.setScope('https://www.googleapis.com/auth/sqlservice.admin
https://www.googleapis.com/auth/cloud-platform')
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
.setParam('login_hint', Session.getActiveUser().getEmail())
// Requests offline access.
.setParam('access_type', 'offline')
// Forces the approval prompt every time. This is useful for testing,
// but not desirable in a production application.
.setParam('approval_prompt', 'force');
}
That’s an error.
Error: redirect_uri_mismatch
The redirect URI in the request, does not match the ones authorized for the OAuth >client. Visit >1 to update the authorized redirect URIs.
Just to address the
That’s an error. Error: redirect_uri_mismatch
Go to your new clientID, below it you may need to add the "oauth2callback" redirect uri like:
http://www.mywebsite.com/oauth2callback

How to use the Gmail API, OAuth2 for Apps Script, and Domain-Wide Delegation to set email signatures for users in a G Suite domain

This is a follow-up to a previous question/answer I posted (How to use the Google Email Settings API and the OAuth2 for Apps Script Library to set email signatures for users in a Google Apps domain), but I'm creating a new question since the Email Settings API has been deprecated and the process is significantly different now.
As the administrator of a G Suite domain, how do you use the Gmail API to programmatically set the email signatures of users in your domain through Google Apps Script?
This method uses the Gmail API, the OAuth2 for Apps Script library, and "Domain-wide Delegation of Authority", which is a way for G Suite admins to make API calls on behalf of users within their domain.
Step 1: Make sure the OAuth2 For Apps Script library is added to your project.
Step 2: Set up "Domain-Wide Delegation of Authority." There's a page here explaining how to do it for the Drive API, but it's pretty much the same for any Google API, including the Gmail API. Follow the steps on that page up to, and including, the "Delegate domain-wide authority to your service account" step.
Step 3: The code below includes how to set the signature after the previous steps are complete:
function setSignatureTest() {
var email = 'test#test.com';
var signature = 'test signature';
var test = setSignature(email, signature);
Logger.log('test result: ' + test);
}
function setSignature(email, signature) {
Logger.log('starting setSignature');
var signatureSetSuccessfully = false;
var service = getDomainWideDelegationService('Gmail: ', 'https://www.googleapis.com/auth/gmail.settings.basic', email);
if (!service.hasAccess()) {
Logger.log('failed to authenticate as user ' + email);
Logger.log(service.getLastError());
signatureSetSuccessfully = service.getLastError();
return signatureSetSuccessfully;
} else Logger.log('successfully authenticated as user ' + email);
var username = email.split("#")[0];
var resource = { signature: signature };
var requestBody = {};
requestBody.headers = {'Authorization': 'Bearer ' + service.getAccessToken()};
requestBody.contentType = "application/json";
requestBody.method = "PUT";
requestBody.payload = JSON.stringify(resource);
requestBody.muteHttpExceptions = false;
var emailForUrl = encodeURIComponent(email);
var url = 'https://www.googleapis.com/gmail/v1/users/me/settings/sendAs/' + emailForUrl;
var maxSetSignatureAttempts = 20;
var currentSetSignatureAttempts = 0;
do {
try {
currentSetSignatureAttempts++;
Logger.log('currentSetSignatureAttempts: ' + currentSetSignatureAttempts);
var setSignatureResponse = UrlFetchApp.fetch(url, requestBody);
Logger.log('setSignatureResponse on successful attempt:' + setSignatureResponse);
signatureSetSuccessfully = true;
break;
} catch(e) {
Logger.log('set signature failed attempt, waiting 3 seconds and re-trying');
Utilities.sleep(3000);
}
if (currentSetSignatureAttempts >= maxSetSignatureAttempts) {
Logger.log('exceeded ' + maxSetSignatureAttempts + ' set signature attempts, deleting user and ending script');
throw new Error('Something went wrong when setting their email signature.');
}
} while (!signatureSetSuccessfully);
return signatureSetSuccessfully;
}
// these two things are included in the .JSON file that you download when creating the service account and service account key
var OAUTH2_SERVICE_ACCOUNT_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxxxxxxxxxxxxxxxxxxxxx\n-----END PRIVATE KEY-----\n';
var OAUTH2_SERVICE_ACCOUNT_CLIENT_EMAIL = 'xxxxxxxxxxxxxxxxxxxxx.iam.gserviceaccount.com';
function getDomainWideDelegationService(serviceName, scope, email) {
Logger.log('starting getDomainWideDelegationService for email: ' + email);
return OAuth2.createService(serviceName + email)
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey(OAUTH2_SERVICE_ACCOUNT_PRIVATE_KEY)
.setIssuer(OAUTH2_SERVICE_ACCOUNT_CLIENT_EMAIL)
// Set the name of the user to impersonate. This will only work for
// Google Apps for Work/EDU accounts whose admin has setup domain-wide
// delegation:
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
.setSubject(email)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope. This must match one of the scopes configured during the
// setup of domain-wide delegation.
.setScope(scope);
}
Please note: the do-while loop with the maxSetSignatureAttempts and currentSetSignatureAttempts variables is not necessary. I added it because if you're trying to set signatures immediately after creating the Google account and assigning a G Suite license, sometimes the Gmail API returns an error as if the user wasn't created yet. That do-while loop basically waits 3 seconds if it gets an error, then tries again, up to x number of times. You shouldn't have that issue if you're setting signatures for existing users. Also, originally I just had a fixed 10-second sleep, but most of the time it didn't need to take that long, but other times it would still fail. So this loop is better than a fixed sleep amount.