GmailApp (Google Apps Script) Displays Inline Images as Attachments - google-apps-script

Hello friendly StackOverflow folks,
I am having the most difficult time getting the GmailApp sendEmail() method to successfully use an existing email (e.g. a draft) that contains images inline as a template for new messages.
It seems like this is a problem, as I have found devs having this problem here and here and proposed solutions here, here, and here. Each of these solutions is 4+ years old, so perhaps they're out of date. I have been unable to use any of these solutions to replicate a success.
Currently, I'm running this code from my Google Scripts backend:
function generateMessageFromTemplate () {
var selectedTemplate = GmailApp.getMessageById('MESSAGE_ID');
//////////////////////////////////////////////////////////////////////////////
// Get inline images and make sure they stay as inline images (via Romain Vialard)
//////////////////////////////////////////////////////////////////////////////
var emailTemplate = selectedTemplate.getBody();
var rawContent = selectedTemplate.getRawContent();
var attachments = selectedTemplate.getAttachments();
var regMessageId = new RegExp(selectedTemplate.getId(), "g");
if (emailTemplate.match(regMessageId) != null) {
var inlineImages = {};
var nbrOfImg = emailTemplate.match(regMessageId).length;
var imgVars = emailTemplate.match(/<img[^>]+>/g);
var imgToReplace = [];
if(imgVars != null){
for (var i = 0; i < imgVars.length; i++) {
if (imgVars[i].search(regMessageId) != -1) {
var id = imgVars[i].match(/realattid=([^&]+)&/);
if (id != null) {
var temp = rawContent.split(id[1])[1];
temp = temp.substr(temp.lastIndexOf('Content-Type'));
var imgTitle = temp.match(/name="([^"]+)"/);
if (imgTitle != null) imgToReplace.push([imgTitle[1], imgVars[i], id[1]]);
}
}
}
}
for (var i = 0; i < imgToReplace.length; i++) {
for (var j = 0; j < attachments.length; j++) {
if(attachments[j].getName() == imgToReplace[i][0]) {
inlineImages[imgToReplace[i][2]] = attachments[j].copyBlob();
attachments.splice(j, 1);
var newImg = imgToReplace[i][1].replace(/src="[^\"]+\"/, "src=\"cid:" + imgToReplace[i][2] + "\"");
emailTemplate = emailTemplate.replace(imgToReplace[i][1], newImg);
}
}
}
}
//////////////////////////////////////////////////////////////////////////////
GmailApp.sendEmail('test#email.com', selectedTemplate.getSubject(), '', {
attachments: attachments,
htmlBody: emailTemplate,
inlineImages: inlineImages
});
};
The Google Scripts documentation on the sendEmail() method is here.
This is the Output of this Function as Is
When I send emails from Apps Script as is, I get emails that look like this:
screenshot
I've replicated the test with an old yahoo.com email account and had the exact same results as a Gmail account.
Again, this dev also has this same issue.
If you can help, I would be extremely grateful!

Related

Extract Inline images from Gmail Body

