├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── index.js
├── launch.sh
├── lib
├── main.js
├── media
│ ├── dock.png
│ ├── gmail.png
│ └── screenshot.png
├── menu.js
├── preload.js
└── ui
│ ├── gmail.css
│ └── gmail.js
└── package.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mac OS
2 | .DS_Store
3 |
4 | # Logs
5 | logs
6 | *.log
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 |
13 | # Directory for instrumented libs generated by jscoverage/JSCover
14 | lib-cov
15 |
16 | # Coverage directory used by tools like istanbul
17 | coverage
18 |
19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
20 | .grunt
21 |
22 | # node-waf configuration
23 | .lock-wscript
24 |
25 | # Compiled binary addons (http://nodejs.org/api/addons.html)
26 | build/Release
27 |
28 | # Dependency directory
29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
30 | node_modules
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Paulo Tanaka
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #
Gmail
2 |
3 | > An Unofficial Gmail native web client built with Electron JS.
4 |
5 |
6 | [](https://github.com/paulot/gmail/releases/latest)
7 | [](https://github.com/paulot/gmail/releases/latest)
8 |
9 | ## Install
10 | Check the current list of [releases](https://github.com/paulot/gmail/releases/latest) for prebuilt binaries.
11 |
12 | #### Mac OS
13 | Simply drag the .app file located in the archive to your dock
14 |
15 | #### Windows/Linux
16 | Still working on a binary. There are still a few issues with the menu that need to be sorted out in Linux.
17 |
18 | #### Running from source
19 | - Clone the repo: `git clone https://github.com/paulot/gmail.git`
20 | - Install dependencies: `npm install`
21 | - Run: `npm start`
22 |
23 | ## Development
24 | Built with [Electron JS](http://electron.atom.io).
25 |
26 | #### Commands
27 | - Init: `$ npm install`
28 | - Run: `$ npm start`
29 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('babel-core/register');
2 | require('./lib/main');
3 |
--------------------------------------------------------------------------------
/launch.sh:
--------------------------------------------------------------------------------
1 | ./node_modules/.bin/electron .
2 |
--------------------------------------------------------------------------------
/lib/main.js:
--------------------------------------------------------------------------------
1 | import app from 'app';
2 | import BrowserWindow from 'browser-window';
3 | import path from 'path';
4 | import fs from 'fs';
5 | import os from 'os';
6 | import electron from 'electron';
7 | import Promise from 'bluebird';
8 | import { menu as appMenu } from './menu';
9 |
10 | let mainWindow = null;
11 |
12 | const gmailURL = 'http://www.gmail.com';
13 | const gmailLogoutRe = 'https://mail.google.com/mail/logout';
14 | const gmailAddAccountRe = 'https://accounts.google.com/AddSession';
15 | const oktaRe = 'https://.*.okta.com/';
16 | const gmailDomainRe = 'https://mail.google.com/';
17 | const editInNewTabRe = 'https://mail.google.com/mail/.*#cmid%253D[0-9]+';
18 |
19 | // Set os specific stuff
20 | electron.ipcMain.on('update-dock', function(event, arg) {
21 | if (os.platform() === 'darwin') {
22 | if (arg > 0) {
23 | // Hide dock badge when unread mail count is 0
24 | app.dock.setBadge(arg.toString());
25 | } else {
26 | app.dock.setBadge('');
27 | }
28 | }
29 | });
30 |
31 |
32 | function createWindow() {
33 | if (mainWindow) return mainWindow;
34 | mainWindow = new BrowserWindow({
35 | title: 'Gmail',
36 | icon: path.join(__dirname, 'media', 'gmail.png'),
37 | width: 800,
38 | height: 600,
39 | minWidth: 400,
40 | minHeight: 200,
41 | titleBarStyle: 'hidden',
42 | webPreferences: {
43 | nodeIntegration: false,
44 | preload: path.join(__dirname, 'preload.js'),
45 | webSecurity: false,
46 | plugins: true
47 | }
48 | });
49 |
50 | mainWindow.loadURL(gmailURL);
51 | mainWindow.maximize();
52 | mainWindow.on('close', app.quit);
53 |
54 | return mainWindow;
55 | }
56 |
57 | function gotoURL(url) {
58 | return new Promise((resolve) => {
59 | mainWindow.webContents.on('did-finish-load', resolve);
60 | mainWindow.webContents.loadURL(url);
61 | });
62 | }
63 |
64 |
65 | app.on('ready', () => {
66 | electron.Menu.setApplicationMenu(appMenu);
67 |
68 | createWindow();
69 | let page = mainWindow.webContents;
70 |
71 | page.on('dom-ready', () => {
72 | page.insertCSS(fs.readFileSync(path.join(__dirname, 'ui', 'gmail.css'), 'utf8'));
73 | });
74 |
75 | // Open links in default browser
76 | page.on('new-window', function(e, url) {
77 | if (url.match(gmailLogoutRe)) {
78 | e.preventDefault();
79 | gotoURL(url).then(() => { gotoURL(gmailURL) });
80 | } else if (url.match(editInNewTabRe)) {
81 | e.preventDefault();
82 | page.send('start-compose');
83 | } else if (url.match(gmailDomainRe) ||
84 | url.match(gmailAddAccountRe) ||
85 | url.match(oktaRe)) {
86 | e.preventDefault();
87 | page.loadURL(url);
88 | } else {
89 | e.preventDefault();
90 | require('shell').openExternal(url);
91 | }
92 | });
93 |
94 | // mainWindow.webContents.openDevTools();
95 | });
96 |
--------------------------------------------------------------------------------
/lib/media/dock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulot/gmail/0a1b8d451d8f330060ea10d09b9481e21dd4226a/lib/media/dock.png
--------------------------------------------------------------------------------
/lib/media/gmail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulot/gmail/0a1b8d451d8f330060ea10d09b9481e21dd4226a/lib/media/gmail.png
--------------------------------------------------------------------------------
/lib/media/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paulot/gmail/0a1b8d451d8f330060ea10d09b9481e21dd4226a/lib/media/screenshot.png
--------------------------------------------------------------------------------
/lib/menu.js:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 | import app from 'app';
3 | import os from 'os';
4 |
5 | const appName = app.getName();
6 |
7 | const darwinTpl = [
8 | {
9 | label: appName,
10 | submenu: [
11 | {
12 | label: `About ${appName}`,
13 | role: 'about'
14 | },
15 | { type: 'separator' },
16 | {
17 | label: 'Preferences...',
18 | accelerator: 'Cmd+,',
19 | click(item, focusedWindow) {
20 | if (focusedWindow)
21 | focusedWindow.webContents.send('navigate', 'settings/general');
22 | }
23 | },
24 | {
25 | label: 'Services',
26 | role: 'services',
27 | submenu: []
28 | },
29 | { type: 'separator' },
30 | {
31 | label: 'Log Out',
32 | click(item, focusedWindow) {
33 | if (focusedWindow)
34 | focusedWindow.webContents.send('logout');
35 | }
36 | },
37 | { type: 'separator' },
38 | {
39 | label: `Hide ${appName}`,
40 | accelerator: 'Cmd+H',
41 | role: 'hide'
42 | },
43 | {
44 | label: 'Hide Others',
45 | accelerator: 'Cmd+Shift+H',
46 | role: 'hideothers'
47 | },
48 | {
49 | label: 'Show All',
50 | role: 'unhide'
51 | },
52 | { type: 'separator' },
53 | {
54 | label: `Quit ${appName}`,
55 | accelerator: 'Cmd+Q',
56 | click() {
57 | app.quit();
58 | }
59 | }
60 | ]
61 | },
62 | {
63 | label: 'Mail',
64 | submenu: [
65 | {
66 | label: 'Compose',
67 | accelerator: 'CmdOrCtrl+N',
68 | click(item, focusedWindow) {
69 | if (focusedWindow)
70 | focusedWindow.webContents.send('start-compose');
71 | }
72 | },
73 | { type: 'separator' },
74 | {
75 | label: 'Go To...',
76 | submenu: [
77 | {
78 | label: 'Inbox',
79 | accelerator: 'CmdOrCtrl+I',
80 | click(item, focusedWindow) {
81 | if (focusedWindow)
82 | focusedWindow.webContents.send('navigate', 'inbox');
83 | }
84 | }, {
85 | label: 'Sent',
86 | accelerator: 'Shift+CmdOrCtrl+S',
87 | click(item, focusedWindow) {
88 | if (focusedWindow)
89 | focusedWindow.webContents.send('navigate', 'sent');
90 | }
91 | }, {
92 | label: 'Starred',
93 | accelerator: 'Shift+CmdOrCtrl+R',
94 | click(item, focusedWindow) {
95 | if (focusedWindow)
96 | focusedWindow.webContents.send('navigate', 'starred');
97 | }
98 | }, {
99 | label: 'Drafts',
100 | accelerator: 'Shift+CmdOrCtrl+D',
101 | click(item, focusedWindow) {
102 | if (focusedWindow)
103 | focusedWindow.webContents.send('navigate', 'drafts');
104 | }
105 | }, {
106 | label: 'Important',
107 | accelerator: 'Shift+CmdOrCtrl+I',
108 | click(item, focusedWindow) {
109 | if (focusedWindow)
110 | focusedWindow.webContents.send('navigate', 'imp');
111 | }
112 | }, {
113 | label: 'Chats',
114 | accelerator: 'Shift+CmdOrCtrl+C',
115 | click(item, focusedWindow) {
116 | if (focusedWindow)
117 | focusedWindow.webContents.send('navigate', 'chats');
118 | }
119 | }, {
120 | label: 'All',
121 | accelerator: 'Shift+CmdOrCtrl+A',
122 | click(item, focusedWindow) {
123 | if (focusedWindow)
124 | focusedWindow.webContents.send('navigate', 'all');
125 | }
126 | }, {
127 | label: 'Spam',
128 | accelerator: 'Shift+CmdOrCtrl+P',
129 | click(item, focusedWindow) {
130 | if (focusedWindow)
131 | focusedWindow.webContents.send('navigate', 'spam');
132 | }
133 | }, {
134 | label: 'Trash',
135 | accelerator: 'Shift+CmdOrCtrl+T',
136 | click(item, focusedWindow) {
137 | if (focusedWindow)
138 | focusedWindow.webContents.send('navigate', 'trash');
139 | }
140 | }
141 | ]
142 | }
143 | ]
144 | },{
145 | label: 'Edit',
146 | submenu: [
147 | {
148 | label: 'Undo',
149 | accelerator: 'CmdOrCtrl+Z',
150 | role: 'undo'
151 | },
152 | {
153 | label: 'Redo',
154 | accelerator: 'Shift+CmdOrCtrl+Z',
155 | role: 'redo'
156 | },
157 | { type: 'separator' },
158 | {
159 | label: 'Cut',
160 | accelerator: 'CmdOrCtrl+X',
161 | role: 'cut'
162 | },
163 | {
164 | label: 'Copy',
165 | accelerator: 'CmdOrCtrl+C',
166 | role: 'copy'
167 | },
168 | {
169 | label: 'Paste',
170 | accelerator: 'CmdOrCtrl+V',
171 | role: 'paste'
172 | },
173 | {
174 | label: 'Select All',
175 | accelerator: 'CmdOrCtrl+A',
176 | role: 'selectall'
177 | }
178 | ]
179 | },{
180 | label: 'Window',
181 | role: 'window',
182 | submenu: [
183 | {
184 | label: 'Minimize',
185 | accelerator: 'CmdOrCtrl+M',
186 | role: 'minimize'
187 | },
188 | {
189 | label: 'Close',
190 | accelerator: 'CmdOrCtrl+W',
191 | role: 'close'
192 | },
193 | { type: 'separator' },
194 | {
195 | label: 'Go Back',
196 | accelerator: 'CmdOrCtrl+Backspace',
197 | click(item, focusedWindow) {
198 | if (focusedWindow && focusedWindow.webContents.canGoBack())
199 | focusedWindow.webContents.goBack();
200 | }
201 | }, {
202 | label: 'Go Forward',
203 | accelerator: 'Cmd+Ctrl+F',
204 | click(item, focusedWindow) {
205 | if (focusedWindow && focusedWindow.webContents.canGoForward())
206 | focusedWindow.webContents.goForward();
207 | }
208 | }, {
209 | label: 'Reload',
210 | accelerator: 'CmdOrCtrl+R',
211 | click(item, focusedWindow) {
212 | if (focusedWindow)
213 | focusedWindow.webContents.reload();
214 | }
215 | },
216 | { type: 'separator' },
217 | {
218 | label: 'Bring All to Front',
219 | role: 'front'
220 | },
221 | {
222 | label: 'Toggle Full Screen',
223 | accelerator: 'Ctrl+Cmd+F',
224 | click(item, focusedWindow) {
225 | if (focusedWindow)
226 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
227 | }
228 | }
229 | ]
230 | },{
231 | label: 'Settings',
232 | submenu: [
233 | {
234 | label: 'General',
235 | click(item, focusedWindow) {
236 | if (focusedWindow)
237 | focusedWindow.webContents.send('navigate', 'settings/general');
238 | }
239 | }, {
240 | label: 'Labels',
241 | click(item, focusedWindow) {
242 | if (focusedWindow)
243 | focusedWindow.webContents.send('navigate', 'settings/labels');
244 | }
245 | }, {
246 | label: 'Inbox',
247 | click(item, focusedWindow) {
248 | if (focusedWindow)
249 | focusedWindow.webContents.send('navigate', 'settings/inbox');
250 | }
251 | }, {
252 | label: 'Accounts and Import',
253 | click(item, focusedWindow) {
254 | if (focusedWindow)
255 | focusedWindow.webContents.send('navigate', 'settings/accounts');
256 | }
257 | }, {
258 | label: 'Filters and Blocked Addresses',
259 | click(item, focusedWindow) {
260 | if (focusedWindow)
261 | focusedWindow.webContents.send('navigate', 'settings/filters');
262 | }
263 | }, {
264 | label: 'Forwarding and POP/IMAP',
265 | click(item, focusedWindow) {
266 | if (focusedWindow)
267 | focusedWindow.webContents.send('navigate', 'settings/fwdandpop');
268 | }
269 | }, {
270 | label: 'Chat',
271 | click(item, focusedWindow) {
272 | if (focusedWindow)
273 | focusedWindow.webContents.send('navigate', 'settings/chat');
274 | }
275 | }, {
276 | label: 'Labs',
277 | click(item, focusedWindow) {
278 | if (focusedWindow)
279 | focusedWindow.webContents.send('navigate', 'settings/labs');
280 | }
281 | }, {
282 | label: 'Offline',
283 | click(item, focusedWindow) {
284 | if (focusedWindow)
285 | focusedWindow.webContents.send('navigate', 'settings/offline');
286 | }
287 | }, {
288 | label: 'Themes',
289 | click(item, focusedWindow) {
290 | if (focusedWindow)
291 | focusedWindow.webContents.send('navigate', 'settings/oldthemes');
292 | }
293 | }
294 | ]
295 | }, {
296 | label: 'Help',
297 | role: 'help'
298 | }
299 | ];
300 |
301 | const helpSubmenu = [
302 | {
303 | label: `${appName}'s Project Website...`,
304 | click() {
305 | electron.shell.openExternal('https://github.com/paulot/gmail');
306 | }
307 | },
308 | {
309 | label: 'Report an Issue...',
310 | click() {
311 | const body = `
312 | **Please succinctly describe your issue and steps to reproduce it.**
313 | -
314 | ${app.getName()} ${app.getVersion()}
315 | ${process.platform} ${process.arch} ${os.release()}`;
316 |
317 | electron.shell.openExternal(`https://github.com/paulot/gmail/issues/new?body=${encodeURIComponent(body)}`);
318 | }
319 | }
320 | ];
321 |
322 | darwinTpl[darwinTpl.length - 1].submenu = helpSubmenu;
323 |
324 | export let menu = electron.Menu.buildFromTemplate(darwinTpl);
325 |
--------------------------------------------------------------------------------
/lib/preload.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 | var GmailApi = require('node-gmail');
3 | var jquery = require('jquery');
4 | var page = require('./ui/gmail.js');
5 | var ipc = require('electron').ipcRenderer
6 |
7 | window.j = jquery;
8 | window.page = new page();
9 | window.Gmail = GmailApi(jquery);
10 |
11 | function updateDock() {
12 | ipc.send('update-dock', window.Gmail.get.unread_inbox_emails());
13 | }
14 |
15 | function setDockUpdaters() {
16 | var updateEvents = ['new_email', 'refresh', 'unread', 'read',
17 | 'delete', 'move_to_inbox', 'move_to_label'];
18 | for (var i = 0; i < updateEvents.length; i++) {
19 | window.Gmail.observe.on(updateEvents[i], updateDock);
20 | }
21 | }
22 |
23 | ipc.on('start-compose', window.Gmail.compose.start_compose.bind(window.page));
24 | ipc.on('logout', window.page.logout.bind(window.page));
25 | ipc.on('navigate', (event, place) => {
26 | window.page.navigateTo(place);
27 | });
28 |
29 | updateDock();
30 | setDockUpdaters();
31 | window.page.adjustProfilePicture();
32 | window.page.adjustLogoutButton();
33 | window.page.applyHangoutsCss();
34 | };
35 |
--------------------------------------------------------------------------------
/lib/ui/gmail.css:
--------------------------------------------------------------------------------
1 | /* side menu */
2 | .nH.oy8Mbf.nn.aeN {
3 | /* display: none; */
4 | }
5 |
6 | /* keyboard thingy */
7 | .aBS.J-J5-Ji {
8 | display: none;
9 | }
10 |
11 | /* other apps button */
12 | #gbwa.gb_ca.gb_Rb.gb_R {
13 | /* display: none; */
14 | }
15 |
16 | /* notification thingy */
17 | .gb_Zb.gb_Rb.gb_R.gb_0b {
18 | display: none;
19 | }
20 |
21 | /* google hangouts/phone */
22 | .akc.aZ6 {
23 | /* display: none; */
24 | }
25 |
26 | /* google hangouts/phone slider */
27 | .aeO {
28 | /* display: none; */
29 | }
30 |
31 | /* google hangouts popout */
32 | .o8qlBb.PJ .gGnOIc.x2.qp.DI.QmCEdd {
33 | display: none;
34 | }
35 |
36 | /* google/company logo */
37 | .gb_tb a.gb_qc.gb_vb {
38 | /* display: none; */
39 | margin-top: 10px;
40 | }
41 |
42 | /* window with emails */
43 | .nH.nn {
44 | /* width: 100%; */
45 | }
46 |
47 | /* Signin page stuff */
48 |
49 | .google-footer-bar {
50 | display: none;
51 | }
52 |
53 | .tagline {
54 | display: none;
55 | }
56 |
57 | .main .one-google .logo-strip {
58 | display: none;
59 | }
60 |
61 | .banner h1 {
62 | display: none;
63 | }
64 |
65 | .main.content.clearfix {
66 | padding-bottom: 0px;
67 | }
68 |
--------------------------------------------------------------------------------
/lib/ui/gmail.js:
--------------------------------------------------------------------------------
1 | var jQuery = require('jquery');
2 |
3 | function Gmail() {
4 | this.gmailRootUrl = 'https://mail.google.com/mail/u/[0-9]+/';
5 |
6 | this.sidebar = jQuery('.nH').find('.no').find('.nH.oy8Mbf.nn.aeN');
7 | this.emailPane = jQuery(jQuery('.nH').find('.no').find('.nH.nn')[3]);
8 | this.profileView = jQuery('.gb_Pb.gb_le.gb_R');
9 | this.logoutButton = jQuery('#gb_71.gb_Ba.gb_vd.gb_Cd.gb_9a');
10 |
11 | this.settings = jQuery('.J-M.asi.aYO.jQjAxd').find('div:contains("Settings")');
12 | }
13 |
14 | Gmail.prototype.adjustProfilePicture = function() {
15 | this.profileView.css('min-width', '20px');
16 | };
17 |
18 | Gmail.prototype.adjustLogoutButton = function() {
19 | this.logoutButton.attr('target', '_blank');
20 | };
21 |
22 | Gmail.prototype.logout = function() {
23 | this.logoutButton[0].click();
24 | };
25 |
26 | Gmail.prototype.navigateTo = function(place) {
27 | var root = window.location.href.match(this.gmailRootUrl);
28 | if (root) {
29 | root = root[0];
30 | window.location.href = root + '#' + place;
31 | }
32 | };
33 |
34 | module.exports = Gmail;
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gmail-app",
3 | "productName": "Gmail",
4 | "version": "0.0.1",
5 | "description": "A native gmail client.",
6 | "main": "index.js",
7 | "bin": { "gmail": "./launch.sh" },
8 | "scripts": {
9 | "start": "./launch.sh",
10 | "build": "electron-packager . Gmail --platform=darwin --arch=x64 --version=0.35.1 --icon=lib/media/gmail.png",
11 | "test": "echo \"Error: no test specified\" && exit 1"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/paulot/gmail.git"
16 | },
17 | "keywords": [
18 | "gmail",
19 | "mail",
20 | "app"
21 | ],
22 | "author": "Paulo Tanaka",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/paulot/gmail/issues"
26 | },
27 | "homepage": "https://github.com/paulot/gmail#readme",
28 | "dependencies": {
29 | "bluebird": "^3.0.5",
30 | "jquery": "^2.1.4",
31 | "node-gmail": "^1.0.0"
32 | },
33 | "devDependencies": {
34 | "babel-preset-es2015": "^6.1.18",
35 | "babel-core": "^6.2.1",
36 | "electron-prebuilt": "^0.35.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------