├── .bowerrc ├── .env ├── .gitignore ├── Gruntfile.js ├── README.md ├── app ├── css │ └── main.css ├── error.html ├── img │ ├── favicon.ico │ └── usage.png ├── index.html ├── js │ ├── app.js │ └── env.js ├── oauthcallback.html └── views │ ├── base.html │ ├── home.html │ ├── label.html │ └── thread.html ├── bower.json ├── circle.yml └── package.json /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower" 3 | } 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export GOOGLE_CLIENT_ID="476023672165-qejsfbu3tgrb826lah0lb8mtjqivbckc.apps.googleusercontent.com" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | app/bower 2 | node_modules/ 3 | dist/ 4 | *.swp 5 | .tmp 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | require('load-grunt-tasks')(grunt); 4 | 5 | grunt.initConfig({ 6 | srcDir: 'app', 7 | buildDir: 'dist', 8 | tmpDir: '.tmp', 9 | 10 | clean: [ 11 | '<%= buildDir %>', 12 | '<%= tmpDir %>' 13 | ], 14 | 15 | replace: { 16 | dist: { 17 | options: { 18 | patterns: [ 19 | { 20 | match: 'GOOGLE_CLIENT_ID', 21 | replacement: process.env.GOOGLE_CLIENT_ID 22 | }, 23 | ] 24 | }, 25 | files: [ 26 | { 27 | expand: true, 28 | flatten: true, 29 | src: ['<%= srcDir %>/js/env.js'], 30 | dest: '<%= buildDir %>/js/' 31 | }, 32 | ] 33 | } 34 | }, 35 | 36 | ngtemplates: { 37 | default: { 38 | cwd: '<%= srcDir %>', 39 | src: 'views/*.html', 40 | dest: '<%= buildDir %>/templates.js', 41 | options: { 42 | usemin: '<%= buildDir %>/app.min.js', 43 | module: 'scriptermail' 44 | } 45 | } 46 | }, 47 | 48 | wiredep: { 49 | default: { 50 | src: ['<%= buildDir %>/index.html'] 51 | } 52 | }, 53 | 54 | useminPrepare: { 55 | html: '<%= buildDir %>/index.html', 56 | options: { 57 | root: '<%= srcDir %>', 58 | dest: '<%= buildDir %>' 59 | } 60 | }, 61 | 62 | usemin: { 63 | html: '<%= buildDir %>/index.html', 64 | }, 65 | 66 | uglify: { 67 | generated: { 68 | options: { 69 | sourceMap: true 70 | } 71 | } 72 | }, 73 | 74 | cssmin: { 75 | generated: { 76 | options: { 77 | processImport: false 78 | } 79 | } 80 | }, 81 | 82 | // Copies remaining files to places other tasks can use 83 | copy: { 84 | dist: { 85 | files: [{ 86 | expand: true, 87 | dot: true, 88 | cwd: '<%= srcDir %>', 89 | dest: '<%= buildDir %>', 90 | src: [ 91 | '*.{ico,png,txt}', 92 | '*.html', 93 | 'views/*.html', 94 | 'img/*', 95 | 'js/**/*.js', 96 | 'css/**/*.css' 97 | ] 98 | }, { 99 | expand: true, 100 | dot: true, 101 | cwd: '<%= srcDir %>', 102 | dest: '<%= buildDir %>/', 103 | src: [ 104 | 'bower/**', 105 | ] 106 | }, { 107 | expand: true, 108 | dot: true, 109 | flatten: true, 110 | cwd: '<%= srcDir %>/bower', 111 | dest: '<%= buildDir %>/fonts/', 112 | src: [ 113 | '**/{,*/}*.{woff,woff2,eot,svg,ttf,otf}' 114 | ] 115 | }] 116 | }, 117 | styles: { 118 | expand: true, 119 | cwd: '<%= srcDir %>/css', 120 | dest: '.tmp/css/', 121 | src: '{,*/}*.css' 122 | }, 123 | fonts: { 124 | expand: true, 125 | dot: true, 126 | flatten: true, 127 | dest: '.tmp/fonts/', 128 | src: [ 129 | '**/{,*/}*.{woff,woff2,eot,svg,ttf,otf}' 130 | ] 131 | } 132 | }, 133 | 134 | connect: { 135 | dev: { 136 | options: { 137 | port: process.env.PORT || 3474, 138 | base: '<%= srcDir %>', 139 | keepalive: true, 140 | middleware: function(connect, options, middlewares) { 141 | // http://stackoverflow.com/a/21514926 142 | var modRewrite = require('connect-modrewrite'); 143 | middlewares.unshift(modRewrite(['!\\.html|\\.js|\\.woff|\\.woff2|\\.ttf|\\.svg|\\.css|\\.png$ /index.html [L]'])); 144 | return middlewares; 145 | } 146 | } 147 | }, 148 | server: { 149 | options: { 150 | port: process.env.PORT || 3474, 151 | base: '<%= buildDir %>', 152 | //keepalive: true, 153 | middleware: function(connect, options, middlewares) { 154 | // http://stackoverflow.com/a/21514926 155 | var modRewrite = require('connect-modrewrite'); 156 | middlewares.unshift(modRewrite(['!\\.html|\\.js|\\.woff|\\.woff2|\\.ttf|\\.svg|\\.css|\\.png$ /index.html [L]'])); 157 | return middlewares; 158 | } 159 | } 160 | } 161 | }, 162 | 163 | watch: { 164 | gruntfile: { 165 | files: ['Gruntfile.js'], 166 | tasks: ['build'] 167 | }, 168 | index: { 169 | files: ['<%= srcDir %>/**'], 170 | tasks: ['build'] 171 | }, 172 | options: { 173 | livereload: true 174 | } 175 | } 176 | }); 177 | 178 | grunt.registerTask('build-usemin', [ 179 | 'useminPrepare', 180 | 'ngtemplates', 181 | 'concat:generated', 182 | 'cssmin:generated', 183 | 'uglify:generated', 184 | 'usemin' 185 | ]); 186 | 187 | grunt.registerTask('build', [ 188 | 'clean', 189 | 'copy:dist', 190 | 'replace', 191 | 'wiredep', 192 | 'build-usemin' 193 | ]); 194 | grunt.registerTask('serve', [ 195 | 'build', 196 | 'connect:server', 197 | 'watch' 198 | ]); 199 | grunt.registerTask('default', ['build']); 200 | }; 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Make Your Own Gmail.com 2 | 3 | A customizable, independent interface to your Gmail account. 4 | 5 | 6 | 7 | This is an editable scaffold for building your own interface for interacting with your Gmail email account. Familiarity with [JavaScript](http://www.tutorialspoint.com/javascript/) and [AngularJS](http://www.tutorialspoint.com/angularjs/) is ideal. 8 | 9 | ### Development 10 | 11 | The only software prerequisite is `npm` ([install instructions](https://www.youtube.com/watch?v=wREima9e6vk)). 12 | 13 | Then the following steps will get your local development server running: 14 | 15 | ``` 16 | $ npm install 17 | $ bower install 18 | $ grunt serve 19 | 20 | Server started on port 3474 21 | ``` 22 | 23 | You're good to go, try accessing http://localhost:3474/ 24 | 25 | --- 26 | 27 | Please replace the GOOGLE_CLIENT_ID in `.env` by obtaining your own 28 | [Google client ID](http://www.sanwebe.com/2012/10/creating-google-oauth-api-key). 29 | 30 | Install [autoenv](https://github.com/kennethreitz/autoenv) to automatically 31 | create this environment variable when using this repo. 32 | 33 | ### Resources 34 | 35 | - https://developers.google.com/gmail/api/quickstart/js 36 | - https://github.com/maximepvrt/angular-google-gapi/ 37 | - https://developers.google.com/api-client-library/javascript/start/start-js 38 | -------------------------------------------------------------------------------- /app/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-bottom: 20px; 3 | color: black; 4 | } 5 | .banner-img { 6 | border: 1px solid #E5E5E5; 7 | margin-top: 30px; 8 | width: 100%; 9 | } 10 | .banner-txt { 11 | margin-top: 30px; 12 | } 13 | .promo-div { 14 | padding: 10px; 15 | margin: 0 2px; 16 | background-color: #f5f5f5; 17 | border: 1px solid #eeeeee; 18 | border-radius: 8px; 19 | } 20 | a { 21 | color: black; 22 | } 23 | a:hover { 24 | font-weight: bold; 25 | } 26 | 27 | .hero a { 28 | color: blue; 29 | text-decoration: underline; 30 | } 31 | 32 | .container { 33 | padding: 0 20px !important; 34 | margin: 0 20px !important; 35 | } 36 | .nav > li > a { 37 | padding: 0 15px; 38 | } 39 | .label { 40 | margin-right: 3px; 41 | } 42 | .threads { 43 | font-size: 1em; 44 | border-bottom: 1px solid #E5E5E5; 45 | } 46 | .thread { 47 | border-top: 1px solid #E5E5E5; 48 | background-color: #F4F4F4; 49 | padding: 2px 5px; 50 | } 51 | .col-thread { 52 | -ms-text-overflow: ellipsis; 53 | -o-text-overflow: ellipsis; 54 | text-overflow: ellipsis; 55 | display: block; 56 | overflow: hidden; 57 | white-space: nowrap; 58 | } 59 | .preview-text { 60 | color: gray; 61 | } 62 | .bold { 63 | font-weight: bold; 64 | } 65 | .btn-auth { 66 | margin: 10px 0; 67 | } 68 | -------------------------------------------------------------------------------- /app/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found 6 | 7 | 8 | 83 | 84 | 85 |
86 |
87 |