I am trying to extract all the images store in Inline body of email, store it in drive folder.
I am tryin to use this code
function GETGMEmails(){
var label = GmailApp.getUserLabelByName ('WHOLESALE REP');
var threads = label.getThreads();
for(var i = threads.length - 1; i >= 0; i--){
var messages = threads[i].getMessages();
for (var j = 0; j < messages.length; j++){
var message = messages[j];
// extractDetails(message,folder)
fetchInlineImage(message)
}
}
}
function fetchInlineImage(message) {
var msg = message;
console.log(message)
var pattern = /<img.*src="([^"]*)"[^>]*>/;
var matches = pattern.exec(msg.getBody());
console.log(matches.length)
if(matches) {
var url = matches[1];
var urlPattern = /^https*\:\/\/.*$/;
// If this matches, that means this was copied and pasted from a browser and it's a
// standard URL that we can urlFetch
if(urlPattern.exec(url)) {
// NO OP!
} else {
// Else this means the user copied and pasted from an OS clipboard and the image
// exists on Google's servers. The image can only be retrieved when logged in. Fortunately,
// if we use URLFetchApp, this will act as a logged in user and be able to URLFetch the image.
// We'll need to prepend a Gmail URL (subject to change)
url = "https://mail.google.com/mail/u/0/" + url;
}
// TODO - there is one more case that we're not covering - embedded images that newsletters use
Logger.log("Fetching image from URL: " + url);
var response = UrlFetchApp.fetch(url);
Logger.log("Response status: " + Utilities.jsonStringify(response.getHeaders()));
var blob = response.getBlob();
Logger.log("Response blob: " + blob.getBytes().length);
Drivefolder.createFile(blob).setName('ss.jpeg')
}
};
Email look like this, with lot of images here and there, and I want to extract each one of them:-
enter image description here
You are trying to access inline images from a GMail message.
When GMail was first introduced there was no ability to access inline images.
In 2012 an Issue:Access to inline images was raised and the script shown in the question was proposed as a workaround. A question was also asked on StackOverflow Parsing inlineImages from Gmail raw content.
These (and some variants) worked for a few years until 2014 when a second Issue:GmailApp.getAttachments Issue was raised.
Workarounds were patchy until 2017 when it was announced that the Issue had been resolved.
In 2018, a new answer was added to the StackOverFlow question. and in 2022 a new StackOverflow question How can I extract inline images from a Gmail email? (all available workarounds do not work anymore) was asked and answered.
In short, the script that you are using is redundant. However, the process to access inline images is simple and straightforward. The key is to examine the body by Regex.
The following script provides a basis for you to identify and log inline images.
Put headers in row 1 of sheet="Images".
A1="ID", B1="Subject", C1="Image"
function so75327302() {
var label = GmailApp.getUserLabelByName ('WHOLESALE REP');
var threads = label.getThreads();
for (var i=0;i<threads.length;i++){
var messages = threads[i].getMessages()
for (m=0;m<messages.length;m++){
// Logger.log("DEBUG: i:"+i+",m:"+m+", subject:"+messages[m].getSubject()+", message ID:"+messages[m].getId())
var body = messages[m].getBody()
var regex1 = RegExp('<img src="([^"]*)"[^>]*>', 'g')
var array1 = new Array
var images = new Array
while ((array1 = regex1.exec(body)) !== null) {
Logger.log("message ID:"+messages[m].getId()+", Subject: "+messages[m].getSubject()+" contains inline images."+ `Found ${array1[0]}`);
images.push([array1[0]])
}
if (images.length > 0){
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sheetName = "Images"
var sheet = ss.getSheetByName(sheetName)
var imageLR = sheet.getLastRow()
sheet.getRange(imageLR+1,1).setValue(messages[m].getId())
sheet.getRange(imageLR+1,2).setValue(messages[m].getSubject())
sheet.getRange(imageLR+1,3,images.length).setValues(images)
}
}
}
}

Grab Answer Key from Short Answer Google Forms

