You are on page 1of 9

Skip to content

DEV Community 👩‍💻👨‍💻


Search...

Log in
Create account

3
Like
1
Unicorn
3
Save

Impression profile imageAaron Dicks


Aaron Dicks for Impression
Posted on 30 Mar 2020 • Updated on 26 Apr 2021

Google Suite domain-level signatures with Google Scripts


#
javascript
Getting Started
In this post I'll show you how to create a Google Apps Script with domain-level
permissions to set your users signatures. This is great for maintaining consistency
across a large or distributed team.

There are a number of Google Suite Domain signature software applications out
there, but we have fairly straightforward requirements and wanted to create a cost-
free solution. Previously we used Signature Satori but for our simple needs, this
was overkill.

Our requirements at Impression were that all signatures were to:

be consistent
contain tracking URLs for Google Analytics
optionally contain a job title
optionally contain a direct dial
optionally contain an additional message, for part time staff
Additionally we have a number of service accounts which we do not want to include a
signature on, as they have account names like "Impression Accounts".

Things to set up
The permissions required by this script span outside of the usual permissions a
Google Script requires, so there's a few bits you'll need to set up in advance.

1. Connect to a Google Cloud Console project


There's a good set of instructions and background here but essentially it's
important to realise that for advanced permission setting, your Google Script must
be incorporated into a Google Cloud Project, with enabled billing. It's from within
here you can enable API access and generate your required credentials.

Enable the following APIs;

Gmail API
Admin SDK
Configure a Service Account under "Credentials" and save the output file

2. OAuth 2.0
OAuth 2.0 can be added to Google Scripts via the Resources > Libraries dialog. Just
so these instructions do not get outdated, please read the originals here on
GitHub.

3. Enable "Admin SDK Directory Service"


This is an advanced admin-only G Suite permission that's required to list domain
users. Read more on how to do this here with the latest Google instructions.

4. Allow domain-wide delegation


From within your Google Admin, enable Domain-Wide Delegation for your Client ID on
the specific following scopes. Follow the steps to add a new configuration.

Scopes:

https://apps-apis.google.com/a/feeds/emailsettings/2.0/
https://www.googleapis.com/auth/gmail.settings.basic
https://www.googleapis.com/auth/gmail.settings.sharing
5. Optionally set up you additional Google User Admin fields
As mentioned above, our requirements here is that we show additional signature
settings, such as job title, working hours, Google Voice numbers. Your usage may
vary, but as we all know it's easy to strip back from a code sample than add to it.
So this code includes all of the above.

To add additional fields (called "custom user attributes" in Google speak), read
this help article.

We have the following fields set up under the new category "Email signature":

Show_job_title_in_signature (Yes/No)
Working_Hours_Description (Text)
Custom user attribute settings inside Google Admin

6. Load up this demo code


Don't forget to skip to the next section to learn which bits you should customise!

There are two files; the first should be pasted over your Code.gs file. The second
code snippet should be added to a new HTML file in the project. Head to File > New
> HTML File to add this. Name it signature.html.

Code.gs
var accountsToIgnore = [
'ignore-me@example.com',
'noreply@example.com'
];

var auth = {
"private_key": "-----BEGIN PRIVATE KEY-----\nABCDE\n-----END PRIVATE KEY-----\n",
"client_email": "name@project-id-XXX.iam.gserviceaccount.com",
"client_id": "INSERT_CLIENT_ID_HERE"
};

