Try it out
Apps
Blog
About
Contact
Developers
Docs
Login
Developers
gcalendar@obindo.com
Written by
Alex
Extract meetings and events from any email and save them to Google Calendar.
/************************** Documentation Links/Description: API References : https://developers.google.com/google-apps/calendar/v3/reference/ : https://developers.google.com/google-apps/calendar/ Insert Event : https://developers.google.com/google-apps/calendar/v3/reference/events/insert Insert Sandbox : https://developers.google.com/google-apps/calendar/v3/reference/events/insert#try-it OAuth2 Agent : https://developers.google.com/accounts/docs/OAuth2UserAgent Refresh Token : https://developers.google.com/accounts/docs/OAuth2WebServer#refresh Must obtain the API Key from creating a Server Key on the Google API Console and inputting IP address 0.0.0.0/0 as the allowed IP address. Make sure to activate the CalendarAPI on the Google API Console. **************************/ var oauth2Credentials = {}; //#region PRIVATE set my oauth2 credentials var requestUrl = 'https://accounts.google.com/o/oauth2/auth?'; var scopeUrl = 'https://www.googleapis.com/auth/calendar'; var tokenUrl = 'https://accounts.google.com/o/oauth2/token'; var apiRoot = 'https://www.googleapis.com/calendar/v3'; var calendarListRoot = 'https://www.googleapis.com/calendar/v3/users/me/calendarList?key=' + oauth2Credentials.apiKey; var maxDescriptionLength= 7500; oauth2.getAuthRedirectUrl= function() { var args= { scope: scopeUrl, state: tokens.getId(), response_type: 'code', client_id: oauth2Credentials.clientID, redirect_uri: oauth2.getCallbackUrl(true), access_type: 'offline', approval_prompt: 'auto' }; //send off the request for a 'code' return requestUrl + util.getQueryArgsFromObject(args); } oauth2.onAuthResponse= function(q) { var qArgs= util.getObjectFromQueryArgs(q); var args= { grant_type: 'authorization_code', client_id: oauth2Credentials.clientID, client_secret: oauth2Credentials.clientSecret, redirect_uri: oauth2.getCallbackUrl(true), code: qArgs.code }; //requrest to exchange the code for an access token var response = post({url: tokenUrl, args: args}); tokens.loadFromId(qArgs.state); tokens.accessToken= util.jsonify(response.text); } oauth2.refreshAccessToken= function() { var rt= tokens.accessToken.refresh_token; var args= { grant_type: "refresh_token", refresh_token: rt, client_id: oauth2Credentials.clientID, client_secret: oauth2Credentials.clientSecret }; var response= post({url: tokenUrl, args: args}); var newToken= util.jsonify(response.text); newToken.refresh_token = rt; tokens.accessToken = newToken; } function getAuthHeader() { return { "Authorization": 'Bearer ' + tokens.accessToken.access_token }; } function getCalendarId() { var response = get({url: calendarListRoot, header: getAuthHeader() }); var calendars = util.jsonify(response.text).items; for (var i = 0; i < calendars.length; i++) if (calendars[i].primary == true) return calendars[i].id; return null; } var oneHour= 1000 * 60 * 60; var oneDay= oneHour * 24; function getFriendlyDate(date) { //assumed "2014-07-11T17:12:55-05:00" var bTime= (date.dateTime); var s= (bTime) ? date.dateTime : date.date; var arr= s.split(/[T\-\:\+]/); if (!bTime) return getDateDisplay(new Date(arr[0], arr[1] - 1, arr[2])); for (var i=0; i < arr.length; i++) arr[i]= parseInt(arr[i], 10); var dt= new Date(arr[0], arr[1] - 1, arr[2], arr[3], arr[4], arr[5]); var hour= dt.getHours(); var z= 'am'; var minDisplay= ''; if (hour == 0) hour == 12; else { if (hour >= 12) z= 'pm'; if (hour > 12) hour-= 12; } if (dt.getMinutes() > 10) minDisplay= ':' + dt.getMinutes(); else if (dt.getMinutes() > 0) minDisplay= ':0' + dt.getMinutes(); return getDateDisplay(dt) + ' ' + hour + minDisplay + z; function getDateDisplay(dt) { var date= dt.toDateString(); //Fri Jul 11 2014 var arrDate= date.split(' '); return arrDate[0] + ' ' + arrDate[1] + ' ' + arrDate[2]; } } function getDateText(ms, includeTime) { var dt= new Date(ms); var s= dt.getUTCFullYear() + '-'; s+= addLeading(dt.getUTCMonth() + 1) + '-'; s+= addLeading(dt.getUTCDate()); if (!includeTime) return s; s+= 'T' + addLeading(dt.getUTCHours()) + ':'; s+= addLeading(dt.getUTCMinutes()) + ':'; s+= addLeading(dt.getUTCSeconds()) + '+00:00'; return s; function addLeading(n) { return (n < 10) ? '0' + n : '' + n; } } function getEvent(start, end, hasStartTime, hasEndTime) { if (!start) //needs a start value return null; var event= { end: {}, start: {} }; if (!hasStartTime && !hasEndTime) { //no times, just days event.start.date= getDateText(start, false); var newEnd= (end) ? end : start; newEnd+= oneDay; //add a day so it spans event.end.date= getDateText(newEnd, false); } else { //if one time exists, set start date and time, even if midnight event.start.dateTime= getDateText(start, true); var newEnd= 0; if (end && hasEndTime) //has everything newEnd= end; else if (end && !hasEndTime) //end day, no time: add a day so it spans newEnd= end + oneDay; else if (!end && !hasEndTime && hasStartTime) //start time with no end, add an hour newEnd= start + oneHour; else //don't know what to do with it return null; event.end.dateTime= getDateText(newEnd, true); } return event; } function postCalendarEvent(event, calendarId) { var header= getAuthHeader(); header["Content-Type"]= "application/json"; var args= { key: oauth2Credentials.apiKey, fields: 'htmlLink,id,end,start,summary' }; if (event.attendees) args.sendNotifications= true; var url = apiRoot + '/calendars/' + calendarId + '/events'; url+= '?' + util.getQueryArgsFromObject(args); var response= post({ url: url, header: header, body: util.stringify(event) }); return util.jsonify(response.text); } function truncate(text, length) { if (text.length < length) return text; var s= text.substring(0, length); var i= s.lastIndexOf(' '); return (i > 0) ? s.substring(0,i) : s; } function getDescription(text) { var s= truncate(text, maxDescriptionLength); var footer= '-- This Google Calendar event was created automatically by emailing googlecal@obindo.com. '; footer+= 'Learn more at http://obindo.com.'; return (s && s.length > 0) ? s + '\n\n' + footer : footer; } function getSubjectInfo(subject, dates, bUpdateTitle) { //if (!subject || !dates) //possible for subject dates to exist with no subject if (!dates) return { title: subject, dates: [] }; var subjectDates= []; var lastDate= null; var lastIndex= -1; for (var i=0; i < dates.length; i++) { if (dates[i].source != 'subject') continue; subjectDates.push(dates[i]); if (dates[i].index > lastIndex) { lastDate= dates[i]; lastIndex= dates[i].index; } } var title= subject; if (bUpdateTitle) { if (lastDate && (lastDate.index + lastDate.text.length == title.length)) { title= title.substring(0, lastDate.index); title= title.replace(/\s+(on)\s+$/i,''); //trim date phrases } title= title.replace(/^\s+|\s+$/g,''); //trim title= title.replace(/\s{2,}/g,' '); //consecutive spaces } return { title: title, dates: subjectDates }; } function getExistingEvents(calendarId, dtStart, dtEnd) { var startTime= getDateText(dtStart, true); var endTime= getDateText(dtEnd, true); var url= apiRoot + '/calendars/'+ calendarId +'/events'; var args= { timeMin: startTime, timeMax: endTime, singleEvents: true, orderBy: 'startTime', fields: 'items(attendees,htmlLink,summary,start,end,id,creator)' }; var response= get({url: url, header: getAuthHeader(), args: args}); return util.jsonify(response.text).items; } function getExistingEvent(calendarId, emailAddress, date, summary, attendees) { var dt= new Date(date.start); var dtStart= new Date(dt.getTime() - (oneHour * 12)); var dtEnd= new Date(dt.getTime() + (oneHour * 12)); var events= getExistingEvents(calendarId, dtStart, dtEnd); if (events.length == 0) return null; var emails= getEmails(emailAddress, attendees); for (var i=0; i < events.length; i++) if (isMatch(events[i])) return events[i]; return null; function isMatch(event) { if (event.summary != summary) return false; var eventEmails= getEmails(event.creator.email, event.attendees); if (emails.length != eventEmails.length) return false; for (var i=0; i < emails.length; i++) { var found= false; for (var j=0; j < eventEmails.length; j++) if (emails[i] == eventEmails[j]) { found= true; break; } if (!found) return false; } return true; } function getEmails(defaultEmail, attendees) { var arr= [ defaultEmail.toLowerCase() ]; if (attendees) for (var i=0; i < attendees.length; i++) arr.push(attendees[i].email.toLowerCase()); return arr; } } function createDefaultEvent(calendarId, text, attendees, title) { var url= apiRoot + '/calendars/'+ calendarId +'/events/quickAdd'; var args= { text: title, fields: 'htmlLink,id,end,start,summary' }; var header= getAuthHeader(); url+= '?' + util.getQueryArgsFromObject(args); var response= post({url: url, header: header }); var event= util.jsonify(response.text); if (text || attendees.length > 0) { url= apiRoot + '/calendars/'+ calendarId +'/events/' + event.id; header["Content-Type"]= "application/json"; var updates= (text) ? { description: text } : {}; if (attendees.length > 0) { updates.attendees= attendees; //url+= '?sendNotifications=true'; //don't notify if a made up date } patch({url: url, header: header, body: util.stringify(updates) }); } return event; } function sendConfirm(email, events, existingEvents, attendees, success) { var existingOnly= (existingEvents.length > 0 && events.length == 0); var attendeesExist= (attendees.length > 0); var bWriteUrls= ((events.length + existingEvents.length) > 1); var subject= (existingOnly) ? 'Obindo found similar events already in your Google Calendar!' : 'Obindo created a Google Calendar event for you!'; var sAttend= getAttendeeDisplay(); var s= getHeader(); s+= getEventsDisplay((existingOnly) ? existingEvents : events); s+= getAttendeeFooter(existingOnly); if (!existingOnly && existingEvents.length > 0) { var plural= (existingEvents.length > 1); s+= '\n\nWe also found ' + ((plural) ? 'these events' : 'this event'); s+= ' that already '+ ((plural) ? 'exist and look' : 'exists and looks') +' similar.'; s+= ' In order to avoid possibly sending out multiple invitations'; s+= ' for the same event, we didn\'t make any changes to ' + ((plural) ? 'them.' : 'it.'); s+= getEventsDisplay(existingEvents); s+= getAttendeeFooter(true); } s+= '\n\nThanks for using Obindo!'; var emailArgs= { to: email, subject: subject, text: s, templateFields: { recipientName: message.from.name } }; if (message.appUrl) { emailArgs.templateFields.buttonText= 'View in Google Calendar'; emailArgs.templateFields.buttonLink= message.appUrl; } sendEmail(emailArgs); function getHeader() { var s= ''; if (!success) { s= 'Sorry we couldn\'t figure out exactly when you wanted this date set up for you, '; s+= ' but we created it and all you\'ll have to do is update the date if necessary.'; } else if (existingOnly) { var plural= (existingEvents.length > 1); s+= 'We found ' + ((plural) ? 'these events' : 'this event'); s+= ' that already '+ ((plural) ? 'exist and look' : 'exists and looks') +' similar.'; s+= ' In order to avoid possibly sending out multiple invitations'; s+= ' for the same event, we didn\'t make any changes to ' + ((plural) ? 'them.' : 'it.'); } else { s= 'We created the following event' + ((events.length == 1) ? '' : 's') + ' for you:'; } return s; } function getAttendeeDisplay() { if (!attendeesExist) return ''; var s= ''; for (var i=0; i < attendees.length; i++) { if (i > 0) s+= ', '; s+= (attendees[i].displayName) ? attendees[i].displayName : attendees[i].email; } return s; } function getAttendeeFooter(existing) { if (!attendeesExist) return ''; if (!success) return '\n\nBecause we weren\'t certain of the date, invitations were not sent to the attendees.'; if (existing) return '\n\nYour attendees were not sent any additional invitations.'; return '\n\nYour attendees will be sent an invitation from Google Calendar.'; } function getEventsDisplay(events) { var s= ''; for (var i=0; i < events.length; i++) { var event= events[i]; s+= '\n\nTitle: ' + event.summary; s+= '\nStart: ' + getFriendlyDate(event.start); s+= '\nEnd: ' + getFriendlyDate(event.end); if (sAttend.length > 0) s+= '\nAttendees: ' + sAttend; if (bWriteUrls) s+= '\n' + event.htmlLink; } return s; } } function getAttendees(message) { var attendees= []; if (message.selections) addAttendees(message.selections.attendees); else { addAttendees(message.to); addAttendees(message.cc); } return attendees; function addAttendees(arr) { if (!arr) return; for (var i=0; i < arr.length; i++) if (arr[i].emailAddress.toLowerCase().indexOf('@obindo.com') < 0) { var a= { email: arr[i].emailAddress }; if (arr[i].name && arr[i].name.length > 0) a.displayName= arr[i].name; attendees.push(a); } } } $(function() { var emailAddress= message.from.emailAddress; var sel= message.selections; var dates= (sel && sel.dates) ? sel.dates : message.dates; var info= (sel) ? getSubjectInfo((sel.title) ? sel.title : '', dates, false) : getSubjectInfo(message.subject, message.dates, true); var calendarId = getCalendarId(); var text= (sel) ? null : getDescription(message.fullText); var attendees= getAttendees(message); if (info.dates.length == 0) { var src= message.source; //don't create dummy event if none found from Get Started if (src && src.type && src.type == 'getstarted') return; var event= createDefaultEvent(calendarId, text, attendees, info.title); message.appUrl= event.htmlLink; sendConfirm(emailAddress, [ event ], [], attendees, false); return; } var events= []; var existingEvents= []; for (var i = 0; i < info.dates.length; i++) { var date= info.dates[i]; var summary= info.title; if (summary.length == 0) summary= (date.title) ? date.title : 'New event from Obindo'; var existing= getExistingEvent(calendarId, emailAddress, date, summary, attendees); if (existing) { existingEvents.push(existing); continue; } var event= getEvent(date.start, date.end, (date.startTime), (date.endTime)); if (event) { if (attendees.length > 0) event.attendees= attendees; event.summary= summary; if (text) event.description= text; events.push(postCalendarEvent(event, calendarId)); } } if (events.length > 0) message.appUrl= events[0].htmlLink; else if (existingEvents.length > 0) message.appUrl= existingEvents[0].htmlLink; sendConfirm(emailAddress, events, existingEvents, attendees, true); });