├── .gitattributes ├── .gitignore ├── LICENSE.TXT ├── README.md ├── app.js ├── authHelper.js ├── package.json ├── pages.js └── static └── styles └── app.css /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | node_modules 46 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | node-calendar-sync, https://github.com/jasonjoh/node-calendar-sync 2 | 3 | Copyright (c) Microsoft Corporation 4 | All rights reserved. 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | ""Software""), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Sample for Calendar Sync with Office 365 # 2 | 3 | ## Installation ## 4 | 5 | npm install 6 | 7 | ## Running the sample 8 | 9 | 1. Register a new app at https://apps.dev.microsoft.com 10 | 1. Copy the **Application Id** and paste this value for the `clientId` value in `authHelper.js`. 11 | 1. Click the **Generate New Password** button and copy the password. Paste this value for the `clientSecret` value in `authHelper.js`. 12 | 1. Click the **Add Platform** button and choose **Web**. Enter `http://localhost:3000/authorize` for the **Redirect URI**. 13 | 1. Click **Save**. 14 | 1. Save changes to `authHelper.js` and start the app: 15 | 16 | npm start 17 | 18 | 1. Open your browser and browse to http://localhost:3000. 19 | 20 | ## Copyright ## 21 | 22 | Copyright (c) Microsoft. All rights reserved. 23 | 24 | ---------- 25 | Connect with me on Twitter [@JasonJohMSFT](https://twitter.com/JasonJohMSFT) 26 | 27 | Follow the [Outlook Dev Blog](http://blogs.msdn.com/b/exchangedev/) -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | 4 | var bodyParser = require('body-parser'); 5 | var cookieParser = require('cookie-parser'); 6 | var session = require('express-session'); 7 | var moment = require('moment'); 8 | var querystring = require('querystring'); 9 | var outlook = require('node-outlook'); 10 | 11 | // Very basic HTML templates 12 | var pages = require('./pages'); 13 | var authHelper = require('./authHelper'); 14 | 15 | // Configure express 16 | // Set up rendering of static files 17 | app.use(express.static('static')); 18 | // Need JSON body parser for most API responses 19 | app.use(bodyParser.json()); 20 | // Set up cookies and sessions to save tokens 21 | app.use(cookieParser()); 22 | app.use(session( 23 | { secret: '0dc529ba-5051-4cd6-8b67-c9a901bb8bdf', 24 | resave: false, 25 | saveUninitialized: false 26 | })); 27 | 28 | // Home page 29 | app.get('/', function(req, res) { 30 | res.send(pages.loginPage(authHelper.getAuthUrl())); 31 | }); 32 | 33 | app.get('/authorize', function(req, res) { 34 | var authCode = req.query.code; 35 | if (authCode) { 36 | console.log(''); 37 | console.log('Retrieved auth code in /authorize: ' + authCode); 38 | authHelper.getTokenFromCode(authCode, tokenReceived, req, res); 39 | } 40 | else { 41 | // redirect to home 42 | console.log('/authorize called without a code parameter, redirecting to login'); 43 | res.redirect('/'); 44 | } 45 | }); 46 | 47 | function tokenReceived(req, res, error, token) { 48 | if (error) { 49 | console.log('ERROR getting token:' + error); 50 | res.send('ERROR getting token: ' + error); 51 | } 52 | else { 53 | // save tokens in session 54 | req.session.access_token = token.token.access_token; 55 | req.session.refresh_token = token.token.refresh_token; 56 | req.session.email = authHelper.getEmailFromIdToken(token.token.id_token); 57 | res.redirect('/logincomplete'); 58 | } 59 | } 60 | 61 | app.get('/logincomplete', function(req, res) { 62 | var access_token = req.session.access_token; 63 | var refresh_token = req.session.access_token; 64 | var email = req.session.email; 65 | 66 | if (access_token === undefined || refresh_token === undefined) { 67 | console.log('/logincomplete called while not logged in'); 68 | res.redirect('/'); 69 | return; 70 | } 71 | 72 | res.send(pages.loginCompletePage(email)); 73 | }); 74 | 75 | app.get('/refreshtokens', function(req, res) { 76 | var refresh_token = req.session.refresh_token; 77 | if (refresh_token === undefined) { 78 | console.log('no refresh token in session'); 79 | res.redirect('/'); 80 | } 81 | else { 82 | authHelper.getTokenFromRefreshToken(refresh_token, tokenReceived, req, res); 83 | } 84 | }); 85 | 86 | app.get('/logout', function(req, res) { 87 | req.session.destroy(); 88 | res.redirect('/'); 89 | }); 90 | 91 | app.get('/sync', function(req, res) { 92 | var token = req.session.access_token; 93 | var email = req.session.email; 94 | if (token === undefined || email === undefined) { 95 | console.log('/sync called while not logged in'); 96 | res.redirect('/'); 97 | return; 98 | } 99 | 100 | // Set the endpoint to API v2 101 | outlook.base.setApiEndpoint('https://outlook.office.com/api/v2.0'); 102 | // Set the user's email as the anchor mailbox 103 | outlook.base.setAnchorMailbox(req.session.email); 104 | // Set the preferred time zone 105 | outlook.base.setPreferredTimeZone('Eastern Standard Time'); 106 | 107 | // Use the syncUrl if available 108 | var requestUrl = req.session.syncUrl; 109 | if (requestUrl === undefined) { 110 | // Calendar sync works on the CalendarView endpoint 111 | requestUrl = outlook.base.apiEndpoint() + '/Me/CalendarView'; 112 | } 113 | 114 | // Set up our sync window from midnight on the current day to 115 | // midnight 7 days from now. 116 | var startDate = moment().startOf('day'); 117 | var endDate = moment(startDate).add(7, 'days'); 118 | // The start and end date are passed as query parameters 119 | var params = { 120 | startDateTime: startDate.toISOString(), 121 | endDateTime: endDate.toISOString() 122 | }; 123 | 124 | // Set the required headers for sync 125 | var headers = { 126 | Prefer: [ 127 | // Enables sync functionality 128 | 'odata.track-changes', 129 | // Requests only 5 changes per response 130 | 'odata.maxpagesize=5' 131 | ] 132 | }; 133 | 134 | var apiOptions = { 135 | url: requestUrl, 136 | token: token, 137 | headers: headers, 138 | query: params 139 | }; 140 | 141 | outlook.base.makeApiCall(apiOptions, function(error, response) { 142 | if (error) { 143 | console.log(JSON.stringify(error)); 144 | res.send(JSON.stringify(error)); 145 | } 146 | else { 147 | if (response.statusCode !== 200) { 148 | console.log('API Call returned ' + response.statusCode); 149 | res.send('API Call returned ' + response.statusCode); 150 | } 151 | else { 152 | var nextLink = response.body['@odata.nextLink']; 153 | if (nextLink !== undefined) { 154 | req.session.syncUrl = nextLink; 155 | } 156 | var deltaLink = response.body['@odata.deltaLink']; 157 | if (deltaLink !== undefined) { 158 | req.session.syncUrl = deltaLink; 159 | } 160 | res.send(pages.syncPage(email, response.body.value)); 161 | } 162 | } 163 | }); 164 | }); 165 | 166 | app.get('/viewitem', function(req, res) { 167 | var itemId = req.query.id; 168 | var access_token = req.session.access_token; 169 | var email = req.session.email; 170 | 171 | if (itemId === undefined || access_token === undefined) { 172 | res.redirect('/'); 173 | return; 174 | } 175 | 176 | var select = { 177 | '$select': 'Subject,Attendees,Location,Start,End,IsReminderOn,ReminderMinutesBeforeStart' 178 | }; 179 | 180 | var getEventParameters = { 181 | token: access_token, 182 | eventId: itemId, 183 | odataParams: select 184 | }; 185 | 186 | outlook.calendar.getEvent(getEventParameters, function(error, event) { 187 | if (error) { 188 | console.log(error); 189 | res.send(error); 190 | } 191 | else { 192 | res.send(pages.itemDetailPage(email, event)); 193 | } 194 | }); 195 | }); 196 | 197 | app.get('/updateitem', function(req, res) { 198 | var itemId = req.query.eventId; 199 | var access_token = req.session.access_token; 200 | 201 | if (itemId === undefined || access_token === undefined) { 202 | res.redirect('/'); 203 | return; 204 | } 205 | 206 | var newSubject = req.query.subject; 207 | var newLocation = req.query.location; 208 | 209 | console.log('UPDATED SUBJECT: ', newSubject); 210 | console.log('UPDATED LOCATION: ', newLocation); 211 | 212 | var updatePayload = { 213 | Subject: newSubject, 214 | Location: { 215 | DisplayName: newLocation 216 | } 217 | }; 218 | 219 | var updateEventParameters = { 220 | token: access_token, 221 | eventId: itemId, 222 | update: updatePayload 223 | }; 224 | 225 | outlook.calendar.updateEvent(updateEventParameters, function(error, event) { 226 | if (error) { 227 | console.log(error); 228 | res.send(error); 229 | } 230 | else { 231 | res.redirect('/viewitem?' + querystring.stringify({ id: itemId })); 232 | } 233 | }); 234 | }); 235 | 236 | app.get('/deleteitem', function(req, res) { 237 | var itemId = req.query.id; 238 | var access_token = req.session.access_token; 239 | 240 | if (itemId === undefined || access_token === undefined) { 241 | res.redirect('/'); 242 | return; 243 | } 244 | 245 | var deleteEventParameters = { 246 | token: access_token, 247 | eventId: itemId 248 | }; 249 | 250 | outlook.calendar.deleteEvent(deleteEventParameters, function(error, event) { 251 | if (error) { 252 | console.log(error); 253 | res.send(error); 254 | } 255 | else { 256 | res.redirect('/sync'); 257 | } 258 | }); 259 | }); 260 | 261 | // Start the server 262 | var server = app.listen(3000, function() { 263 | var host = server.address().address; 264 | var port = server.address().port; 265 | 266 | console.log('Example app listening at http://%s:%s', host, port); 267 | }); -------------------------------------------------------------------------------- /authHelper.js: -------------------------------------------------------------------------------- 1 | var clientId = 'YOUR APP ID HERE'; 2 | var clientSecret = 'YOUR APP PASSWORD HERE'; 3 | var redirectUri = 'http://localhost:3000/authorize'; 4 | 5 | var scopes = [ 6 | 'openid', 7 | 'profile', 8 | 'offline_access', 9 | 'https://outlook.office.com/calendars.readwrite' 10 | ]; 11 | 12 | var credentials = { 13 | clientID: clientId, 14 | clientSecret: clientSecret, 15 | site: 'https://login.microsoftonline.com/common', 16 | authorizationPath: '/oauth2/v2.0/authorize', 17 | tokenPath: '/oauth2/v2.0/token' 18 | } 19 | var oauth2 = require('simple-oauth2')(credentials) 20 | 21 | module.exports = { 22 | getAuthUrl: function() { 23 | var returnVal = oauth2.authCode.authorizeURL({ 24 | redirect_uri: redirectUri, 25 | scope: scopes.join(' ') 26 | }); 27 | console.log(''); 28 | console.log('Generated auth url: ' + returnVal); 29 | return returnVal; 30 | }, 31 | 32 | getTokenFromCode: function(auth_code, callback, request, response) { 33 | oauth2.authCode.getToken({ 34 | code: auth_code, 35 | redirect_uri: redirectUri, 36 | scope: scopes.join(' ') 37 | }, function (error, result) { 38 | if (error) { 39 | console.log('Access token error: ', error.message); 40 | callback(request ,response, error, null); 41 | } 42 | else { 43 | var token = oauth2.accessToken.create(result); 44 | console.log(''); 45 | console.log('Token created: ', token.token); 46 | callback(request, response, null, token); 47 | } 48 | }); 49 | }, 50 | 51 | getEmailFromIdToken: function(id_token) { 52 | // JWT is in three parts, separated by a '.' 53 | var token_parts = id_token.split('.'); 54 | 55 | // Token content is in the second part, in urlsafe base64 56 | var encoded_token = new Buffer(token_parts[1].replace('-', '+').replace('_', '/'), 'base64'); 57 | 58 | var decoded_token = encoded_token.toString(); 59 | 60 | var jwt = JSON.parse(decoded_token); 61 | 62 | // Email is in the preferred_username field 63 | return jwt.preferred_username 64 | }, 65 | 66 | getTokenFromRefreshToken: function(refresh_token, callback, request, response) { 67 | var token = oauth2.accessToken.create({ refresh_token: refresh_token, expires_in: 0}); 68 | token.refresh(function(error, result) { 69 | if (error) { 70 | console.log('Refresh token error: ', error.message); 71 | callback(request, response, error, null); 72 | } 73 | else { 74 | console.log('New token: ', result.token); 75 | callback(request, response, null, result); 76 | } 77 | }); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-calendar-sync", 3 | "version": "1.0.0", 4 | "description": "Sample Node.js app that connects to Office 365 and syncs calendar data", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node app.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/jasonjoh/node-calendar-sync.git" 13 | }, 14 | "keywords": [ 15 | "office365", 16 | "REST", 17 | "calendar", 18 | "files", 19 | "outlook", 20 | "onedrive", 21 | "node" 22 | ], 23 | "author": "Jason Johnston", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/jasonjoh/node-calendar-sync/issues" 27 | }, 28 | "homepage": "https://github.com/jasonjoh/node-calendar-sync#readme", 29 | "dependencies": { 30 | "body-parser": "^1.14.1", 31 | "cookie-parser": "^1.4.0", 32 | "express": "^4.13.3", 33 | "express-session": "^1.11.3", 34 | "moment": "^2.10.6", 35 | "node-outlook": "^1.1.3", 36 | "simple-oauth2": "^0.2.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pages.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | 3 | var baseHtml = '' + 4 | '
' + 5 | '' + 6 | '' + 7 | '' + JSON.stringify(changes, null, 2) + ''; 145 | return baseHtml.replace('%title%', 'Sync').replace('%body%', html); 146 | }, 147 | 148 | itemDetailPage: function(userEmail, event) { 149 | var html = '
' + JSON.stringify(event, null, 2) + ''; 246 | // end grid 247 | html += '