function go() {
var pageToken;
var page;

do {
page = AdminDirectory.Users.list({
domain: 'example.com',
orderBy: 'familyName',
maxResults: 250,
pageToken: pageToken,
projection: 'full',
// query: "email=your.email@example.com"
});
if (page.users) {
page.users.forEach( function (user){
if (accountsToIgnore.indexOf(user.primaryEmail) == -1) {

var service = getOAuthService(user.primaryEmail);


// Pull in the signatire template file contents into this variable
var signatureTemplate =
HtmlService.createHtmlOutputFromFile("signature").getContent();

// Set up a userData variable, with some blank defaults as backups


var userData = {
email: user.primaryEmail,
firstName: user.name.givenName,
lastName: user.name.familyName,
jobTitle: "",
showJobTitle: true,
workingHours: "",
directPhone: ""
};
if (typeof user.customSchemas !== 'undefined') { // Email sig settings
are set
if (typeof user.customSchemas.Email_signature !== 'undefined') {

if (typeof
user.customSchemas.Email_signature.Show_job_title_in_signature !== 'undefined' &&
user.customSchemas.Email_signature.Show_job_title_in_signature == false) {
userData.showJobTitle = false;
}

if (typeof
user.customSchemas.Email_signature.Working_Hours_Description !== 'undefined' &&
user.customSchemas.Email_signature.Working_Hours_Description != "") {
userData.workingHours = "<br /><br
/><i>"+user.customSchemas.Email_signature.Working_Hours_Description+"</i><br />";
}

}
}

if (user.hasOwnProperty('organizations') &&
user.organizations[0].hasOwnProperty('title') && typeof user.organizations[0].title
!== "undefined" && userData.showJobTitle == true) {
userData.jobTitle = user.organizations[0].title+"<br />";
}

if (user.hasOwnProperty('phones') && Array.isArray(user.phones) &&


user.phones.length >0) {
for (var p = 0; p < user.phones.length; p++) {
if (user.phones[p].customType == "Google Voice") {
// Depending on where in the world you are, you may need to adjust
this formatting for your own needs... This replaces the +44 UK country code with a
local "0" and adds a space after the local area code for formatting.
userData.directPhone = "<br />D: " +
user.phones[p].value.replace('+44', '0').replace('1158', '1158 ');
}
}
}

// Replace the placeholders as seen in the signature.html file with the


actual data from the userData variable set up earlier.
var userSig = signatureTemplate
.replace(/(\r\n|\n|\r)/gm, "")
.replace(/{email}/g, userData.email)
.replace(/{firstName}/g, userData.firstName)
.replace(/{lastName}/g, userData.lastName)
.replace(/{jobTitle}/g, userData.jobTitle)
.replace(/{workingHours}/g, userData.workingHours)
.replace(/{directNumber}/g, userData.directPhone);

var sigAPIUrl =
Utilities.formatString('https://www.googleapis.com/gmail/v1/users/%s/settings/
sendAs/%s',userData.email, userData.email);

var response = UrlFetchApp.fetch(sigAPIUrl, {


method: "PUT",
muteHttpExceptions: true,
contentType: "application/json",
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
},
payload: JSON.stringify({
'signature': userSig
})
});

if (response.getResponseCode() !== 200) {


Logger.log('There was an error: ' + response.getContentText());
} else {
Logger.log("Signature updated for "+user.primaryEmail);
}
}
});

} else {
Logger.log('No users found.');
}
pageToken = page.nextPageToken;
} while (pageToken);
}

