Manage Google Apps Domain Shared Contacts with Google Apps Script

IMG_GoogleThe Google Apps Shared Contacts API allows client applications to retrieve and update external contacts that are shared to all users in a Google Apps domain. Shared contacts are visible to all users of a Google Apps domain and all Google services have access to the contact list.

Google Apps Script provides a Contacts Service that allows the user to interact with his own contacts but doesn’t support the management of domain shared contacts, so in this article we will provide you some recipes on how to implement shared contact support inside Google Apps Script.

Note: The Domain Shared Contacts API is only available to Google Apps for Business and Education accounts. To enable the API, log in to your admin account, and click the User accounts tab. Then click the Settings subtab, select the checkbox to enable the Provisioning API and save your changes. Please note changes can take up to 24 hours to be reflected in the email address auto-complete and the contact manager.

The domain shared contacts API support AuthSub, clientLogin  and oAuth as authentication and authorization methods, in this example i’ll use the oAuth method which is becoming the de-facto standard in the industry. Following is a code snippet that implements the oAuth protocol and handles UrlFetch service parameters based on the request type:

/**
 * Google authentication loader
 * @param {String} method the HTTP method to use for the UrlFetch operation, possible values are: GET, POST, PUT, DELETE
 * @param {String} payload the payload to use if needed
 * @return {Object} configuration options for UrlFetch, including oAuth parameters
 */
function googleOAuth_(method, payload) {
  // Shared configuration for all methods
  var oAuthConfig = UrlFetchApp.addOAuthService(APPNAME);
  oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope='+encodeURIComponent(SCOPE));
  oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
  oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
  oAuthConfig.setConsumerKey('anonymous');
  oAuthConfig.setConsumerSecret('anonymous');

  // Detect the required method
  switch(method) {
    case "GET":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always'};
      break;
    case "POST":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always', payload: payload, contentType: 'application/atom+xml', method: "POST"};
      break;
    case "PUT":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always', payload: payload, contentType: 'application/atom+xml', method: "PUT"};
      break;
    case "DELETE":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always', method: "DELETE"};
      break;
    default:
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always'};
      break;
  }
}

To create or updated a shared contact it’s necessary to create or alter the contact XML. Google Apps Script provides an Xml Service to parse and navigate XML Documents. Unfortunately the Xml Services doesn’t provide any method to manipulate the XML DOM and this is, in some way, required by the domain shared contacts API. For this reason I’ve decided to employ a stunning new feature of Google Apps Script called HTML Service. Using HTML service it’s possible to create HTML and XML templates and fill them using Google Apps Script. Here is a template snippet:

<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gd="http://schemas.google.com/g/2005">
   <id><?=id?></id>
   <updated><?=updated?></updated>
   <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact" />
   <title type="text"><?=title?></title>
   <link rel="http://schemas.google.com/contacts/2008/rel#edit-photo" type="image/*" href="<?=photoLink?>" />
   <link rel="self" type="application/atom+xml" href="<?=selfLink?>" />
   <link rel="edit" type="application/atom+xml" href="<?=editLink?>" />
   <gd:organization rel="http://schemas.google.com/g/2005#work">
      <gd:orgName><?=orgName?></gd:orgName>
      <gd:orgTitle><?=orgTitle?></gd:orgTitle>
   </gd:organization>
   <gd:email rel="http://schemas.google.com/g/2005#work" address="<?=email?>" primary="true" />
</entry>

To try the script of this example you need to create a Spreadsheet file as the following:

Domain shared contacts sample spreadsheet

Create now a new script with the following 3 files:

Shared Contacts Group.gs

var ORGANIZATIONWORK = 'http://schemas.google.com/g/2005#work';
var ORGANIZATIONOTHER = 'http://schemas.google.com/g/2005#other';

var EMAILWORK = 'http://schemas.google.com/g/2005#work';
var EMAILHOME = 'http://schemas.google.com/g/2005#home';
var EMAILOTHER = 'http://schemas.google.com/g/2005#other';

var PHONENUMBERFAX = 'http://schemas.google.com/g/2005#fax';
var PHONENUMBERWORKFAX = 'http://schemas.google.com/g/2005#work_fax';
var PHONENUMBERWORK = 'http://schemas.google.com/g/2005#work';
var PHONENUMBERHOMEFAX = 'http://schemas.google.com/g/2005#home_fax';
var PHONENUMBERHOME = 'http://schemas.google.com/g/2005#home';
var PHONENUMBERMOBILE = 'http://schemas.google.com/g/2005#mobile';
var PHONENUMBEROTHER = 'http://schemas.google.com/g/2005#other';
var PHONENUMBERPAGER = 'http://schemas.google.com/g/2005#pager';

