function getPrinterList(auth) { //returns a list of online printers, except "save to Google Drive." // I left off G Drive for target audience needs, but don't have enough info to do the same for fedex office. var printlist = []; var name; //cloud print is not paart of hte google apps script suite, so has to be called with good old fashioned http. //However, it works more easily from here than directly from the lambda function. var responseString = UrlFetchApp.fetch('https://www.google.com/cloudprint/search', { headers: { Authorization: 'Bearer ' + auth }, muteHttpExceptions: true }).getContentText(); try { //need to parse the response here, to only return the desired printers. var responseObject = JSON.parse(responseString); //cloud print returns "success":true for successful calls. If so, check printers for online and not G-Drive. if(responseObject.success) { var printers=responseObject.printers; for (var p in printers) { //if you want 'save to google drive as an option, uncomment the following and remove the existing if statement: //if(printers[p].connectionStatus=="ONLINE"){ if(printers[p].type!=="DRIVE"&&printers[p].connectionStatus=="ONLINE"){ printlist.push([printers[p].id, printers[p].displayName]); } } } else {printlist=[];} //prefer to return empty result vs error, for graceful skill behavior. } catch(e){printlist=[];} return printlist; } function print(auth,printerID, messageID,attachmentIndex){ //same function is called for 2 purposes. First call prints to google drive, grabs thee returned page count, //and then deleted the resulting file. Most reiable way I found to provide user a page count estimate //to confirm printing, absent a visible widget or other user interface. //Second call will send the same job to the user's printer, after their confirmation. var message=GmailApp.getMessageById(messageID); var fetchString='https://www.google.com/cloudprint/submit'; var printData, dataType, title,jobReturn; //If attachmentIndex is not passed, the call is for the message body. if(!attachmentIndex&&attachmentIndex!==0){ printData='Email Printed from Alexa

To: '+message.getTo()+'
From: '+message.getFrom()+'
Date Received: '+message.getDate()+'

'+message.getBody(); dataType="text/html"; title ="Alexa Email Print"; } else{ //print attachment. The calling function already processed error handling var attachments = message.getAttachments(); printData=attachments[attachmentIndex]; dataType=printData.getContentType(); title = printData.getName(); } var ticket = { version: "1.0", print: {} }; var payload = { "printerid" : printerID, "title" : title, "content" : printData, "contentType": dataType, "ticket" : JSON.stringify(ticket) }; var responseString = UrlFetchApp.fetch(fetchString, { method: "POST", payload: payload, headers: { Authorization: 'Bearer ' + auth }, "muteHttpExceptions": true }).getContentText(); var files = DriveApp.getFilesByName(title); while(files.hasNext()){ fileID=files.next().getId(); localDelete(fileID,auth); } try { //need to parse the response here, to only return the desired printers, but returns as a string and reparses //in the lambda function because it was behaving badly returning the object. var responseObject = JSON.parse(responseString); //cloud print returns "success":true for successful calls. If so, check printers for online and not G-Drive. if(responseObject.success) { jobReturn=[responseObject.success,responseObject.job.numberOfPages]; } else {jobReturn=[0];} } catch(e){jobReturn=[0];} //return responseString; //this will be parsed in the lambda function return jobReturn; } function getAttachments(messageID){ //checks for attachments to a message. This is far more straightforward than writing code to search //various mime types manually, and is the main reason apps script was first added to the skill. var message=GmailApp.getMessageById(messageID); var attachmentblobs = message.getAttachments(); var attachments=[]; var storedType, blobType, namesplit; if(attachmentblobs){ for (var i in attachmentblobs){ //splitting serves a coulpe of purposes. First, eases reporting to the user about the file types. //second, provides the file name without Alexa trying (badly) to read the extension. namesplit=attachmentblobs[i].getName().split(".") storedType =attachmentblobs[i].getContentType(); switch(namesplit[1].substr(0,3)){ //identify file type by first 3 chars of extension - helps with docx,pptx,etc. //If you want to identify and report more file types, add them here. However, do not change the jpeg and png //from "an image" and do not give other image types this exact name. The "show me" feature uses this label to //determine whether an image can be displayed on a card in the Alexaa app. Keep in mind that the skill will //say that attachment {3} is {blobtype} called {filename}. case 'jpg': blobType='an image'; break; case 'jpe': blobType='an image'; break; case 'png': blobType='an image'; break; case 'tif': blobType='a tiff image'; break; case 'doc': blobType='a word processing document'; break; case 'xls': blobType='a spreadsheet'; break; case 'txt': blobType='a text file'; break; case 'exe': blobType='a program'; break; case 'bat': blobType='a program'; break; case 'ppt': blobType='a PowerPoint presentation'; break; case 'pdf': blobType='a PDF file'; break; default: blobType="a file type I don't recognize"; } attachments.push([blobType,namesplit[0]]); //add the result to an array that will be returned } } return attachments; } function getPlainBody(messageID){ //fetches the message body as plain text for Alexa reading. Much simpleer than trying to manually extract it //from a multipart email message. var message=GmailApp.getMessageById(messageID); var plainBody = message.getPlainBody(); return plainBody; } function sendReply(messageID,content){ var message=GmailApp.getMessageById(messageID); var x = message.reply(content); return "OK"; } function sendReplyAll(messageID,content){ var message=GmailApp.getMessageById(messageID); var x = message.replyAll(content); return "OK"; } function setpin(mypin){ //sets the skill pin, saving it in a file called "AlexaPIN" on their Google Drive. //the passed parameter will be either a 4-digit pin, or the string 'notneeded' to indicate that //the user has declined to set a PIN and caan have access without one. if(mypin==0){mypin="0000";} //zero messes with file comparison unless changed to a string var myname = "AlexaPIN"; var pinset; //check for existing file with this name. var foldername="Alexa-Skill-Files" var fileID, files, folder; var folders = DriveApp.getFoldersByName(foldername) if(!folders.hasNext()){ DriveApp.createFolder(foldername); folders = DriveApp.getFoldersByName(foldername)} folder=folders.next(); files = folder.getFilesByName(myname) if (files.hasNext()){ pinset=files.next().setContent(mypin); } else { //create the PIN file if it does not exist pinset = folder.createFile(myname, mypin); } return pinset.getId(); //id is not needed in the skill, but returning a file attribute allows evaluation of success. } function checkpin(mypin,auth){ //check whether a PIN is needed on skill opening, and/or check the PIN entered by the user. //if user opened without saying a PIN (typical use case) this will first pass in null, and return match if not needed, //or nomatch otherwise, so that the skill will either prompt for a PIN or continue executing the user intent. //calls again when user says a PIN (or anything else) until there is a match. On the 5th try the skill is locked and user //must go to the skill's web site or Google Drive to reset the PIN. (3 tries is too low for target audience) if(mypin===0){mypin="0000";} var myname = "AlexaPIN"; var pincheck, pinresult,myFile,files, folder, fileID; var foldername="Alexa-Skill-Files" var folders = DriveApp.getFoldersByName(foldername) if(!folders.hasNext()){ DriveApp.createFolder(foldername); folders = DriveApp.getFoldersByName(foldername)} folder=folders.next(); files = folder.getFilesByName(myname) if(files.hasNext()){ myFile=files.next(); fileID = myFile.getId(); pincheck=myFile.getBlob().getDataAsString();//content of file is only a 4 digit PIN, the string 'notneeded' or 'locked' switch(pincheck){ case mypin: pinresult='match'; break; case 'notneeded': pinresult='match'; break; case 'locked': pinresult='locked'; break; default: //only get here by no match, but could be a PIN attempt or some other intent if(mypin){ //user provided an incorrect PIN myname='failedPINCount'; files = folder.getFilesByName(myname); if(files.hasNext()){ //if failed attempt counter already exists, increment and test for too many myFile=files.next(); pincheck=parseInt(myFile.getBlob().getDataAsString('ascii')); //reused existing variable instead of declaring another ++pincheck; myFile.setContent(pincheck.toString()); //update attempt counter file with increment if(pincheck>=5){ //if you want more or less attempts before lockout change the "5" here. pinresult='locked'; DriveApp.getFileById(fileID).setContent('locked'); } } else { //start a failed attempt counter file with value 1; folder.createFile(myname, "1"); } } if(pinresult!='locked'){pinresult='nomatch';} //send back nomatch, unless it was reset to locked } } else { //no PIN file exists pinresult = 'notset'; } //check for a previous failed PIN attempt counter, and delete it if PIN is now OK if(pinresult=='notset'||pinresult=='match'){ //could be either of these after user resets PIN myname='failedPINCount'; files = folder.getFilesByName(myname); while (files.hasNext()){ myFile=files.next().getId(); localDelete(myFile,auth); } } return pinresult; } function showAttachment(messageID,attachmentIndex){ //saves image to google drive and gets file id to use when user says "show me" to send to an Alexa card var myname="cardImage"; //temp file name to use when storing image var requestedblob=GmailApp.getMessageById(messageID).getAttachments()[attachmentIndex]; var foldername="Alexa-Skill-Files" var folders = DriveApp.getFoldersByName(foldername) if(!folders.hasNext()){ DriveApp.createFolder(foldername); folders = DriveApp.getFoldersByName(foldername)} var folder = folders.next(); fileID=folder.createFile(requestedblob).getId() return fileID; } function modifyMsg(messageID,intent){ //processes changes to messages var message=GmailApp.getMessageById(messageID); var result; switch(intent) { case 'MarkReadIntent': result = message.markRead(); break; case 'MarkUnReadIntent': result = message.markUnread(); break; case 'AMAZON.YesIntent': result = message.moveToTrash(); break; case 'StarIntent': result = message.star(); break; case 'UnStarIntent': result = message.unstar(); } return "OK"; } function deleteFiles(auth){ //used to clean up old stored files on opening the skill var myname="cardImage"; //temp file name used when storing image var foldername="Alexa-Skill-Files" var fileID, files, folder; var folders = DriveApp.getFoldersByName(foldername) if(folders.hasNext()){ folder=folders.next(); files = folder.getFilesByName(myname) while (files.hasNext()){ //clear any "show me" image files fileID=files.next().getId(); localDelete(fileID,auth); } } else {DriveApp.createFolder(foldername);} return; } function localDelete(fileID,auth) { var options = { headers: { Authorization: 'Bearer ' + auth },'method' : 'DELETE'} var fetch='https://www.googleapis.com/drive/v2/files/'+fileID var resp = UrlFetchApp.fetch(fetch,options); return; }