├── .editorconfig
├── .eslintrc.js
├── .github
├── CODE_OF_CONDUCT.md
└── CONTRIBUTING.md
├── .gitignore
├── .travis.yml
├── .vscode
└── launch.json
├── LICENSE
├── README.md
├── app
├── main
│ ├── analytics.js
│ ├── config.js
│ ├── index.js
│ ├── menu.js
│ ├── preferences.js
│ ├── report.js
│ ├── tray.js
│ └── utils.js
├── renderer
│ ├── browser.css
│ ├── browser.js
│ ├── preferences
│ │ ├── index.css
│ │ ├── index.html
│ │ └── index.js
│ ├── unreads.js
│ └── utils.js
└── static
│ ├── Icon.png
│ ├── Icon@2x.png
│ ├── IconSnoozed.png
│ ├── IconTray.png
│ ├── IconTray@2x.png
│ ├── IconTrayUnread.png
│ ├── IconTrayUnread@2x.png
│ └── gmail_48px.png
├── build
├── background.png
├── background@2x.png
├── icon.icns
└── icon.ico
├── media
├── inboxer-linux-desktop.png
├── inboxer-linux.png
├── inboxer-mac-desktop.png
├── inboxer-mac.png
├── inboxer-windows-desktop.png
├── inboxer-windows.png
└── inboxer.sketch
├── package-lock.json
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": "airbnb-base",
3 | "env": {
4 | "browser": true,
5 | "node": true,
6 | },
7 | "rules": {
8 | "import/no-extraneous-dependencies": [2, { "devDependencies": true }]
9 | }
10 | };
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at denysdovhan@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | By participating in this project, you agree to abide by the [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md)
4 |
5 | ## Getting Started
6 |
7 | 1. Fork this repo by clicking `Fork` button.
8 | 1. Clone the repo: `git clone git@github.com:your-username/inboxer.git`.
9 | 1. Install the project dependencies: `cd inboxer && npm i`.
10 | 1. Start the app: `npm start`.
11 | 1. Add and test your changes, make sure it is working as expected.
12 | 1. Commit your changes (better to commit to branch other that `master`)
13 | 1. After confirming the correctness of your change, push it to your fork and [submit a pull request](https://github.com/denysdovhan/inboxer/compare/)
14 | 1. Wait or mention someone to review your pull request.
15 |
16 | ### Additional Resources
17 |
18 | * [How to write a good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
19 | * [How to write the perfect pull request](https://github.com/blog/1943-how-to-write-the-perfect-pull-request)
20 | * [General GitHub documentation](https://help.github.com/)
21 | * [GitHub pull request documentation](https://help.github.com/articles/creating-a-pull-request/)
22 | * [Github requesting a pull request review documentation](https://help.github.com/articles/requesting-a-pull-request-review/)
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | yarn.lock
5 | npm-debug.log
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | os: osx
2 | osx_image: xcode10.1
3 | language: node_js
4 | node_js:
5 | - "11.0"
6 | cache:
7 | directories:
8 | - node_modules
9 | script:
10 | - npm test
11 | - npm run dist
12 | branches:
13 | except:
14 | - "/^v\\d+\\.\\d+\\.\\d+$/"
15 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Main Process",
6 | "type": "node",
7 | "request": "launch",
8 | "cwd": "${workspaceRoot}",
9 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
10 | "windows": {
11 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
12 | },
13 | "args" : ["app/main"]
14 | },
15 | {
16 | "name": "Renderer Process",
17 | "type": "chrome",
18 | "request": "launch",
19 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
20 | "windows": {
21 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd"
22 | },
23 | "runtimeArgs": [
24 | "${workspaceRoot}/app/main/index.js",
25 | "--remote-debugging-port=9222"
26 | ],
27 | "webRoot": "${workspaceRoot}/app/renderer"
28 | }
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Denys Dovhan
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > ## 🚧 This project is deprecated! 🚧
2 | >
3 | > As you know Google is going to take down Inbox service.
4 | >
5 | > **This project is not maintained anymore. Since Inbox is closed this project has to be closed as well. It just cannot operate normally without Inbox.**
6 | >
7 | > You can find more information [here](https://github.com/denysdovhan/inboxer/issues/101).
8 |
9 |
10 |
11 |
Inboxer
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | > Unofficial, free and open-source Google Gmail Desktop App
23 |
24 | Inboxer — started out as an unofficial Inbox by Gmail client for desktop platforms. However, Google plans to discontinue Inbox at the end of March 2019 ([see official announcement](https://www.blog.google/products/gmail/inbox-signing-find-your-favorite-features-new-gmail/)). To keep Inboxer alive we have converted it to work with Gmail. Inboxer is now built on top of Gmail web-version. It has pleasant UI and supports useful keyboard shortcuts.
25 |
26 | Inboxer is highly inspired by these projects:
27 |
28 | * [Caprine](https://github.com/sindresorhus/caprine) by [Sindre Sorhus](https://github.com/sindresorhus)
29 | * [Ramme](https://github.com/terkelg/ramme) by [Terkel Gjervig Nielsen](https://github.com/terkelg)
30 | * [Keep](https://github.com/andrepolischuk/keep) by [Andrey Polischuk](https://github.com/andrepolischuk)
31 |
32 | Check out these ones as great examples of Electron applications. All of them are under the MIT license.
33 |
34 | ## Features
35 |
36 | * Familiar Gmail interface
37 | * Cross-platform (macOS/Linux/Windows)
38 | * Useful Keyboard shortcuts
39 | * Multiple accounts
40 | * Optional Always on Top
41 | * Auto-updates to the latest version
42 | * Desktop notifications
43 | * **TODO:** Dark theme
44 | * **TODO:** Custom text size
45 | * **TODO:** Ability to use as default mail client
46 |
47 | All feature requests and contributions are welcome!
48 |
49 | ## Screenshot
50 |
51 | * **TODO:** Update screenshot to show latest version working with Gmail
52 | 
53 |
54 | ## Installation
55 |
56 | Inboxer works well on **macOS 10.9+**, **Linux** and **Windows 7+**. You can download the latest version on [Releases](https://github.com/denysdovhan/inboxer/releases) page or below.
57 | Inboxer is also available through the nix package manager (see [package in nix](https://nixos.org/nixos/packages.html#inboxer))
58 |
59 | ### macOS
60 |
61 | [**Download**][download] the `.dmg` file or install via [Homebrew-Cask](https://caskroom.github.io/):
62 |
63 | ```
64 | $ brew cask install inboxer
65 | ```
66 |
67 | ### Linux
68 |
69 | [**Download**][download] the `.AppImage`* or `.deb` or `.snap` file.
70 |
71 | \* — Notice, that the `AppImage` needs to be [made executable](http://discourse.appimage.org/t/how-to-make-an-appimage-executable/80) after download.
72 |
73 | ### Windows
74 |
75 | [**Download**][download] the `.exe` file.
76 |
77 | ### Package Inboxer from source
78 |
79 | You'll need `electron` and `npm` installed in your system.
80 | [**Download**][download] and extract the source code (`.zip` or `.tar.gz` file).
81 | Inside the source code folder, run:
82 | ```
83 | $ npm install
84 | ```
85 | Then package the app with:
86 | ```
87 | $ npm run dist
88 | ```
89 | This builds packages for all architectures. Use `dist:mac`, `dist:linux`, or `dist:win` to package a single architecture. The packages will be found in the `dist` folder.
90 |
91 | ## Keyboard shortcuts
92 |
93 | Inboxer supports all Gmail keyboard shortcuts, system-specific keybindings and more. Additional keybindings are listed below:
94 |
95 | | Description | Keys
96 | |------------------------------|---------
97 | | Preferences | Cmd/Ctrl P
98 | | Compose Message | Cmd/Ctrl N
99 | | Go to Inbox | Cmd/Ctrl I
100 | | Go to Snoozed | Cmd/Ctrl S
101 | | Go to Done | Cmd/Ctrl D
102 | | Drafts | Cmd/Ctrl Shift D
103 | | Sent | Cmd/Ctrl Shift S
104 | | Trash | Alt Shift T
105 | | Spam | Alt Shift S
106 | | Open Contacts | Cmd/Ctrl Shift C
107 | | Search | Cmd/Ctrl F
108 | | Toggle Sidebar | Cmd/Ctrl /
109 | | Toggle "Always on Top" | Cmd/Ctrl Shift T
110 | | Keyboard Shortcuts Reference | Shift / or ?
111 | | Toggle Developer Tools | Option Cmd I _(macOS)_ or Ctrl Shift I
112 |
113 | ## Disclaimer
114 |
115 | This code is in no way affiliated with, authorised, maintained, sponsored or endorsed by Google or any of its affiliates or subsidiaries. This is an independent and unofficial Gmail app. Use it at your own risk.
116 |
117 | ## End User License Agreement
118 |
119 | * You **will not** use this repository for sending mass spam or any other malicious activity.
120 | * We / You **will not** support anyone who is violating this EULA conditions.
121 | * Repository is just for learning / personal purposes thus **should not** be part of any service available on the Internet that is trying to do any malicious activity (mass bulk request, spam etc).
122 |
123 | ## Donate
124 |
125 | Hi! I work on this project in my spare time, beside my primary job. I hope enjoy using Inboxer, and if you do, please, [support this project 🙏🏻][donate-card-url].
126 |
127 | | Credit/Debit card | Bitcoin | Ethereum |
128 | |:-----------------:|:-------:|:--------:|
129 | | [Donate with LiqPay][donate-card-url] | `1FrPrQb6ACTkbSBAz9PduJWrDFfq41Ggb4` | `0x6aF39C917359897ae6969Ad682C14110afe1a0a1` |
130 | | | | |
131 |
132 | I would appreciate your support! _Thank you!_
133 |
134 | [donate-readme]: https://github.com/denysdovhan/inboxer#donate
135 | [donate-card-url]: https://www.liqpay.com/en/checkout/380951100392
136 | [donate-card-image]: https://img.shields.io/badge/donate-LiqPay-blue.svg?style=flat-square
137 | [donate-btc-image]: https://img.shields.io/badge/donate-BTC-yellow.svg?style=flat-square
138 | [donate-eth-image]: https://img.shields.io/badge/donate-ETH-gray.svg?style=flat-square
139 |
140 | ## License
141 |
142 | MIT © [Denys Dovhan](http://denysdovhan.com)
143 |
144 |
145 |
146 | [download]: https://github.com/denysdovhan/inboxer/releases/latest
147 |
--------------------------------------------------------------------------------
/app/main/analytics.js:
--------------------------------------------------------------------------------
1 | const firstRun = require('first-run');
2 | const Insight = require('insight');
3 |
4 | const pkg = require('../../package');
5 |
6 | const trackingCode = 'UA-40496885-12';
7 |
8 | const insight = new Insight({
9 | trackingCode,
10 | pkg,
11 | });
12 |
13 | module.exports = {
14 | init() {
15 | if (firstRun()) {
16 | insight.track('install');
17 | }
18 | insight.track('start');
19 | },
20 | track(...paths) {
21 | insight.track(...paths);
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/app/main/config.js:
--------------------------------------------------------------------------------
1 | const ElectronStore = require('electron-store');
2 |
3 | module.exports = new ElectronStore({
4 | defaults: {
5 | windowState: {
6 | width: 900,
7 | height: 600,
8 | },
9 | alwaysOnTop: false,
10 | showUnreadBadge: true,
11 | bounceDockIcon: false,
12 | flashWindowOnMessage: false,
13 | autoHideMenuBar: false,
14 | notify: {
15 | unread: true,
16 | snoozed: true,
17 | download: true,
18 | period: 2,
19 | },
20 | sendAnalytics: true,
21 | displayMigrationInfo: true,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/app/main/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const {
4 | app, BrowserWindow, Menu, shell, ipcMain, nativeImage, Notification, dialog,
5 | } = require('electron');
6 | const log = require('electron-log');
7 | const isDev = require('electron-is-dev');
8 | const { autoUpdater } = require('electron-updater');
9 | const minimatch = require('minimatch-all');
10 | const electronDL = require('electron-dl');
11 | const { isDarwin, isLinux, isWindows } = require('./utils');
12 | const config = require('./config');
13 | const appMenu = require('./menu');
14 | const appTray = require('./tray');
15 | const analytics = require('./analytics');
16 |
17 | app.setAppUserModelId('com.denysdovhan.inboxer');
18 |
19 | require('electron-context-menu')();
20 |
21 | const mainURL = 'https://mail.google.com/';
22 |
23 | let mainWindow;
24 | let isQuitting = false;
25 | let prevUnreadCount = 0;
26 |
27 | const gotTheLock = app.requestSingleInstanceLock();
28 | if (!gotTheLock) {
29 | app.quit();
30 | process.exit();
31 | }
32 |
33 | function allowedUrl(url) {
34 | const urls = [
35 | 'https://accounts.google.com/@(u|AccountChooser|AddSession|ServiceLogin|CheckCookie|Logout){**/**,**}',
36 | 'https://accounts.google.com/signin/@(usernamerecovery|recovery|challenge|selectchallenge){**/**,**}',
37 | 'http://www.google.*/accounts/Logout2**',
38 | 'https://www.google.com/a/**/acs',
39 | 'https://**.okta.com/**',
40 | 'https://google.*/accounts/**',
41 | 'https://www.google.**/accounts/signin/continue**',
42 | 'https://mail.google.com/**',
43 | 'https://drive.google.com/**',
44 | 'https://docs.google.com/**',
45 | 'https://www.google.com/calendar**',
46 | 'https://sites.google.com/**',
47 | 'https://chat.google.com/**',
48 | 'https://contacts.google.com/**',
49 | path.join('file://', __dirname, '../renderer/preferences**'),
50 | ];
51 |
52 | return minimatch(url, urls);
53 | }
54 |
55 | // Inform the user about Google's plan to discontinue Inbox
56 | function showMigrationDialog(win) {
57 | if (config.get('displayMigrationInfo') === 'no >1.3.0') { // indicates dialog was dismissed from version >1.3.0
58 | return;
59 | }
60 |
61 | const message = 'This version of Inboxer has been migrated to use Gmail';
62 | const detail = `Inboxer was originally developed to provide a view of Google's Inbox packaged \
63 | in a desktop app. However, Google has announced plans to discontinue Inbox at the end of March 2019.
64 | See Google's official announcement here:
65 | https://www.blog.google/products/gmail/inbox-signing-find-your-favorite-features-new-gmail/
66 |
67 | Versions >= 1.3.0 have been migrated from Inbox to Gmail to ensure Inboxer continues \
68 | to work after Google pulls the plug on Inbox.
69 | Versions 1.2.x will continue working with Inbox until the bitter end.`;
70 |
71 | dialog.showMessageBox(win, {
72 | type: 'info',
73 | icon: nativeImage.createFromPath(path.join(__dirname, '..', 'static/Icon.png')),
74 | title: 'Important Message',
75 | message,
76 | detail,
77 | checkboxLabel: 'Show this window again',
78 | checkboxChecked: true,
79 | buttons: ['Ok'],
80 | defaultId: 0,
81 | }, (response, checkBoxChecked) => {
82 | if (!checkBoxChecked) {
83 | config.set('displayMigrationInfo', 'no >1.3.0');
84 | }
85 | });
86 | }
87 |
88 | function createMainWindow() {
89 | const windowState = config.get('windowState');
90 |
91 | const win = new BrowserWindow({
92 | show: false, // Hide application until your page has loaded
93 | title: app.getName(),
94 | x: windowState.x,
95 | y: windowState.y,
96 | width: windowState.width,
97 | height: windowState.height,
98 | minWidth: 890,
99 | minHeight: 400,
100 | alwaysOnTop: config.get('alwaysOnTop'),
101 | autoHideMenuBar: config.get('autoHideMenuBar'),
102 | backgroundColor: '#f2f2f2',
103 | icon: path.join(__dirname, '..', 'static/Icon.png'),
104 | titleBarStyle: 'hidden-inset',
105 | webPreferences: {
106 | preload: path.join(__dirname, '..', 'renderer', 'browser.js'),
107 | nodeIntegration: false,
108 | },
109 | });
110 |
111 | if (isDarwin) {
112 | win.setSheetOffset(40);
113 | }
114 |
115 | win.loadURL(mainURL);
116 |
117 | // Show window after loading the DOM
118 | // Docs: https://electronjs.org/docs/api/browser-window#showing-window-gracefully
119 | win.once('ready-to-show', () => {
120 | win.show();
121 | showMigrationDialog(win);
122 | });
123 |
124 | win.on('close', (e) => {
125 | if (!isQuitting) {
126 | e.preventDefault();
127 |
128 | if (isDarwin) {
129 | app.hide();
130 | } else {
131 | win.hide();
132 | }
133 | }
134 | });
135 |
136 | return win;
137 | }
138 |
139 | app.on('ready', () => {
140 | Menu.setApplicationMenu(appMenu);
141 | mainWindow = createMainWindow();
142 | appTray.create(mainWindow);
143 |
144 | if (config.get('sendAnalytics')) analytics.init();
145 |
146 | if (!isDev && !isLinux) {
147 | autoUpdater.logger = log;
148 | autoUpdater.logger.transports.file.level = 'info';
149 | autoUpdater.checkForUpdatesAndNotify();
150 | }
151 |
152 | const { webContents } = mainWindow;
153 |
154 | webContents.on('dom-ready', () => {
155 | webContents.insertCSS(fs.readFileSync(path.join(__dirname, '../renderer/browser.css'), 'utf8'));
156 | });
157 |
158 | webContents.on('will-navigate', (e, url) => {
159 | if (config.get('sendAnalytics')) analytics.track('will-navigate');
160 | if (!allowedUrl(url)) {
161 | e.preventDefault();
162 | shell.openExternal(url);
163 | }
164 | });
165 |
166 | webContents.on('new-window', (e, url) => {
167 | if (config.get('sendAnalytics')) analytics.track('new-window');
168 | e.preventDefault();
169 | if (allowedUrl(url)) {
170 | webContents.loadURL(url);
171 | return;
172 | }
173 | shell.openExternal(url);
174 | });
175 | });
176 |
177 | app.on('second-instance', () => {
178 | if (mainWindow) {
179 | if (mainWindow.isMinimized()) {
180 | mainWindow.restore();
181 | }
182 | if (!mainWindow.isVisible()) {
183 | mainWindow.show();
184 | }
185 | mainWindow.focus();
186 | }
187 | });
188 |
189 | app.on('activate', () => {
190 | mainWindow.show();
191 | });
192 |
193 | app.on('before-quit', () => {
194 | if (config.get('sendAnalytics')) analytics.track('quit');
195 | isQuitting = true;
196 |
197 | if (!mainWindow.isFullScreen()) {
198 | config.set('windowState', mainWindow.getBounds());
199 | }
200 | });
201 |
202 | ipcMain.on('update-unreads-count', (e, unreadCount) => {
203 | if (isDarwin || isLinux) {
204 | let isUpdated = config.get('showUnreadBadge') ? app.setBadgeCount(unreadCount) : false;
205 | if (!config.get('showUnreadBadge')) {
206 | app.setBadgeCount(0);
207 | isUpdated = false;
208 | }
209 | if (isDarwin && config.get('bounceDockIcon') && prevUnreadCount !== unreadCount && isUpdated) {
210 | app.dock.bounce('informational');
211 | prevUnreadCount = unreadCount;
212 | }
213 | }
214 |
215 | if ((isLinux || isWindows) && config.get('showUnreadBadge')) {
216 | appTray.setBadge(unreadCount);
217 | } else if ((isLinux || isWindows)) {
218 | appTray.setBadge(false);
219 | }
220 |
221 | if (isWindows) {
222 | if (config.get('showUnreadBadge')) {
223 | if (unreadCount === 0) {
224 | mainWindow.setOverlayIcon(null, '');
225 | } else {
226 | // Delegate drawing of overlay icon to renderer process
227 | mainWindow.webContents.send('render-overlay-icon', unreadCount);
228 | }
229 | } else {
230 | mainWindow.setOverlayIcon(null, '');
231 | }
232 |
233 | if (config.get('flashWindowOnMessage')) {
234 | mainWindow.flashFrame(unreadCount !== 0);
235 | }
236 | }
237 | });
238 |
239 | ipcMain.on('update-overlay-icon', (e, image, count) => {
240 | mainWindow.setOverlayIcon(nativeImage.createFromDataURL(image), count);
241 | });
242 |
243 | ipcMain.on('show-window', () => {
244 | mainWindow.show();
245 | });
246 |
247 | function downloadStarted(downloadItem) {
248 | if (!config.get('notify.download')) {
249 | return;
250 | }
251 | downloadItem.on('done', (event, state) => { // notify user on download complete
252 | if (state === 'completed') {
253 | const filename = downloadItem.getSavePath();
254 | const notification = new Notification({
255 | title: 'Download Complete',
256 | body: filename,
257 | });
258 | notification.on('click', () => {
259 | shell.showItemInFolder(filename);
260 | });
261 | notification.show();
262 | }
263 | });
264 | }
265 | electronDL({ onStarted: downloadStarted });
266 |
--------------------------------------------------------------------------------
/app/main/menu.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const {
3 | app, shell, dialog, Menu,
4 | } = require('electron');
5 | const pkg = require('../../package');
6 | const {
7 | isDarwin, isWindows, sendAction, sendKeybinding,
8 | } = require('./utils');
9 | const config = require('./config');
10 | const report = require('./report');
11 | const preferences = require('./preferences');
12 |
13 | const settingsItems = [
14 | {
15 | label: 'Show Unread Badge',
16 | type: 'checkbox',
17 | checked: config.get('showUnreadBadge'),
18 | click(menuItem) {
19 | config.set('showUnreadBadge', menuItem.checked);
20 | },
21 | },
22 | {
23 | label: 'Bounce Dock on Notification',
24 | type: 'checkbox',
25 | checked: config.get('bounceDockIcon'),
26 | visible: isDarwin,
27 | click(menuItem) {
28 | config.set('bounceDockIcon', menuItem.checked);
29 | },
30 | },
31 | {
32 | label: 'Flash Window on Message',
33 | type: 'checkbox',
34 | checked: config.get('flashWindowOnMessage'),
35 | visible: isWindows,
36 | click(menuItem) {
37 | config.set('flashWindowOnMessage', menuItem.checked);
38 | },
39 | },
40 | {
41 | label: 'Auto Hide Menu Bar',
42 | type: 'checkbox',
43 | checked: config.get('autoHideMenuBar'),
44 | visible: !isDarwin,
45 | click(menuItem, focusedWindow) {
46 | config.set('autoHideMenuBar', menuItem.checked);
47 | focusedWindow.setAutoHideMenuBar(menuItem.checked);
48 | focusedWindow.setMenuBarVisibility(!menuItem.checked);
49 | },
50 | },
51 |
52 | // NOTE: Account Preferences instead of Preferences (for Inboxer Preferences)
53 |
54 | {
55 | label: 'Account Preferences',
56 | accelerator: 'CmdOrCtrl+,',
57 | click(menuItem, focusedWindow) {
58 | sendAction(focusedWindow, 'show-preferences');
59 | },
60 | },
61 |
62 | // TODO: Create Preferences window
63 |
64 | {
65 | label: 'Preferences',
66 | accelerator: 'CmdOrCtrl+P',
67 | click() {
68 | preferences.showPreferencesWindow();
69 | },
70 | },
71 | ];
72 |
73 | // TODO: Switch accounts
74 | const accountItems = [
75 | {
76 | label: 'Add Account',
77 | click(menuItem, focusedWindow) {
78 | sendAction(focusedWindow, 'add-account');
79 | },
80 | },
81 | {
82 | label: 'Sign Out',
83 | click(menuItem, focusedWindow) {
84 | sendAction(focusedWindow, 'sign-out');
85 | },
86 | },
87 | ];
88 |
89 | const fileItems = [
90 | {
91 | label: 'Compose Message',
92 | accelerator: 'CmdOrCtrl+N',
93 | click(menuItem, focusedWindow) {
94 | sendKeybinding(focusedWindow, 'c');
95 | },
96 | },
97 | ];
98 |
99 | const viewItems = [
100 | {
101 | label: 'Go to Inbox',
102 | accelerator: 'CmdOrCtrl+I',
103 | click(menuItem, focusedWindow) {
104 | sendAction(focusedWindow, 'go-to-inbox');
105 | },
106 | },
107 | {
108 | label: 'Go to Snoozed',
109 | accelerator: 'CmdOrCtrl+S',
110 | click(menuItem, focusedWindow) {
111 | sendAction(focusedWindow, 'go-to-snoozed');
112 | },
113 | },
114 | {
115 | label: 'Go to Done',
116 | accelerator: 'CmdOrCtrl+D',
117 | click(menuItem, focusedWindow) {
118 | sendAction(focusedWindow, 'go-to-done');
119 | },
120 | },
121 | { type: 'separator' },
122 | {
123 | label: 'Drafts',
124 | accelerator: 'CmdOrCtrl+Shift+D',
125 | click(menuItem, focusedWindow) {
126 | sendAction(focusedWindow, 'go-to-drafts');
127 | },
128 | },
129 | {
130 | label: 'Sent',
131 | accelerator: 'CmdOrCtrl+Shift+S',
132 | click(menuItem, focusedWindow) {
133 | sendAction(focusedWindow, 'go-to-sent');
134 | },
135 | },
136 | {
137 | label: 'Trash',
138 | accelerator: 'Alt+Shift+T',
139 | click(menuItem, focusedWindow) {
140 | sendAction(focusedWindow, 'go-to-trash');
141 | },
142 | },
143 | {
144 | label: 'Spam',
145 | accelerator: 'Alt+Shift+S',
146 | click(menuItem, focusedWindow) {
147 | sendAction(focusedWindow, 'go-to-spam');
148 | },
149 | },
150 | {
151 | label: 'Contacts',
152 | accelerator: 'CmdOrCtrl+Shift+C',
153 | click(menuItem, focusedWindow) {
154 | sendAction(focusedWindow, 'go-to-contacts');
155 | },
156 | },
157 | { type: 'separator' },
158 | {
159 | label: 'Search…',
160 | accelerator: 'CmdOrCtrl+F',
161 | click(menuItem, focusedWindow) {
162 | sendAction(focusedWindow, 'go-to-search');
163 | },
164 | },
165 | { type: 'separator' },
166 | {
167 | label: 'Toggle Sidebar',
168 | accelerator: 'CmdOrCtrl+/',
169 | click(menuItem, focusedWindow) {
170 | sendAction(focusedWindow, 'toggle-sidebar');
171 | },
172 | },
173 | ];
174 |
175 | const editItems = [
176 | { role: 'undo' },
177 | { role: 'redo' },
178 | { type: 'separator' },
179 | { role: 'cut' },
180 | { role: 'copy' },
181 | { role: 'paste' },
182 | { role: 'delete' },
183 | { type: 'separator' },
184 | { role: 'selectall' },
185 | { type: 'separator' },
186 | ];
187 |
188 | const listItems = [
189 | {
190 | label: 'Open',
191 | click(menuItem, focusedWindow) {
192 | sendKeybinding(focusedWindow, 'o');
193 | },
194 | },
195 | {
196 | label: 'Close',
197 | click(menuItem, focusedWindow) {
198 | sendKeybinding(focusedWindow, 'u');
199 | },
200 | },
201 | { type: 'separator' },
202 | {
203 | label: 'Next Item',
204 | click(menuItem, focusedWindow) {
205 | sendKeybinding(focusedWindow, 'j');
206 | },
207 | },
208 | {
209 | label: 'Previous Item',
210 | click(menuItem, focusedWindow) {
211 | sendKeybinding(focusedWindow, 'k');
212 | },
213 | },
214 | {
215 | label: 'First Item',
216 | accelerator: 'Home',
217 | click(menuItem, focusedWindow) {
218 | sendKeybinding(focusedWindow, 'Home');
219 | },
220 | },
221 | { type: 'separator' },
222 | {
223 | label: 'Next Message',
224 | click(menuItem, focusedWindow) {
225 | sendKeybinding(focusedWindow, 'n');
226 | },
227 | },
228 | {
229 | label: 'Previous Message',
230 | click(menuItem, focusedWindow) {
231 | sendKeybinding(focusedWindow, 'p');
232 | },
233 | },
234 | ];
235 |
236 | const itemItems = [
237 | {
238 | label: 'Mark Done',
239 | click(menuItem, focusedWindow) {
240 | sendKeybinding(focusedWindow, 'e');
241 | },
242 | },
243 | {
244 | label: 'Mark Done and Forward',
245 | click(menuItem, focusedWindow) {
246 | sendKeybinding(focusedWindow, '[');
247 | },
248 | },
249 | {
250 | label: 'Mark Done and Backward',
251 | click(menuItem, focusedWindow) {
252 | sendKeybinding(focusedWindow, ']');
253 | },
254 | },
255 | { type: 'separator' },
256 | {
257 | label: 'Snooze',
258 | click(menuItem, focusedWindow) {
259 | sendKeybinding(focusedWindow, 'b');
260 | },
261 | },
262 | {
263 | label: 'Star',
264 | click(menuItem, focusedWindow) {
265 | sendKeybinding(focusedWindow, 's');
266 | },
267 | },
268 | // {
269 | // label: 'Pin',
270 | // click(menuItem, focusedWindow) {
271 | // sendKeybinding(focusedWindow, 'Shift+p');
272 | // },
273 | // },
274 | { type: 'separator' },
275 | {
276 | label: 'Reply',
277 | click(menuItem, focusedWindow) {
278 | sendKeybinding(focusedWindow, 'r');
279 | },
280 | },
281 | // {
282 | // label: 'Reply in a new window',
283 | // click(menuItem, focusedWindow) {
284 | // sendKeybinding(focusedWindow, 'Shift+r');
285 | // },
286 | // },
287 | {
288 | label: 'Reply All',
289 | click(menuItem, focusedWindow) {
290 | sendKeybinding(focusedWindow, 'a');
291 | },
292 | },
293 | // {
294 | // label: 'Reply all in a new window',
295 | // click(menuItem, focusedWindow) {
296 | // sendKeybinding(focusedWindow, 'Shift+a');
297 | // },
298 | // },
299 | {
300 | label: 'Forward',
301 | click(menuItem, focusedWindow) {
302 | sendKeybinding(focusedWindow, 'f');
303 | },
304 | },
305 | { type: 'separator' },
306 | {
307 | label: 'Trash',
308 | click(menuItem, focusedWindow) {
309 | sendKeybinding(focusedWindow, '#');
310 | },
311 | },
312 | {
313 | label: 'Report as Spam',
314 | click(menuItem, focusedWindow) {
315 | sendKeybinding(focusedWindow, '!');
316 | },
317 | },
318 | {
319 | label: 'Mute',
320 | click(menuItem, focusedWindow) {
321 | sendKeybinding(focusedWindow, 'm');
322 | },
323 | },
324 | {
325 | label: 'Move to…',
326 | click(menuItem, focusedWindow) {
327 | sendKeybinding(focusedWindow, 'v');
328 | },
329 | },
330 | {
331 | label: 'Select…',
332 | click(menuItem, focusedWindow) {
333 | sendKeybinding(focusedWindow, 'x');
334 | },
335 | },
336 | ];
337 |
338 | const windowItems = [
339 | {
340 | type: 'checkbox',
341 | label: 'Always on Top',
342 | accelerator: 'CmdOrCtrl+Shift+T',
343 | checked: config.get('alwaysOnTop'),
344 | click(menuItem, focusedWindow) {
345 | config.set('alwaysOnTop', menuItem.checked);
346 | focusedWindow.setAlwaysOnTop(menuItem.checked);
347 | },
348 | },
349 | ];
350 |
351 | const helpItems = [
352 | {
353 | label: 'Keyboard Shortcuts Reference',
354 | accelerator: ['Shift+/', '?'],
355 | click(menuItem, focusedWindow) {
356 | sendKeybinding(focusedWindow, '?');
357 | },
358 | },
359 | { type: 'separator' },
360 | {
361 | label: `${app.getName()} Website`,
362 | click() {
363 | shell.openExternal(pkg.homepage);
364 | },
365 | },
366 | {
367 | label: 'Source Code',
368 | click() {
369 | shell.openExternal('https://github.com/denysdovhan/inboxer');
370 | },
371 | },
372 | {
373 | label: 'Report an Issue…',
374 | click() {
375 | shell.openExternal(`${pkg.bugs.url}/new?body=${encodeURIComponent(report)}`);
376 | },
377 | },
378 | { type: 'separator' },
379 | {
380 | label: 'Toggle Developer Tools',
381 | type: 'checkbox',
382 | accelerator: isDarwin ? 'Option+Cmd+I' : 'Ctrl+Shift+I',
383 | click(item, focusedWindow) {
384 | focusedWindow.toggleDevTools();
385 | },
386 | },
387 | ];
388 |
389 | const darwinTemplate = [
390 | {
391 | label: app.getName(),
392 | submenu: [
393 | { role: 'about' },
394 | { type: 'separator' },
395 | ...settingsItems,
396 | { type: 'separator' },
397 | {
398 | role: 'services',
399 | submenu: [],
400 | },
401 | { type: 'separator' },
402 | ...accountItems,
403 | { type: 'separator' },
404 | { role: 'hide' },
405 | { role: 'hideothers' },
406 | { role: 'unhide' },
407 | { type: 'separator' },
408 | { role: 'quit' },
409 | ],
410 | },
411 | {
412 | label: 'File',
413 | submenu: fileItems,
414 | },
415 | {
416 | label: 'View',
417 | submenu: viewItems,
418 | },
419 | {
420 | label: 'Edit',
421 | submenu: editItems,
422 | },
423 | {
424 | label: 'List',
425 | submenu: listItems,
426 | },
427 | {
428 | label: 'Item',
429 | submenu: itemItems,
430 | },
431 | {
432 | role: 'window',
433 | submenu: [
434 | { role: 'minimize' },
435 | { role: 'close' },
436 | { type: 'separator' },
437 | { role: 'front' },
438 | { role: 'togglefullscreen' },
439 | { type: 'separator' },
440 | ...windowItems,
441 | ],
442 | },
443 | {
444 | role: 'help',
445 | submenu: helpItems,
446 | },
447 | ];
448 |
449 | const otherTemplate = [
450 | {
451 | label: 'File',
452 | submenu: [
453 | ...fileItems,
454 | { type: 'separator' },
455 | ...accountItems,
456 | { type: 'separator' },
457 | { role: 'quit' },
458 | ],
459 | },
460 | {
461 | label: 'View',
462 | submenu: [
463 | ...viewItems,
464 | ...windowItems,
465 | ],
466 | },
467 | {
468 | label: 'Edit',
469 | submenu: [...editItems, ...settingsItems],
470 | },
471 | {
472 | label: 'List',
473 | submenu: listItems,
474 | },
475 | {
476 | label: 'Item',
477 | submenu: itemItems,
478 | },
479 | {
480 | role: 'help',
481 | submenu: [
482 | ...helpItems,
483 | { type: 'separator' },
484 | {
485 | role: 'about',
486 | click() {
487 | dialog.showMessageBox({
488 | title: `About ${app.getName()}`,
489 | message: `${app.getName()} ${app.getVersion()}`,
490 | detail: `Created by ${pkg.author.name}`,
491 | icon: path.join(__dirname, '..', 'static/Icon.png'),
492 | });
493 | },
494 | },
495 | ],
496 | },
497 | ];
498 |
499 | module.exports = Menu.buildFromTemplate(isDarwin ? darwinTemplate : otherTemplate);
500 |
--------------------------------------------------------------------------------
/app/main/preferences.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { BrowserWindow } = require('electron');
3 |
4 | let preferencesWindow = null;
5 |
6 | function createPreferencesWindow() {
7 | const preferencesPath = path.join('file://', __dirname, '../renderer/preferences/index.html');
8 | preferencesWindow = new BrowserWindow({
9 | width: 600,
10 | height: 400,
11 | icon: path.join(__dirname, '..', 'static/Icon.png'),
12 | title: 'Preferences',
13 | });
14 |
15 | preferencesWindow.on('close', () => {
16 | preferencesWindow = null;
17 | });
18 |
19 | preferencesWindow.setResizable(false);
20 | preferencesWindow.setSkipTaskbar(true);
21 | preferencesWindow.setMenu(null);
22 | preferencesWindow.loadURL(preferencesPath);
23 | }
24 |
25 | module.exports = {
26 | showPreferencesWindow() {
27 | if (!preferencesWindow) createPreferencesWindow();
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/app/main/report.js:
--------------------------------------------------------------------------------
1 | const os = require('os');
2 | const { app } = require('electron');
3 |
4 | module.exports = `
5 |
6 | ---
7 | ${app.getName()} ${app.getVersion()}
8 | Electron ${process.versions.electron}
9 | ${process.platform} ${process.arch} ${os.release()}`;
10 |
--------------------------------------------------------------------------------
/app/main/tray.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { app, Tray, Menu } = require('electron');
3 | const { isDarwin, sendAction } = require('./utils');
4 |
5 | const iconTrayFile = 'IconTray.png';
6 | const iconTrayUnreadFile = 'IconTrayUnread.png';
7 |
8 | let tray = null;
9 |
10 | const contextMenu = focusedWindow => [
11 | {
12 | label: 'Open/Close',
13 | click() {
14 | return focusedWindow.isVisible() ? focusedWindow.hide() : focusedWindow.show();
15 | },
16 | },
17 | { type: 'separator' },
18 | {
19 | label: 'Go to Inbox',
20 | click() {
21 | sendAction(focusedWindow, 'go-to-inbox');
22 | },
23 | },
24 | {
25 | label: 'Go to Snoozed',
26 | click() {
27 | sendAction(focusedWindow, 'go-to-snoozed');
28 | },
29 | },
30 | {
31 | label: 'Go to Done',
32 | click() {
33 | sendAction(focusedWindow, 'go-to-done');
34 | },
35 | },
36 | { type: 'separator' },
37 | {
38 | label: 'Sign Out',
39 | click() {
40 | sendAction(focusedWindow, 'sign-out');
41 | },
42 | },
43 | { type: 'separator' },
44 | { role: 'quit' },
45 | ];
46 |
47 | function create(win) {
48 | if (isDarwin || tray) return;
49 |
50 | const iconPath = path.join(__dirname, '..', `static/${iconTrayFile}`);
51 |
52 | tray = new Tray(iconPath);
53 | tray.setToolTip(app.getName());
54 | tray.setContextMenu(Menu.buildFromTemplate(contextMenu(win)));
55 |
56 | tray.on('click', () => (win.isVisible() ? win.hide() : win.show()));
57 | }
58 |
59 | function setBadge(shouldDisplayUnread) {
60 | if (isDarwin || !tray) return;
61 |
62 | const iconPath = path.join(__dirname, '..', `static/${shouldDisplayUnread ? iconTrayUnreadFile : iconTrayFile}`);
63 | tray.setImage(iconPath);
64 | }
65 |
66 | module.exports = {
67 | create,
68 | setBadge,
69 | };
70 |
--------------------------------------------------------------------------------
/app/main/utils.js:
--------------------------------------------------------------------------------
1 | const analytics = require('./analytics');
2 | const config = require('./config');
3 |
4 | const isDarwin = process.platform === 'darwin';
5 | const isLinux = process.platform === 'linux';
6 | const isWindows = process.platform === 'win32';
7 |
8 | function sendAction(win, action) {
9 | if (config.get('sendAnalytics')) analytics.track(action);
10 | if (isDarwin) win.restore();
11 | win.webContents.send(action);
12 | }
13 |
14 | // @FIXME: Shift keybindings do not work.
15 | // https://stackoverflow.com/q/47378160/5508862
16 | function sendKeybinding(win, keyCode) {
17 | win.webContents.sendInputEvent({ type: 'keyDown', keyCode });
18 | win.webContents.sendInputEvent({ type: 'char', keyCode });
19 | win.webContents.sendInputEvent({ type: 'keyUp', keyCode });
20 | }
21 |
22 | module.exports = {
23 | isDarwin,
24 | isLinux,
25 | isWindows,
26 | sendAction,
27 | sendKeybinding,
28 | };
29 |
--------------------------------------------------------------------------------
/app/renderer/browser.css:
--------------------------------------------------------------------------------
1 | /* Tooltip to display link contents */
2 | /* contains message view area (right side of window) */
3 | div.AO a[href]::after {
4 | content: attr(href);
5 | position: absolute;
6 | transform: translateY(100%);
7 | left: 0;
8 | background: rgba(0,0,0,0.7);
9 | text-align: center;
10 | color: #fff;
11 | font-size: small;
12 | border-radius: 5px;
13 | pointer-events: none;
14 | padding: 4px 8px;
15 | z-index:99;
16 | opacity:0;
17 | transition: opacity 0.5s;
18 | }
19 |
20 | div.AO a[href]:hover::after {
21 | opacity:1
22 | }
23 |
--------------------------------------------------------------------------------
/app/renderer/browser.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer: ipc } = require('electron');
2 | const checkUnreads = require('./unreads');
3 | const { $, renderOverlayIcon } = require('./utils');
4 |
5 | const settingsURL = 'https://mail.google.com/mail/u/0/#settings/general';
6 | const doneURL = 'https://mail.google.com/mail/u/0/#search/-in%3Ainbox+-in%3Aspam+-in%3Atrash+-in%3Achats+-in%3Asnoozed+-in%3Adrafts+-in%3Asent';
7 | const contactsURL = 'https://contacts.google.com/';
8 | const addAccountURL = 'https://accounts.google.com/AddSession';
9 |
10 | ipc.on('toggle-sidebar', () => $('div.gb_td div[aria-label="Main menu"]').click());
11 |
12 | function selectFolder(name) {
13 | const selector = `div.TK div.aim div.TO[data-tooltip="${name}"]`;
14 | const folder = $(selector);
15 | if (folder) {
16 | folder.click();
17 | } else {
18 | // if folder was not found, try loading correct URL
19 | const urlName = name.split(' ')[0].toLowerCase();
20 | const url = `https://mail.google.com/mail/u/0/#${urlName}`;
21 | window.location.assign(url);
22 | }
23 | }
24 |
25 | function loadURL(url) {
26 | window.location.assign(url);
27 | }
28 |
29 | ipc.on('show-preferences', () => loadURL(settingsURL));
30 |
31 | // primary folder shortcuts
32 |
33 | ipc.on('go-to-inbox', () => selectFolder('Inbox'));
34 | ipc.on('go-to-snoozed', () => selectFolder('Snoozed'));
35 | ipc.on('go-to-done', () => loadURL(doneURL));
36 |
37 | // secondary folder shortcuts
38 |
39 | ipc.on('go-to-drafts', () => selectFolder('Drafts'));
40 | ipc.on('go-to-sent', () => selectFolder('Sent'));
41 | ipc.on('go-to-trash', () => selectFolder('Trash'));
42 | ipc.on('go-to-spam', () => selectFolder('Spam'));
43 | ipc.on('go-to-contacts', () => loadURL(contactsURL));
44 |
45 | ipc.on('go-to-search', () => {
46 | const searchBar = $('div.gb_td input[aria-label="Search mail"]');
47 | if (searchBar) {
48 | searchBar.focus();
49 | }
50 | });
51 |
52 | ipc.on('sign-out', () => $('#gb_71').click());
53 | ipc.on('add-account', () => loadURL(addAccountURL));
54 |
55 | ipc.on('render-overlay-icon', (event, unreadsCount) => {
56 | ipc.send(
57 | 'update-overlay-icon',
58 | renderOverlayIcon(unreadsCount).toDataURL(),
59 | unreadsCount.toString(),
60 | );
61 | });
62 |
63 | document.addEventListener('DOMContentLoaded', () => {
64 | document.documentElement.classList.add(`platform-${process.platform}`);
65 |
66 | checkUnreads();
67 |
68 | // Change application title on login page
69 | const appTitle = $('.wrapper .banner h1');
70 | if (appTitle) {
71 | appTitle.innerHTML = 'Inboxer';
72 | }
73 |
74 | // Put the name of active user on the topbar
75 | const activeUserName = $('div.gb_Bb.gb_Ab');
76 | const topbarUserLink = $('a.gb_b.gb_fb.gb_R');
77 | if (activeUserName && topbarUserLink) {
78 | const span = document.createElement('span');
79 | span.textContent = activeUserName.textContent;
80 | span.classList.add('active-user-name');
81 | topbarUserLink.appendChild(span);
82 | }
83 | });
84 |
--------------------------------------------------------------------------------
/app/renderer/preferences/index.css:
--------------------------------------------------------------------------------
1 | body, html {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | font-family: Arial, Helvetica, sans-serif;
6 | font-size: 0.95em;
7 | }
8 |
9 | .container {
10 | position: relative;
11 | width: 100%;
12 | height: 100%;
13 | display: flex;
14 | flex-direction: column;
15 | }
16 |
17 | .title {
18 | padding-top: 24px;
19 | padding-bottom: 24px;
20 | padding-left: 24px;
21 | border-bottom: 1px solid #e0e0e0;
22 | font-weight: bolder;
23 | font-size: large;
24 | }
25 |
26 | .preferences-container {
27 | width: 100%;
28 | height: 100%;
29 | display: flex;
30 | flex-direction: row;
31 | }
32 |
33 | .category-container {
34 | width: 30%;
35 | border-right: 1px solid #e0e0e0;
36 | }
37 |
38 | .category {
39 | padding-left: 24px;
40 | padding-bottom: 10px;
41 | padding-top: 10px;
42 | margin: 0;
43 | cursor: pointer;
44 | }
45 |
46 | .active {
47 | background-color: #f2f2f2;
48 | font-weight: bold;
49 | cursor: default;
50 | }
51 |
52 | .category-preferences {
53 | position: relative;
54 | width: 70%;
55 | display: flex;
56 | flex-direction: column;
57 | justify-content: space-between;
58 | }
59 |
60 | .preference-list {
61 | width: 100%;
62 | }
63 |
64 | .preference {
65 | padding-left: 24px;
66 | }
67 |
68 | .actions-container {
69 | width: calc(100% - 24px);
70 | height: 48px;
71 | margin-right: 24px;
72 | display: flex;
73 | flex-direction: row;
74 | justify-content: flex-end;
75 | align-items: center;
76 | }
77 |
78 | .action {
79 | height: 32px;
80 | line-height: 32px;
81 | font-weight: bold;
82 | color: #3367d6;
83 | cursor: pointer;
84 | padding: 0px 16px 0px 16px;
85 | border-radius: 2px;
86 | border:none;
87 | }
88 |
89 | .action:hover {
90 | background-color: rgba(10,10,10,0.2)
91 | }
92 |
93 | input[type="number"] {
94 | width: 4em;
95 | margin-left: 1em;
96 | margin-right: 0.5em;
97 | }
98 |
--------------------------------------------------------------------------------
/app/renderer/preferences/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
Preferences
12 |
13 |
16 |
17 |
18 |
19 |
Notify on New messages
20 |
21 |
22 |
Notify on Snoozed messages
23 |
24 |
25 |
Notify on Downloaded files
26 |
27 |
28 |
Notification update period: seconds
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
Send usage reports
41 |
42 |
43 |
44 | DONE
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/app/renderer/preferences/index.js:
--------------------------------------------------------------------------------
1 | const { remote } = require('electron');
2 |
3 | const config = remote.require('../../app/main/config');
4 |
5 | const PreferencesWindow = {};
6 |
7 | PreferencesWindow.savePreferencesButton = document.querySelector('#savePreferencesButton');
8 | PreferencesWindow.sendAnalyticsCheckbox = document.querySelector('#sendAnalyticsCheckbox');
9 | PreferencesWindow.notifyUnread = document.querySelector('#notifyUnread');
10 | PreferencesWindow.notifySnoozed = document.querySelector('#notifySnoozed');
11 | PreferencesWindow.notifyDownload = document.querySelector('#notifyDownload');
12 | PreferencesWindow.notifyPeriod = document.querySelector('#notifyPeriod');
13 |
14 | PreferencesWindow.setEventListeners = () => {
15 | const self = this;
16 | this.savePreferencesButton.addEventListener('click', () => {
17 | config.set('sendAnalytics', self.sendAnalyticsCheckbox.checked);
18 | config.set('notify.unread', self.notifyUnread.checked);
19 | config.set('notify.snoozed', self.notifySnoozed.checked);
20 | config.set('notify.download', self.notifyDownload.checked);
21 | config.set('notify.period', parseFloat(self.notifyPeriod.value));
22 | remote.getCurrentWindow().close();
23 | });
24 | };
25 |
26 | PreferencesWindow.init = () => {
27 | this.sendAnalyticsCheckbox.checked = config.get('sendAnalytics');
28 | this.notifyUnread.checked = config.get('notify.unread');
29 | this.notifySnoozed.checked = config.get('notify.snoozed');
30 | this.notifyDownload.checked = config.get('notify.download');
31 | this.notifyPeriod.value = config.get('notify.period');
32 | PreferencesWindow.setEventListeners();
33 | };
34 |
35 | window.addEventListener('DOMContentLoaded', () => {
36 | PreferencesWindow.init();
37 | });
38 |
--------------------------------------------------------------------------------
/app/renderer/unreads.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer: ipc, remote } = require('electron');
2 | const path = require('path');
3 | const {
4 | $, $$, sendNotification, sendClick,
5 | } = require('./utils');
6 |
7 | const config = remote.require('../../app/main/config');
8 |
9 | const seenMessages = new Map();
10 |
11 | // gmail logo from https://gsuite.google.com/setup/resources/logos/
12 | const iconMail = path.join(__dirname, '..', 'static/gmail_48px.png');
13 | // snoozed logo copied from Inboxer
14 | const iconSnoozed = path.join(__dirname, '..', 'static/IconSnoozed.png');
15 |
16 | function keyByMessage({
17 | messageType, subject, sender, conversationLength,
18 | }) {
19 | try {
20 | return JSON.stringify({
21 | messageType, subject, sender, conversationLength,
22 | });
23 | } catch (error) {
24 | console.error(error); // eslint-disable-line
25 | return undefined;
26 | }
27 | }
28 |
29 | function extractSubject(message) {
30 | return $('.y6 span span', message).textContent;
31 | }
32 |
33 | function extractSender(message) {
34 | return $('span.bA4', message).textContent;
35 | }
36 |
37 | function extractConversationLength(message) {
38 | const lenSpan = $('span.bx0', message);
39 | return (lenSpan) ? lenSpan.textContent : null;
40 | }
41 |
42 | // name of currently selected folder: Inbox, Sent, ...
43 | function folderName() {
44 | const folder = $('div.TK div.aim.ain div.TO');
45 | return (folder) ? folder.getAttribute('data-tooltip') : null;
46 | }
47 |
48 | // extract number of unread messages in Inbox from the left column
49 | // works even if we're not in Inbox
50 | function extractNumberUnread() {
51 | // div.TK: left column, main folders
52 | // div.aim: each folder (Inbox, Starred, Sent, ...)
53 | // div.TO with data-tooltip="Inbox": Inbox folder
54 | // div.bsU: contains number of unread messages
55 | const numUnreadDiv = $('div.TK div.aim div.TO[data-tooltip="Inbox"] div.bsU');
56 | const numUnread = (numUnreadDiv) ? parseInt(numUnreadDiv.textContent, 10) : 0;
57 | return (Number.isNaN(numUnread)) ? 0 : numUnread;
58 | }
59 |
60 | // returns array of notifications: {message, title, body, icon}
61 | function findUnreadSnoozedMessages() {
62 | const messageTable = $('div.Cp table.F');
63 | if (messageTable === null) {
64 | return [];
65 | }
66 | const notifications = [];
67 |
68 | // mark already seen messages false
69 | seenMessages.forEach((value, key, map) => {
70 | map.set(key, false);
71 | });
72 |
73 | const notifyUnread = config.get('notify.unread');
74 | const notifySnoozed = config.get('notify.snoozed');
75 | // iterate through all messages (rows in table)
76 | $$('table.F > tbody > tr', messageTable).forEach((message) => {
77 | let messageType = null;
78 | if (message.className.includes('zA zE')) { // unread message
79 | messageType = 'unread';
80 | } else if ($('td.byZ div.by1', message) !== null) { // snoozed message
81 | messageType = 'snoozed';
82 | }
83 |
84 | if (messageType !== null) {
85 | const subject = extractSubject(message);
86 | const sender = extractSender(message);
87 | const conversationLength = extractConversationLength(message);
88 | const key = keyByMessage({
89 | messageType,
90 | subject,
91 | sender,
92 | conversationLength,
93 | });
94 |
95 | // if message hasn't been seen before, schedule notification
96 | if (!seenMessages.has(key)) {
97 | if ((messageType === 'unread' && notifyUnread)
98 | || (messageType === 'snoozed' && notifySnoozed)) {
99 | const icon = (messageType === 'unread') ? iconMail : iconSnoozed;
100 | notifications.push({
101 | message,
102 | title: sender,
103 | body: subject,
104 | icon: `file://${icon}`,
105 | });
106 | }
107 | }
108 | seenMessages.set(key, true); // mark message as seen
109 | }
110 | });
111 |
112 | // delete any seenMessages still marked false
113 | seenMessages.forEach((value, key, map) => {
114 | if (value === false) {
115 | map.delete(key);
116 | }
117 | });
118 |
119 | return notifications;
120 | }
121 |
122 | function checkUnreads() {
123 | if (typeof checkUnreads.haveUnread === 'undefined') {
124 | checkUnreads.haveUnread = false;
125 | }
126 |
127 | let period = parseFloat(config.get('notify.period'), 10) * 1000; // convert seconds to milliseconds
128 | if (period < 100) {
129 | period = 100; // no faster than every 100 ms
130 | }
131 |
132 | const numUnread = extractNumberUnread();
133 | if (checkUnreads.haveUnread !== (numUnread > 0)) {
134 | ipc.send('update-unreads-count', numUnread);
135 | checkUnreads.haveUnread = (numUnread > 0);
136 | }
137 |
138 | // skip if we're not inside the inbox
139 | if (folderName() !== 'Inbox') {
140 | setTimeout(checkUnreads, period);
141 | return;
142 | }
143 |
144 | if (typeof checkUnreads.startingUp === 'undefined') {
145 | checkUnreads.startingUp = true;
146 | }
147 |
148 | // notifications for new unread or snoozed messages
149 | const notifications = findUnreadSnoozedMessages();
150 | if (!checkUnreads.startingUp) { // send notifications only if we're not just starting up
151 | notifications.reverse().forEach((notification) => {
152 | const {
153 | message, title, body, icon,
154 | } = notification;
155 | sendNotification({
156 | title,
157 | body,
158 | icon,
159 | }).addEventListener('click', () => {
160 | ipc.send('show-window', true);
161 | sendClick(message);
162 | });
163 | });
164 | }
165 |
166 | if (checkUnreads.startingUp) {
167 | checkUnreads.startingUp = false;
168 | }
169 |
170 | setTimeout(checkUnreads, period);
171 | }
172 |
173 | module.exports = checkUnreads;
174 |
--------------------------------------------------------------------------------
/app/renderer/utils.js:
--------------------------------------------------------------------------------
1 | function $(selector, context = document) {
2 | return context.querySelector(selector);
3 | }
4 |
5 | function $$(selector, context = document) {
6 | return context.querySelectorAll(selector);
7 | }
8 |
9 | function createEvent(type) {
10 | return new MouseEvent(type, {
11 | view: window,
12 | bubbles: true,
13 | cancelable: true,
14 | });
15 | }
16 |
17 | function sendClick(el) {
18 | el.dispatchEvent(createEvent('mousedown'));
19 | el.dispatchEvent(createEvent('click'));
20 | }
21 |
22 | function sendNotification(notification) {
23 | const { title, body, icon } = notification;
24 | return new Notification(title, { body, icon });
25 | }
26 |
27 | function ancestor(el, selector) {
28 | return el.closest(selector);
29 | }
30 |
31 | // Drawing overlay icon for main proccess
32 | // https://github.com/sindresorhus/caprine/blob/f67cc47fd4c9e5a44e171a5cc51c3e5a11cea600/browser.js#L104-L119
33 | function renderOverlayIcon(unreadsCount) {
34 | const canvas = document.createElement('canvas');
35 | canvas.height = 128;
36 | canvas.width = 128;
37 | canvas.style.letterSpacing = '-5px';
38 |
39 | const ctx = canvas.getContext('2d');
40 | ctx.fillStyle = '#f42020';
41 | ctx.beginPath();
42 | ctx.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI);
43 | ctx.fill();
44 | ctx.textAlign = 'center';
45 | ctx.fillStyle = 'white';
46 | ctx.font = '90px sans-serif';
47 | ctx.fillText(Math.min(99, unreadsCount).toString(), 64, 96);
48 |
49 | return canvas;
50 | }
51 |
52 | module.exports = {
53 | $,
54 | $$,
55 | createEvent,
56 | sendClick,
57 | sendNotification,
58 | ancestor,
59 | renderOverlayIcon,
60 | };
61 |
--------------------------------------------------------------------------------
/app/static/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/Icon.png
--------------------------------------------------------------------------------
/app/static/Icon@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/Icon@2x.png
--------------------------------------------------------------------------------
/app/static/IconSnoozed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/IconSnoozed.png
--------------------------------------------------------------------------------
/app/static/IconTray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/IconTray.png
--------------------------------------------------------------------------------
/app/static/IconTray@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/IconTray@2x.png
--------------------------------------------------------------------------------
/app/static/IconTrayUnread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/IconTrayUnread.png
--------------------------------------------------------------------------------
/app/static/IconTrayUnread@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/IconTrayUnread@2x.png
--------------------------------------------------------------------------------
/app/static/gmail_48px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/app/static/gmail_48px.png
--------------------------------------------------------------------------------
/build/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/build/background.png
--------------------------------------------------------------------------------
/build/background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/build/background@2x.png
--------------------------------------------------------------------------------
/build/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/build/icon.icns
--------------------------------------------------------------------------------
/build/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/build/icon.ico
--------------------------------------------------------------------------------
/media/inboxer-linux-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/media/inboxer-linux-desktop.png
--------------------------------------------------------------------------------
/media/inboxer-linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/media/inboxer-linux.png
--------------------------------------------------------------------------------
/media/inboxer-mac-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/media/inboxer-mac-desktop.png
--------------------------------------------------------------------------------
/media/inboxer-mac.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/media/inboxer-mac.png
--------------------------------------------------------------------------------
/media/inboxer-windows-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/media/inboxer-windows-desktop.png
--------------------------------------------------------------------------------
/media/inboxer-windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/media/inboxer-windows.png
--------------------------------------------------------------------------------
/media/inboxer.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denysdovhan/inboxer/c3f49fa5c5d55045074fb9bfafd161b84810683d/media/inboxer.sketch
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "inboxer",
4 | "productName": "Inboxer",
5 | "version": "1.3.2",
6 | "description": "Simple client for Google Inbox",
7 | "author": {
8 | "name": "Denys Dovhan",
9 | "email": "denysdovhan@gmail.com",
10 | "url": "https://denysdovhan.com"
11 | },
12 | "license": "MIT",
13 | "homepage": "https://denysdovhan.com/inboxer",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/denysdovhan/inboxer.git"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/denysdovhan/inboxer/issues"
20 | },
21 | "keywords": [
22 | "client",
23 | "inbox",
24 | "gmail",
25 | "electron",
26 | "app"
27 | ],
28 | "main": "app/main",
29 | "scripts": {
30 | "postinstall": "electron-builder install-app-deps",
31 | "start": "electron app/main",
32 | "lint": "eslint app",
33 | "lint:fix": "npm run lint -- --fix",
34 | "test": "npm-run-all lint",
35 | "pack": "electron-builder --dir",
36 | "dist": "npm-run-all test dist:*",
37 | "dist:mac": "electron-builder --mac",
38 | "dist:linux": "electron-builder --linux",
39 | "dist:win": "electron-builder --win",
40 | "format": "prettier-eslint --write app/**/*.js",
41 | "icon": "npm-run-all icon:*",
42 | "icon:icns": "png2icns app/static/Icon@2x.png -o build/icon.icns",
43 | "icon:ico": "png2icons app/static/Icon@2x.png build/icon -ico"
44 | },
45 | "devDependencies": {
46 | "electron": "^3.0",
47 | "electron-builder": "^20.39.0",
48 | "eslint": "^5.14.1",
49 | "eslint-config-airbnb-base": "^13.1.0",
50 | "eslint-plugin-import": "^2.16.0",
51 | "husky": "^1.3.1",
52 | "lint-staged": "^6.0.0",
53 | "npm-run-all": "^4.1.2",
54 | "png2icns": "0.0.1",
55 | "png2icons": "^0.9.1",
56 | "prettier-eslint-cli": "^4.7.1"
57 | },
58 | "dependencies": {
59 | "asar": "^1.0.0",
60 | "electron-context-menu": "^0.11.0",
61 | "electron-dl": "^1.13.0",
62 | "electron-is-dev": "^1.0.1",
63 | "electron-log": "^3.0.1",
64 | "electron-store": "^2.0.0",
65 | "electron-updater": "^4.0.0",
66 | "first-run": "^1.2.0",
67 | "insight": "^0.10.1",
68 | "minimatch-all": "^1.1.0"
69 | },
70 | "build": {
71 | "appId": "com.denysdovhan.inboxer",
72 | "files": [
73 | "**/*"
74 | ],
75 | "mac": {
76 | "category": "public.app-category.social-networking",
77 | "target": [
78 | "dmg",
79 | "zip"
80 | ]
81 | },
82 | "dmg": {
83 | "iconSize": 160,
84 | "contents": [
85 | {
86 | "x": 180,
87 | "y": 170
88 | },
89 | {
90 | "x": 480,
91 | "y": 170,
92 | "type": "link",
93 | "path": "/Applications"
94 | }
95 | ]
96 | },
97 | "linux": {
98 | "target": [
99 | "AppImage",
100 | "deb",
101 | "snap"
102 | ]
103 | },
104 | "win": {
105 | "target": [
106 | "nsis"
107 | ]
108 | }
109 | },
110 | "lint-staged": {
111 | "app/**/*.js": [
112 | "lint",
113 | "format"
114 | ]
115 | }
116 | }
117 |
--------------------------------------------------------------------------------