var POSTALADDRESSWORK = 'http://schemas.google.com/g/2005#work';
var POSTALADDRESSHOME = 'http://schemas.google.com/g/2005#home';
var POSTALADDRESSOTHER = 'http://schemas.google.com/g/2005#other';

/**
 * Script configuration
 */
var SCOPE = 'http://www.google.com/m8/feeds/';
var APPNAME = "domainsharedcontacts";
//!!!!! CHANGE THIS !!!!!!
var URL = 'https://www.google.com/m8/feeds/contacts/YOURDOMAINNAME.COM/full';

/**
 * Main function run at spreadsheet opening
 */
function onOpen() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var menuEntries = [ 
    {name: "Initialize shared contacts", functionName: "init"},
    {name: "Clear shared contacts", functionName: "clearRange"},
    {name: "Syncronize shared contacts", functionName: "getContactsAction"},
  ];
  ss.addMenu("Domain Shared Contacts", menuEntries);
}

/**
 * Initialize domain shared contacts
 */
function init() {
  // Get the shared contact list
  var response = UrlFetchApp.fetch(URL, googleOAuth_('GET'));

  var xmlResponse = Xml.parse(response.getContentText()); 

  var totalResults = xmlResponse.feed.getElement('http://a9.com/-/spec/opensearchrss/1.0/','totalResults').getText();
  var startIndex = xmlResponse.feed.getElement('http://a9.com/-/spec/opensearchrss/1.0/','startIndex').getText();
  var itemsPerPage = xmlResponse.feed.getElement('http://a9.com/-/spec/opensearchrss/1.0/','itemsPerPage').getText();

  var entries = xmlResponse.feed.getElements("entry");

  // Scan contact
  for(var i=0; i < entries.length;i++) {
    var contactEntryObject = new Object();

    contactEntryObject.id = entries[i].getElement("id").getText();

    contactEntryObject.updated = entries[i].getElement("updated").getText();

    contactEntryObject.category = entries[i].getElement("category").getText();

    contactEntryObject.title = entries[i].getElement("title").getText();

    // Links
    var contactLinkElements = entries[i].getElements('link');
    for(var j=0; j<contactLinkElements.length;j++) {
      var contactLinkRel = contactLinkElements[j].getAttribute('rel').getValue();
      if(contactLinkRel == 'edit') {
        var contactLinkEdit = contactLinkElements[j].getAttribute('href').getValue();
        contactEntryObject.editLink = contactLinkEdit;
      } else if(contactLinkRel == 'self') {
        var contactLinkSelf = contactLinkElements[j].getAttribute('href').getValue();
        contactEntryObject.selfLink = contactLinkSelf;        
      } else if(contactLinkRel == 'http://schemas.google.com/contacts/2008/rel#edit-photo') {
        var contactLinkPhoto = contactLinkElements[j].getAttribute('href').getValue();
        contactEntryObject.photoLink = contactLinkPhoto;       
      }
    }

    // Organization
    var contactOrgElement = entries[i].getElement('http://schemas.google.com/g/2005','organization');
    // Check if contact has organization set
    if(contactOrgElement != null) {
      // Handle orgName element
      var orgName = contactOrgElement.getElement('http://schemas.google.com/g/2005', "orgName");
      if(orgName != null) {
        contactEntryObject.orgName = contactOrgElement.getElement('http://schemas.google.com/g/2005', "orgName").getText();
      } else {
        contactEntryObject.orgName = null;
      }
      // Handle orgType element
      var orgTitle = contactOrgElement.getElement('http://schemas.google.com/g/2005', "orgTitle");
      if(orgTitle != null) {
        contactEntryObject.orgTitle = contactOrgElement.getElement('http://schemas.google.com/g/2005', "orgTitle").getText();
      } else {
        contactEntryObject.orgTitle = null;
      }
    } else {
      contactEntryObject.orgName = null;
      contactEntryObject.orgTitle = null;
    }

    // Email addresses
    var emailAddressArray = new Array();
    var emailAddressObject = {};
    var contactEmailElements = entries[i].getElements('http://schemas.google.com/g/2005','email');
    for(var j=0; j<contactEmailElements.length;j++) {
      var emailAddress = contactEmailElements[j].getAttribute('address').getValue();
      var emailAddressType = contactEmailElements[j].getAttribute('rel').getValue();
      emailAddressObject.emailAddress = emailAddress;
      emailAddressObject.emailAddressType = emailAddressType;
      emailAddressArray.push(emailAddressObject);
    }   
    contactEntryObject.email = emailAddressArray;

    // Phone numbers
    var phoneNumberArray = new Array();
    var phoneNumberObject = {};    
    var contactPhoneNumbers = entries[i].getElements('http://schemas.google.com/g/2005','phoneNumber');
    for(var j=0; j<contactPhoneNumbers.length;j++) {
      var phoneNumber = contactPhoneNumbers[j].getText();
      var phoneNumberType = contactPhoneNumbers[j].getAttribute('rel').getValue();
      phoneNumberObject.phoneNumber = phoneNumber;
      phoneNumberObject.phoneNumberType = phoneNumberType;
      phoneNumberArray.push(phoneNumberObject);
    }
    contactEntryObject.phoneNumber = phoneNumberArray;

    // Postal Addresses
    var postalAddressArray = new Array();
    var postalAddressObject = {};     
    var contactPostalAddresses = entries[i].getElements('http://schemas.google.com/g/2005','postalAddress');
    for(var j=0; j<contactPostalAddresses.length;j++) {
      var postalAddress = contactPostalAddresses[j].getText();
      var postalAddressType = contactPostalAddresses[j].getAttribute('rel').getValue();
      postalAddressObject.postalAddress = postalAddress;
      postalAddressObject.postalAddressType = postalAddressType;
      postalAddressArray.push(postalAddressObject);      
    }   
    contactEntryObject.postalAddress = postalAddressArray;

    dumpContactInfo(contactEntryObject);

  }
}