I am attempting to grab the answer key from the short answer in Google forms.
I am attempting to grab the following answer key from a short answer question (refer to the screenshot below). Below the Short answer text, it shows you the correct answer.
However, when reading the documentation, there seems there is no way to do so. Is there an alternative way to grab the answer key from the short answer in Google Forms?
The following is the script I am using to grab the information from the quiz:
function extractFormData(fullNme) {
var form = FormApp.getActiveForm();
var items = form.getItems();
var quizAdded = new Date().toLocaleString("en-US");
var uniqueQuizID = Utilities.getUuid(), question_listID = uniqueQuizID.concat("-questions");
var constructedJSON = {};
var answer_val = false;
var errorJSON = {};
var email = form.getEditors()[0].getEmail();
var quizInfo = {
"quiz_name": form.getTitle(),
"quiz_owner": fullNme,
"quiz_description": form.getDescription(),
"quiz_owner_email": email,
"quiz_submited_date":quizAdded
};
//console.log(JSON.stringify(quizInfo));
for (var i = 0; i < items.length; i++) {
var item = items[i];
switch(item.getType()) {
case FormApp.ItemType.MULTIPLE_CHOICE:
var question = item.asMultipleChoiceItem();
var ques = question.getTitle();
var question_type = "Multiple Choice";
var optns = [];
var answr;
var answers = question.getChoices();
answer_val = false;
for (var j = 0; j < answers.length; j++) {
var clean = answers[j].getValue();
optns.push(clean);
if(answers[j].isCorrectAnswer()){
answr = answers[j].getValue();
for(var x = 0; x < optns.length; x++){
if(answr == optns[x]){
answer_val = true;
break;
}
}
}
}
var multiJSON = makeJSON(ques, question_type, optns, answr);
constructedJSON[i+1] = multiJSON;
break;
case FormApp.ItemType.CHECKBOX:
var question = item.asCheckboxItem();
var ques = question.getTitle();
var question_type = "CheckBox";
var optns = [];
var answr = [];
var answers = question.getChoices();
for (var j = 0; j < answers.length; j++) {
var clean = answers[j].getValue();
optns.push(clean);
if(answers[j].isCorrectAnswer()){
answr.push(answers[j].getValue());
}
}
var checkJSON = makeJSON(ques, question_type, optns, answr);
constructedJSON[i+1] = checkJSON;
break;
case FormApp.ItemType.PARAGRAPH_TEXT:
var question = item.asParagraphTextItem();
var ques = question.getTitle();
var question_type = "free response";
var optns = [];
var answr;
var paraJSON = makeJSON(ques, question_type, optns, answr);
constructedJSON[i+1] = paraJSON;
break;
case FormApp.ItemType.TEXT:
var question = item.asTextItem();
var ques = question.getTitle();
var question_type = "free response";
var optns = "";
var answr = "";
var textJSON = makeJSON(ques, question_type, optns, answr);
constructedJSON[i+1] = textJSON;
break;
}
if(!answer_val){
errorJSON = {"Question":ques, "Options":optns, "Answer": answr, "Sucess": false};
//error(ques);
break;
}
}
if(answer_val){
notifyUser();
} else {
return errorJSON;
}
}
The method that you are looking for is not yet supported in Apps Script.
Therefore, you have two options in this situation:
Create a feature request on Google's Issue Tracker here and provide all the necessary details.
Make use of Forms API by using the Forms API advanced service in Apps Script and try Tanaike's proposed solution.
If you check the documentation, you can see that resources of type CorrectAnswer have a field available:
{
"value": string
}
A single correct answer for a question. For multiple-valued (CHECKBOX) questions, several CorrectAnswers may be needed to represent a single correct response option.
Reference
Forms API.
I believe your goal is as follows.
You want to retrieve the correct answer from each question of Google Form using Google Apps Script.
In this case, when I checked the method for directly achieving your goal using Google Form service (FormApp), unfortunately, I couldn't find it. But fortunately, I confirmed that when Google Forms API is used, your goal can be achieved. So, in this answer, I would like to propose to achieve your goal using Google Forms API.
Usage:
1. Linking Google Cloud Platform Project to Google Apps Script Project for New IDE.
In order to use Forms API, please link Google Cloud Platform Project to Google Apps Script Project for New IDE, and please enable Forms API at API console. Ref
2. Sample script.
function myFunction() {
const formId = "###"; // Please set your Google Form ID.
const url = "https://forms.googleapis.com/v1/forms/" + formId + "?fields=*";
const res = UrlFetchApp.fetch(url, { headers: { authorization: "Bearer " + ScriptApp.getOAuthToken() } });
const obj = JSON.parse(res.getContentText());
const values = obj.items.map(({ title, questionItem }) => (
{ title, point: questionItem.question.grading.pointValue, answer: questionItem.question.grading.correctAnswers ? questionItem.question.grading.correctAnswers.answers.map(({ value }) => value) : [] }
));
console.log(values)
}
In this script, please include the scope of https://www.googleapis.com/auth/forms.body.readonly.
3. Testing.
When this script is run to the Google Form, the following sample value is obtained.
[
{"title":"sample question 1","point":5,"answer":["sample1","sample2"]},
{"title":"sample question 2","point":3,"answer":["Option 2","Option 3"]},
{"title":"sample question 3","point":3,"answer":["Option 2"]}
]
This is a sample value.
Note:
When you want to simply check this, you can also use "Try this method" of Forms API.
When the correct answer is not set, an error occurred. So I modified it. By this, when the correct answer is not set, [] is returned.
References:
Linking Google Cloud Platform Project to Google Apps Script Project for New IDE
Method: forms.get

.getEditors() returns 'DriveUser,DriveUser'

