├── .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 |
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 |
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 |
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 |
19 |
--------------------------------------------------------------------------------
/app/views/thread.html:
--------------------------------------------------------------------------------
1 | Hey
2 | {{messages[0].snippet}}
3 |
4 | -
5 |
6 |
9 | {{label}}
10 |
11 |
12 |
13 |
14 |
25 |
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 |
--------------------------------------------------------------------------------