function dumpContactInfo(contactEntryObject) {
  var sheet = SpreadsheetApp.getActiveSheet();
  var emailToWrite = "";
  // Get the email we care
  for(var i = 0; i<contactEntryObject.email.length; i++) {
    if(contactEntryObject.email[i].emailAddressType == EMAILWORK) {
      var emailToWrite = contactEntryObject.email[i].emailAddress;
      break;
    }
  }
  sheet.appendRow([contactEntryObject.id, contactEntryObject.selfLink, contactEntryObject.editLink, contactEntryObject.photoLink, contactEntryObject.updated, contactEntryObject.title, contactEntryObject.orgName, contactEntryObject.orgTitle, emailToWrite]);
}

/**
 * Get actions requested on contacts
 */
function getContactsAction() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var entireDataRange = sheet.getDataRange().getValues();
  for(var i=1; i<entireDataRange.length;i++) {
    if(entireDataRange[i][9] == "DELETE") {
       deleteContact(entireDataRange[i][2]);
    } else if(entireDataRange[i][9] == "UPDATE") {
      updateContact(entireDataRange[i][1], entireDataRange[i][2], entireDataRange[i]);
    } else if(entireDataRange[i][9] == "CREATE") {
      createContact(entireDataRange[i]);
    }
  }
}

/**
 * Delete the specified domain shared contact
 *
 * @param {String} editLink the edit link of the contact to delete
 */
function deleteContact(editLink) {
  var response = UrlFetchApp.fetch(editLink, googleOAuth_('DELETE'));
  Logger.log(response.getContentText());
}

/**
 * Update the specified domain shared contact
 *
 * @param {String} selfLink the self link of the contact to update 
 * @param {String} editLink the edit link of the contact to update
 * @param {Array[]} updateData data to update
 */
function updateContact(selfLink, editLink, updateData) {
  // Get current value of the entry to update
  var response = UrlFetchApp.fetch(selfLink, googleOAuth_('GET'));

  // Generate the updated entry from a template
  var template = HtmlService.createTemplateFromFile("entryTemplate");
  template.id = updateData[0];
  template.updated = updateData[4];
  template.title = updateData[5];
  template.photoLink = updateData[3];
  template.selfLink = updateData[1];
  template.editLink = updateData[2];
  template.orgName = updateData[6];
  template.orgTitle = updateData[7];
  template.email = updateData[8];
  var output = template.evaluate().getContent();

  // Send the updated entry
  var response = UrlFetchApp.fetch(updateData[2],googleOAuth_('PUT', output));
  Logger.log(response.getContentText());
}

/**
 * Create the specified domain shared contact
 *
 * @param {Array[]} createData data to update
 */
function createContact(createData) {
  // Generate the new entry from a template
  var template = HtmlService.createTemplateFromFile("entryCreateTemplate");
  template.title = createData[5];
  template.orgName = createData[6];
  template.orgTitle = createData[7];
  template.email = createData[8];
  var output = template.evaluate().getContent();  

  var response = UrlFetchApp.fetch(URL,googleOAuth_('POST', output));
  Logger.log(response.getContentText());
}

/**
 * Clear the current cell data range
 */
function clearRange() {
  var sheet = SpreadsheetApp.getActiveSheet();
  sheet.getRange("A2:J26").clear();
}