Before adding new editors to a Google drive folder, I'd like to check first which ones already exist in that folder's editors. This is to avoid unnecessary share notification if user is already an existing editor.
However, .getEditors() always returns 'DriveUser,DriveUser' so all editors get added even if existing already.
Thanks to advise if you have a solution to this.
Here's my code:
var dropboxID = "zzxxccvv112233";
var folderList = DriveApp.getFolderById(dropboxID).getFoldersByName(employee);
if (folderList.hasNext()) {
var employeeFolder = folderList.next(); //folder already exists so just add new Editors, if any
var currentEditors = employeeFolder.getEditors();
Logger.log("currentEditors = " + currentEditors);
var newEditors = emailTo + "," + emailCc;
newEditors = newEditors.replace(/\s/g, '');
newEditors = newEditors.split(',');
var editorsToAdd = [];
for (var i=0 ; i<newEditors.length ; i++) {
if (currentEditors.indexOf(newEditors[i]) < 0) {
editorsToAdd.push(newEditors[i]);
}
}
employeeFolder.addEditors(editorsToAdd);
} else {
and Logger shows this:
[20-03-05 21:23:46:637 HKT] currentEditors = DriveUser,DriveUser
You should be comparing by email address with User.getEmail()
// Get all of the current editor emails in one array
var currentEditorEmails = currentEditors.map(function (editor) { return editor.getEmail() });
// Check if the new editor emails exist in the currentEditorEmails array
for (var i=0 ; i<newEditors.length ; i++) {
if (currentEditorEmails.indexOf(newEditors[i]) < 0) {
editorsToAdd.push(newEditors[i]);
}
}
If you're using V8, you could use an arrow function instead.
const currentEditorEmails = currentEditors.map(editor => editor.getEmail());

How to filter out all emails that came from a mailing list in Gmail

Is there a way to filter out all emails that came from a mailing list within Gmail or Google Apps Script using a search query. I know you can filter out a specific email address using list:info#example.com. But I want a catch-all type of query or even a query to catch-all from a specific domain such as list:#example.com. However, this does not work. Any ideas? Any help is greatly appreciated, thank you!
This function will trash all messages from all inbox thread that are not in the list.
function emailFilter() {
var list=['a#company.com','b#company.com','c#company.com','d#company.com','e#company.com'];
var threads=GmailApp.getInboxThreads();
var token=null;
for(var i=0;i<threads.length;i++) {
if(threads[i].getMessageCount()) {
var messages=threads[i].getMessages();
for(var j=0;j<messages.length;j++) {
if(list.indexOf(messages[j].getFrom()==-1)) {
messages[j].moveToTrash();
}
}
}
}
}
I haven't tested it because I keep my inbox empty all of the time. You might want to replace 'moveToTrash()' to 'star()' for testing
What I could understand from your question and your comments, you need to filter the emails in a user's inbox that he has received, which don't only contain a certain label, but also a certain domain. If I understood well this code can help you:
function checkLabels() {
// Get the threads from the label you want
var label = GmailApp.getUserLabelByName("Label Test List");
var threadArr = label.getThreads();
// Init variable for later use
var emailDomain = '';
// Iterate over all the threads
for (var i = 0; i < threadArr.length; i++) {
// for each message in a thread, do something
threadArr[i].getMessages().forEach(function(message){
// Let's get the domains from the the users the messages were from
// example: list:#example.com -> Result: example.com
emailDomain = message.getFrom().split('<').pop().split('>')[0].split('#')[1];
// if emailDomain is equal to example.com, then do something
if(emailDomain === 'example.com'){
Logger.log(message.getFrom());
}
});
}
}
Using the Class GmailApp I got a certain label with the .getUserLabels() method and iterate through the threads thanks to the .getInboxThreads method. With a second loop and the .getMessages() you can get all the messages in a thread and for knowing the one who sent them, just use the .getFrom() method.
Docs
For more info check:
Gmail Service.
Class GmailMessage.
Class GmailThread.
So I was able to avoid replying to emails that come from a mailing list address by using the getRawContent() method and then searching that string for "Mailing-list:". So far the script is working like a charm.
function autoReply() {
var interval = 5; // if the script runs every 5 minutes; change otherwise
var date = new Date();
var day = date.getDay();
var hour = date.getHours();
var noReply = ["email1#example.com",
"email2#example.com"];
var replyMessage = "Hello!\n\nYou have reached me during non-business hours. I will respond by 9 AM next business day.\n\nIf you have any Compass.com related questions, check out Compass Academy! Learn about Compass' tools and get your questions answered at academy.compass.com.\n\nBest,\n\nShamir Wehbe";
var noReplyId = [];
if ([6,0].indexOf(day) > -1 || (hour < 9) || (hour >= 17)) {
var timeFrom = Math.floor(date.valueOf()/1000) - 60 * interval;
var threads = GmailApp.search('from:#example.com is:inbox after:' + timeFrom);
var label = GmailApp.getUserLabelByName("autoReplied");
var repliedThreads = GmailApp.search('label:autoReplied newer_than:4d');
// loop through emails from the last 4 days that have already been replied to
for (var i = 0; i < repliedThreads.length; i++) {
var repliedThreadsId = repliedThreads[i].getMessages()[0].getId();
noReplyId.push(repliedThreadsId);
}
for (var i = 0; i < threads.length; i++) {
var message = threads[i].getMessages()[0];
var messagesFrom = message.getFrom();
var email = messagesFrom.substring(messagesFrom.lastIndexOf("<") + 1, messagesFrom.lastIndexOf(">"));
var threadsId = message.getId();
var rawMessage = message.getRawContent();
var searchForList = rawMessage.search("Mailing-list:");
var searchForUnsubscribe = rawMessage.search("Unsubscribe now");
// if the message is unread, not on the no reply list, hasn't already been replied to, doesn't come from a mailing list, and not a marketing email then auto reply
if (message.isUnread() && noReply.indexOf(email) == -1 && noReplyId.indexOf(threadsId) == -1 && searchForList === -1 && searchForUnsubscribe === -1){
message.reply(replyMessage);
threads[i].addLabel(label);
}
}
}
}

Is there a limit to how many G-Forms i can link to a singe google spreadshee?

I have a script that is designed to create about 120 forms and link them to a single spreadsheet where I will analyze the data. I don't have any issues with the script until my spreadsheet has about a dozen forms linked to it. Then I get an error saying the destination ID is invalid, after logging the id, and entering it manually into a url I see no issues with the ID....
var ssSummaryId = '******redacted*******';
var form = FormApp.create('RM#' + rmNumber).setDestination(FormApp.DestinationType.SPREADSHEET, ssSummaryId);
form.setDescription(valuesSummary[ii][2])
.setConfirmationMessage('Thanks for the update on room ' + rmNumber)
.setShowLinkToRespondAgain(false);
// form.setDestination(FormApp.DestinationType.SPREADSHEET, ssSummaryId);
var formId = form.getId();
var formUrl = form.getPublishedUrl();
EDIT I'm adding my complete script and some additional info, just in case someone wants to check my code out and point out all it rookie mistakes.
I'm using Google scripts to build a spreadsheet and then create 120 slightly altered Google forms that are linked to a single spreadsheet, all the responses are by design on separate sheets named "form responses n". I consistently hit a wall once I exceed 10 forms linked to one sheet. Note; in initial testing I remember having a spreadsheet with 46 forms (and therefore sheets) linked to it. As you can see in the code below, I have the app restart from where it's left off after every 5 forms are created, so I'm not getting any 'extended runtime errors". Just the error below, typically after the script runs twice from Google Scripts IDE.
I'm just finally getting the hand of basic javascript after years of using and modifying js in web developing. So I apologize in advanced for the poor code.
Failed to set response destination. Verify the destination ID and try again. (line 54, file "Code")
function spreadsheet_builder() {
var ssSummaryId = '<<REDACTED>>';
var ssFormDataId = '<<REDACTED>>';
var batchSize = 5;
var buildStatus = false;
var ssSummary = SpreadsheetApp.openById(ssSummaryId);
SpreadsheetApp.setActiveSpreadsheet(ssSummary);
if (ssSummary.getSheetByName('Summary') == null) {
var sheetSummary = ssSummary.getSheetByName('Sheet1');
} else {
var sheetSummary = ssSummary.getSheetByName('Summary');
}
var rangeSummary = sheetSummary.getDataRange();
var valuesSummary = rangeSummary.getValues();
buildStatus = get_last_position(valuesSummary, buildStatus); //either returns last position in array or 'true' if task is complete
if (buildStatus != true || buildStatus > 0) {
var formCreation = [];
var formData = get_form_data(ssFormDataId); // Get form questions from form Data ss, might be better to keep everything on the same sheet
batchSize = buildStatus + batchSize;
for ( var ii = buildStatus; ii < batchSize; ii++ ) {
if (valuesSummary[ii][1] != '') {
var formCreationReturn = form_builder(formData, valuesSummary, ii, ssSummaryId);
formCreation.push(formCreationReturn);
}
}
var aSum = [ssSummary, sheetSummary, rangeSummary];
final_storing_operation(formCreation, aSum, buildStatus);
}
if (sheetSummary.getName() != 'Summary') {
SpreadsheetApp.setActiveSpreadsheet(ssSummary);
sheetSummary.activate().setName('Summary');
sheetSummary.setFrozenColumns(3);
sheetSummary.setFrozenRows(1);
sheetSummary.hideColumns(1);
//var sSumIndex = sheetSummary.getIndex();
}
SpreadsheetApp.setActiveSpreadsheet(ssSummary);
sheetSummary.activate();
ssSummary.moveActiveSheet(1);
}
function form_builder(formData, valuesSummary, ii, ssSummaryId) {
var lastFormCreated = ii;
var rmNumber = valuesSummary[ii][1];
var form = FormApp.create('RM#' + rmNumber).setDestination(FormApp.DestinationType.SPREADSHEET, ssSummaryId);
form.setDescription(valuesSummary[ii][2])
.setConfirmationMessage('Thanks for the update on room ' + rmNumber)
.setShowLinkToRespondAgain(false);
// form.setDestination(FormApp.DestinationType.SPREADSHEET, ssSummaryId);
var formId = form.getId();
var formUrl = form.getPublishedUrl();
var sectionHeader = 'SECTION_HEADER'; //preformatted form question types.
var list = 'LIST';
var paragraphText = 'PARAGRAPH_TEXT';
for (var j = 1; j < formData.length; j++) { //top row is header
switch (formData[j][0]) {
case sectionHeader:
form.addSectionHeaderItem().setTitle(formData[j][1]);
break;
case list:
var item = form.addListItem();
item.setTitle(formData[j][1]).setHelpText(formData[j][2]);
item.setChoices([
item.createChoice(formData[j][3]),
item.createChoice(formData[j][4]),
item.createChoice(formData[j][5])
]);
break;
case paragraphText:
form.addParagraphTextItem().setTitle(formData[j][1]);
break;
default:
form.addSectionHeaderItem().setTitle('OOPS u\'don MESSED up');
break;
}
}
return [formId, formUrl, lastFormCreated, rmNumber];
}
function final_storing_operation(formCreation, aSum, buildStatus) {
SpreadsheetApp.setActiveSpreadsheet(aSum[0]);
aSum[1].activate();
var startRow = formCreation[0][2] + 1;
var newRange = aSum[1].getRange(startRow, 1, formCreation.length, 2); //row, clmn, rows, columns
var newValues = [];
for ( var ij = 0; ij < formCreation.length; ij++) {
var values = [formCreation[ij][0], "\=HYPERLINK(\"" + formCreation[ij][1] + "\", \"RM#" + formCreation[ij][3] + "\")"];
newValues.push(values);
}
newRange.setValues(newValues);
}
function get_last_position (valuesSummary, buildStatus) {
var rowPos = 1; // start at 1 to ignore headers
while (valuesSummary[rowPos][1] != '') {
if (valuesSummary[rowPos][0] == '') {
return rowPos;
}
rowPos++;
}
if(valuesSummary[rowPos][0] != '' && valuesSummary[rowPos][1] != '') {
buildStatus = true;
return buildStatus;
}
}
function get_form_data (ssFormDataId) {
var ssFormData = SpreadsheetApp.openById(ssFormDataId);
SpreadsheetApp.setActiveSpreadsheet(ssFormData);
var sheetFormData = ssFormData.getSheets()[0];
var rangeFormData = sheetFormData.getDataRange();
var valuesFormData = rangeFormData.getValues();
return valuesFormData;
}
As an alternative, you could create the forms, and intentionally not link them to a spreadsheet, then have some code that looped through every form, and extracted the data. You'd probably want to put the forms into their own folder.
Or, you'd need to build a form with Apps Script HTML Service, embed it into a Apps Script Gadget in a Google Site, and have everyone fill out the form from Sites.