function getOAuthService(userId) {
return OAuth2.createService("Signature Setter "+userId)
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(auth.private_key)
.setIssuer(auth.client_email)
.setPropertyStore(PropertiesService.getScriptProperties())
.setSubject(userId)
.setParam('access_type', 'offline')
.setScope('https://www.googleapis.com/auth/gmail.settings.basic
https://www.googleapis.com/auth/gmail.settings.sharing');
}

signature.html
<br />
<b style='font-size:small'>{firstName} {lastName}</b><br />
{jobTitle}Impression<br />
<a href='http://www.impression.co.uk/?
utm_source=personalemail&amp;utm_medium=email&amp;utm_campaign=EmailSig&amp;utm_con
tent={firstName}-{lastName}' rel='nofollow'><img alt='Impression' height='66'
src='https://www.google.com/a/impression.co.uk/images/logo.gif' width='160'
/></a><br />
<br />
T: 01158 242 212 {directNumber}<br />
E: {email}<br />
W: <a href='https://www.impression.co.uk/?
utm_source=personalemail&amp;utm_medium=email&amp;utm_campaign=EmailSig&amp;utm_con
tent={firstName}-{lastName}' rel='nofollow'>www.impression.co.uk</a><br />
<br />
Specialists in <a href='https://www.impression.co.uk/digital-marketing/seo/?
utm_source=personalemail&amp;utm_medium=email&amp;utm_campaign=EmailSig&amp;utm_con
tent={firstName}-{lastName}' rel='nofollow'>SEO</a>, <a
href='https://www.impression.co.uk/digital-marketing/ppc/?
utm_source=personalemail&amp;utm_medium=email&amp;utm_campaign=EmailSig&amp;utm_con
tent={firstName}-{lastName}' rel='nofollow'>PPC</a>, <a
href='https://www.impression.co.uk/digital-marketing/digital-pr/?
utm_source=personalemail&amp;utm_medium=email&amp;utm_campaign=EmailSig&amp;utm_con
tent={firstName}-{lastName}' rel='nofollow'>Digital PR</a> &amp; <a
href='https://www.impression.co.uk/digital-marketing/analytics/?
utm_source=personalemail&amp;utm_medium=email&amp;utm_campaign=EmailSig&amp;utm_con
tent={firstName}-{lastName}' rel='nofollow'>Analytics</a>{workingHours}
By looking through the signature.html file you'll see plenty of placeholder values
that we're using, like "{lastName}", embedded between our signature.

This aspect is one that you'll definitely want to customise to fit your own needs.
Here's what our signature looks like:

Aaron Dicks email signature

7. Fill in your service credentials and authenticate the script


Insert your credentials from the Service Account you created inside your Google
Cloud Project inside the "auth" variable in Code.gs. These should be self
explanatory, and can be found if you open the JSON file in a text editor.

8. Testing
When testing, see the commented "query" line. Use this to query specific accounts,
like your own, which comes in handy for live testing this solution.

Bear in in mind that your browser will not show a new email signature until you
fully refresh all open Gmail windows.

9. Scheduling
Once created, it's worth thinking about using a daily trigger to ensure consistency
is maintained across your business over time. We run the "go" script on a timer
trigger, once nightly.

Thank you
I hope this is enough to get you started on your own custom signature adventures.
Please hit me up on twitter @aarondicks if you have any specific questions!

Discussion (3)
Subscribe
pic
Add to the discussion
cdwarak profile image
Dwarakanath Cheyyur

Aug 10 '21

Hi Aaron

I am getting an error - "Error: Access not granted or expired.


Service_.getAccessToken @ Service.gs:466". any pointers? I have created the OAuth
consent screen and the service account as detailed. The authorization takes place
and then this error. I have set all the scopes as well. Could it be the private key
has some issue?

1
like
Like
Reply

macarthuror profile image


macarthuror

Jan 14 '21

Do you have any other Docs or any tip of how to implement OAuth ?

I'm trying your code but I don't find out how to implement the auth part. I was
looking for some example about it in the Gmail API and Workspace API but didn't
have luck.

1
like
Like
Reply

aarondicks profile image


Aaron Dicks

May 25 '21

Hi, sorry, I've just seen this comment. I've since updated the domain-wide oauth
delegation step above to include what I think you might have been missing.
Apologies for the inconvenience. Thanks, Aaron

1
like
Like
Reply
Code of Conduct • Report abuse
🌱 DEV runs on 100% open source code known as Forem.

Contribute to the codebase or learn how to host your own.

Read next
rmion profile image
Leonardo's Monorail
Robert Mion - Aug 11

testrigtech profile image


Cypress vs. Playwright: Which One is Better for You?
TestrigTech - Aug 11

subhamx profile image


OneSocial: the ultimate super app for creators and their audience
Subham Sahu - Aug 29

parables profile image


What's new in `svelte-kit, 1.0.0-next.445`: (group) layout
Parables - Aug 29

Impression
Follow
Trending on DEV Community 👩‍💻👨‍💻
Michael Tharrington profile image
What was your win this week?
#discuss #weeklyretro
Ben Halpern profile image
Meme Monday 🌴
#discuss #watercooler #jokes
Sloan profile image
What would you do if you had a year off between jobs?
#anonymous #discuss #career #watercooler
var accountsToIgnore = [
'ignore-me@example.com',
'noreply@example.com'
];

var auth = {
"private_key": "-----BEGIN PRIVATE KEY-----\nABCDE\n-----END PRIVATE KEY-----\n",
"client_email": "name@project-id-XXX.iam.gserviceaccount.com",
"client_id": "INSERT_CLIENT_ID_HERE"
};

function go() {
var pageToken;
var page;

do {
page = AdminDirectory.Users.list({
domain: 'example.com',
orderBy: 'familyName',
maxResults: 250,
pageToken: pageToken,
projection: 'full',
// query: "email=your.email@example.com"
});
if (page.users) {
page.users.forEach( function (user){
if (accountsToIgnore.indexOf(user.primaryEmail) == -1) {

var service = getOAuthService(user.primaryEmail);


// Pull in the signatire template file contents into this variable
var signatureTemplate =
HtmlService.createHtmlOutputFromFile("signature").getContent();
// Set up a userData variable, with some blank defaults as backups
var userData = {
email: user.primaryEmail,
firstName: user.name.givenName,
lastName: user.name.familyName,
jobTitle: "",
showJobTitle: true,
workingHours: "",
directPhone: ""
};
if (typeof user.customSchemas !== 'undefined') { // Email sig settings
are set
if (typeof user.customSchemas.Email_signature !== 'undefined') {

if (typeof
user.customSchemas.Email_signature.Show_job_title_in_signature !== 'undefined' &&
user.customSchemas.Email_signature.Show_job_title_in_signature == false) {
userData.showJobTitle = false;
}

if (typeof
user.customSchemas.Email_signature.Working_Hours_Description !== 'undefined' &&
user.customSchemas.Email_signature.Working_Hours_Description != "") {
userData.workingHours = "<br /><br
/><i>"+user.customSchemas.Email_signature.Working_Hours_Description+"</i><br />";
}

}
}

if (user.hasOwnProperty('organizations') &&
user.organizations[0].hasOwnProperty('title') && typeof user.organizations[0].title
!== "undefined" && userData.showJobTitle == true) {
userData.jobTitle = user.organizations[0].title+"<br />";
}

if (user.hasOwnProperty('phones') && Array.isArray(user.phones) &&


user.phones.length >0) {
for (var p = 0; p < user.phones.length; p++) {
if (user.phones[p].customType == "Google Voice") {
// Depending on where in the world you are, you may need to adjust
this formatting for your own needs... This replaces the +44 UK country code with a
local "0" and adds a space after the local area code for formatting.
userData.directPhone = "<br />D: " +
user.phones[p].value.replace('+44', '0').replace('1158', '1158 ');
}
}
}

// Replace the placeholders as seen in the signature.html file with the


actual data from the userData variable set up earlier.
var userSig = signatureTemplate
.replace(/(\r\n|\n|\r)/gm, "")
.replace(/{email}/g, userData.email)
.replace(/{firstName}/g, userData.firstName)
.replace(/{lastName}/g, userData.lastName)
.replace(/{jobTitle}/g, userData.jobTitle)
.replace(/{workingHours}/g, userData.workingHours)
.replace(/{directNumber}/g, userData.directPhone);

var sigAPIUrl =
Utilities.formatString('https://www.googleapis.com/gmail/v1/users/%s/settings/
sendAs/%s',userData.email, userData.email);

var response = UrlFetchApp.fetch(sigAPIUrl, {


method: "PUT",
muteHttpExceptions: true,
contentType: "application/json",
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
},
payload: JSON.stringify({
'signature': userSig
})
});

if (response.getResponseCode() !== 200) {


Logger.log('There was an error: ' + response.getContentText());
} else {
Logger.log("Signature updated for "+user.primaryEmail);
}
}
});

} else {
Logger.log('No users found.');
}
pageToken = page.nextPageToken;
} while (pageToken);
}

function getOAuthService(userId) {
return OAuth2.createService("Signature Setter "+userId)
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
.setPrivateKey(auth.private_key)
.setIssuer(auth.client_email)
.setPropertyStore(PropertiesService.getScriptProperties())
.setSubject(userId)
.setParam('access_type', 'offline')
.setScope('https://www.googleapis.com/auth/gmail.settings.basic
https://www.googleapis.com/auth/gmail.settings.sharing');
}

DEV Community 👩‍💻👨‍💻 — A constructive and inclusive social network for software
developers. With you every step of your journey.

Built on Forem — the open source software that powers DEV and other inclusive
communities.

Made with love and Ruby on Rails. DEV Community 👩‍💻👨‍💻 © 2016 - 2022.

You might also like