/**
 * Google authentication loader
 * @param {String} method the HTTP method to use for the UrlFetch operation, possible values are: GET, POST, PUT, DELETE
 * @param {String} payload the payload to use if needed
 * @return {Object} configuration options for UrlFetch, including oAuth parameters
 */
function googleOAuth_(method, payload) {
  // Shared configuration for all methods
  var oAuthConfig = UrlFetchApp.addOAuthService(APPNAME);
  oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope='+encodeURIComponent(SCOPE));
  oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
  oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
  oAuthConfig.setConsumerKey('anonymous');
  oAuthConfig.setConsumerSecret('anonymous');

  // Detect the required method
  switch(method) {
    case "GET":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always'};
      break;
    case "POST":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always', payload: payload, contentType: 'application/atom+xml', method: "POST"};
      break;
    case "PUT":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always', payload: payload, contentType: 'application/atom+xml', method: "PUT"};
      break;
    case "DELETE":
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always', method: "DELETE"};
      break;
    default:
      return {oAuthServiceName:APPNAME, oAuthUseToken:'always'};
      break;
  }
}

entryTemplate.html

<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gd="http://schemas.google.com/g/2005">
   <id><?=id?></id>
   <updated><?=updated?></updated>
   <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact" />
   <title type="text"><?=title?></title>
   <link rel="http://schemas.google.com/contacts/2008/rel#edit-photo" type="image/*" href="<?=photoLink?>" />
   <link rel="self" type="application/atom+xml" href="<?=selfLink?>" />
   <link rel="edit" type="application/atom+xml" href="<?=editLink?>" />
   <gd:organization rel="http://schemas.google.com/g/2005#work">
      <gd:orgName><?=orgName?></gd:orgName>
      <gd:orgTitle><?=orgTitle?></gd:orgTitle>
   </gd:organization>
   <gd:email rel="http://schemas.google.com/g/2005#work" address="<?=email?>" primary="true" />
</entry>

entryCreateTemplate.html

<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gd="http://schemas.google.com/g/2005">
   <category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact" />
   <title type="text"><?=title?></title>
   <gd:organization rel="http://schemas.google.com/g/2005#work">
      <gd:orgName><?=orgName?></gd:orgName>
      <gd:orgTitle><?=orgTitle?></gd:orgTitle>
   </gd:organization>
   <gd:email rel="http://schemas.google.com/g/2005#work" address="<?=email?>" primary="true" />
</entry>

Do not hesitate to contact me for any additional info. Happy scripting!

UPDATE:
As noted by Алексей Исаченко to make the script work, before using the menu item “Domain Shared Contacts -> Initialize shared contacts” you have to manually run the init() function from the script editor to execute the Google authorization process. Once authorized you can use the ”Domain Shared Contacts” menu.

Consultant, Lean Thinker, Agilist, Technology Lover. Dream: Being worth a TED talk. Project Manager in a wide variety of business applications. Particularly interested in innovation projects, as well as close interaction with costumers.

Taggato con: , ,
Pubblicato in Domain Shared Contacts, Google Apps, Google Apps Script
4 commenti su “Manage Google Apps Domain Shared Contacts with Google Apps Script
  1. Arndt says:

    thank you very much for this! I created the spreadsheet and added the script and the HTML templates. After I ran the init() function I have the new menue item in my spreadsheet. Great!
    But: How do I use it? If I click on “Domain Shared Contacts -> Intialize shared contacts” nothing happens.
    How do I add a shared contact?
    Can I add phone numbers as well? If yes, how do I do it.
    Again, many thanks for this application.

  2. Hello

    I have the same problem, After I ran the init() function I have the new menue item in my spreadsheet. Great!
    But: How do I use it? If I click on “Domain Shared Contacts -> Intialize shared contacts” nothing happens.

    Please help me !!

  3. Rob says:

    Hi,

    I’ve implemented this script fine but it seems incomplete with the spreadsheet missing out a persons name, postal address and phone number from the spreadsheet.

    How does someone add these elements to it?

  4. Ciprian says:

    Hi Marcello, thanks for this nice script. It can definitely be useful to develop something or to debug.
    For everyone wanting to use this, please note this script will only work for the defined field values.
    To make the script work, after creating the 3 files, make sure you have changed the ‘YOURDOMAINNAME.COM’ to your actual domain in ‘Shared Contacts Group.gs’ line 27.
    Run the ‘init’ function which will authorise access to contacts.
    Then, in the spreadsheet select the ‘Initialize shared contact’ menu which will populate the spreadsheet, and the ‘Clear shared contacts’ will clear the spreadsheet to start fresh.
    To delete, add or edit a contact you will need to add the CREATE/UPDATE/DELETE values to the Action column and then select ‘Synchronize shared contact’ menu

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>