404 Not Found

88 |

Sorry, we were unable to locate anything at this address.

89 |

Should there be something here?

90 |

Get Help via Email or Common Deploy Issues Documentation

91 | 92 | 93 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /app/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmautner/make-your-own-gmail/453279e039126d20db13e8dd93a6ca29967c6a5a/app/img/favicon.ico -------------------------------------------------------------------------------- /app/img/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmautner/make-your-own-gmail/453279e039126d20db13e8dd93a6ca29967c6a5a/app/img/usage.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Make Your Own Gmail 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('scriptermail', [ 2 | 'ui.router', 3 | 'angular-google-gapi', 4 | 'angularMoment', 5 | 'angular-growl', 6 | 'ngAnimate', 7 | 'ngCkeditor' 8 | ]); 9 | 10 | app.filter('num', function() { 11 | return function(input) { 12 | return parseInt(input, 10); 13 | }; 14 | }); 15 | app.filter('html', ['$sce', function ($sce) { 16 | return function (text) { 17 | return $sce.trustAsHtml(text); 18 | }; 19 | }]); 20 | app.filter('titlecase', function() { 21 | return function(str) { 22 | return (str == undefined || str === null) ? '' : str.replace(/_|-/, ' ').replace(/\w\S*/g, function(txt){ 23 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 24 | }); 25 | } 26 | }); 27 | app.filter('joinParticipants', function() { 28 | return function(participants) { 29 | //return participants.join(', '); 30 | return participants[participants.length-1]; 31 | } 32 | }); 33 | 34 | app.service('favico', [function() { 35 | var favico = new Favico({ 36 | animation : 'fade' 37 | }); 38 | 39 | var badge = function(num) { 40 | favico.badge(num); 41 | }; 42 | var reset = function() { 43 | favico.reset(); 44 | }; 45 | 46 | return { 47 | badge : badge, 48 | reset : reset 49 | }; 50 | }]); 51 | app.service('email', ['$rootScope', function($rootScope) { 52 | 53 | // http://stackoverflow.com/a/28622096/468653 54 | var b64encode = function (msg) { 55 | return btoa(msg).replace(/\//g, '_').replace(/\+/g, '-') 56 | }; 57 | var b64decode = function(msg) { 58 | return atob(msg.replace(/-/g, '+').replace(/_/g, '/')) 59 | }; 60 | 61 | var convertHeadersToObject = function(headers) { 62 | var d = {}; 63 | for (var i = 0; i < headers.length; i++) { 64 | d[headers[i].name] = headers[i].value; 65 | }; 66 | return d; 67 | }; 68 | 69 | var getSubject = function(headers) { 70 | var headers = convertHeadersToObject(headers); 71 | if ('Subject' in headers) { 72 | return headers['Subject']; 73 | } 74 | }; 75 | 76 | var getParticipants = function(msgs) { 77 | var participants = []; 78 | for (var i = 0; i < msgs.length; i++) { 79 | var headers = convertHeadersToObject(msgs[i].payload.headers); 80 | var interestingHeaders = ['From', 'To', 'Cc', 'Bcc']; 81 | var myEmail = $rootScope.gapi.user.email; 82 | for (var j = 0; j < interestingHeaders.length; j++) { 83 | if (headers[interestingHeaders[j]]) { 84 | var parsedAddr = emailAddresses.parseOneAddress(headers[interestingHeaders[j]]); 85 | if (parsedAddr) { 86 | if (parsedAddr.name) { 87 | participants.push(parsedAddr.name); 88 | } 89 | else if (parsedAddr.address) { 90 | participants.push(parsedAddr.address); 91 | } 92 | } 93 | }; 94 | }; 95 | } 96 | return participants; 97 | }; 98 | 99 | var getUnread = function(thread) { 100 | var unreadMsgs = []; 101 | for (var i = 0; i < thread.messages.length; i++) { 102 | if (thread.messages[i].labelIds.indexOf('UNREAD') > -1) { 103 | unreadMsgs.push(thread.messages[i]); 104 | } 105 | }; 106 | return unreadMsgs; 107 | }; 108 | 109 | var getDatetimes = function(thread) { 110 | var datetimes = []; 111 | for (var i = 0; i < thread.messages.length; i++) { 112 | var headers = convertHeadersToObject(thread.messages[i].payload.headers); 113 | if ('Date' in headers) { 114 | datetimes.push(new Date(headers['Date'])); 115 | } 116 | } 117 | return datetimes 118 | }; 119 | 120 | var rfc2822 = function(fromaddr, toaddrs, ccaddrs, bccaddrs, subject, body) { 121 | /* 122 | * fromaddr: {'name': 'xxx', 'email': 'xxx'} 123 | * toaddrs/ccaddrs/bccaddrs: [{'name': 'xxx', 'email': 'xxx'}, ...] 124 | * subject: 'normal string' (limited to 76 chars) 125 | * body: 'html string' 126 | */ 127 | var payload = "From: " + fromaddr.email + 128 | '\r\n' + "To: " + toaddrs[0].email + 129 | '\r\n' + "Subject: " + subject + 130 | '\r\n\r\n' + body; 131 | return b64encode(payload); 132 | }; 133 | 134 | return { 135 | b64encode: b64encode, 136 | b64decode: b64decode, 137 | getSubject: getSubject, 138 | getParticipants: getParticipants, 139 | getUnread: getUnread, 140 | getDatetimes: getDatetimes, 141 | rfc2822: rfc2822 142 | } 143 | }]); 144 | 145 | app.controller('MainCtrl', ['$scope', 'GAuth', '$state', 'growl', 146 | function($scope, GAuth, $state, growl) { 147 | 148 | $scope.doSignup = function() { 149 | GAuth.login().then(function(){ 150 | $state.go('label', {id: "inbox"}); 151 | }, function() { 152 | growl.error('Login failed'); 153 | }); 154 | }; 155 | 156 | }]); 157 | 158 | app.service('FetchLabels', ['$window', 'GApi', 'InterestingLabels', 159 | function($window, GApi, InterestingLabels) { 160 | return function() { 161 | 162 | var batch = $window.gapi.client.newBatch() 163 | for (var i = 0; i < InterestingLabels.length; i++) { 164 | var req = GApi.createRequest( 165 | 'gmail', 166 | 'users.labels.get', 167 | { 168 | 'userId': 'me', 169 | 'id': InterestingLabels[i].name, 170 | 'fields': 'id,name,threadsTotal,threadsUnread,type' 171 | } 172 | ); 173 | batch.add(req, {id: InterestingLabels[i].name}); 174 | } 175 | return batch.then(function(data) { 176 | return data.result; 177 | }); 178 | }; 179 | } 180 | ]); 181 | 182 | app.service('FetchMessages', ['$window', 'GApi', 'email', 'InterestingLabels', 'favico', 183 | function($window, GApi, email, InterestingLabels, favico) { 184 | // *NEEDS* caching 185 | return function(label) { 186 | var query = 'in:' + label; 187 | return GApi.executeAuth('gmail', 'users.threads.list', { 188 | 'userId': 'me', 189 | 'q': query 190 | }) 191 | .then(function(threadResult) { 192 | if (threadResult.threads) { 193 | var batch = $window.gapi.client.newBatch() 194 | for (var i = 0; i < threadResult.threads.length; i++) { 195 | var req = GApi.createRequest( 196 | 'gmail', 197 | 'users.threads.get', { 198 | 'userId': 'me', 199 | 'id': threadResult.threads[i].id 200 | } 201 | ); 202 | batch.add(req, {id: threadResult.threads[i].id}); 203 | } 204 | 205 | return batch 206 | .then(function(msgResult) { 207 | // post-processing of threads 208 | var unreadThreads = 0; 209 | for (var i = 0; i < threadResult.threads.length; i++) { 210 | var threadId = threadResult.threads[i].id; 211 | var thread = msgResult.result[threadId].result; 212 | var participants = email.getParticipants(thread.messages); 213 | var subject = email.getSubject(thread.messages[thread.messages.length-1].payload.headers); 214 | var unreadMsgs = email.getUnread(thread); 215 | var datetimes = email.getDatetimes(thread); 216 | 217 | threadResult.threads[i].messages = thread.messages; 218 | threadResult.threads[i].participants = participants; 219 | threadResult.threads[i].subject = subject; 220 | threadResult.threads[i].unreadMsgs = unreadMsgs; 221 | threadResult.threads[i].datetimes = datetimes; 222 | 223 | if (unreadMsgs.length > 0) { unreadThreads++; } 224 | } 225 | // update favicon count 226 | favico.badge(unreadThreads); 227 | 228 | return threadResult.threads; 229 | }); 230 | } else { 231 | favico.badge(0); 232 | return []; 233 | } 234 | }); 235 | 236 | }; 237 | } 238 | ]); 239 | 240 | 241 | app.controller('BaseCtrl', [ 242 | '$scope', 243 | 'InterestingLabels', 244 | 'labels', 245 | function($scope, InterestingLabels, labels) { 246 | 247 | $scope.labels = labels; 248 | $scope.interestingLabels = InterestingLabels; 249 | 250 | } 251 | ]); 252 | 253 | app.controller('LabelCtrl', [ 254 | '$scope', 255 | '$stateParams', 256 | 'threads', 257 | function($scope, $stateParams, threads) { 258 | 259 | $scope.title = $stateParams.id.toUpperCase(); 260 | $scope.threads = threads; 261 | 262 | }]); 263 | 264 | app.controller('ThreadCtrl', ['GApi', '$scope', '$rootScope', '$stateParams', 'growl', 'email', 265 | function(GApi, $scope, $rootScope, $stateParams, growl, email) { 266 | 267 | GApi.executeAuth('gmail', 'users.threads.get', { 268 | 'userId': 'me', 269 | 'id': $stateParams.threadId.toUpperCase(), 270 | 'format': 'full' // more expensive? 271 | }).then(function(data) { 272 | // requires parsing of messages 273 | $scope.messages = data.messages; 274 | }, function(error) { 275 | growl.error(error); 276 | }); 277 | 278 | $scope.save = function() { 279 | console.log('saving'); 280 | }; 281 | 282 | $scope.send = function() { 283 | base64encodedEmail = email.rfc2822( 284 | {'email': $rootScope.gapi.user.email}, 285 | [{'email': 'max.mautner@gmail.com'}], 286 | [], 287 | [], 288 | 'Hi', 289 | $scope.emailBody); 290 | 291 | GApi.executeAuth('gmail', 'users.messages.send', { 292 | 'userId': 'me', 293 | 'resource': { 294 | 'raw': base64encodedEmail 295 | } 296 | }).then(function(data) { 297 | console.log(data); 298 | growl.success('Email sent!'); 299 | }, function(error) { 300 | growl.error(error); 301 | }); 302 | }; 303 | 304 | $scope.editorOptions = { 305 | height: 200 306 | }; 307 | 308 | }]); 309 | 310 | 311 | app.constant('GoogleClientId', SPICY_CONFIG.GOOGLE_CLIENT_ID); 312 | app.constant('GoogleScopes', [ 313 | 'https://mail.google.com/', 314 | 'https://www.googleapis.com/auth/userinfo.email' 315 | ]); 316 | app.constant('InterestingLabels', [ 317 | {name: 'INBOX', icon: 'envelope'}, 318 | //{name: 'IMPORTANT', icon: ''}, 319 | {name: 'STARRED', icon: 'star'}, 320 | //{name: 'UNREAD', icon: ''}, 321 | {name: 'DRAFT', icon: 'pencil'}, 322 | {name: 'SENT', icon: 'send'}, 323 | {name: 'TRASH', icon: 'trash'}, 324 | {name: 'CHAT', icon: 'comment'}, 325 | {name: 'SPAM', icon: 'ban'} 326 | ]); 327 | 328 | app.config(['growlProvider', function(growlProvider) { 329 | growlProvider.globalTimeToLive(2000); 330 | }]); 331 | 332 | app.config(['$stateProvider', '$urlRouterProvider', '$locationProvider', 333 | function($stateProvider, $urlRouterProvider, $locationProvider) { 334 | 335 | $locationProvider.html5Mode(true); 336 | $urlRouterProvider.otherwise("/"); 337 | 338 | $stateProvider 339 | .state('home', { 340 | url: '/', 341 | controller: 'MainCtrl', 342 | templateUrl: 'views/home.html', 343 | resolve: { 344 | isLoggedIn: ['$state', 'GAuth', function($state, GAuth) { 345 | return GAuth.checkAuth().then(function () { 346 | $state.go('label', {id: "inbox"}); 347 | }, function() { 348 | return; 349 | }); 350 | }] 351 | } 352 | }) 353 | .state('secureRoot', { 354 | templateUrl: 'views/base.html', 355 | controller: 'BaseCtrl', 356 | resolve: { 357 | isLoggedIn: ['$state', 'GAuth', function($state, GAuth) { 358 | return GAuth.checkAuth() 359 | .then(function () { 360 | return; 361 | }, function() { 362 | $state.go('home'); 363 | }); 364 | }], 365 | labels: ['isLoggedIn', 'FetchLabels', function(isLoggedIn, FetchLabels) { 366 | return FetchLabels(); 367 | }] 368 | } 369 | }) 370 | .state('label', { 371 | parent: 'secureRoot', 372 | url: '/label/:id', 373 | controller: 'LabelCtrl', 374 | templateUrl: 'views/label.html', 375 | resolve: { 376 | threads: ['FetchMessages', '$stateParams', 'isLoggedIn', 377 | function(FetchMessages, $stateParams) { 378 | return FetchMessages($stateParams.id.toUpperCase()); 379 | } 380 | ] 381 | } 382 | }) 383 | .state('thread', { 384 | parent: 'secureRoot', 385 | url: '/thread/:threadId', 386 | controller: 'ThreadCtrl', 387 | templateUrl: 'views/thread.html' 388 | }); 389 | }]); 390 | 391 | app.run([ 392 | 'GApi', 393 | 'GAuth', 394 | 'GoogleClientId', 395 | 'GoogleScopes', 396 | function(GApi, GAuth, GoogleClientId, GoogleScopes) { 397 | GApi.load('gmail', 'v1'); 398 | GAuth.setClient(GoogleClientId); 399 | GAuth.setScope(GoogleScopes.join(' ')); 400 | } 401 | ]); 402 | 403 | app.run([ 404 | '$rootScope', 405 | function($rootScope) { 406 | 407 | $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) { 408 | //console.log(event, toState, toParams, fromState, fromParams); 409 | $rootScope.stateLoaded = false; 410 | }); 411 | $rootScope.$on('$stateChangeSuccess', function(event, toState, toParams, fromState, fromParams) { 412 | //console.log(event, toState, toParams, fromState, fromParams); 413 | $rootScope.stateLoaded = true; 414 | }); 415 | $rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) { 416 | console.log(event, toState, toParams, fromState, fromParams, error); 417 | $rootScope.stateLoaded = true; 418 | }); 419 | }]); 420 | 421 | -------------------------------------------------------------------------------- /app/js/env.js: -------------------------------------------------------------------------------- 1 | 2 | var SPICY_CONFIG = { 3 | "GOOGLE_CLIENT_ID": "@@GOOGLE_CLIENT_ID" 4 | }; 5 | -------------------------------------------------------------------------------- /app/oauthcallback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/views/base.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Labels

4 | 17 |
18 |
19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /app/views/home.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 |

Make-Your-Own-Gmail

7 |

To view your own Gmail account:

8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |

About

17 |

This was built as a prototype to demo how to build your own version of 18 | Gmail.com, using the Google JavaScript SDK.

19 |

The source code 20 | is available under an MIT license on Github.

21 |

The author is me, Max Mautner! I'm 22 | a huge fan of email, and you can feel free to email me ;)

23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /app/views/label.html: -------------------------------------------------------------------------------- 1 |

{{::title | titlecase}}

2 |
3 |
4 | 5 |
6 | {{ ithread.participants | joinParticipants }} 7 | ({{ithread.messages.length}}) 8 |
9 |
10 | {{ithread.subject}} 11 | 12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /app/views/thread.html: -------------------------------------------------------------------------------- 1 |

Hey

2 |

{{messages[0].snippet}}

3 | 14 | 25 |
26 | 27 |
28 |
29 | 30 | 31 |
32 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "make-your-own-gmail", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Max Mautner " 6 | ], 7 | "license": "MIT", 8 | "ignore": [ 9 | "**/.*", 10 | "node_modules", 11 | "bower_components", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": { 16 | "angular": "~1.4.6", 17 | "angular-google-gapi": "git@github.com:mmautner/angular-google-gapi.git#master", 18 | "bootstrap": "~3.3.5", 19 | "angular-ui-router": "~0.2.15", 20 | "moment": "~2.10.6", 21 | "angular-moment": "~0.10.3", 22 | "async": "~1.4.2", 23 | "angular-growl-v2": "~0.7.5", 24 | "angular-animate": "~1.4.6", 25 | "ng-ckeditor": "~0.2.1", 26 | "font-awesome": "~4.4.0", 27 | "angular-cache": "~4.3.2", 28 | "email-addresses": "~2.0.1", 29 | "favico.js": "~0.3.9" 30 | }, 31 | "resolutions": { 32 | "angular": "~1.4.6" 33 | }, 34 | "overrides": { 35 | "bootstrap": { 36 | "main": [ 37 | "dist/css/bootstrap.css", 38 | "dist/js/bootstrap.js" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 0.12.0 4 | 5 | dependencies: 6 | pre: 7 | - npm install 8 | - bower install -f 9 | 10 | test: 11 | override: 12 | - > 13 | echo 'No tests.' 14 | 15 | deployment: 16 | production: 17 | branch: master 18 | commands: 19 | - grunt build 20 | - npm install -g surge 21 | - surge --project ./dist/ --domain "gmail.maxmautner.com" 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "make-your-own-gmail", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "grunt": "^0.4.5", 13 | "grunt-angular-templates": "^0.5.7", 14 | "grunt-cli": "^0.1.13", 15 | "grunt-contrib-clean": "^0.6.0", 16 | "grunt-contrib-concat": "^0.5.1", 17 | "grunt-contrib-copy": "^0.8.1", 18 | "grunt-contrib-cssmin": "^0.14.0", 19 | "grunt-contrib-uglify": "^0.9.2", 20 | "grunt-filerev": "^2.3.1", 21 | "grunt-usemin": "^3.1.1", 22 | "grunt-wiredep": "^2.0.0", 23 | "load-grunt-tasks": "^3.3.0", 24 | "bower": "^1.6.7", 25 | "connect-modrewrite": "^0.8.2", 26 | "grunt-contrib-connect": "^0.11.2", 27 | "grunt-contrib-watch": "^0.6.1", 28 | "grunt-replace": "^0.11.0" 29 | } 30 | } 31 | --------------------------------------------------------------------------------