Skip to content

Instantly share code, notes, and snippets.

@devluis
Forked from JuanCanham/gist:2917132
Last active August 29, 2015 14:18

Revisions

  1. JuanCanham revised this gist Sep 3, 2012. 1 changed file with 6 additions and 6 deletions.
    12 changes: 6 additions & 6 deletions gistfile1.js
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,3 @@
    //moved these outside to prevent multiple calls as they need be consitent for the duration of the scripts run anyway
    var regOpen = sanitize(UserProperties.getProperty('regOpen'));
    var tagOpen = sanitize(UserProperties.getProperty('tagOpen'));
    var regClose = sanitize(UserProperties.getProperty('regClose'));
    var tagClose = sanitize(UserProperties.getProperty('tagClose'));

    function getSignature() {
    //pretty basic function for testing
    if ( startupChecks()) { return; }
    @@ -136,6 +130,12 @@ function sanitize(text){


    function tagReplace(tag, value, text){
    var regOpen = sanitize(UserProperties.getProperty('regOpen'));
    var tagOpen = sanitize(UserProperties.getProperty('tagOpen'));
    var regClose = sanitize(UserProperties.getProperty('regClose'));
    var tagClose = sanitize(UserProperties.getProperty('tagClose'));


    var regex = new RegExp("(.*)"+regOpen+'(.*?)'+tagOpen+tag+tagClose+'(.*?)'+regClose+"(.*)","g");
    value = value.toString().replace("$","\\$");
    if ((value !== "")) { value = "$2"+value+"$3"; }
  2. JuanCanham revised this gist Sep 3, 2012. 1 changed file with 90 additions and 50 deletions.
    140 changes: 90 additions & 50 deletions gistfile1.js
    Original file line number Diff line number Diff line change
    @@ -1,8 +1,14 @@
    //moved these outside to prevent multiple calls as they need be consitent for the duration of the scripts run anyway
    var regOpen = sanitize(UserProperties.getProperty('regOpen'));
    var tagOpen = sanitize(UserProperties.getProperty('tagOpen'));
    var regClose = sanitize(UserProperties.getProperty('regClose'));
    var tagClose = sanitize(UserProperties.getProperty('tagClose'));

    function getSignature() {
    //pretty basic function for testing
    if ( startupChecks()) { return; }
    var email = SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue().toString();
    if ( email == "" ) {
    if ( email === "" ) {
    Browser.msgBox("No email selected", "Please select a cell containing a user's email" , Browser.Buttons.OK);
    return;
    }
    @@ -11,32 +17,37 @@ function getSignature() {
    }

    function setIndividualSignature() {
    Logger.log('[%s]\t Starting setIndividualSignature run', Date());
    if ( startupChecks()) { return; }
    var userData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Summary');
    var template = getTemplate();

    var row = SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getRow();
    if (userData.getRange(row, 1).isBlank() === true) {
    var msg = "Please select a cell on a row containing the user who's signature you wish to update";
    Browser.msgBox('No user selected', msg, Browser.Buttons.OK);
    Browser.msgBox('No valid user selected', msg, Browser.Buttons.OK);
    } else {
    setSignature(template, userData, row);
    }
    Logger.log('[%s]\t Completed setIndividualSignature run', Date());
    }

    function setAllSignatures() {
    Logger.log('[%s]\t Starting setAllSignatures run', Date());
    if ( startupChecks()) { return; }
    var userData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Summary');

    var template = getTemplate();

    //Go through each user listing
    for ( row = 2; (userData.getRange(row, 1).isBlank() === false); row++) {
    for ( row = 2; row <= userData.getLastRow() ; row++) {
    setSignature(template, userData, row);
    }
    Logger.log('[%s]\t Completed setAllSignatures run', Date());
    }

    function getTemplate(){
    Logger.log('[%s]\t Getting Template', Date());
    var settings = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Signature Settings');
    var template = settings.getRange(2, 1).getValue().toString();

    @@ -51,70 +62,90 @@ function setSignature(template, userData, row){

    //Google Apps Scripts always deals in ranges even if you just want one cell
    //getValue returns an object, so convert it to a string
    var email = userData.getRange(row, 1).getValue().toString();
    var email = userData.getRange(row, 1).getValue().toString();

    //quick exit if the user isn't in the domain
    if (!checkUserIsValid(email)){
    Logger.log('[%s]\t Skipping user %s',Date(),email);
    return;
    }

    //Substitute in group variables, e.g those for groups of users
    //this must be done before filling out user specific data as it was added after initial design
    Logger.log('[%s]\t Substituting Group Variables for user %s',Date(),email);
    var signature = substituteGroupVariables(template, userData, groupData, row);

    //Fill out the template with the data from the user's row to form the signatures
    Logger.log('[%s]\t Substituting Individual Variables for user %s',Date(),email);
    signature = substituteVariablesFromRow(signature, userData, row);

    //The API docs say there is a 10,000 character limit
    //https://developers.google.com/google-apps/email-settings/#updating_a_signature
    if (signature.length > 10000) { Browser.msgBox('signature over 10000 characters for:' + email); }

    Logger.log('[%s]\t Sending signature for user %s',Date(),email);
    sendSignature(email, signature);
    Logger.log('[%s]\t Processing complete for user %s',Date(),email);
    }

    function substituteVariablesFromRow(text, sheet, row) {
    for ( col = 1; sheet.getRange(1, col).isBlank() === false; col++) {
    var tag = sheet.getRange(1, col).getValue().toString();
    var value = sheet.getRange(row, col).getValue().toString();
    text = tagReplace(tag,value,text);
    function substituteVariablesFromRow(text, sheet, row) {
    //Generating two lists avoids the need to do lots of individual calls to the sheet
    var tags = sheet.getSheetValues(1, 1, 1, sheet.getLastColumn())[0];
    var values = sheet.getSheetValues(row, 1, 1, sheet.getLastColumn())[0];
    for ( v = 0; v < values.length; v++){
    text = tagReplace(tags[v],values[v],text);
    }
    return text;
    }

    function substituteGroupVariables(text, dataSheet, lookupSheet, row) {
    //this function isn't great but for now it will have to do
    //this function is still not great but at least it makes use of getSheet
    var tags = dataSheet.getSheetValues(1, 1, 1, dataSheet.getLastColumn())[0];
    var values = dataSheet.getSheetValues(row, 1, 1, dataSheet.getLastColumn())[0];
    var GroupVariables = lookupSheet.getSheetValues(1, 1, lookupSheet.getLastRow(),1);

    //iterate over the column names in the datasheet (e.g Primary Office, Certificates)
    for ( dataCol = 1; dataSheet.getRange(1, dataCol).isBlank() === false; dataCol++) {

    //the tag is the column heading as that is what will appear in the template (e.g "{Primary Office}")
    var tag = dataSheet.getRange(1, dataCol).getValue().toString() ;
    //for each GroupVariable
    for (j = 0; j < GroupVariables.length ; j += 3){

    //iterate over the rows in the group settings sheet to find if a column has a set of values to lookup
    for ( lookupRow = 1; lookupSheet.getRange(lookupRow, 1).isBlank() === false; lookupRow += 3) {
    if ( tag === lookupSheet.getRange(lookupRow, 1).getValue().toString() ) {
    var propertyValue = dataSheet.getRange(row, dataCol).getValue().toString();
    //find the column for later changing the value
    for (i = 0; i < tags.length; i++){
    if (tags[i] === GroupVariables[j][0]){

    //iterate over the possible settings (e.g London, Edinburg, Madrid)
    for ( lookupCol = 2; lookupSheet.getRange(lookupRow, lookupCol).isBlank() === false; lookupCol++) {

    //search for substring matches as this allows for lists (e.g a list of certifications)
    if ( lookupSheet.getRange(lookupRow, lookupCol).getValue().toString().indexOf(propertyValue) !== -1 ) {
    var value = lookupSheet.getRange(lookupRow+1, lookupCol).getValue().toString();
    text = tagReplace(tag, value, text);
    //and build a lookup table to switch it out
    var lookupTable = lookupSheet.getSheetValues(j+1,2,2,lookupSheet.getLastColumn()-1);
    for ( k=0;k<lookupTable[0].length;k++) {
    if (values[i] === lookupTable[0][k]){
    text = tagReplace(tags[i], lookupTable[1][k], text);
    }
    }
    }
    }
    }

    }
    }
    }

    return text;
    }

    function sanitize(text){
    var invalid = ["[","^","$",".","|","?","*","+","(",")"];
    for(m=0;m<invalid.length;m++){
    text = text.replace(invalid[m],"\\"+invalid[m]);
    }
    return text;
    }


    function tagReplace(tag, value, text){
    var escapeChar = ScriptProperties.getProperty('escapeChar');
    var unEscapeChar = ScriptProperties.getProperty('unEscapeChar');
    var regex = new RegExp("(.*)"+regOpen+'(.*?)'+tagOpen+tag+tagClose+'(.*?)'+regClose+"(.*)","g");
    value = value.toString().replace("$","\\$");
    if ((value !== "")) { value = "$2"+value+"$3"; }
    value = "$1"+value+"$4";

    tag = escapeChar + tag + unEscapeChar;

    //could be done as a regex to avoid this loop but then I'd have two problems
    while (text.indexOf(tag) !== -1) {
    text = text.replace(tag, value);
    //I'm sure this can be avoided by making the regex more complicated, but this will do for now
    for(q=0; ((text.match(regex)) && q<128); q++ ){
    text = text.replace(regex,value);
    }

    return text;
    }

    @@ -132,6 +163,14 @@ function sendSignature(email, signature) {
    }
    }

    function checkUserIsValid(user){
    var userList = UserManager.getAllUsers();
    for ( u=0 ; u < userList.length ; u++ ) {
    if (userList[u].getEmail() === user){ return true; }
    }
    return false;
    }

    function getPayload(signature) {
    //First line is needed for XML, second isn't but we might as well do it for consistency
    signature = signature.replace(/&/g, '&amp;').replace(/</g, '&lt;');
    @@ -152,8 +191,8 @@ function authorisedUrlFetch(email, requestData) {
    // The scope from https://developers.google.com/google-apps/email-settings/ has to be URIcomponent encoded

    var oAuthConfig = UrlFetchApp.addOAuthService('google');
    oAuthConfig.setConsumerSecret(ScriptProperties.getProperty('oAuthConsumerSecret'));
    oAuthConfig.setConsumerKey(ScriptProperties.getProperty('oAuthClientID'));
    oAuthConfig.setConsumerSecret(UserProperties.getProperty('oAuthConsumerSecret'));
    oAuthConfig.setConsumerKey(UserProperties.getProperty('oAuthClientID'));
    oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope=https%3A%2F%2Fapps-apis.google.com%2Fa%2Ffeeds%2Femailsettings%2F');
    oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
    oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
    @@ -185,7 +224,6 @@ function onOpen() {
    menuEntries.push({name: 'Set Individual Signature', functionName: 'setIndividualSignature'});
    menuEntries.push({name: 'Get Signature', functionName: 'getSignature'});
    ss.addMenu('Signatures', menuEntries);
    startupChecks();
    }

    function startupChecks() {
    @@ -200,35 +238,37 @@ function startupChecks() {
    'The script may then need authorising, this can be done by running one of the scripts from the script editor';
    requiredProperties.push({name: 'oAuthClientID', help: oAuthHelp});
    requiredProperties.push({name: 'oAuthConsumerSecret', help: oAuthHelp});
    requiredProperties.push({name: 'escapeChar', help: 'A character or sequence to go before variables that will be substituted, e.g { ` ${'});
    requiredProperties.push({name: 'unEscapeChar', help: 'A character or sequence to go after variables that will be substituted, e.g } ` }$'});
    requiredProperties.push({name: 'regOpen', help: 'A character or sequence to go before sections to be substituded, e.g ${'});
    requiredProperties.push({name: 'regClose', help: 'A character or sequence to go after sections that will be substituted, e.g } or }$'});
    requiredProperties.push({name: 'tagOpen', help: 'A character or sequence to go before tags to be substituded, e.g {'});
    requiredProperties.push({name: 'tagClose', help: 'A character or sequence to go after tags that will be substituted, e.g } or }$'});

    var requiredSheets = [];
    requiredSheets.push({name: 'Summary', help: 'A "Summary" sheet must exist that contains a 1 header row and 1 row per user, with no gaps in either the 1st column or row, the 1st row must be the users usernames'});
    requiredSheets.push({name: 'Signature Settings', help: 'A "Signature Settings" sheet must exist that contains a the template in cell 2A and then has 1 header row and 1 row per company wide variable, with no empty header cells'});
    requiredSheets.push({name: 'Signature Group Settings', help: 'A "Signature Group Settings" sheet must exist that contains 3 Rows (setting values, what to substitute, comments) with every third row containing a column header'});

    var fail = false;
    for ( i = 0; i < requiredProperties.length; i++) {
    var property = ScriptProperties.getProperty(requiredProperties[i].name);
    for ( s = 0; s < requiredProperties.length; s++) {
    var property = UserProperties.getProperty(requiredProperties[s].name);
    if (property == null) {
    var title = 'Script Property ' + requiredProperties[i].name + ' is required';
    var prompt = requiredProperties[i].help;
    var title = 'Script Property ' + requiredProperties[s].name + ' is required';
    var prompt = requiredProperties[s].help;
    var newValue = Browser.inputBox(title, prompt, Browser.Buttons.OK_CANCEL);
    if ((newValue === '') || (newValue === 'cancel')) {
    fail = true;
    } else {
    ScriptProperties.setProperty(requiredProperties[i].name, newValue);
    UserProperties.setProperty(requiredProperties[s].name, newValue);
    }
    }
    }

    for ( i = 0; i < requiredSheets.length; i++) {
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(requiredSheets[i].name);
    for ( s = 0; s < requiredSheets.length; s++) {
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(requiredSheets[s].name);
    if (sheet == null) {
    fail = true;
    var title = 'Sheet ' + requiredSheets[i].name + ' is required';
    var prompt = requiredSheets[i].help;
    var title = 'Sheet ' + requiredSheets[s].name + ' is required';
    var prompt = requiredSheets[s].help;
    Browser.msgBox(title, prompt, Browser.Buttons.OK);
    }
    }
  3. JuanCanham created this gist Jun 12, 2012.
    237 changes: 237 additions & 0 deletions gistfile1.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,237 @@
    function getSignature() {
    //pretty basic function for testing
    if ( startupChecks()) { return; }
    var email = SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getValue().toString();
    if ( email == "" ) {
    Browser.msgBox("No email selected", "Please select a cell containing a user's email" , Browser.Buttons.OK);
    return;
    }
    var result = authorisedUrlFetch(email, {});
    Browser.msgBox(result.getContentText());
    }

    function setIndividualSignature() {
    if ( startupChecks()) { return; }
    var userData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Summary');
    var template = getTemplate();

    var row = SpreadsheetApp.getActiveSpreadsheet().getActiveCell().getRow();
    if (userData.getRange(row, 1).isBlank() === true) {
    var msg = "Please select a cell on a row containing the user who's signature you wish to update";
    Browser.msgBox('No user selected', msg, Browser.Buttons.OK);
    } else {
    setSignature(template, userData, row);
    }
    }

    function setAllSignatures() {
    if ( startupChecks()) { return; }
    var userData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Summary');

    var template = getTemplate();

    //Go through each user listing
    for ( row = 2; (userData.getRange(row, 1).isBlank() === false); row++) {
    setSignature(template, userData, row);
    }
    }

    function getTemplate(){
    var settings = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Signature Settings');
    var template = settings.getRange(2, 1).getValue().toString();

    //Substitute the company wide variables into the template
    template = substituteVariablesFromRow(template, settings, 2);

    return template;
    }

    function setSignature(template, userData, row){
    var groupData = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Signature Group Settings');

    //Google Apps Scripts always deals in ranges even if you just want one cell
    //getValue returns an object, so convert it to a string
    var email = userData.getRange(row, 1).getValue().toString();

    //Substitute in group variables, e.g those for groups of users
    //this must be done before filling out user specific data as it was added after initial design
    var signature = substituteGroupVariables(template, userData, groupData, row);

    //Fill out the template with the data from the user's row to form the signatures
    signature = substituteVariablesFromRow(signature, userData, row);

    //The API docs say there is a 10,000 character limit
    //https://developers.google.com/google-apps/email-settings/#updating_a_signature
    if (signature.length > 10000) { Browser.msgBox('signature over 10000 characters for:' + email); }

    sendSignature(email, signature);
    }

    function substituteVariablesFromRow(text, sheet, row) {
    for ( col = 1; sheet.getRange(1, col).isBlank() === false; col++) {
    var tag = sheet.getRange(1, col).getValue().toString();
    var value = sheet.getRange(row, col).getValue().toString();
    text = tagReplace(tag,value,text);
    }
    return text;
    }

    function substituteGroupVariables(text, dataSheet, lookupSheet, row) {
    //this function isn't great but for now it will have to do

    //iterate over the column names in the datasheet (e.g Primary Office, Certificates)
    for ( dataCol = 1; dataSheet.getRange(1, dataCol).isBlank() === false; dataCol++) {

    //the tag is the column heading as that is what will appear in the template (e.g "{Primary Office}")
    var tag = dataSheet.getRange(1, dataCol).getValue().toString() ;

    //iterate over the rows in the group settings sheet to find if a column has a set of values to lookup
    for ( lookupRow = 1; lookupSheet.getRange(lookupRow, 1).isBlank() === false; lookupRow += 3) {
    if ( tag === lookupSheet.getRange(lookupRow, 1).getValue().toString() ) {
    var propertyValue = dataSheet.getRange(row, dataCol).getValue().toString();

    //iterate over the possible settings (e.g London, Edinburg, Madrid)
    for ( lookupCol = 2; lookupSheet.getRange(lookupRow, lookupCol).isBlank() === false; lookupCol++) {

    //search for substring matches as this allows for lists (e.g a list of certifications)
    if ( lookupSheet.getRange(lookupRow, lookupCol).getValue().toString().indexOf(propertyValue) !== -1 ) {
    var value = lookupSheet.getRange(lookupRow+1, lookupCol).getValue().toString();
    text = tagReplace(tag, value, text);
    }
    }
    }
    }
    }
    return text;
    }

    function tagReplace(tag, value, text){
    var escapeChar = ScriptProperties.getProperty('escapeChar');
    var unEscapeChar = ScriptProperties.getProperty('unEscapeChar');

    tag = escapeChar + tag + unEscapeChar;

    //could be done as a regex to avoid this loop but then I'd have two problems
    while (text.indexOf(tag) !== -1) {
    text = text.replace(tag, value);
    }
    return text;
    }

    function sendSignature(email, signature) {
    // https://developers.google.com/google-apps/email-settings/#updating_a_signature
    var requestData = {
    'method': 'PUT',
    'contentType': 'application/atom+xml',
    'payload': getPayload(signature)
    };
    var result = authorisedUrlFetch(email, requestData);
    if (result.getResponseCode() != 200) {
    var msg = 'There was an error sending ' + email + "'s signature to Google";
    Browser.msgBox('Error settings signature', msg, Browser.Buttons.OK);
    }
    }

    function getPayload(signature) {
    //First line is needed for XML, second isn't but we might as well do it for consistency
    signature = signature.replace(/&/g, '&amp;').replace(/</g, '&lt;');
    signature = signature.replace(/>/g, '&gt;').replace(/'/g, '&apos;').replace(/"/g, '&quot;');

    //Unfortunately when inside app script document.createElement doesn't work so lets just hardcode the XML for now
    var xml = '<?xml version="1.0" encoding="utf-8"?>' +
    '<atom:entry xmlns:atom="http://www.w3.org/2005/Atom" xmlns:apps="http://schemas.google.com/apps/2006" >' +
    '<apps:property name="signature" value="'+signature+'" /></atom:entry>';
    return xml;
    }

    function authorisedUrlFetch(email, requestData) {
    //takes request data and wraps oauth authentication around it before sending out the request
    // https://developers.google.com/apps-script/class_oauthconfig
    // http://support.google.com/a/bin/answer.py?hl=en&hlrm=en&answer=162105
    // https://developers.google.com/apps-script/articles/picasa_google_apis#section2a
    // The scope from https://developers.google.com/google-apps/email-settings/ has to be URIcomponent encoded

    var oAuthConfig = UrlFetchApp.addOAuthService('google');
    oAuthConfig.setConsumerSecret(ScriptProperties.getProperty('oAuthConsumerSecret'));
    oAuthConfig.setConsumerKey(ScriptProperties.getProperty('oAuthClientID'));
    oAuthConfig.setRequestTokenUrl('https://www.google.com/accounts/OAuthGetRequestToken?scope=https%3A%2F%2Fapps-apis.google.com%2Fa%2Ffeeds%2Femailsettings%2F');
    oAuthConfig.setAuthorizationUrl('https://www.google.com/accounts/OAuthAuthorizeToken');
    oAuthConfig.setAccessTokenUrl('https://www.google.com/accounts/OAuthGetAccessToken');
    UrlFetchApp.addOAuthService(oAuthConfig);

    requestData['oAuthServiceName'] = 'google';
    requestData['oAuthUseToken'] = 'always';

    var emailParts = email.split('@');
    var url = 'https://apps-apis.google.com/a/feeds/emailsettings/2.0/' + emailParts[1] + '/' + emailParts[0] + '/signature';
    var result = UrlFetchApp.fetch(url, requestData);
    if ( result.getResponseCode() != 200 ) {
    //Do some logging if something goes wrong
    //Too deep to give the user a meaningful error though so pass the result back up anyway
    Logger.log('Error on fetch on' + url);
    Logger.log(requestData);
    Logger.log(result.getResponseCode());
    Logger.log(result.getHeaders());
    Logger.log(result.getContentText());
    }
    return result;
    }

    function onOpen() {
    //add a toolbar and list the functions you want to call externally
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var menuEntries = [];
    menuEntries.push({name: 'Set All Signatures', functionName: 'setAllSignatures'});
    menuEntries.push({name: 'Set Individual Signature', functionName: 'setIndividualSignature'});
    menuEntries.push({name: 'Get Signature', functionName: 'getSignature'});
    ss.addMenu('Signatures', menuEntries);
    startupChecks();
    }

    function startupChecks() {
    //Check that everything that is needed to run is there
    //I don't check that any of it makes sense, just that it exists.
    var requiredProperties = [];

    //the help text looks pretty terrible but it is better than nothing
    var oAuthHelp = 'Goto https://code.google.com/apis/console#:access and register as an "Installed application" \n'+
    'Then add the ClientID to authorised 3rd party clients \n'+
    'With scope https://apps-apis.google.com/a/feeds/emailsettings/ \n'+
    'The script may then need authorising, this can be done by running one of the scripts from the script editor';
    requiredProperties.push({name: 'oAuthClientID', help: oAuthHelp});
    requiredProperties.push({name: 'oAuthConsumerSecret', help: oAuthHelp});
    requiredProperties.push({name: 'escapeChar', help: 'A character or sequence to go before variables that will be substituted, e.g { ` ${'});
    requiredProperties.push({name: 'unEscapeChar', help: 'A character or sequence to go after variables that will be substituted, e.g } ` }$'});

    var requiredSheets = [];
    requiredSheets.push({name: 'Summary', help: 'A "Summary" sheet must exist that contains a 1 header row and 1 row per user, with no gaps in either the 1st column or row, the 1st row must be the users usernames'});
    requiredSheets.push({name: 'Signature Settings', help: 'A "Signature Settings" sheet must exist that contains a the template in cell 2A and then has 1 header row and 1 row per company wide variable, with no empty header cells'});
    requiredSheets.push({name: 'Signature Group Settings', help: 'A "Signature Group Settings" sheet must exist that contains 3 Rows (setting values, what to substitute, comments) with every third row containing a column header'});

    var fail = false;
    for ( i = 0; i < requiredProperties.length; i++) {
    var property = ScriptProperties.getProperty(requiredProperties[i].name);
    if (property == null) {
    var title = 'Script Property ' + requiredProperties[i].name + ' is required';
    var prompt = requiredProperties[i].help;
    var newValue = Browser.inputBox(title, prompt, Browser.Buttons.OK_CANCEL);
    if ((newValue === '') || (newValue === 'cancel')) {
    fail = true;
    } else {
    ScriptProperties.setProperty(requiredProperties[i].name, newValue);
    }
    }
    }

    for ( i = 0; i < requiredSheets.length; i++) {
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(requiredSheets[i].name);
    if (sheet == null) {
    fail = true;
    var title = 'Sheet ' + requiredSheets[i].name + ' is required';
    var prompt = requiredSheets[i].help;
    Browser.msgBox(title, prompt, Browser.Buttons.OK);
    }
    }

    return fail;
    }