├── .gitignore ├── AUTHORS ├── doc ├── ace.png ├── chat.png ├── pdf.png ├── vnc.png ├── google.png ├── archiver.png └── terminal.png ├── Chat ├── user.png ├── package.json ├── webpack.config.js ├── README.md ├── metadata.json ├── main.css ├── scheme.html └── main.js ├── GoogleMail ├── icon.png ├── starred.png ├── unstar.png ├── webpack.config.js ├── metadata.json ├── main.css ├── window-settings.js ├── window-message.js ├── scheme.html ├── window-main.js ├── main.js └── mailer.js ├── AceEditor ├── ace-logo.png ├── package.json ├── webpack.config.js ├── metadata.json ├── scheme.html ├── main.css └── main.js ├── GoogleContacts ├── icon.png ├── webpack.config.js ├── metadata.json ├── scheme.html ├── main.css └── main.js ├── .gitmodules ├── PDFjs ├── package.json ├── metadata.json ├── webpack.config.js ├── scheme.html ├── main.css └── main.js ├── repository.json ├── VNC ├── metadata.json ├── webpack.config.js ├── main.css ├── scheme.html └── main.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | node_modules 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Anders Evenrud 2 | -------------------------------------------------------------------------------- /doc/ace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/doc/ace.png -------------------------------------------------------------------------------- /doc/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/doc/chat.png -------------------------------------------------------------------------------- /doc/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/doc/pdf.png -------------------------------------------------------------------------------- /doc/vnc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/doc/vnc.png -------------------------------------------------------------------------------- /Chat/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/Chat/user.png -------------------------------------------------------------------------------- /doc/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/doc/google.png -------------------------------------------------------------------------------- /doc/archiver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/doc/archiver.png -------------------------------------------------------------------------------- /doc/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/doc/terminal.png -------------------------------------------------------------------------------- /GoogleMail/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/GoogleMail/icon.png -------------------------------------------------------------------------------- /AceEditor/ace-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/AceEditor/ace-logo.png -------------------------------------------------------------------------------- /GoogleMail/starred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/GoogleMail/starred.png -------------------------------------------------------------------------------- /GoogleMail/unstar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/GoogleMail/unstar.png -------------------------------------------------------------------------------- /GoogleContacts/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/os-js/OS.js-extras/HEAD/GoogleContacts/icon.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "VNC/vendor/noVNC"] 2 | path = VNC/vendor/noVNC 3 | url = https://github.com/kanaka/noVNC.git 4 | -------------------------------------------------------------------------------- /Chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osjs-chat", 3 | "dependencies": { 4 | "strophe.js": "^1.2.14", 5 | "webpack": "^3.4.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /PDFjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osjs-pdfjs", 3 | "dependencies": { 4 | "pdfjs-dist": "^1.8.609", 5 | "webpack": "^3.4.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /AceEditor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osjs-ace-editor", 3 | "dependencies": { 4 | "ace-builds": "^1.2.8", 5 | "webpack": "^3.4.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extras", 3 | "description": "OS.js Extra Packages", 4 | "maintainer": "Anders Evenrud ", 5 | "url": "https://github.com/os-js/OS.js-extras" 6 | } 7 | -------------------------------------------------------------------------------- /VNC/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "className": "ApplicationVNC", 3 | "name": "noVNC v0.5", 4 | "mime": null, 5 | "icon": "apps/preferences-desktop-remote-desktop.png", 6 | "category": "network", 7 | "preload": [ 8 | {"src": "main.js", "type": "javascript"}, 9 | {"src": "main.css", "type": "stylesheet"} 10 | ] 11 | } 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chat/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const osjs = require('osjs-build'); 3 | 4 | module.exports = new Promise((resolve, reject) => { 5 | const metadataFile = path.join(__dirname, 'metadata.json'); 6 | 7 | osjs.webpack.createPackageConfiguration(metadataFile).then((result) => { 8 | resolve(result.config); 9 | }).catch(reject); 10 | }); 11 | -------------------------------------------------------------------------------- /VNC/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const osjs = require('osjs-build'); 3 | 4 | module.exports = new Promise((resolve, reject) => { 5 | const metadataFile = path.join(__dirname, 'metadata.json'); 6 | 7 | osjs.webpack.createPackageConfiguration(metadataFile).then((result) => { 8 | resolve(result.config); 9 | }).catch(reject); 10 | }); 11 | -------------------------------------------------------------------------------- /AceEditor/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const osjs = require('osjs-build'); 3 | 4 | module.exports = new Promise((resolve, reject) => { 5 | const metadataFile = path.join(__dirname, 'metadata.json'); 6 | 7 | osjs.webpack.createPackageConfiguration(metadataFile).then((result) => { 8 | resolve(result.config); 9 | }).catch(reject); 10 | }); 11 | -------------------------------------------------------------------------------- /GoogleMail/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const osjs = require('osjs-build'); 3 | 4 | module.exports = new Promise((resolve, reject) => { 5 | const metadataFile = path.join(__dirname, 'metadata.json'); 6 | 7 | osjs.webpack.createPackageConfiguration(metadataFile).then((result) => { 8 | resolve(result.config); 9 | }).catch(reject); 10 | }); 11 | -------------------------------------------------------------------------------- /GoogleContacts/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const osjs = require('osjs-build'); 3 | 4 | module.exports = new Promise((resolve, reject) => { 5 | const metadataFile = path.join(__dirname, 'metadata.json'); 6 | 7 | osjs.webpack.createPackageConfiguration(metadataFile).then((result) => { 8 | resolve(result.config); 9 | }).catch(reject); 10 | }); 11 | -------------------------------------------------------------------------------- /Chat/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | You'll need https://github.com/twonds/punjab/blob/master/INSTALL.txt 4 | 5 | And then make sure the proxy is reachable by OS.js server. 6 | 7 | # Notes 8 | 9 | Connect with your Google Mail account. 10 | 11 | You have to enable "Allow less secure apps" in your Account Settings to allow authentication. 12 | 13 | https://myaccount.google.com/security?pli=1 14 | -------------------------------------------------------------------------------- /GoogleContacts/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "className": "ApplicationGoogleContacts", 3 | "name": "Google Contacts", 4 | "mime": null, 5 | "category": "office", 6 | "singular": true, 7 | "icon": "./icon.png", 8 | "build": { 9 | "copy": ["icon.png"] 10 | }, 11 | "preload": [ 12 | {"src": "main.js", "type": "javascript"}, 13 | {"src": "main.css", "type": "stylesheet"} 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /PDFjs/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "className": "ApplicationPDFjs", 3 | "name": "PDFjs", 4 | "description":"PDF Viewer", 5 | "mime": ["application\/pdf"], 6 | "category": "office", 7 | "icon": "mimetypes/x-office-document.png", 8 | "preload": [ 9 | {"src": "main.js", "type": "javascript"}, 10 | {"src": "main.css", "type": "stylesheet"} 11 | ] 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /PDFjs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const osjs = require('osjs-build'); 3 | 4 | module.exports = new Promise((resolve, reject) => { 5 | const metadataFile = path.join(__dirname, 'metadata.json'); 6 | 7 | osjs.webpack.createPackageConfiguration(metadataFile).then((result) => { 8 | result.config.entry = { 9 | 'main': result.config.entry, 10 | 'pdf.worker': 'node_modules/pdfjs-dist/build/pdf.worker.entry' 11 | }; 12 | 13 | resolve(result.config); 14 | }).catch(reject); 15 | }); 16 | -------------------------------------------------------------------------------- /Chat/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "className": "ApplicationChat", 3 | "name": "Chat", 4 | "description":"Chat with your buddies", 5 | "mime": null, 6 | "icon": "status/user-available.png", 7 | "category": "network", 8 | "singular": true, 9 | "splash": true, 10 | "build": { 11 | "copy": [ 12 | "user.png" 13 | ] 14 | }, 15 | "preload": [ 16 | {"src": "main.js", "type": "javascript"}, 17 | {"src": "main.css", "type": "stylesheet"} 18 | ] 19 | } 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /GoogleMail/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "className": "ApplicationGmail", 3 | "name": "Google Mail", 4 | "mime": null, 5 | "icon": "./icon.png", 6 | "category": "office", 7 | "singular": true, 8 | "loading": true, 9 | "compability":["indexedDB"], 10 | "settings": { 11 | "maxPages": 2 12 | }, 13 | "build": { 14 | "copy": ["icon.png", "starred.png", "unstar.png"] 15 | }, 16 | "preload": [ 17 | {"src": "main.js", "type": "javascript"}, 18 | {"src": "main.css", "type": "stylesheet"} 19 | ] 20 | } 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /AceEditor/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "className": "ApplicationAceEditor", 3 | "name": "AceEditor", 4 | "mime": null, 5 | "icon": "categories/applications-system.png", 6 | "category": "development", 7 | "mime": ["^text", "inode\\/x\\-empty", "application\\/x\\-empty", "application\\/x\\-python", "application\\/javascript"], 8 | "icon": "./ace-logo.png", 9 | "build": { 10 | "copy": [ 11 | "ace-logo.png", 12 | { 13 | "from": "node_modules/ace-builds/src-min-noconflict", 14 | "to": "ace" 15 | } 16 | ] 17 | }, 18 | "preload": [ 19 | {"src": "ace/ace.js", "type": "javascript"}, 20 | {"src": "main.js", "type": "javascript"}, 21 | {"src": "main.css", "type": "stylesheet"} 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /GoogleContacts/scheme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Name 20 | Address 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /AceEditor/scheme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /PDFjs/scheme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /PDFjs/main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | .ApplicationPDFjsWindow gui-container { 32 | background : #fff; 33 | } 34 | -------------------------------------------------------------------------------- /GoogleContacts/main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | .Window_ApplicationGoogleContactsWindow .WindowWrapper { 32 | } 33 | 34 | .Window_ApplicationGoogleContactsWindow .WindowWrapper .GUIListView { 35 | position : absolute; 36 | bottom : 0px; 37 | right : 0px; 38 | left : 0px; 39 | top : 5px; 40 | } 41 | -------------------------------------------------------------------------------- /AceEditor/main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | .ApplicationAceEditorWindow application-window-content { 32 | } 33 | 34 | 35 | .ApplicationAceEditorWindow application-window-content div[data-id="AceContainer"] { 36 | width : auto; 37 | height : auto; 38 | top : 4px; 39 | left : 4px; 40 | bottom : 4px; 41 | right : 4px; 42 | } 43 | -------------------------------------------------------------------------------- /VNC/main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | .ApplicationVNCWindow application-window-content { 32 | } 33 | 34 | .ApplicationVNCWindow application-window-content gui-scroll-container { 35 | width : 100%; 36 | } 37 | 38 | .ApplicationVNCWindow application-window-content gui-canvas { 39 | width : 100%; 40 | height : 100%; 41 | background : #000; 42 | } 43 | 44 | .ApplicationVNCWindow application-window-content canvas { 45 | position : absolute; 46 | top : 50%; 47 | left : 50%; 48 | -webkit-transform : translate(-50%, -50%); 49 | -moz-transform : translate(-50%, -50%); 50 | -ms-transform : translate(-50%, -50%); 51 | transform : translate(-50%, -50%); 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OS.js Extra Packages 2 | 3 | This repository contains *some* Extra packages for [OS.js](https://github.com/os-js/OS.js). 4 | 5 | [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/os-js/OS.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 6 | [![Tips](https://img.shields.io/gratipay/os-js.svg)](https://gratipay.com/os-js/) 7 | [![Donate](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=andersevenrud%40gmail%2ecom&lc=NO¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted) 8 | [![Support](https://img.shields.io/badge/patreon-support-orange.svg)](https://www.patreon.com/user?u=2978551&ty=h&u=2978551) 9 | 10 | ``` 11 | ./bin/add-package-repo.sh extras https://github.com/os-js/OS.js-extras.git 12 | ``` 13 | 14 | ## GoogleMail and GoogleContacts 15 | 16 | Google Mail and Contacts clients 17 | 18 | ![ScreenShot](https://raw.githubusercontent.com/os-js/OS.js-extras/master/doc/google.png) 19 | 20 | *Will be moved to OS.js master branch when done* 21 | 22 | You need to add your Google Client API ID. For more information look at [this article](https://manual.os-js.org/configuration/vfs) (Guide says VFS, but it relates to this as well). 23 | 24 | ## PDFjs 25 | 26 | Read PDF documents. 27 | 28 | Requires `gulp` to build. 29 | 30 | ![ScreenShot](https://raw.githubusercontent.com/os-js/OS.js-extras/master/doc/pdf.png) 31 | 32 | ## Ace Editor 33 | 34 | A code editor. 35 | 36 | ![ScreenShot](https://raw.githubusercontent.com/os-js/OS.js-extras/master/doc/ace.png) 37 | 38 | ## Chat 39 | 40 | A XMPP client for Google Talk. Includes proxy for Apache (See INSTALL file). 41 | 42 | ![ScreenShot](https://raw.githubusercontent.com/os-js/OS.js-extras/master/doc/chat.png) 43 | 44 | Requires a twist server running on punjab and a HTTP proxy (Included in vendor files) 45 | 46 | How to set up: https://github.com/os-js/OS.js-extras/blob/master/Chat/README.md 47 | 48 | ## VNC 49 | 50 | A [noVNC](https://github.com/kanaka/noVNC) implementation. 51 | 52 | ![ScreenShot](https://raw.githubusercontent.com/os-js/OS.js-extras/master/doc/vnc.png) 53 | 54 | Tested with OSX, Windows and Linux using *websockify* (included) 55 | 56 | How to set up: https://github.com/kanaka/noVNC/wiki/Advanced-usage 57 | -------------------------------------------------------------------------------- /GoogleMail/main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | .ApplicationGmailWindow gui-progress-bar { 32 | position : absolute; 33 | bottom : 4px; 34 | right : 4px; 35 | height : 1em; 36 | width : 20%; 37 | line-height : 20px; 38 | z-index : 999999; 39 | } 40 | .ApplicationGmailWindow gui-progress-bar span { 41 | display : none; 42 | } 43 | .ApplicationGmailWindow gui-list-view-row.unread { 44 | font-weight : bold; 45 | } 46 | .ApplicationGmailWindow gui-list-view-body > gui-list-view-row > gui-list-view-column:nth-child(1) { 47 | } 48 | .ApplicationGmailWindow gui-list-view-body > gui-list-view-row > gui-list-view-column:nth-child(1):after { 49 | content : ""; 50 | position : absolute; 51 | left : 50%; 52 | top : 50%; 53 | width : 8px; 54 | height : 8px; 55 | margin : -4px; 56 | border-radius : 8px; 57 | background-color : #D3D3D3; 58 | } 59 | .ApplicationGmailWindow gui-list-view-body > gui-list-view-row.unread > gui-list-view-column:nth-child(1):after { 60 | background-color : #A0C3FF; 61 | } 62 | .ApplicationGmailWindow gui-list-view-body > gui-list-view-row > gui-list-view-column:nth-child(2) { 63 | background : transparent url('unstar.png') no-repeat center center; 64 | } 65 | .ApplicationGmailWindow gui-list-view-body > gui-list-view-row.starred > gui-list-view-column:nth-child(2) { 66 | background-image : url('starred.png'); 67 | } 68 | -------------------------------------------------------------------------------- /GoogleMail/window-settings.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | const Window = OSjs.require('core/window'); 31 | 32 | export default class ApplicationGmailSettingsWindow extends Window { 33 | 34 | constructor(app, metadata, maxPages) { 35 | super('ApplicationGmailSettingsWindow', { 36 | icon: metadata.icon, 37 | title: metadata.name + ' - Settings', 38 | allow_session: false, 39 | allow_resize: false, 40 | allow_maximize: false, 41 | width: 400, 42 | height: 200 43 | }, app); 44 | 45 | this.defaultMaxPages = metadata.settings ? metadata.settings.maxPages : 10; 46 | this.currentMaxPages = maxPages || this.defaultMaxPages; 47 | } 48 | 49 | init(wmRef, app) { 50 | var self = this; 51 | var root = super.init(...arguments); 52 | 53 | // Load and set up scheme (GUI) here 54 | this._render('GmailSettingsWindow', require('osjs-scheme-loader!scheme.html')); 55 | 56 | function save(maxPages) { 57 | if ( maxPages && self._appRef ) { 58 | maxPages = parseInt(maxPages, 10); 59 | if ( isNaN(maxPages) || maxPages < 0 || maxPages > self.defaultMaxPages ) { 60 | maxPages = self.defaultMaxPages; 61 | } 62 | 63 | app._setSetting('maxPages', maxPages, true, function() { 64 | self._close(); 65 | }); 66 | return; 67 | } 68 | self._close(); 69 | } 70 | 71 | var maxPages = this._find('MaxPages').set('value', this.currentMaxPages); 72 | 73 | this._find('ButtonClose').on('click', function() { 74 | save(false); 75 | }); 76 | this._find('ButtonSave').on('click', function() { 77 | save(maxPages.get('value')); 78 | }); 79 | 80 | return root; 81 | } 82 | } 83 | 84 | -------------------------------------------------------------------------------- /Chat/main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | 31 | .ApplicationChatWindow application-window-content { 32 | } 33 | 34 | .ApplicationConversationWindow gui-container { 35 | box-sizing : border-box; 36 | } 37 | 38 | .ApplicationConversationWindow gui-container ul { 39 | position : relative; 40 | margin : 0; 41 | padding : 0; 42 | list-style-type : none; 43 | } 44 | 45 | .ApplicationConversationWindow gui-container ul > li { 46 | position : relative; 47 | display : block; 48 | margin : 10px; 49 | padding : 0; 50 | padding : 10px; 51 | padding-bottom : 25px; 52 | background-color : #e6e6e6; 53 | border-radius : 5px; 54 | } 55 | .ApplicationConversationWindow gui-container ul > li.remote { 56 | margin-right : 50px; 57 | } 58 | .ApplicationConversationWindow gui-container ul > li.self { 59 | margin-left : 50px; 60 | } 61 | 62 | .ApplicationConversationWindow gui-container ul > li > p { 63 | display : block; 64 | margin : 0; 65 | padding : 0; 66 | } 67 | 68 | .ApplicationConversationWindow gui-container ul > li > span { 69 | opacity : .8; 70 | font-size : 10px; 71 | position : absolute; 72 | bottom : 5px; 73 | right : 5px; 74 | display : inline-block; 75 | } 76 | .ApplicationConversationWindow gui-container ul > li > img { 77 | display : inline-block; 78 | width : 32px; 79 | height : 32px; 80 | border-radius : 32px; 81 | position : absolute; 82 | top : 50%; 83 | margin-top : -16px; 84 | left : -45px; 85 | background-color : #e6e6e6; 86 | } 87 | .ApplicationConversationWindow gui-container ul > li.remote > img { 88 | left : auto; 89 | right : -45px; 90 | } 91 | 92 | .ApplicationConversationWindow gui-container ul > li:before { 93 | content : ''; 94 | background : #e6e6e6; 95 | position : absolute; 96 | left : -6px; 97 | top : 50%; 98 | margin-top : -6px; 99 | width : 12px; 100 | height : 12px; 101 | transform : rotate(45deg); 102 | } 103 | .ApplicationConversationWindow gui-container ul > li.remote:before { 104 | left : auto; 105 | right : -6px; 106 | } 107 | -------------------------------------------------------------------------------- /Chat/scheme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Connect with your Google Mail account.
You have to enable "Allow less secure apps" in your Account Settings to allow authentication. Your information is stored in the browser and the server does not log. 6 |

7 |
8 | 9 | 10 | Username 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Password 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | LBL_SAVE 28 | LBL_CLOSE 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
    54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /PDFjs/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | import {PDFJS} from 'pdfjs-dist'; 31 | const DOM = OSjs.require('utils/dom'); 32 | const Utils = OSjs.require('utils/misc'); 33 | const DefaultApplication = OSjs.require('helpers/default-application'); 34 | const DefaultApplicationWindow = OSjs.require('helpers/default-application-window'); 35 | 36 | class ApplicationPDFjsWindow extends DefaultApplicationWindow { 37 | 38 | /** 39 | * Main Window Constructor 40 | */ 41 | constructor(app, metadata, file) { 42 | super('ApplicationPDFjsWindow', { 43 | allow_drop: true, 44 | icon: metadata.icon, 45 | title: metadata.name, 46 | width: 500, 47 | height: 400 48 | }, app, file); 49 | 50 | this.pageCount = 0; 51 | this.pageIndex = 0; 52 | this.pdf = null; 53 | this.currentScale = 1.5; 54 | } 55 | 56 | init(wmRef, app) { 57 | var root = super.init(...arguments); 58 | var self = this; 59 | 60 | // Load and set up scheme (GUI) here 61 | this._render('PDFWindow', require('osjs-scheme-loader!scheme.html')); 62 | 63 | this._find('Prev').on('click', function() { 64 | self.prevPage(); 65 | }); 66 | this._find('Next').on('click', function() { 67 | self.nextPage(); 68 | }); 69 | this._find('In').on('click', function() { 70 | self.zoomIn(); 71 | }); 72 | this._find('Out').on('click', function() { 73 | self.zoomOut(); 74 | }); 75 | 76 | return root; 77 | } 78 | 79 | destroy() { 80 | this.pdf = null; 81 | return super.destroy(...arguments); 82 | } 83 | 84 | open(file, url) { 85 | } 86 | 87 | page(pageNum) { 88 | var self = this; 89 | 90 | if ( this.pageCount <= 0 || pageNum <= 0 || pageNum > this.pageCount ) { 91 | return; 92 | } 93 | 94 | var container = this._find('Content').$element; 95 | DOM.$empty(container); 96 | 97 | this.pageIndex = pageNum; 98 | 99 | var statustext = Utils.format('Page {0}/{1} - {2}%', this.pageIndex, this.pageCount, this.currentScale * 100); 100 | this._find('Statusbar').set('value', statustext); 101 | 102 | this.pdf.getPage(this.pageIndex).then(function getPageHelloWorld(page) { 103 | var scale = self.currentScale; 104 | var viewport = page.getViewport(scale); 105 | var canvas = document.createElement('canvas'); 106 | var context = canvas.getContext('2d'); 107 | canvas.height = viewport.height; 108 | canvas.width = viewport.width; 109 | 110 | container.appendChild(canvas); 111 | 112 | var renderContext = { 113 | canvasContext: context, 114 | viewport: viewport 115 | }; 116 | page.render(renderContext); 117 | }); 118 | } 119 | 120 | prevPage() { 121 | this.page(this.pageIndex - 1); 122 | } 123 | 124 | nextPage() { 125 | this.page(this.pageIndex + 1); 126 | } 127 | 128 | zoomIn() { 129 | this.currentScale += 0.5; 130 | this.page(this.pageIndex); 131 | } 132 | 133 | zoomOut() { 134 | if ( this.currentScale > 0.5 ) { 135 | this.currentScale -= 0.5; 136 | } 137 | this.page(this.pageIndex); 138 | } 139 | 140 | showFile(file, result) { 141 | var self = this; 142 | var container = this._find('Content').$element; 143 | 144 | DOM.$empty(container); 145 | 146 | this.pageCount = 0; 147 | this.pageIndex = 0; 148 | 149 | PDFJS.getDocument(result).then(function getPdfHelloWorld(pdf) { 150 | self.pageCount = pdf.numPages; 151 | self.pdf = pdf; 152 | self.page(1); 153 | }); 154 | 155 | return super.showFile(...arguments); 156 | } 157 | } 158 | 159 | class ApplicationPDFjs extends DefaultApplication { 160 | constructor(args, metadata) { 161 | super('ApplicationPDFjs', args, metadata, { 162 | readData: false 163 | }); 164 | 165 | PDFJS.workerSrc = this._getResource('pdf.worker.js'); 166 | } 167 | 168 | init(settings, metadata) { 169 | super.init(...arguments); 170 | var file = this._getArgument('file'); 171 | this._addWindow(new ApplicationPDFjsWindow(this, metadata, file)); 172 | } 173 | } 174 | 175 | OSjs.Applications.ApplicationPDFjs = ApplicationPDFjs; 176 | 177 | -------------------------------------------------------------------------------- /GoogleMail/window-message.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | const Window = OSjs.require('core/window'); 31 | const Menu = OSjs.require('gui/menu'); 32 | const DOM = OSjs.require('utils/dom'); 33 | 34 | export default class ApplicationGmailMessageWindow extends Window { 35 | 36 | constructor(app, metadata, mailArgs) { 37 | mailArgs = mailArgs || {}; 38 | super('ApplicationGmailMessageWindow', { 39 | icon: metadata.icon, 40 | title: metadata.name + ': ' + (mailArgs.to || mailArgs.subject || ''), 41 | allow_session: false, 42 | width: 500, 43 | height: 400 44 | }, app); 45 | 46 | this.mailArgs = mailArgs; 47 | this.attachmentMenu = []; 48 | } 49 | 50 | init(wmRef, app) { 51 | var self = this; 52 | var root = super.init(...arguments); 53 | 54 | // Load and set up scheme (GUI) here 55 | this._render('GmailMessageWindow', require('osjs-scheme-loader!scheme.html')); 56 | 57 | var input = this._find('Input'); 58 | var subject = this._find('Subject').set('value', this.mailArgs.subject || ''); 59 | var to = this._find('To').set('value', this.mailArgs.to || ''); 60 | 61 | var menuMap = { 62 | MenuClose: function() { 63 | self._close(); 64 | }, 65 | Reply: function() { 66 | self._toggleLoading(true); 67 | app.replyToMessage(self.mailArgs.id, function() { 68 | self._toggleLoading(false); 69 | }); 70 | 71 | self._close(); 72 | }, 73 | Send: function() { 74 | app.sendMessage(self, to.get('value'), subject.get('value'), input.get('value')); 75 | }, 76 | Attachments: function(ev) { 77 | var pos = DOM.$position(ev.target); 78 | Menu.create(self.attachmentMenu, {x: pos.left, y: pos.top}); 79 | } 80 | }; 81 | 82 | function menuEvent(ev) { 83 | if ( menuMap[ev.detail.id] ) { 84 | menuMap[ev.detail.id](ev); 85 | } 86 | } 87 | 88 | if ( this.mailArgs && this.mailArgs.id ) { 89 | this._find('ViewInput').hide(); 90 | this._find('EntrySubject').hide(); 91 | this._find('EntryTo').hide(); 92 | 93 | this._find('Send').set('disabled', true); 94 | } else { 95 | this._find('ViewMessage').hide(); 96 | this._find('Attachments').set('disabled', true); 97 | this._find('Reply').set('disabled', true); 98 | } 99 | 100 | this._find('SubmenuFile').on('select', menuEvent); 101 | this._find('Menubar').on('select', menuEvent); 102 | 103 | return root; 104 | } 105 | 106 | _inited() { 107 | super._inited(...arguments); 108 | 109 | if ( this.mailArgs.id && this._app ) { 110 | this._app.recieveMessage(this, this.mailArgs.id); 111 | } 112 | } 113 | 114 | onPrepareReceive() { 115 | this._toggleLoading(true); 116 | this._find('Statusbar').set('value', 'Preparing to receive email...'); 117 | } 118 | 119 | onEndReceive(parsed) { 120 | var self = this; 121 | 122 | this._toggleLoading(false); 123 | 124 | if ( this.mailArgs.onRecieved ) { 125 | this.mailArgs.onRecieved(); 126 | } 127 | 128 | if ( !parsed ) { 129 | return; 130 | } 131 | 132 | var l = parsed.attachments.length; 133 | var items = []; 134 | parsed.attachments.forEach(function(i) { 135 | items.push({ 136 | title: i.filename + ' (' + i.mime + ', ' + i.size + 'b)', 137 | onClick: function() { 138 | if ( self._app ) { 139 | self._app.downloadAttachment(self, i); 140 | } 141 | } 142 | }); 143 | }); 144 | 145 | this.attachmentMenu = items; 146 | 147 | var text = 'Message downloaded (' + l + ' attachments)'; 148 | this._find('Statusbar').set('value', text); 149 | this._find('Message').set('value', parsed.raw); 150 | } 151 | 152 | onPrepareSend() { 153 | this._toggleLoading(true); 154 | this._find('Statusbar').set('value', 'Preparing to send email...'); 155 | } 156 | 157 | onEndSend(result, error) { 158 | this._toggleLoading(false); 159 | 160 | if ( result ) { 161 | this._close(); 162 | } else { 163 | this._find('Statusbar').set('value', 'FAILED TO SEND MESSAGE: ' + error); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /AceEditor/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | const ace = window.ace || {}; 31 | 32 | const DefaultApplication = OSjs.require('helpers/default-application'); 33 | const DefaultApplicationWindow = OSjs.require('helpers/default-application-window'); 34 | const Utils = OSjs.require('utils/misc'); 35 | 36 | class ApplicationAceEditorWindow extends DefaultApplicationWindow { 37 | constructor(app, metadata, file) { 38 | super('ApplicationAceEditorWindow', { 39 | allow_drop: true, 40 | icon: metadata.icon, 41 | title: metadata.name, 42 | width: 500, 43 | height: 500 44 | }, app, file); 45 | 46 | this.editor = null; 47 | this._on('destroy', () => (this.editor = null)); 48 | this._on('resize', () => { 49 | if ( this.editor ) { 50 | this.editor.resize(); 51 | } 52 | }); 53 | this._on('blur', () => { 54 | if ( this.editor ) { 55 | this.editor.blur(); 56 | } 57 | }); 58 | this._on('focus', () => { 59 | if ( this.editor ) { 60 | this.editor.focus(); 61 | } 62 | }); 63 | } 64 | 65 | init(wmRef, app) { 66 | if ( typeof window.__aceEditorCount === 'undefined' ) { 67 | window.__aceEditorCount = 0; 68 | } 69 | 70 | const root = super.init(...arguments); 71 | 72 | // Load and set up scheme (GUI) here 73 | this._render('AceEditorWindow', require('osjs-scheme-loader!scheme.html')); 74 | 75 | var editor; 76 | var statusbar = this._find('Statusbar'); 77 | var container = this._find('AceContainer').$element; 78 | var id = 'AceEditor' + window.__aceEditorCount.toString(); 79 | 80 | container.id = id; 81 | 82 | function updateStatusbar() { 83 | var c = editor.selection.getCursor(); 84 | var l = editor.session.getLength(); 85 | var txt = Utils.format('Row: {0}, Col: {1}, Lines: {2}', c.row, c.column, l); 86 | statusbar.set('value', txt); 87 | } 88 | 89 | editor = this.editor = ace.edit(id); 90 | this.editor.setTheme('ace/theme/monokai'); 91 | this.editor.getSession().setMode('ace/mode/javascript'); 92 | this.editor.getSession().selection.on('changeCursor', function(e) { 93 | updateStatusbar(); 94 | }); 95 | updateStatusbar(); 96 | 97 | window.__aceEditorCount++; 98 | 99 | return root; 100 | } 101 | 102 | updateFile(file) { 103 | super.updateFile(...arguments); 104 | if ( this.editor ) { 105 | this.setSyntaxMode(file); 106 | 107 | this.editor.focus(); 108 | } 109 | } 110 | 111 | showFile(file, content) { 112 | this.editor.setValue(content || ''); 113 | super.showFile(...arguments); 114 | 115 | this.setSyntaxMode(file); 116 | } 117 | 118 | getFileData() { 119 | return this.editor.getValue(); 120 | } 121 | 122 | setSyntaxMode(file) { 123 | if ( !this.editor || !file ) { 124 | return; 125 | } 126 | 127 | var mode = 'text'; 128 | if ( file.filename.match(/\.js$/i) ) { 129 | mode = 'javascript'; 130 | } else if ( file.filename.match(/\.py$/i) ) { 131 | mode = 'python'; 132 | } else if ( file.filename.match(/\.css$/i) ) { 133 | mode = 'css'; 134 | } else if ( file.filename.match(/\.x?html?$/i) ) { 135 | mode = 'html'; 136 | } 137 | 138 | this.editor.session.setMode({ 139 | path: 'ace/mode/' + mode, 140 | v: Date.now() 141 | }); 142 | } 143 | 144 | } 145 | 146 | class ApplicationAceEditor extends DefaultApplication { 147 | 148 | constructor(args, metadata) { 149 | super('ApplicationAceEditor', args, metadata, { 150 | extension: 'txt', 151 | mime: 'text/plain', 152 | filename: 'New ace file.txt', 153 | filetypes: [ 154 | { 155 | label: 'Plain Text', 156 | mime: 'text/plain', 157 | extension: 'txt' 158 | }, 159 | { 160 | label: 'JavaScript', 161 | mime: 'application/javascript', 162 | extension: 'js' 163 | }, 164 | { 165 | label: 'CSS', 166 | mime: 'text/css', 167 | extension: 'css' 168 | }, 169 | { 170 | label: 'HTML', 171 | mime: 'text/html', 172 | extension: 'html' 173 | }, 174 | { 175 | label: 'XML', 176 | mime: 'application/xml', 177 | extension: 'xml' 178 | }, 179 | { 180 | label: 'Python', 181 | mime: 'application/x-python', 182 | extension: 'py' 183 | }, 184 | { 185 | label: 'PHP', 186 | mime: 'application/php', 187 | extension: 'php' 188 | } 189 | ] 190 | }); 191 | } 192 | 193 | init(settings, metadata) { 194 | super.init(...arguments); 195 | 196 | const path = this._getResource('ace'); 197 | ace.config.set('basePath', path); 198 | /* 199 | ace.config.set('modePath', '/path/to/src'); 200 | ace.config.set('workerPath', '/path/to/src'); 201 | ace.config.set('themePath', '/path/to/src'); 202 | */ 203 | 204 | const file = this._getArgument('file'); 205 | this._addWindow(new ApplicationAceEditorWindow(this, metadata, file)); 206 | } 207 | } 208 | 209 | OSjs.Applications.ApplicationAceEditor = ApplicationAceEditor; 210 | 211 | -------------------------------------------------------------------------------- /GoogleMail/scheme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Max Pages per folder (10 is absolute maximum) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | LBL_SAVE 14 | LBL_CLOSE 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | Sender 130 | Subject 131 | Date 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /GoogleContacts/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | import Promise from 'bluebird'; 31 | import jsonp from 'then-jsonp'; 32 | 33 | const Window = OSjs.require('core/window'); 34 | const Application = OSjs.require('core/application'); 35 | const GoogleAPI = OSjs.require('helpers/google-api'); 36 | 37 | var pullContacts = (function() { 38 | var ginst; 39 | function connect(cb) { 40 | if ( ginst ) { 41 | cb(false, true); 42 | return; 43 | } 44 | 45 | var scope = ['https://www.googleapis.com/auth/contacts.readonly']; 46 | GoogleAPI.create({scope: scope}, function(error, result, inst) { 47 | ginst = inst; 48 | cb(error, error ? false : true); 49 | }); 50 | } 51 | 52 | function parseContacts(list) { 53 | var parsed = []; 54 | var entries = list.feed ? list.feed.entry : []; 55 | entries.forEach(function(e) { 56 | var emails = []; 57 | var name = e.gd$name || {}; 58 | 59 | (e.gd$email || []).forEach(function(ee) { 60 | emails.push(ee.address); 61 | }); 62 | 63 | if ( emails.length ) { 64 | var item = { 65 | id: e.id.$t, 66 | title: e.title.$t, 67 | emails: emails, 68 | names: { 69 | givenName: name.gd$givenName ? name.gd$givenName.$t : '', 70 | familyName: name.gd$familyName ? name.gd$familyName.$t : '', 71 | additionalName: name.gd$additionalName ? name.gd$additionalName.$t : '', 72 | fullName: name.gd$fullName ? name.gd$fullName.$t : '' 73 | } 74 | }; 75 | 76 | if ( !item.title && item.names.fullName ) { 77 | item.title = item.names.fullName; 78 | } 79 | if ( !item.title && item.names.givenName ) { 80 | item.title = item.names.givenName; 81 | } 82 | 83 | parsed.push(item); 84 | } 85 | }); 86 | 87 | parsed.sort(function(a, b) { 88 | var keyA = a.title; //new Date(a.date), 89 | var keyB = b.title; //new Date(b.date); 90 | return (keyA < keyB) ? -1 : ((keyA > keyB) ? 1 : 0); 91 | }); 92 | 93 | return parsed; 94 | } 95 | 96 | function getContacts(win, cb) { 97 | var t = (ginst || {}).accessToken; 98 | if ( !ginst || !t ) { 99 | cb('Google API instance was not created or invalid token!'); 100 | return; 101 | } 102 | 103 | win._toggleLoading(true); 104 | 105 | Promise.resolve(jsonp('GET', 'https://www.google.com/m8/feeds/contacts/default/full', { 106 | qs: { 107 | alt: 'json', 108 | access_token: t, 109 | 'max-results': 700, 110 | v: '3.0' 111 | } 112 | })).then((data) => { 113 | return cb(false, parseContacts(data)); 114 | }).catch((err) => { 115 | cb('Failed to fetch contacts from google: ' + err); 116 | }).finally(() => { 117 | win._toggleLoading(false); 118 | }); 119 | } 120 | 121 | return function(win, callback) { 122 | win._toggleLoading(true); 123 | 124 | connect(function(error) { 125 | if ( error ) { 126 | callback(error); 127 | return; 128 | } 129 | 130 | getContacts(win, function(error, result) { 131 | callback(error, result); 132 | }); 133 | }); 134 | }; 135 | })(); 136 | 137 | class ApplicationGoogleContactsWindow extends Window { 138 | constructor(app, metadata) { 139 | super('ApplicationGoogleContactsWindow', { 140 | icon: metadata.icon, 141 | title: metadata.name, 142 | width: 400, 143 | height: 200 144 | }, app); 145 | } 146 | 147 | init(wmRef, app) { 148 | var root = super.init(...arguments); 149 | var self = this; 150 | 151 | // Load and set up scheme (GUI) here 152 | this._render('ApplicationGoogleContactsWindow', require('osjs-scheme-loader!scheme.html')); 153 | 154 | this._find('View').on('activate', function(ev) { 155 | self.activateContact(ev.detail.entries[0].data); 156 | }); 157 | this._find('SubmenuFile').on('select', function(ev) { 158 | if ( ev.detail.id === 'MenuRefresh' ) { 159 | app.sync(); 160 | } else if ( ev.detail.id === 'MenuClose' ) { 161 | self._close(); 162 | } 163 | }); 164 | 165 | return root; 166 | } 167 | 168 | renderGoogleContacts(contacts) { 169 | contacts = contacts || []; 170 | 171 | var rows = []; 172 | contacts.forEach(function(c) { 173 | rows.push({ 174 | value: c, 175 | columns: [ 176 | {label: c.title}, 177 | {label: (c.emails ? c.emails[0] : '')} 178 | ] 179 | }); 180 | }); 181 | 182 | var view = this._find('View'); 183 | view.clear().add(rows); 184 | } 185 | 186 | editContact(contact) { 187 | if ( !contact ) { 188 | return; 189 | } 190 | console.debug('editContact', contact); 191 | } 192 | 193 | deleteContact(contact) { 194 | if ( !contact ) { 195 | return; 196 | } 197 | console.debug('deleteContact', contact); 198 | } 199 | 200 | createContact() { 201 | console.debug('createContact'); 202 | } 203 | 204 | activateContact(contact) { 205 | if ( !contact && !contact.emails.length ) { 206 | return; 207 | } 208 | 209 | console.debug('activateContact', contact); 210 | 211 | Application.create('ApplicationGmail', { 212 | action: 'create', 213 | title: contact.title, 214 | email: contact.emails[0] 215 | }); 216 | } 217 | 218 | getSelectedContact() { 219 | var view = this.contactView; 220 | if ( view ) { 221 | return view.getSelected(); 222 | } 223 | 224 | return false; 225 | } 226 | } 227 | 228 | class ApplicationGoogleContacts extends Application { 229 | 230 | constructor(args, metadata) { 231 | super('ApplicationGoogleContacts', args, metadata); 232 | } 233 | 234 | init(settings, metadata) { 235 | super.init(...arguments); 236 | 237 | this._addWindow(new ApplicationGoogleContactsWindow(this, metadata)); 238 | this.sync(); 239 | } 240 | 241 | sync() { 242 | var mainWindow = this.__windows[0]; 243 | 244 | pullContacts(mainWindow, function(error, result) { 245 | if ( error ) { 246 | OSjs.error('Google Calendar Error', 'An error occured while getting contacts', error); 247 | } 248 | 249 | if ( mainWindow ) { 250 | if ( result ) { 251 | mainWindow.renderGoogleContacts(result); 252 | } 253 | } 254 | }); 255 | } 256 | } 257 | 258 | OSjs.Applications.ApplicationGoogleContacts = ApplicationGoogleContacts; 259 | 260 | -------------------------------------------------------------------------------- /VNC/scheme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |   30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | LBL_CONNECT 55 | LBL_CANCEL 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | Host: 66 | 67 | 68 | localhost 69 | 70 | 71 | 72 | Port: 73 | 74 | 75 | 9501 76 | 77 | 78 | 79 | Password: 80 | 81 | 82 | 83 | 84 | 85 | 86 | Token: 87 | 88 | 89 | 90 | 91 | 92 | 93 | Connection Path: 94 | 95 | 96 | 97 | 98 | 99 | 100 | Repeater ID: 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | Scaling Mode: 112 | 113 | 114 | 115 | None 116 | Local scaling 117 | Local downscaling 118 | Remote resizing 119 | 120 | 121 | 122 | 123 | Repeater ID: 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Encrypt: 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | True Color: 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | Local Cursor: 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | Clip To Window: 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | Shared Mode: 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | View Only: 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /GoogleMail/window-main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | import {MessageStates} from './mailer.js'; 31 | const Window = OSjs.require('core/window'); 32 | const Theme = OSjs.require('core/theme'); 33 | const Menu = OSjs.require('gui/menu'); 34 | const DOM = OSjs.require('utils/dom'); 35 | 36 | function _sort(a, b) { 37 | var keyA = a.title; 38 | var keyB = b.title; 39 | return (keyA < keyB) ? -1 : (keyA < keyB ? -1 : (keyA > keyB ? 1 : 0)); 40 | } 41 | 42 | function resolveFolders(folders, current) { 43 | var fiters = []; 44 | var items = []; 45 | 46 | folders.forEach(function(i) { 47 | if ( i.name.match(/^CATEGORY_/) ) { 48 | return; 49 | } 50 | 51 | if ( i.name.match(/^(\[Imap\]|CHAT|DRAFT|UNREAD|INBOX|TRASH|IMPORTANT|STARRED|SPAM|SENT)/) ) { 52 | items.push({ 53 | label: i.name, 54 | value: i.id, 55 | icon: Theme.getIcon('places/folder.png') 56 | }); 57 | } else { 58 | fiters.push({ 59 | label: i.name, 60 | value: i.id, 61 | icon: Theme.getIcon('places/folder.png') 62 | }); 63 | } 64 | }); 65 | 66 | items.sort(_sort); 67 | fiters.sort(_sort); 68 | 69 | fiters.unshift({ 70 | label: '[Google]', 71 | value: null, 72 | icon: Theme.getIcon('places/folder.png'), 73 | entries: items 74 | }); 75 | 76 | return fiters; 77 | } 78 | 79 | function toggleReadState(id, messageView) { 80 | if ( messageView ) { 81 | var el = messageView.get('entry', id, 'id'); 82 | if ( el ) { 83 | DOM.$removeClass(el, 'unread'); 84 | } 85 | } 86 | } 87 | 88 | function toggleMessageState(id, state, row, messageView) { 89 | if ( messageView ) { 90 | var states = MessageStates; 91 | if ( row ) { 92 | DOM.$removeClass(row, 'unread'); 93 | DOM.$removeClass(row, 'starred'); 94 | 95 | if ( state & states.UNREAD ) { 96 | DOM.$addClass(row, 'unread'); 97 | } 98 | if ( state & states.STARRED ) { 99 | DOM.$addClass(row, 'starred'); 100 | } 101 | } 102 | } 103 | } 104 | 105 | function createMoveMenu(app, ev) { 106 | function resolveMenu(folders) { 107 | var fiters = []; 108 | var items = []; 109 | 110 | function cb(id) { 111 | app.moveMessage(self, id, function() { 112 | app.sync(self); 113 | }); 114 | } 115 | 116 | folders.forEach(function(i) { 117 | if ( i.name.match(/^CATEGORY_/) ) { 118 | return; 119 | } 120 | 121 | if ( i.name.match(/^(\[Imap\]|CHAT|DRAFT|UNREAD|INBOX|TRASH|IMPORTANT|STARRED|SPAM|SENT)/) ) { 122 | items.push({ 123 | title: i.name, 124 | onClick: function() { 125 | cb(i.id); 126 | } 127 | }); 128 | } else { 129 | fiters.push({ 130 | title: i.name, 131 | onClick: function() { 132 | cb(i.id); 133 | } 134 | }); 135 | } 136 | }); 137 | 138 | items.sort(_sort); 139 | fiters.sort(_sort); 140 | 141 | fiters.unshift({ 142 | title: '[Google]', 143 | menu: items 144 | }); 145 | 146 | return fiters; 147 | } 148 | 149 | if ( app ) { 150 | app.getFolderCache(function(error, list) { 151 | setTimeout(function() { 152 | Menu.create(resolveMenu(list), {x: ev.clientX, y: ev.clientY}); 153 | }, 100); 154 | }); 155 | } 156 | } 157 | 158 | export default class ApplicationGmailWindow extends Window { 159 | constructor(app, metadata, settings) { 160 | super('ApplicationGmailWindow', { 161 | icon: metadata.icon, 162 | title: metadata.name + ' v0.5', 163 | min_width: 500, 164 | min_height: 400, 165 | width: 600, 166 | height: 400 167 | }, app); 168 | } 169 | 170 | init(wmRef, app) { 171 | var root = super.init(...arguments); 172 | var self = this; 173 | 174 | // Load and set up scheme (GUI) here 175 | this._render('GmailWindow', require('osjs-scheme-loader!scheme.html')); 176 | 177 | var messageView; 178 | 179 | var menuMap = { 180 | MenuClose: function() { 181 | self._close(); 182 | }, 183 | MenuFolderNew: function() { 184 | app.createFolder(self); 185 | }, 186 | MenuFolderRename: function(ev) { 187 | const sel = ev.detail; 188 | app.renameFolder(self, sel.id, sel.title); 189 | }, 190 | MenuFolderDelete: function(ev) { 191 | const sel = ev.detail; 192 | app.removeFolder(self, sel.id, sel.title); 193 | }, 194 | MessageNew: function() { 195 | app.openMessageWindow(); 196 | }, 197 | MessageReply: function() { 198 | if ( app && app.currentMessage ) { 199 | self._toggleLoading(true); 200 | app.replyToMessage(app.currentMessage, function() { 201 | if ( self ) { 202 | self._toggleLoading(false); 203 | } 204 | }); 205 | } 206 | }, 207 | MessageDelete: function() { 208 | if ( app && app.currentMessage ) { 209 | app.removeMessage(self, function() { 210 | app.sync(self); 211 | }); 212 | } 213 | }, 214 | MessageMove: function(ev) { 215 | if ( app && app.currentMessage ) { 216 | createMoveMenu(app, ev); 217 | } 218 | }, 219 | 220 | MarkRead: function() { 221 | app.markMessage('read', self, function(err, res, id, state) { 222 | if ( !err && res ) { 223 | var row = messageView.get('entry', id, 'id'); 224 | toggleMessageState(id, state, row, messageView); 225 | } 226 | }); 227 | }, 228 | 229 | MarkUnread: function() { 230 | app.markMessage('unread', self, function(err, res, id, state) { 231 | if ( !err && res ) { 232 | var row = messageView.get('entry', id, 'id'); 233 | toggleMessageState(id, state, row, messageView); 234 | } 235 | }); 236 | }, 237 | 238 | MarkStarred: function() { 239 | app.markMessage('starred', self, function(err, res, id, state) { 240 | if ( !err && res ) { 241 | var row = messageView.get('entry', id, 'id'); 242 | toggleMessageState(id, state, row, messageView); 243 | } 244 | }); 245 | }, 246 | 247 | MarkUnstar: function() { 248 | app.markMessage('unstar', self, function(err, res, id, state) { 249 | if ( !err && res ) { 250 | var row = messageView.get('entry', id, 'id'); 251 | toggleMessageState(id, state, row, messageView); 252 | } 253 | }); 254 | }, 255 | 256 | OpenSettings: function() { 257 | if ( app ) { 258 | app.openSettingsWindow(self); 259 | } 260 | }, 261 | 262 | ForceUpdate: function() { 263 | if ( app ) { 264 | app.forceSync(self); 265 | } 266 | }, 267 | 268 | GetMessages: function() { 269 | app.sync(self); 270 | }, 271 | 272 | OpenContacts: function() { 273 | app.openContacts(); 274 | } 275 | 276 | }; 277 | 278 | function menuEvent(ev) { 279 | if ( menuMap[ev.detail.id] ) { 280 | menuMap[ev.detail.id](); 281 | } 282 | } 283 | 284 | this._find('SubmenuFile').on('select', menuEvent); 285 | var folderMenu = this._find('SubmenuFolder').on('select', menuEvent); 286 | var messageMenu = this._find('SubmenuMessage').on('select', menuEvent); 287 | this._find('SubmenuMarkAs').on('select', menuEvent); 288 | this._find('SubmenuOptions').on('select', menuEvent); 289 | this._find('Menubar').on('select', menuEvent); 290 | 291 | this._find('Folders').on('activate', function(ev) { 292 | if ( ev.detail.entries && ev.detail.entries.length ) { 293 | var item = ev.detail.entries[0].data; 294 | app.setFolder(item); 295 | } 296 | }).on('contextmenu', function(ev) { 297 | folderMenu.show(ev); 298 | }); 299 | 300 | messageView = this._find('Messages').on('select', function(ev) { 301 | if ( ev.detail.entries && ev.detail.entries.length ) { 302 | var item = ev.detail.entries[0].data; 303 | app.setMessage(item.id); 304 | } 305 | }).on('render', function(ev) { 306 | var row = ev.detail.element; 307 | var iter = ev.detail.data; 308 | toggleMessageState(iter.id, iter.state, row, messageView); 309 | }).on('activate', function(ev) { 310 | if ( ev.detail.entries && ev.detail.entries.length ) { 311 | var item = ev.detail.entries[0].data; 312 | 313 | app.openMessageWindow({ 314 | id: item.id, 315 | subject: item.subject, 316 | sender: item.sender, 317 | onRecieved: function() { 318 | toggleReadState(item.id, messageView); 319 | } 320 | }); 321 | } 322 | }).on('contextmenu', function(ev) { 323 | messageMenu.show(ev); 324 | }); 325 | 326 | return root; 327 | } 328 | 329 | updateStatusBar(args) { 330 | args = args || {}; 331 | 332 | var percentage = typeof args.progress === 'undefined' ? -1 : args.progress; 333 | var message = args.message || ''; 334 | 335 | var statusbar = this._find('Statusbar'); 336 | statusbar.set('value', message); 337 | 338 | var progressBar = this._find('Progressbar'); 339 | progressBar.set('value', percentage); 340 | if ( percentage < 0 ) { 341 | progressBar.hide(); 342 | } else { 343 | progressBar.show(); 344 | } 345 | } 346 | 347 | updateTitleBar(args) { 348 | args = args || {}; 349 | this._setTitle(this._opts.title + ' - ' + args.name); 350 | } 351 | 352 | renderMessages(messages, current) { 353 | var list = []; 354 | (messages || []).forEach(function(i) { 355 | list.push({ 356 | value: i, 357 | columns: [ 358 | {label: ''}, 359 | {label: ''}, 360 | {label: i.sender}, 361 | {label: i.subject}, 362 | {label: i.date} 363 | ] 364 | }); 365 | }); 366 | 367 | var view = this._find('Messages'); 368 | view.clear(); 369 | view.add(list); 370 | view.set('value', current); 371 | } 372 | 373 | renderFolders(folders, current) { 374 | var view = this._find('Folders'); 375 | view.clear(); 376 | view.add(resolveFolders(folders, current)); 377 | view.set('value', current); 378 | } 379 | 380 | setSelectedFolder(id) { 381 | var view = this._find('Folders'); 382 | view.set('value', id); 383 | } 384 | 385 | } 386 | -------------------------------------------------------------------------------- /VNC/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Cloud/Web Desktop Platform 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | // TODO: Clipboard 31 | // TODO: Clip cursor 32 | 33 | import RFB from './vendor/noVNC/core/rfb.js'; 34 | const Window = OSjs.require('core/window'); 35 | const Application = OSjs.require('core/application'); 36 | 37 | class ApplicationVNCWindow extends Window { 38 | constructor(app, metadata) { 39 | super('ApplicationVNCWindow', { 40 | icon: metadata.icon, 41 | title: metadata.name, 42 | width: 640, 43 | height: 480, 44 | key_capture: true // IMPORTANT 45 | }, app); 46 | 47 | this.connectionDialog = null; 48 | this.lastKeyboardinput = null; 49 | this.resizeTimeout = null; 50 | } 51 | 52 | init(wmRef, app) { 53 | var root = super.init(...arguments); 54 | var self = this; 55 | 56 | // Load and set up scheme (GUI) here 57 | this._render('VNCWindow', require('osjs-scheme-loader!scheme.html')); 58 | 59 | var canvas = this._find('Canvas').$element.children[0]; 60 | var menuMap = { 61 | MenuConnect: function() { 62 | app.connect(self, canvas); 63 | }, 64 | MenuDisconnect: function() { 65 | app.disconnect(self); 66 | }, 67 | MenuClose: function() { 68 | self._close(); 69 | } 70 | }; 71 | 72 | this._find('SubmenuFile').on('select', function(ev) { 73 | if ( menuMap[ev.detail.id] ) { 74 | menuMap[ev.detail.id](); 75 | } 76 | }); 77 | 78 | this._on('blur', () => app.toggleFocus(false)); 79 | this._on('focus', () => app.toggleFocus(true)); 80 | 81 | return root; 82 | } 83 | 84 | destroy() { 85 | this.resizeTimeout = clearTimeout(this.resizeTimeout); 86 | return super.destroy(...arguments); 87 | } 88 | 89 | _resize() { 90 | if ( super._resize(...arguments) ) { 91 | if ( this._app && this._app.rfb ) { 92 | var rfb = this._app.rfb; 93 | var display = rfb.get_display(); 94 | 95 | if ( display ) { 96 | var scaletype = this._app.connectionSettings.scaling; 97 | var canvas = this._find('Canvas').$element; 98 | 99 | var size = { 100 | w: canvas.offsetWidth, 101 | h: canvas.offsetHeight 102 | }; 103 | 104 | if ( scaletype === 'remote' ) { 105 | this.resizeTimeout = clearTimeout(this.resizeTimeout); 106 | this.resizeTimeout = setTimeout(function() { 107 | if ( display ) { 108 | display.set_maxWidth(size.w); 109 | display.set_maxHeight(size.h); 110 | rfb.setDesktopSize(size.w, size.h); 111 | } 112 | }, 500); 113 | 114 | } else if ( scaletype === 'scale' || scaletype === 'downscale' ) { 115 | var downscaleOnly = scaletype === 'downscale'; 116 | var scaleRatio = display.autoscale(size.w, size.h, downscaleOnly); 117 | rfb.get_mouse().set_scale(scaleRatio); 118 | } 119 | } 120 | } 121 | return true; 122 | } 123 | return false; 124 | } 125 | 126 | /* 127 | _onKeyEvent(ev, type) { 128 | if ( this._app && type === 'keydown' ) { 129 | //this._app.sendKey(ev.keyCode || ev.which); 130 | } 131 | }; 132 | */ 133 | 134 | setStatus(state, msg) { 135 | var value = state; 136 | if ( msg ) { 137 | value += ': ' + msg; 138 | } 139 | 140 | var statusBar = this._find('Statusbar'); 141 | if ( statusBar ) { 142 | statusBar.set('value', value); 143 | } 144 | } 145 | 146 | openConnectionDialog(cb) { 147 | var self = this; 148 | 149 | if ( this.connectionDialog ) { 150 | this.connectionDialog._focus(); 151 | return; 152 | } 153 | 154 | var w = new ApplicationVNCDialog(this._app, this._app.__metadata, function(btn, conn) { 155 | self.connectionDialog = null; 156 | cb(conn); 157 | }); 158 | this.connectionDialog = this._addChild(w, true, true); 159 | } 160 | 161 | } 162 | 163 | class ApplicationVNCDialog extends Window { 164 | constructor(app, metadata, callback) { 165 | super('ApplicationVNCDialog', { 166 | icon: metadata.icon, 167 | title: metadata.name, 168 | width: 450, 169 | height: 400, 170 | allow_resize: false, 171 | allow_maximize: false 172 | }, app); 173 | 174 | this.callback = callback || function() {}; 175 | } 176 | 177 | init(wmRef, app) { 178 | var root = super.init(...arguments); 179 | var self = this; 180 | 181 | // Load and set up scheme (GUI) here 182 | this._render('VNCDialog', require('osjs-scheme-loader!scheme.html')); 183 | 184 | function getSettings() { 185 | return { 186 | host: self._find('InputHost').get('value'), 187 | port: self._find('InputPort').get('value'), 188 | token: self._find('InputToken').get('value'), 189 | password: self._find('InputPassword').get('value'), 190 | path: self._find('InputPath').get('value'), 191 | repeaterid: self._find('InputRepeaterID').get('value'), 192 | encrypt: self._find('ToggleEncrypt').get('value'), 193 | truecolor: self._find('ToggleTrueColor').get('value'), 194 | shared: self._find('ToggleSharedMode').get('value'), 195 | viewonly: self._find('ToggleViewOnly').get('value'), 196 | localcursor: self._find('ToggleLocalCursor').get('value'), 197 | scaling: self._find('InputScalingMode').get('value') 198 | }; 199 | } 200 | 201 | this._find('InputHost').set('value', app.connectionSettings.host || 'localhost'); 202 | this._find('InputPort').set('value', app.connectionSettings.port || 5900); 203 | this._find('InputToken').set('value', app.connectionSettings.token || ''); 204 | this._find('InputPassword').set('value', app.connectionSettings.password || ''); 205 | this._find('InputPath').set('value', app.connectionSettings.path || 'websockify'); 206 | this._find('InputRepeaterID').set('value', app.connectionSettings.repeaterid || ''); 207 | this._find('ToggleEncrypt').set('value', app.connectionSettings.encrypt); 208 | this._find('ToggleTrueColor').set('value', app.connectionSettings.truecolor); 209 | this._find('ToggleSharedMode').set('value', app.connectionSettings.shared); 210 | this._find('ToggleViewOnly').set('value', app.connectionSettings.viewonly); 211 | this._find('ToggleLocalCursor').set('value', app.connectionSettings.localcursor); 212 | this._find('InputScalingMode').set('value', app.connectionSettings.scaling || 'off'); 213 | 214 | this._find('ButtonOK').on('click', function() { 215 | self._close('ok', getSettings()); 216 | }); 217 | 218 | this._find('ButtonCancel').on('click', function() { 219 | self._close(); 220 | }); 221 | 222 | return root; 223 | } 224 | 225 | _close(button, result) { 226 | this.callback(button, result); 227 | return super._close(...arguments); 228 | } 229 | 230 | } 231 | 232 | class ApplicationVNC extends Application { 233 | constructor(args, metadata) { 234 | super('ApplicationVNC', args, metadata); 235 | 236 | this.rfb = null; 237 | this.connectionState = null; 238 | this.connectionSettings = { 239 | host: 'localhost', 240 | port: 5901, 241 | token: '', 242 | path: 'websockify', 243 | repeaterid: '', 244 | encrypt: window.location.protocol === 'https:', 245 | truecolor: true, 246 | localcursor: true, 247 | shared: true, 248 | viewonly: false, 249 | password: '', 250 | scaling: 'off' 251 | }; 252 | } 253 | 254 | destroy() { 255 | this.disconnect(); 256 | return super.destroy(...arguments); 257 | } 258 | 259 | init(settings, metadata) { 260 | var self = this; 261 | super.init(...arguments); 262 | 263 | if ( settings && typeof settings.lastConnection === 'object' ) { 264 | Object.keys(settings.lastConnection).forEach(function(k) { 265 | self.connectionSettings[k] = settings.lastConnection[k]; 266 | }); 267 | } 268 | 269 | this._addWindow(new ApplicationVNCWindow(this, metadata)); 270 | } 271 | 272 | onUpdateState(rfb, state, oldstate, msg) { 273 | this.connectionState = state; 274 | var win = this._getMainWindow(); 275 | if ( !win ) { 276 | return; 277 | } 278 | 279 | win.setStatus(state, msg); 280 | win._focus(true); 281 | } 282 | 283 | onXvpInit(ver) { 284 | } 285 | 286 | onClipboard(rfb, text) { 287 | } 288 | 289 | onFBUComplete() { 290 | this.rfb.set_onFBUComplete(function() { }); 291 | } 292 | 293 | onFBResize(rfb, width, height) { 294 | var win = this._getMainWindow(); 295 | if ( win ) { 296 | win._resize(width + 40, height + 80, true); // FIXME 297 | } 298 | } 299 | 300 | onDesktopName(rfb, name) { 301 | var win = this._getMainWindow(); 302 | if ( win ) { 303 | win._setTitle(name, true); 304 | } 305 | } 306 | 307 | connect(win, canvas) { 308 | var self = this; 309 | 310 | function initRFB() { 311 | self.rfb = new RFB({ 312 | target: canvas, 313 | onUpdateState: function() { 314 | self.onUpdateState.apply(self, arguments); 315 | }, 316 | onXvpInit: function() { 317 | self.onXvpInit.apply(self, arguments); 318 | }, 319 | onClipboard: function() { 320 | self.onClipboard.apply(self, arguments); 321 | }, 322 | onFBUComplete: function() { 323 | self.onFBUComplete.apply(self, arguments); 324 | }, 325 | onFBResize: function() { 326 | self.onFBResize.apply(self, arguments); 327 | }, 328 | onDesktopName: function() { 329 | self.onDesktopName.apply(self, arguments); 330 | } 331 | }); 332 | return true; 333 | } 334 | 335 | win.openConnectionDialog(function(conn) { 336 | console.log('ApplicationVNC::connect()', canvas, conn); 337 | 338 | if ( conn ) { 339 | self.disconnect(win); 340 | 341 | self._setSetting('lastConnection', conn, true); 342 | 343 | if ( initRFB() ) { 344 | Object.keys(conn).forEach(function(k) { 345 | self.connectionSettings[k] = conn[k]; 346 | }); 347 | 348 | self.rfb.set_encrypt(conn.encrypt); 349 | //self.rfb.set_true_color(conn.truecolor); 350 | self.rfb.set_local_cursor(conn.localcursor); 351 | self.rfb.set_shared(conn.shared); 352 | self.rfb.set_view_only(conn.viewonly); 353 | self.rfb.set_repeaterID(conn.repeaterid); 354 | 355 | self.rfb.connect(conn.host, conn.port, conn.password, conn.path); 356 | } 357 | } 358 | }); 359 | } 360 | 361 | disconnect(win) { 362 | if ( this.rfb ) { 363 | this.rfb.disconnect(); 364 | this.rfb.set_onFBUComplete(this.FBUComplete); 365 | } 366 | this.rfb = null; 367 | } 368 | 369 | toggleFocus(focus) { 370 | if ( this.rfb ) { 371 | this.rfb.get_keyboard().set_focused(focus); 372 | this.rfb.get_mouse().set_focused(focus); 373 | } 374 | } 375 | 376 | sendKey(key) { 377 | if ( this.rfb ) { 378 | this.rfb.sendKey(key); 379 | } 380 | } 381 | 382 | } 383 | 384 | OSjs.Applications.ApplicationVNC = ApplicationVNC; 385 | -------------------------------------------------------------------------------- /GoogleMail/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | import MainWindow from './window-main.js'; 31 | import MessageWindow from './window-message.js'; 32 | import SettingsWindow from './window-settings.js'; 33 | import {GoogleMail} from './mailer.js'; 34 | 35 | const Utils = OSjs.require('utils/misc'); 36 | const Application = OSjs.require('core/application'); 37 | 38 | function safeName(str) { 39 | return (str || '').replace(/[^A-z0-9\s\+\-_\&]/g, ''); 40 | } 41 | 42 | class ApplicationGmail extends Application { 43 | constructor(args, metadata) { 44 | super('ApplicationGmail', args, metadata); 45 | 46 | this.mainWindow = null; 47 | this.currentFolder = 'INBOX'; 48 | this.currentMessage = null; 49 | } 50 | 51 | destroy() { 52 | if ( this.mailer ) { 53 | this.mailer.destroy(); 54 | } 55 | return super.destroy(...arguments); 56 | } 57 | 58 | init(settings, metadata) { 59 | var self = this; 60 | 61 | super.init(...arguments); 62 | 63 | var defaultSettings = metadata.settings || {}; 64 | settings = Utils.argumentDefaults(settings, defaultSettings); 65 | 66 | this.mainWindow = this._addWindow(new MainWindow(this, metadata, settings)); 67 | if ( !this._getArgument('__resume__') ) { 68 | var action = self._getArgument('action'); 69 | if ( action ) { 70 | self.handleAction(self._getArguments()); 71 | } 72 | } 73 | 74 | this.mailer = new GoogleMail({ 75 | maxPages: settings.maxPages, 76 | onAbortStart: function() { 77 | if ( self.mainWindow ) { 78 | self.mainWindow._toggleLoading(true); 79 | } 80 | }, 81 | onAbortEnd: function() { 82 | if ( self.mainWindow ) { 83 | self.mainWindow._toggleLoading(false); 84 | } 85 | } 86 | }, function(error, result) { 87 | if ( self.mainWindow && self.mailer.user ) { 88 | self.mainWindow.updateTitleBar(self.mailer.user); 89 | } 90 | self.sync(); 91 | }); 92 | 93 | this._on('destroyWindow', function(obj) { 94 | if ( obj._name === 'ApplicationGmailWindow' ) { 95 | self.destroy(); 96 | } 97 | }); 98 | this._on('attention', function(args) { 99 | self.handleAction(args); 100 | }); 101 | } 102 | 103 | handleAction(args) { 104 | args = args || {}; 105 | if ( args.action ) { 106 | if ( args.action === 'create' && args.email ) { 107 | this.openMessageWindow({to: args.email}); 108 | } 109 | } 110 | } 111 | 112 | openMessageWindow(args) { 113 | this._addWindow(new MessageWindow(this, this.__metadata, args)); 114 | } 115 | 116 | openSettingsWindow() { 117 | if ( !this.mailer ) { 118 | return; 119 | } 120 | 121 | var win = this._getWindowByName('ApplicationGmailSettingsWindow'); 122 | if ( win ) { 123 | win._restore(); 124 | return; 125 | } 126 | 127 | var maxPages = this.mailer.args.maxPages; 128 | win = this._addWindow(new SettingsWindow(this, this.__metadata, maxPages)); 129 | win._focus(); 130 | } 131 | 132 | openContacts(win) { 133 | Application.create('ApplicationGoogleContacts'); 134 | } 135 | 136 | replyToMessage(id, cb) { 137 | var self = this; 138 | 139 | this.mailer.recieveMessage({ 140 | id: id, 141 | returnFull: true 142 | }, function(error, parsed) { 143 | if ( parsed && parsed.data ) { 144 | self.openMessageWindow({ 145 | to: parsed.data.sender, 146 | subject: 'RE: ' + parsed.data.subject, 147 | message: 'TODO' 148 | }); 149 | } 150 | 151 | cb(); 152 | }); 153 | } 154 | 155 | downloadAttachment(win, att) { 156 | if ( !this.mailer ) { 157 | return; 158 | } 159 | this.mailer.downloadAttachment({ 160 | id: att.messageId, 161 | mime: att.mime, 162 | filename: att.filename, 163 | attachmentId: att.id 164 | }); 165 | } 166 | 167 | moveMessage(win, moveTo, cb) { 168 | cb = cb || function() {}; 169 | if ( !this.mailer ) { 170 | return; 171 | } 172 | 173 | if ( win ) { 174 | win._toggleLoading(true); 175 | } 176 | 177 | var id = this.currentMessage; 178 | var folder = this.currentFolder; 179 | this.mailer.move({ 180 | id: id, 181 | to: moveTo, 182 | from: folder 183 | }, function(error, result) { 184 | if ( win ) { 185 | win._toggleLoading(false); 186 | } 187 | cb(); 188 | }); 189 | } 190 | 191 | markMessage(markAs, win, cb) { 192 | cb = cb || function() {}; 193 | if ( !this.mailer ) { 194 | return; 195 | } 196 | 197 | if ( win ) { 198 | win._toggleLoading(true); 199 | } 200 | 201 | var id = this.currentMessage; 202 | this.mailer.markMessage({ 203 | markAs: markAs, 204 | id: id 205 | }, function(error, result, id, state) { 206 | if ( win ) { 207 | win._toggleLoading(false); 208 | } 209 | cb(error, result, id, state); 210 | }); 211 | } 212 | 213 | removeMessage(win, cb) { 214 | cb = cb || function() {}; 215 | if ( !this.mailer ) { 216 | return; 217 | } 218 | 219 | if ( win ) { 220 | win._toggleLoading(true); 221 | } 222 | 223 | var id = this.currentMessage; 224 | this.mailer.removeMessage({ 225 | id: id 226 | }, function(error, result) { 227 | if ( win ) { 228 | win._toggleLoading(false); 229 | } 230 | cb(); 231 | }); 232 | } 233 | 234 | recieveMessage(win, id) { 235 | if ( !this.mailer ) { 236 | return; 237 | } 238 | 239 | if ( win ) { 240 | win.onPrepareReceive(); 241 | } 242 | 243 | this.mailer.recieveMessage({ 244 | id: id, 245 | markRead: true 246 | }, function(error, result) { 247 | if ( win ) { 248 | win.onEndReceive(result); 249 | } 250 | }); 251 | } 252 | 253 | sendMessage(win, to, subject, message) { 254 | if ( !this.mailer ) { 255 | return; 256 | } 257 | 258 | this.mailer.sendMessage({ 259 | to: to, 260 | subject: subject, 261 | message: message 262 | }, function(error, result) { 263 | if ( win && result ) { 264 | win._close(); 265 | } 266 | }); 267 | } 268 | 269 | getFolderCache(cb) { 270 | this.mailer.getFolders({cache: true}, function(error, result) { 271 | cb(false, result || []); 272 | }); 273 | } 274 | 275 | forceSync(win) { 276 | win = win || this.mainWindow; 277 | 278 | if ( !this.mailer ) { 279 | return; 280 | } 281 | 282 | this.sync(win, true); 283 | } 284 | 285 | sync(win, force, onlyFolders) { 286 | force = force === true; 287 | 288 | var self = this; 289 | 290 | win = win || this.mainWindow; 291 | 292 | if ( !this.mailer ) { 293 | return; 294 | } 295 | 296 | var folderId = this.currentFolder; 297 | this.mailer.getFolders({force: force}, function(error, result) { 298 | if ( win && result ) { 299 | win.renderFolders(result, folderId); 300 | } 301 | 302 | if ( onlyFolders ) { 303 | return; 304 | } 305 | 306 | self.mailer.getMessages({ 307 | force: force, 308 | folder: folderId, 309 | onPageAdvance: function(json) { 310 | if ( win ) { 311 | var per = json.pageCurrent / json.pagesTotal * 100; 312 | var msg = 'Fetching page ' + json.pageCurrent; 313 | if ( force ) { 314 | msg += ' (Forced)'; 315 | } 316 | win.updateStatusBar({message: msg, progress: per}); 317 | 318 | // This will make it so listview is not cleared when refreshing 319 | // a folder 320 | if ( json.refresh && json.pageCurrent < json.pagesTotal ) { 321 | return; 322 | } 323 | 324 | var messages = json.messages || []; 325 | win.renderMessages(messages, self.currentMessage); 326 | } 327 | }, 328 | onMessageQueue: function(json) { 329 | if ( win ) { 330 | var per = json.messageCurrent / json.messagesTotal * 100; 331 | var msg = Utils.format('Fetching message (batch {0}/{1}, message {2}/{3}, index {4}) in {5}', 332 | json.pageCurrent, 333 | json.pagesTotal, 334 | json.messageCurrent, 335 | json.messagesTotal, 336 | json.messageIndex, 337 | json.folder 338 | ); 339 | if ( force ) { 340 | msg += ' (Forced)'; 341 | } 342 | 343 | win.updateStatusBar({message: msg, progress: per}); 344 | } 345 | }, 346 | onStart: function() { 347 | if ( win ) { 348 | win.updateStatusBar({message: 'Fetching messages...'}); 349 | } 350 | }, 351 | onEnd: function() { 352 | if ( win ) { 353 | win.updateStatusBar(); 354 | } 355 | } 356 | }, function(error, result) { 357 | if ( win ) { 358 | win.renderMessages(result, self.currentMessage); 359 | } 360 | }); 361 | }); 362 | } 363 | 364 | createFolder(win) { 365 | var self = this; 366 | win = win || this.mainWindow; 367 | if ( !win || !this.mailer ) { 368 | return; 369 | } 370 | 371 | win._toggleDisabled(true); 372 | var msg = 'Enter a name for the folder'; 373 | this._createDialog('Input', [msg, '', function(btn, input) { 374 | if ( win ) { 375 | win._toggleDisabled(false); 376 | } 377 | if ( !input || btn !== 'ok' ) { 378 | return; 379 | } 380 | if ( win ) { 381 | win._toggleLoading(true); 382 | } 383 | input = safeName(input); 384 | 385 | self.mailer.createFolder({ 386 | name: input 387 | }, function() { 388 | if ( win ) { 389 | win._toggleLoading(false); 390 | } 391 | self.sync(null, false, true); 392 | }); 393 | }]); 394 | } 395 | 396 | renameFolder(win, id, title) { 397 | var self = this; 398 | win = win || this.mainWindow; 399 | if ( !win || !this.mailer ) { 400 | return; 401 | } 402 | 403 | win._toggleDisabled(true); 404 | var msg = 'Enter a name for the folder'; 405 | this._createDialog('Input', [msg, title, function(btn, input) { 406 | if ( win ) { 407 | win._toggleDisabled(false); 408 | } 409 | if ( !input || btn !== 'ok' ) { 410 | return; 411 | } 412 | if ( win ) { 413 | win._toggleLoading(true); 414 | } 415 | input = safeName(input); 416 | 417 | self.mailer.renameFolder({ 418 | id: id, 419 | name: input 420 | }, function() { 421 | if ( win ) { 422 | win._toggleLoading(false); 423 | } 424 | self.sync(null, false, true); 425 | }); 426 | }]); 427 | } 428 | 429 | removeFolder(win, id, title) { 430 | var self = this; 431 | win = win || this.mainWindow; 432 | if ( !win || !this.mailer ) { 433 | return; 434 | } 435 | 436 | win._toggleDisabled(true); 437 | var msg = 'Are you sure you want to delete this folder (' + title + ')?'; 438 | this._createDialog('Confirm', [msg, function(btn) { 439 | if ( win ) { 440 | win._toggleDisabled(false); 441 | } 442 | if ( btn !== 'ok' ) { 443 | return; 444 | } 445 | if ( win ) { 446 | win._toggleLoading(true); 447 | } 448 | 449 | self.mailer.removeFolder({id: id}, function() { 450 | if ( win ) { 451 | win._toggleLoading(false); 452 | } 453 | if ( self.currentFolder === id ) { 454 | self.setFolder('INBOX', true); 455 | return; 456 | } 457 | 458 | self.sync(null, false, true); 459 | }); 460 | }]); 461 | } 462 | 463 | setMessage(msg) { 464 | this.currentMessage = msg; 465 | } 466 | 467 | setFolder(folder, setUI) { 468 | if ( folder && folder !== this.currentFolder || setUI ) { 469 | this.currentFolder = folder; 470 | this.currentMessage = null; 471 | if ( setUI && this.mainWindow ) { 472 | this.mainWindow.setSelectedFolder(folder); 473 | } 474 | this.sync(); 475 | } 476 | } 477 | 478 | _setSetting(k, v, save, saveCallback) { 479 | var self = this; 480 | super._setSetting(k, v, save, function() { 481 | if ( self.mailer && k === 'maxPages' ) { 482 | self.mailer.setMaxPages(v); 483 | } 484 | (saveCallback || function() {}).apply(this, arguments); 485 | }); 486 | } 487 | } 488 | 489 | OSjs.Applications.ApplicationGmail = ApplicationGmail; 490 | -------------------------------------------------------------------------------- /Chat/main.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | import {Strophe, $pres, $msg, $iq} from 'strophe.js'; 31 | 32 | const ExtendedDate = OSjs.require('helpers/date'); 33 | const Utils = OSjs.require('utils/misc'); 34 | const Theme = OSjs.require('core/theme'); 35 | const Dialog = OSjs.require('core/dialog'); 36 | const Window = OSjs.require('core/window'); 37 | const Application = OSjs.require('core/application'); 38 | const Notification = OSjs.require('gui/notification'); 39 | 40 | // https://code.google.com/p/aap-etsiit-ugr/source/browse/trunk/2011/chat_xmpp/scripts/presencia.strophe.js?r=3008 41 | // http://strophe.im/strophejs/doc/ 42 | 43 | var iconMap = { 44 | 'busy': 'status/user-busy.png', 45 | 'away': 'status/user-busy.png', 46 | 'chat': 'status/user-available.png', 47 | 'online': 'status/user-available.png', 48 | 'offline': 'status/user-offline.png', 49 | 'invisible': 'status/user-invisible.png' 50 | }; 51 | 52 | function getDisplayName(contact) { 53 | if ( contact.vcard && contact.vcard.name ) { 54 | return contact.vcard.name + ' (' + contact.username + ')'; 55 | } 56 | return contact.username; 57 | } 58 | 59 | function getVcardImage(app, vcard) { 60 | vcard = vcard || {}; 61 | if ( vcard.photo && vcard.photo.type && vcard.photo.data ) { 62 | return Utils.format('data:{0};base64,{1}', vcard.photo.type, vcard.photo.data); 63 | } 64 | 65 | var url = app._getResource('user.png'); 66 | return url; 67 | } 68 | 69 | ///////////////////////////////////////////////////////////////////////////// 70 | // CLASSES 71 | ///////////////////////////////////////////////////////////////////////////// 72 | 73 | class StropheConnection { 74 | 75 | constructor(app) { 76 | this.presenseTimeout = null; 77 | this.app = app; 78 | this.bindings = {}; 79 | this.contacts = {}; 80 | this.connected = false; 81 | this.user = { 82 | id: -1, 83 | vcard: null 84 | }; 85 | 86 | this.connection = new Strophe.Connection('/http-bind-jabber/'); 87 | 88 | this.connection.rawInput = function(data) { 89 | }; 90 | 91 | this.connection.rawOutput = function(data) { 92 | }; 93 | 94 | var _vctimeout; 95 | var self = this; 96 | this.on('contacts', function(contacts, vcard) { 97 | if ( vcard ) { 98 | return; 99 | } 100 | 101 | Object.keys(self.contacts).forEach(function(i) { 102 | var c = self.contacts[i]; 103 | 104 | self.vcard(c.username, function() { 105 | clearTimeout(_vctimeout); 106 | _vctimeout = setTimeout(function() { 107 | self._trigger('contacts', self.contacts, true); 108 | }, 2000); 109 | }); 110 | }); 111 | }); 112 | } 113 | 114 | destroy() { 115 | this.disconnect(); 116 | this.connection = null; 117 | this.app = null; 118 | } 119 | 120 | on(k, f) { 121 | if ( typeof this.bindings[k] === 'undefined' ) { 122 | this.bindings[k] = []; 123 | } 124 | this.bindings[k].push(f); 125 | } 126 | 127 | _trigger(k) { 128 | var self = this; 129 | var args = Array.prototype.slice.call(arguments, 1); 130 | 131 | console.log('StropheConnection::_trigger()', k, args); 132 | (this.bindings[k] || []).forEach(function(f) { 133 | f.apply(self, args); 134 | }); 135 | } 136 | 137 | setStatus(s, text) { 138 | var pres = $pres({ 139 | from: this.user.id 140 | }); 141 | pres.c('show', s); 142 | if ( text ) { 143 | pres.c('status', text); 144 | } 145 | 146 | this._trigger('status', s); 147 | 148 | console.log('StropheConnection::setStatus()', s, text, pres); 149 | this.connection.send(pres.tree()); 150 | } 151 | 152 | disconnect() { 153 | console.log('StropheConnection::disconnect()'); 154 | 155 | if ( this.connected ) { 156 | this.connection.disconnect(); 157 | } 158 | 159 | this.connected = false; 160 | this.user.id = -1; 161 | this.user.vcard = null; 162 | 163 | this._trigger('disconnected'); 164 | } 165 | 166 | connect() { 167 | console.log('StropheConnection::connect()'); 168 | 169 | var self = this; 170 | var states = {}; 171 | 172 | states[Strophe.Status.ATTACHED] = { 173 | text: 'Attached', 174 | fn: function() { 175 | self._trigger('attached'); 176 | } 177 | }; 178 | states[Strophe.Status.AUTHENTICATING] = { 179 | text: 'Authenticating', 180 | fn: function() { 181 | self._trigger('authenticating'); 182 | } 183 | }; 184 | states[Strophe.Status.ERROR] = { 185 | text: 'Error', 186 | fn: function() { 187 | self._trigger('error'); 188 | } 189 | }; 190 | states[Strophe.Status.REDIRECT] = { 191 | text: 'Redirected', 192 | fn: function() { 193 | self._trigger('redirect'); 194 | } 195 | }; 196 | states[Strophe.Status.AUTHFAIL] = { 197 | text: 'Authentication Failure', 198 | fn: function() { 199 | self._onAuthFail(); 200 | } 201 | }; 202 | states[Strophe.Status.CONNECTED] = { 203 | text: 'Connected', 204 | fn: function() { 205 | self._onConnected(); 206 | } 207 | }; 208 | states[Strophe.Status.CONNECTING] = { 209 | text: 'Connecting', 210 | fn: function() { 211 | self._onConnecting(); 212 | } 213 | }; 214 | states[Strophe.Status.CONNFAIL] = { 215 | text: 'Connection Failure', 216 | fn: function() { 217 | self._onConnectionFail(); 218 | } 219 | }; 220 | states[Strophe.Status.DISCONNECTED] = { 221 | text: 'Disconnected', 222 | fn: function() { 223 | self._onDisconnected(); 224 | } 225 | }; 226 | states[Strophe.Status.DISCONNECTING] = { 227 | text: 'Disconnecting', 228 | fn: function() { 229 | self._onDisconnecting(); 230 | } 231 | }; 232 | 233 | this.disconnect(); 234 | 235 | function _connect(stat) { 236 | if ( states[stat] && states[stat].fn ) { 237 | states[stat].fn(); 238 | } 239 | } 240 | 241 | var settings = this.app._getSetting('account') || {}; 242 | if ( !settings.username || !settings.password ) { 243 | this.disconnect(); 244 | Dialog.create('Error', { 245 | title: 'Chat Error', 246 | error: 'Cannot connect without username or password' 247 | }, function() {}, this.app); 248 | return; 249 | } 250 | 251 | try { 252 | this.connection.restore(null, _connect); 253 | } catch ( e ) { 254 | console.warn('StropheConnection::connect()', 'Could not restore', e, e.stack); 255 | console.warn('reconnecting instead'); 256 | this.connection.connect(settings.username, settings.password, _connect); 257 | } 258 | } 259 | 260 | send(msg) { 261 | console.log('StropheConnection::send()', msg); 262 | 263 | var reply = $msg({to: msg.jid, from: this.user.id, type: 'chat'}) 264 | .cnode(Strophe.xmlElement('body', msg.message)).up() 265 | .c('active', {xmlns: 'http://jabber.org/protocol/chatstates'}); 266 | 267 | this.connection.send(reply.tree()); 268 | } 269 | 270 | vcard(jid, callback) { 271 | callback = callback || function() {}; 272 | 273 | console.log('StropheConnection::vcard()', jid); 274 | var username = jid.split('/')[0]; 275 | if ( this.contacts[username] && this.contacts[username].vcard ) { 276 | callback(this.contacts[username].vcard); 277 | return; 278 | } 279 | 280 | var self = this; 281 | var iq = $iq({type: 'get', to: jid, id: this.connection.getUniqueId('vCard')}) 282 | .c('vCard', {xmlns: 'vcard-temp'}).tree(); 283 | 284 | this.connection.sendIQ(iq, function(response) { 285 | console.log('StropheConnection::vcard()', '=>', response); 286 | 287 | var item = null; 288 | try { 289 | var elem = response.getElementsByTagName('vCard'); 290 | 291 | if ( elem.length ) { 292 | var fn = Strophe.getText(elem[0].getElementsByTagName('FN')[0]); 293 | var url = Strophe.getText(elem[0].getElementsByTagName('URL')[0]); 294 | var photo = null; 295 | 296 | try { 297 | var pel = elem[0].getElementsByTagName('PHOTO')[0]; 298 | photo = { 299 | type: Strophe.getText(pel.getElementsByTagName('TYPE')[0]), 300 | data: Strophe.getText(pel.getElementsByTagName('BINVAL')[0]) 301 | }; 302 | } catch ( e ) { 303 | } 304 | 305 | item = { 306 | name: fn, 307 | url: url, 308 | photo: photo 309 | }; 310 | 311 | if ( self.contacts[username] ) { 312 | self.contacts[username].vcard = item; 313 | } 314 | } 315 | } catch ( e ) { 316 | console.warn(e, e.stack); 317 | } 318 | 319 | callback(item); 320 | }); 321 | } 322 | 323 | _onConnectionFail() { 324 | this._trigger('connfail'); 325 | } 326 | 327 | _onAuthFail() { 328 | this._trigger('authfail'); 329 | } 330 | 331 | _onConnecting() { 332 | this._trigger('connecting'); 333 | } 334 | 335 | _onConnected() { 336 | var self = this; 337 | 338 | this.connected = true; 339 | this.user.id = this.connection.jid; 340 | 341 | this.connection.addHandler(function(pres) { 342 | return self._onPresence(pres); 343 | }, null, 'presence', null, null, null); 344 | 345 | this.connection.addHandler(function(msg) { 346 | return self._onMessage(msg); 347 | }, null, 'message', null, null, null); 348 | 349 | this.connection.addHandler(function(x) { 350 | return true; 351 | }, 'jabber:iq:roster', 'iq', 'set'); 352 | 353 | this.connection.send($pres().tree()); 354 | 355 | this.vcard(self.user.id, function(item) { 356 | self.user.vcard = item || null; 357 | 358 | self._trigger('connected'); 359 | }); 360 | 361 | this._trigger('status', 'chat'); 362 | } 363 | 364 | _onDisconnecting() { 365 | this._trigger('disconnecting'); 366 | } 367 | 368 | _onDisconnected() { 369 | this.connected = false; 370 | this.disconnect(); 371 | } 372 | 373 | _onPresence(pres) { 374 | var self = this; 375 | 376 | function parsePresence(pres) { 377 | var from = pres.getAttribute('from') || ''; 378 | var error = pres.querySelector('error'); 379 | if ( error ) { 380 | console.warn('StropheConnection::_onPrecense()', 'error', Strophe.getText(pres), pres); 381 | return null; 382 | } 383 | 384 | var contact = { 385 | id: from, 386 | vcard: null, 387 | group: null, 388 | photo: null, 389 | username: from.split('/')[0], // Contact 390 | account: pres.getAttribute('to'), // Myself 391 | type: pres.getAttribute('type'), 392 | state: 'offline', 393 | subscription: 'unavailable' 394 | }; 395 | 396 | if ( contact.account ) { 397 | var elems, test; 398 | 399 | //var showEl = pres.querySelector('show'); 400 | //var text = showEl ? Strophe.getText(showEl) : null; 401 | 402 | elems = pres.getElementsByTagName('show'); 403 | test = elems.length ? Strophe.getText(elems[0]) : ''; 404 | contact.state = test ? test : (contact.subscription === 'unavailable' ? 'offline' : 'chat'); 405 | 406 | elems = pres.getElementsByTagName('photo'); 407 | contact.photo = Strophe.getText(elems[0]) || null; 408 | 409 | return contact; 410 | } 411 | 412 | return null; 413 | } 414 | 415 | this._trigger('presence', pres); 416 | 417 | // This makes sure we collect all contacts before rendering etc. 418 | clearTimeout(this.presenseTimeout); 419 | 420 | try { 421 | var c = parsePresence(pres); 422 | if ( c ) { 423 | if ( this.contacts[c.username] ) { 424 | Object.keys(c).forEach(function(k) { 425 | if ( !self.contacts[c.username][k] ) { 426 | self.contacts[c.username][k] = c[k]; 427 | } 428 | }); 429 | } else { 430 | this.contacts[c.username] = c; 431 | } 432 | } 433 | } catch ( e ) { 434 | console.warn(e, e.stack); 435 | } 436 | 437 | this.presenseTimeout = setTimeout(function() { 438 | self._trigger('contacts', self.contacts); 439 | }, 2000); 440 | 441 | return true; 442 | } 443 | 444 | _onMessage(msg) { 445 | var self = this; 446 | 447 | try { 448 | var elems = msg.getElementsByTagName('body'); 449 | //var to = msg.getAttribute('to') || ''; 450 | var from = msg.getAttribute('from') || ''; 451 | //var type = msg.getAttribute('type') || ''; 452 | var jid = from.split('/')[0]; 453 | 454 | console.log('StropheConnection::_onMessage()', from, jid, msg); 455 | 456 | if ( elems.length ) { 457 | var message = Strophe.getText(elems[0]); 458 | this._trigger('message', { 459 | jid: jid, 460 | message: message 461 | }); 462 | } else { 463 | msg.children.forEach(function(child) { 464 | var tagName = child.tagName.toLowerCase(); 465 | if ( tagName === 'cha:composing' ) { 466 | self._trigger('compose', { 467 | jid: jid, 468 | state: true 469 | }); 470 | } else if ( tagName === 'cha:paused' ) { 471 | self._trigger('compose', { 472 | jid: jid, 473 | state: false 474 | }); 475 | } 476 | }); 477 | } 478 | } catch ( e ) { 479 | console.warn(e, e.stack); 480 | } 481 | 482 | return true; 483 | } 484 | } 485 | 486 | class ApplicationSettingsWindow extends Window { 487 | 488 | constructor(app, metadata) { 489 | super('ApplicationChatSettingsWindow', { 490 | icon: metadata.icon, 491 | title: metadata.name, 492 | allow_resize: false, 493 | allow_maximize: false, 494 | width: 500, 495 | height: 300 496 | }, app); 497 | } 498 | 499 | init(wmRef, app) { 500 | var root = super.init(...arguments); 501 | var self = this; 502 | 503 | // Load and set up scheme (GUI) here 504 | this._render('SettingsWindow', require('osjs-scheme-loader!scheme.html')); 505 | 506 | var acc = app._getSetting('account') || {}; 507 | var username = this._find('Username').set('value', acc.username || ''); 508 | var password = this._find('Password').set('value', acc.password || ''); 509 | 510 | this._find('ButtonSave').on('click', function() { 511 | app._setSetting('account', { 512 | username: username.get('value'), 513 | password: password.get('value') 514 | }, true); 515 | 516 | app.connection.connect(); 517 | 518 | self._close(); 519 | }); 520 | 521 | this._find('ButtonClose').on('click', function() { 522 | self._close(); 523 | }); 524 | 525 | return root; 526 | } 527 | } 528 | 529 | class ApplicationConversationWindow extends Window { 530 | 531 | constructor(id, app, metadata) { 532 | super('ApplicationConversationWindow_' + String(id), { 533 | tag: 'ApplicationConversationWindow', 534 | icon: metadata.icon, 535 | title: metadata.name, 536 | width: 400, 537 | height: 400 538 | }, app); 539 | 540 | this._id = id; 541 | this._timeout = null; 542 | } 543 | 544 | init(wmRef, app) { 545 | var root = super.init(...arguments); 546 | var self = this; 547 | 548 | // Load and set up scheme (GUI) here 549 | this._render('ConversationWindow', require('osjs-scheme-loader!scheme.html')); 550 | 551 | this._find('Input').on('enter', function(ev) { 552 | var msg = { 553 | jid: self._id, 554 | message: ev.detail 555 | }; 556 | 557 | app.connection.send(msg); 558 | self.message(msg, null, true); 559 | }); 560 | 561 | return root; 562 | } 563 | 564 | message(msg, jid, myself) { 565 | var username = (jid || '').split('/')[0]; 566 | var contact = jid ? this._app.connection.contacts[username] : null; 567 | var now = new ExtendedDate(); 568 | var text = msg.message; 569 | 570 | var root = this._find('Conversation'); 571 | var container = document.createElement('li'); 572 | container.className = myself ? 'self' : 'remote'; 573 | 574 | var image = document.createElement('img'); 575 | var vcard = myself ? this._app.connection.vcard : contact.vcard; 576 | image.width = 32; 577 | image.height = 32; 578 | image.src = getVcardImage(this._app, vcard); 579 | 580 | var name = vcard.name || username || jid; 581 | var stamp = (myself ? 'me' : name) + ' | ' + now.format('isoTime'); 582 | 583 | var p = document.createElement('p'); 584 | p.appendChild(document.createTextNode(text)); 585 | 586 | var span = document.createElement('span'); 587 | span.appendChild(document.createTextNode(stamp)); 588 | 589 | container.appendChild(image); 590 | container.appendChild(p); 591 | container.appendChild(span); 592 | root.append(container); 593 | 594 | var rel = root.$element.parentNode; 595 | rel.scrollTop = rel.scrollHeight; 596 | setTimeout(function() { 597 | rel.scrollTop = rel.scrollHeight; 598 | }, 100); 599 | 600 | if ( myself ) { 601 | var input = this._find('Input'); 602 | setTimeout(function() { 603 | input.set('value', ''); 604 | }, 10); 605 | } 606 | } 607 | 608 | compose(cmp, jid) { 609 | var statusbar = this._find('Statusbar'); 610 | 611 | this._timeout = clearTimeout(this._timeout); 612 | this._timeout = setTimeout(function() { 613 | statusbar.set('value', ''); 614 | }, 5 * 1000); 615 | 616 | if ( cmp.state ) { 617 | statusbar.set('value', cmp.jid + ' is typing...'); 618 | } else { 619 | statusbar.set('value', cmp.jid + ' stopped typing...'); 620 | } 621 | } 622 | 623 | destroy() { 624 | this._timeout = clearTimeout(this._timeout); 625 | 626 | return super.destroy(...arguments); 627 | } 628 | } 629 | 630 | class ApplicationChatWindow extends Window { 631 | 632 | constructor(app, metadata) { 633 | super('ApplicationChatWindow', { 634 | icon: metadata.icon, 635 | title: metadata.name + ' v0.1', 636 | width: 300, 637 | height: 500 638 | }, app); 639 | } 640 | 641 | init(wmRef, app) { 642 | var root = super.init(...arguments); 643 | var self = this; 644 | 645 | // Load and set up scheme (GUI) here 646 | this._render('ChatWindow', require('osjs-scheme-loader!scheme.html')); 647 | 648 | var statusMenu = this._find('SubmenuStatus'); 649 | 650 | var menuMap = { 651 | MenuClose: function() { 652 | self._close(true); 653 | }, 654 | 655 | AccountSettings: function() { 656 | app.openSettingsWindow(); 657 | }, 658 | AccountConnect: function() { 659 | app.connection.connect(); 660 | }, 661 | AccountDisconnect: function() { 662 | app.connection.disconnect(); 663 | }, 664 | 665 | StatusOnline: function() { 666 | app.connection.setStatus('chat'); 667 | //app.connection.setStatus('online'); 668 | //statusMenu.set('checked', 'StatusOnline', true); 669 | }, 670 | StatusAway: function() { 671 | app.connection.setStatus('away'); 672 | //statusMenu.set('checked', 'StatusAway', true); 673 | }, 674 | StatusBusy: function() { 675 | app.connection.setStatus('busy'); 676 | //statusMenu.set('checked', 'StatusBusy', true); 677 | }, 678 | StatusExtendedAway: function() { 679 | self._toggleDisabled(true); 680 | Dialog.create('Input', {message: 'Away message'}, function(ev, button, result) { 681 | self._toggleDisabled(false); 682 | if ( button === 'ok' && result ) { 683 | app.connection.setStatus('xa', result); 684 | } 685 | }); 686 | //statusMenu.set('checked', 'StatusExtendedAway', true); 687 | } 688 | }; 689 | 690 | function menuEvent(ev) { 691 | if ( menuMap[ev.detail.id] ) { 692 | menuMap[ev.detail.id](); 693 | } 694 | } 695 | 696 | this._find('SubmenuFile').on('select', menuEvent); 697 | this._find('SubmenuAccount').on('select', menuEvent); 698 | this._find('SubmenuStatus').on('select', menuEvent); 699 | 700 | var statusbar = this._find('Statusbar'); 701 | var view = this._find('Contacts'); 702 | 703 | view.on('activate', function(ev) { 704 | if ( ev.detail.entries.length ) { 705 | app.openChatWindow(ev.detail.entries[0].data.username); 706 | } 707 | }); 708 | 709 | function renderContacts(contacts) { 710 | var entries = []; 711 | 712 | Object.keys(contacts).forEach(function(c) { 713 | var iter = contacts[c]; 714 | if ( c === app.connection.user.id.split('/')[0] ) { 715 | return; 716 | } 717 | 718 | entries.push({ 719 | icon: Theme.getIcon(iconMap[iter.state]), 720 | label: getDisplayName(iter), 721 | value: iter 722 | }); 723 | }); 724 | 725 | view.clear(); 726 | view.add(entries); 727 | } 728 | 729 | function setConnected(online) { 730 | self._find('MenuStatus').set('disabled', !online); 731 | self._find('AccountConnect').set('disabled', online); 732 | self._find('AccountDisconnect').set('disabled', !online); 733 | } 734 | 735 | app.connection.on('error', function() { 736 | statusbar.set('value', 'An error occured...'); 737 | setConnected(false); 738 | }); 739 | 740 | app.connection.on('attached', function() { 741 | statusbar.set('value', 'Attached...'); 742 | setConnected(false); 743 | }); 744 | 745 | app.connection.on('authenticating', function() { 746 | statusbar.set('value', 'Authenticating...'); 747 | setConnected(false); 748 | }); 749 | 750 | app.connection.on('redirected', function() { 751 | statusbar.set('value', 'Connection redirected...'); 752 | setConnected(false); 753 | }); 754 | 755 | app.connection.on('connected', function() { 756 | statusbar.set('value', 'Connected'); 757 | setConnected(true); 758 | }); 759 | 760 | app.connection.on('connecting', function() { 761 | statusbar.set('value', 'Connecting...'); 762 | setConnected(false); 763 | }); 764 | 765 | app.connection.on('disconnecting', function() { 766 | statusbar.set('value', 'Disconnecting...'); 767 | setConnected(false); 768 | }); 769 | 770 | app.connection.on('disconnected', function() { 771 | view.clear(); 772 | statusbar.set('value', 'Disconnected'); 773 | setConnected(false); 774 | }); 775 | 776 | app.connection.on('authfail', function() { 777 | statusbar.set('value', 'Authentication failure...'); 778 | setConnected(false); 779 | }); 780 | 781 | app.connection.on('connfail', function() { 782 | statusbar.set('value', 'Connection failure...'); 783 | setConnected(false); 784 | }); 785 | 786 | app.connection.on('status', function(s) { 787 | var map = { 788 | 'online': 'Online', 789 | 'chat': 'Online', 790 | 'away': 'Away', 791 | 'busy': 'Busy', 792 | 'xa': 'ExtendedAway' 793 | }; 794 | 795 | statusMenu.set('checked', 'Status' + map[s], true); 796 | 797 | if ( app.notification ) { 798 | app.notification.setImage(Theme.getIcon(iconMap[s])); 799 | } 800 | }); 801 | 802 | app.connection.on('contacts', function(contacts) { 803 | renderContacts(contacts || {}); 804 | }); 805 | 806 | statusMenu.set('checked', 'StatusOnline', true); 807 | setConnected(false); 808 | 809 | return root; 810 | } 811 | 812 | _close(doit) { 813 | if ( !doit ) { 814 | this._minimize(); 815 | return false; 816 | } 817 | return super._close(...arguments); 818 | } 819 | } 820 | 821 | class ApplicationChat extends Application { 822 | constructor(args, metadata) { 823 | super('ApplicationChat', args, metadata); 824 | 825 | this.notification = null; 826 | this.connection = new StropheConnection(this); 827 | } 828 | 829 | destroy() { 830 | Notification.destroyIcon('ApplicationChatNotificationIcon'); 831 | 832 | if ( this.connection ) { 833 | this.connection.destroy(); 834 | } 835 | 836 | if ( this.notification ) { 837 | this.notification.destroy(); 838 | } 839 | 840 | this.notification = null; 841 | this.connection = null; 842 | 843 | return super.destroy(...arguments); 844 | } 845 | 846 | init(settings, metadata) { 847 | super.init(...arguments); 848 | 849 | var self = this; 850 | var mainWindow = null; 851 | 852 | this.connection.on('message', function(msg) { 853 | if ( msg.jid ) { 854 | var username = msg.jid.split('/')[0]; 855 | var message = (msg.message || '').substr(0, 100); 856 | self.openChatWindow(username, function(w) { 857 | if ( !w._state.focused ) { 858 | Notification.create({ 859 | icon: Theme.getIcon('status/user-available.png'), 860 | title: username, 861 | message: message, 862 | onClick: function() { 863 | w._restore(); 864 | } 865 | }); 866 | } 867 | 868 | try { 869 | w.message(msg, msg.jid); 870 | } catch ( e ) { 871 | console.warn(e, e.stack); 872 | } 873 | }); 874 | } 875 | }); 876 | 877 | this.connection.on('compose', function(cmp) { 878 | if ( cmp.jid ) { 879 | self.openChatWindow(cmp.jid.split('/')[0], function(w) { 880 | if ( w ) { 881 | w.compose(cmp, cmp.jid); 882 | } 883 | }, true); 884 | } 885 | }); 886 | 887 | this.notification = Notification.createIcon('ApplicationChatNotificationIcon', { 888 | image: Theme.getIcon('status/user-invisible.png'), 889 | className: 'ApplicationChatNotificationIcon', 890 | tooltip: '', 891 | onDestroy: function() { 892 | self.notification = null; 893 | }, 894 | onClick: function(ev) { 895 | if ( mainWindow ) { 896 | mainWindow._restore(); 897 | } 898 | }, 899 | onContextMenu: function(ev) { 900 | if ( mainWindow ) { 901 | mainWindow._restore(); 902 | } 903 | } 904 | }); 905 | 906 | mainWindow = this._addWindow(new ApplicationChatWindow(this, metadata)); 907 | 908 | var acc = this._getSetting('account') || {}; 909 | if ( acc.username && acc.password ) { 910 | this.connection.connect(); 911 | } else { 912 | this.openSettingsWindow(); 913 | } 914 | } 915 | 916 | openChatWindow(id, cb, check) { 917 | cb = cb || function() {}; 918 | 919 | var win = this._getWindowByName('ApplicationConversationWindow_' + String(id)); 920 | if ( !win && !check ) { 921 | win = this._addWindow(new ApplicationConversationWindow(id, this, this.__metadata)); 922 | } 923 | 924 | this.connection.vcard(id, function(vc) { 925 | if ( vc && vc.name ) { 926 | win._setTitle(vc.name, true); 927 | } 928 | cb(win); 929 | }); 930 | } 931 | 932 | openSettingsWindow() { 933 | var win = this._getWindowByName('ApplicationChatSettingsWindow'); 934 | if ( win ) { 935 | win._restore(); 936 | } else { 937 | win = this._addWindow(new ApplicationSettingsWindow(this, this.__metadata)); 938 | } 939 | } 940 | } 941 | 942 | ///////////////////////////////////////////////////////////////////////////// 943 | // EXPORTS 944 | ///////////////////////////////////////////////////////////////////////////// 945 | 946 | OSjs.Applications.ApplicationChat = ApplicationChat; 947 | -------------------------------------------------------------------------------- /GoogleMail/mailer.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * OS.js - JavaScript Operating System 3 | * 4 | * Copyright (c) 2011-2017, Anders Evenrud 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, this 11 | * list of conditions and the following disclaimer. 12 | * 2. Redistributions in binary form must reproduce the above copyright notice, 13 | * this list of conditions and the following disclaimer in the documentation 14 | * and/or other materials provided with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 20 | * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | * 27 | * @author Anders Evenrud 28 | * @licence Simplified BSD License 29 | */ 30 | const FS = OSjs.require('utils/fs'); 31 | const Utils = OSjs.require('utils/misc'); 32 | const GoogleAPI = OSjs.require('helpers/google-api'); 33 | const Notification = OSjs.require('gui/notification'); 34 | const PackageManager = OSjs.require('core/package-manager'); 35 | 36 | var ENABLE_NOTIFICATIONS = false; 37 | 38 | var MAX_PAGES = 10; 39 | 40 | //var INTERVAL_FOLDER = (30 * 1000); 41 | //var INTERVAL_MESSAGES = (30 * 1000); 42 | var CONNECTION_TIMEOUT = 600 * 1000; 43 | 44 | var STATE_EMPTY = 0; 45 | var STATE_CREATED = 1; 46 | var STATE_UNREAD = 2; 47 | var STATE_STARRED = 4; 48 | var STATE_TRASHED = 8; 49 | 50 | /* 51 | * https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB 52 | * https://developers.google.com/gmail/api/v1/reference/users/messages/list 53 | * https://developers.google.com/gmail/api/v1/reference/users/labels 54 | * 55 | * TODO: 56 | * - Clean up events 57 | * - Check if sync is required 58 | * - Option to pull messages every X seconds (60 minimum) 59 | * - Create HTML Mail 60 | * - Send Attachments 61 | * - Settings/Options 62 | * - Trashing support 63 | * - Drafting support 64 | * - Searching 65 | * - Nested folder view 66 | * - Better error handling and display 67 | * - Folder Cache (just as with messages) 68 | */ 69 | 70 | var indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; 71 | //var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; 72 | //var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; 73 | var gapi = window.gapi || {}; 74 | 75 | ///////////////////////////////////////////////////////////////////////////// 76 | // MESSAGE STORAGE 77 | ///////////////////////////////////////////////////////////////////////////// 78 | 79 | function GoogleMailStorage(callback) { 80 | this.db = null; 81 | this.lastMessage = null; 82 | 83 | var self = this; 84 | 85 | function done(ev) { 86 | self.db = ev.target.result; 87 | callback(false, true, self); 88 | } 89 | 90 | function create(ev) { 91 | var db = ev.target.result; 92 | 93 | try { 94 | db.deleteObjectStore('messages'); 95 | } catch ( e ) {} 96 | 97 | var objectStore = db.createObjectStore('messages', {keyPath: 'id'}); 98 | objectStore.createIndex('sender', 'sender', {unique: false}); 99 | objectStore.createIndex('subject', 'subject', {unique: false}); 100 | objectStore.createIndex('date', 'date', {unique: false}); 101 | objectStore.createIndex('threadId', 'threadId', {unique: false}); 102 | objectStore.createIndex('state', 'state', {unique: false}); 103 | objectStore.createIndex('attachments', 'attachments', {unique: false}); 104 | 105 | objectStore.transaction.oncomplete = function(evt) { 106 | done(ev); 107 | }; 108 | objectStore.transaction.onerror = function(evt) { 109 | //console.warn("ERROR", evt); 110 | }; 111 | } 112 | 113 | console.log('GoogleMailStorage::construct()'); 114 | 115 | var request = indexedDB.open('GoogleMailStorage', 1); 116 | request.onerror = function(ev) { 117 | callback(ev.target.error.message, false, self); 118 | }; 119 | request.onsuccess = function(ev) { 120 | done(ev); 121 | }; 122 | request.onupgradeneeded = function(ev) { 123 | create(ev); 124 | }; 125 | } 126 | 127 | GoogleMailStorage.prototype.addMessage = function(args, callback, put) { 128 | if ( !this.db ) { 129 | callback('No database connection'); 130 | return; 131 | } 132 | 133 | //console.debug('GoogleMailStorage::addMessage()', args); 134 | 135 | var store = this.db.transaction('messages', 'readwrite').objectStore('messages'); 136 | var request = store[put ? 'put' : 'add'](args); 137 | request.onsuccess = function(ev) { 138 | callback(false, ev.result || true); 139 | }; 140 | request.onerror = function(ev) { 141 | callback(ev.target.error.message, false); 142 | }; 143 | }; 144 | 145 | GoogleMailStorage.prototype.getMessage = function(id, callback) { 146 | if ( !this.db ) { 147 | callback('No database connection'); 148 | return; 149 | } 150 | 151 | //console.debug('GoogleMailStorage::getMessage()', id); 152 | 153 | var transaction = this.db.transaction(['messages'], 'readwrite'); 154 | var objectStore = transaction.objectStore('messages'); 155 | var request = objectStore.get(id); 156 | 157 | request.onsuccess = function(ev) { 158 | callback(false, ev.target.result || null, objectStore); 159 | }; 160 | request.onerror = function(ev) { 161 | callback(ev.target.error.message, false); 162 | }; 163 | }; 164 | 165 | GoogleMailStorage.prototype.removeMessage = function(id, callback) { 166 | if ( !this.db ) { 167 | callback('No database connection'); 168 | return; 169 | } 170 | 171 | var request = this.db.transaction(['messages'], 'readwrite') 172 | .objectStore('messages') 173 | .delete(id); 174 | 175 | request.onsuccess = function(ev) { 176 | callback(false, ev.result || true); 177 | }; 178 | request.onerror = function(ev) { 179 | callback(ev.target.error.message, false); 180 | }; 181 | }; 182 | 183 | ///////////////////////////////////////////////////////////////////////////// 184 | // HELPERS 185 | ///////////////////////////////////////////////////////////////////////////// 186 | 187 | function checkIfInternal(name) { 188 | return name.match(/^(\[Imap\]|CATEGORY_|CHAT|DRAFT|UNREAD|INBOX|TRASH|IMPORTANT|STARRED|SPAM|SENT)/); 189 | } 190 | 191 | function createInboxNotification(folder, unreadCount) { 192 | if ( !folder || !unreadCount ) { 193 | return; 194 | } 195 | 196 | Notification.create({ 197 | icon: PackageManager.getPackageResource('ApplicationGmail', './icon.png'), 198 | title: 'Google Mail', 199 | message: Utils.format('{0} has {1} unread message(s)', folder, unreadCount) 200 | }); 201 | } 202 | 203 | /** 204 | * Check if required argumets is set 205 | */ 206 | function checkArguments(args, check, cb) { 207 | var result = true; 208 | 209 | check.forEach(function(c) { 210 | if ( result === false ) { 211 | return false; 212 | } 213 | 214 | if ( !args[c] ) { 215 | result = false; 216 | cb('You have to specify argument: ' + c); 217 | } 218 | 219 | return true; 220 | }); 221 | 222 | return result; 223 | } 224 | 225 | /** 226 | * Parses a Folder response 227 | */ 228 | function parseFolderResponse(resp) { 229 | var folders = []; 230 | (resp.labels || []).forEach(function(i) { 231 | folders.push({ 232 | name: i.name, 233 | id: i.id 234 | }); 235 | }); 236 | return folders; 237 | } 238 | 239 | /** 240 | * Parses a Message raw response 241 | */ 242 | function parseMessageDataResponse(messageId, payload) { 243 | var parsed = { 244 | raw: '', 245 | text: [], 246 | html: [], 247 | attachments: [] 248 | }; 249 | 250 | function parse(load) { 251 | 252 | if ( load.body ) { 253 | if ( load.filename ) { 254 | parsed.attachments.push({ 255 | mime: load.mimeType, 256 | size: load.body.size, 257 | filename: load.filename, 258 | messageId: messageId, 259 | id: load.body.attachmentId 260 | }); 261 | } 262 | 263 | var data = ''; 264 | if ( load.mimeType.match(/^text/) ) { 265 | data = FS.atobUrlsafe(load.body.data); 266 | if ( load.mimeType.match(/^text\/html/) ) { 267 | parsed.text.push(data); 268 | } else { 269 | parsed.html.push(data); 270 | } 271 | } 272 | 273 | parsed.raw += data; 274 | } 275 | 276 | if ( load.parts ) { 277 | load.parts.forEach(function(part) { 278 | parse(part); 279 | }); 280 | } 281 | } 282 | 283 | parse(payload); 284 | 285 | return parsed; 286 | } 287 | 288 | /** 289 | * Parses a Message metadata response 290 | */ 291 | function parseMessageResponse(resp) { 292 | var data = { 293 | id: resp.id, 294 | subject: resp.snippet, 295 | state: STATE_CREATED 296 | }; 297 | 298 | if ( resp.labelIds ) { 299 | if ( resp.labelIds.indexOf('UNREAD') !== -1 ) { 300 | data.state |= STATE_UNREAD; 301 | } 302 | if ( resp.labelIds.indexOf('STARRED') !== -1 ) { 303 | data.state |= STATE_STARRED; 304 | } 305 | if ( resp.labelIds.indexOf('TRASH') !== -1 ) { 306 | data.state |= STATE_STARRED; 307 | } 308 | } 309 | 310 | if ( resp.payload && resp.payload.headers ) { 311 | resp.payload.headers.forEach(function(header) { 312 | var name = header.name.toLowerCase(); 313 | var map = {from: 'sender'}; 314 | if ( ['from', 'date', 'subject'].indexOf(name) !== -1 ) { 315 | data[map[name] || name] = header.value; 316 | } 317 | }); 318 | } 319 | 320 | return data; 321 | } 322 | 323 | /** 324 | * Parses a Page response 325 | */ 326 | function parsePageResponse(resp) { 327 | var messages = []; 328 | (resp.messages || []).forEach(function(i) { 329 | messages.push({ 330 | id: i.id, 331 | threadId: i.threadId, 332 | sender: null, 333 | subject: null, 334 | date: null, 335 | state: 0 336 | }); 337 | }); 338 | return messages; 339 | } 340 | 341 | /** 342 | * Generates the raw data required to send a message 343 | */ 344 | function generateRawData(user, to, subject, message) { 345 | var from = '"' + user.name + '" <' + user.email + '>'; 346 | var lines = []; 347 | lines.push('From: ' + from); 348 | lines.push('To: ' + to); 349 | lines.push('Content-type: text/html;charset=iso8859-1'); 350 | lines.push('MIME-Version: 1.0'); 351 | lines.push('Subject: ' + subject); 352 | lines.push(''); 353 | lines.push(message); 354 | 355 | var raw = lines.join('\r\n'); 356 | return FS.btoaUrlsafe(raw); 357 | } 358 | 359 | ///////////////////////////////////////////////////////////////////////////// 360 | // MAILER 361 | ///////////////////////////////////////////////////////////////////////////// 362 | 363 | export class GoogleMail { 364 | constructor(args, cb) { 365 | var self = this; 366 | 367 | //console.clear(); 368 | console.group('GoogleMail::construct()'); 369 | 370 | this.destroy(); 371 | 372 | this.args = Object.assign({ 373 | maxPages: MAX_PAGES, 374 | onAbortStart: function() {}, 375 | onAbortEnd: function() {} 376 | }, args || {}); 377 | 378 | if ( args.maxPages > MAX_PAGES ) { 379 | args.maxPages = MAX_PAGES; 380 | } 381 | 382 | console.debug('Arguments', this.args); 383 | 384 | this.storage = new GoogleMailStorage(function() { 385 | self._connect(function() { 386 | console.debug('GoogleMail was inited'); 387 | console.groupEnd(); 388 | 389 | cb(); 390 | }); 391 | }); 392 | } 393 | 394 | destroy() { 395 | this.args = {}; 396 | this.busy = false; 397 | this.aborting = false; 398 | this.ginst = null; 399 | 400 | this.folderCache = []; 401 | this.lastFolderId = null; 402 | this.lastConnection = null; 403 | this.lastFolderSync = null; 404 | this.lastMessageSync = {}; 405 | 406 | this.user = { 407 | id: null, 408 | name: 'osjs', 409 | email: 'osjs@osjs' 410 | }; 411 | } 412 | 413 | /** 414 | * Loads and initializes the Google API 415 | */ 416 | _connect(cb) { 417 | if ( this.lastConnection ) { 418 | var now = new Date(); 419 | if ( now - this.lastConnection < CONNECTION_TIMEOUT ) { 420 | this.lastConnection = now; 421 | 422 | cb(false, true); 423 | return; 424 | } 425 | } 426 | 427 | var self = this; 428 | var load = [['gmail', 'v1']]; 429 | var scope = [ 430 | 'https://www.googleapis.com/auth/gmail.modify', 431 | 'https://www.googleapis.com/auth/gmail.readonly', 432 | 'https://www.googleapis.com/auth/gmail.compose', 433 | 'https://www.googleapis.com/auth/plus.profile.emails.read', 434 | 'https://mail.google.com', 435 | 'openid' 436 | ]; 437 | 438 | console.log('GoogleMail::_connect()'); 439 | 440 | GoogleAPI.create({load: load, scope: scope, client: true}, function(error, result, inst) { 441 | if ( !inst ) { 442 | cb('Failed to load Google API'); 443 | return; 444 | } 445 | 446 | gapi = window.gapi || {}; 447 | 448 | self.ginst = inst; 449 | self.lastConnection = new Date(); 450 | 451 | self._getUserInfo(function(err, user) { 452 | if ( user ) { 453 | self.user = user; 454 | } 455 | cb(err, true); 456 | }); 457 | }); 458 | } 459 | 460 | /** 461 | * Pull user information about used account 462 | */ 463 | _getUserInfo(cb) { 464 | console.log('GoogleMail::_getUserInfo()'); 465 | 466 | if ( this.user.id !== null ) { 467 | cb(false, false); 468 | return; 469 | } 470 | 471 | function parseUser(resp) { 472 | if ( resp ) { 473 | var user = {}; 474 | 475 | if ( resp.emails ) { 476 | resp.emails.forEach(function(i) { 477 | if ( i.type === 'account' ) { 478 | user.email = i.value; 479 | return false; 480 | } 481 | return true; 482 | }); 483 | } 484 | 485 | user.id = resp.id; 486 | user.name = resp.displayName; 487 | 488 | return user; 489 | } 490 | return null; 491 | } 492 | 493 | gapi.client.load('plus', 'v1', function() { 494 | gapi.client.plus.people.get({ 495 | userId: 'me' 496 | }).execute(function(resp) { 497 | var user = parseUser(resp); 498 | console.debug('GoogleMail::_getUserInfo()', '=>', user); 499 | 500 | cb(user ? false : 'Failed to fetch user information.', user); 501 | }); 502 | }); 503 | } 504 | 505 | /** 506 | * Check if connection is established etc. 507 | */ 508 | _checkConnection(cb) { 509 | cb = cb || function() {}; 510 | if ( !this.ginst ) { 511 | cb('No Google API instance'); 512 | return false; 513 | } 514 | if ( !this.storage ) { 515 | cb('No Google Mail storage instance'); 516 | return false; 517 | } 518 | return true; 519 | } 520 | 521 | // 522 | // FOLDERS 523 | // 524 | 525 | /** 526 | * Gets all folders 527 | */ 528 | getFolders(args, cb) { 529 | cb = cb || function() {}; 530 | if ( !this._checkConnection(cb) ) { 531 | return; 532 | } 533 | 534 | var self = this; 535 | args = Object.assign({ 536 | cache: false, 537 | onStart: function() {}, 538 | onEnd: function() {} 539 | }, args); 540 | 541 | console.log('GoogleMail::getFolders()', args); 542 | 543 | if ( args.cache ) { 544 | cb(false, this.folderCache); 545 | return; 546 | } 547 | 548 | /* 549 | var now = new Date(); 550 | if ( this.lastFolderSync && (now - this.lastFolderSync) < INTERVAL_FOLDER ) { 551 | console.warn('GoogleMail::getFolders() aborted due to timeout'); 552 | cb('You have to wait ' + INTERVAL_FOLDER/1000 + ' seconds between folder sync'); 553 | return; 554 | } 555 | */ 556 | 557 | args.onStart(); 558 | 559 | this._connect(function(error) { 560 | if ( error ) { 561 | args.onEnd(); 562 | cb(error); 563 | return; 564 | } 565 | 566 | var request = gapi.client.gmail.users.labels.list({ 567 | userId: 'me' 568 | }); 569 | 570 | request.execute(function(resp) { 571 | var folders = parseFolderResponse(resp); 572 | self.folderCache = folders; 573 | args.onEnd(); 574 | 575 | cb(false, folders); 576 | 577 | self.lastFolderSync = new Date(); 578 | }); 579 | }); 580 | } 581 | 582 | /** 583 | * Creates a folder 584 | */ 585 | createFolder(args, cb) { 586 | cb = cb || function() {}; 587 | if ( !this._checkConnection(cb) ) { 588 | return; 589 | } 590 | args = Object.assign({ 591 | name: null 592 | }, args); 593 | 594 | console.log('GoogleMail::createFolder()', args); 595 | if ( !checkArguments(args, ['name'], cb) ) { 596 | return; 597 | } 598 | if ( checkIfInternal(args.name) ) { 599 | cb('Invalid folder name'); 600 | return; 601 | } 602 | 603 | var request = gapi.client.gmail.users.labels.create({ 604 | userId: 'me', 605 | name: args.name 606 | }); 607 | 608 | request.execute(function(resp) { 609 | var error = !resp || !resp.result ? 'Failed to rename folder' : false; 610 | cb(error, !!error); 611 | }); 612 | } 613 | 614 | /** 615 | * Renames a folder 616 | */ 617 | renameFolder(args, cb) { 618 | cb = cb || function() {}; 619 | if ( !this._checkConnection(cb) ) { 620 | return; 621 | } 622 | args = Object.assign({ 623 | id: null, 624 | name: null 625 | }, args); 626 | 627 | console.log('GoogleMail::renameFolder()', args); 628 | if ( !checkArguments(args, ['id', 'name'], cb) ) { 629 | return; 630 | } 631 | if ( checkIfInternal(args.name) ) { 632 | cb('Invalid folder name'); 633 | return; 634 | } 635 | 636 | var request = gapi.client.gmail.users.labels.patch({ 637 | userId: 'me', 638 | id: args.id, 639 | name: args.name 640 | }); 641 | 642 | request.execute(function(resp) { 643 | var error = !resp || !resp.result ? 'Failed to rename folder' : false; 644 | cb(error, !!error); 645 | }); 646 | } 647 | 648 | /** 649 | * Removes a folder 650 | */ 651 | removeFolder(args, cb) { 652 | cb = cb || function() {}; 653 | if ( !this._checkConnection(cb) ) { 654 | return; 655 | } 656 | args = Object.assign({ 657 | id: null 658 | }, args); 659 | 660 | console.log('GoogleMail::removeFolder()', args); 661 | if ( !checkArguments(args, ['id'], cb) ) { 662 | return; 663 | } 664 | 665 | if ( checkIfInternal(args.id) ) { 666 | cb('Invalid folder name'); 667 | return; 668 | } 669 | 670 | var request = gapi.client.gmail.users.labels['delete']({ 671 | userId: 'me', 672 | id: args.id 673 | }); 674 | 675 | request.execute(function(resp) { 676 | cb(resp ? 'Failed to delete folder' : false, !resp); 677 | }); 678 | } 679 | 680 | /** 681 | * Gets a folders information 682 | */ 683 | _getFolder(args, cb) { 684 | cb = cb || function() {}; 685 | if ( !this._checkConnection(cb) ) { 686 | return; 687 | } 688 | args = Object.assign({ 689 | id: null 690 | }, args); 691 | 692 | console.log('GoogleMail::_getFolder()', args); 693 | if ( !checkArguments(args, ['id'], cb) ) { 694 | return; 695 | } 696 | 697 | var request = gapi.client.gmail.users.labels.get({ 698 | userId: 'me', 699 | id: args.id 700 | }); 701 | 702 | request.execute(function(resp) { 703 | var data = resp && resp.result ? resp.result : null; 704 | console.log('GoogleMail::_getFolder()', '=>', data); 705 | cb(data ? false : 'Failed to get folder', data); 706 | }); 707 | } 708 | 709 | // 710 | // ATTACHMENTS 711 | // 712 | 713 | /** 714 | * Get a Message Attachment 715 | */ 716 | downloadAttachment(args, cb) { 717 | cb = cb || function() {}; 718 | function _download(data) { 719 | var blob = new Blob([data], {type: args.mime}); 720 | var url = URL.createObjectURL(blob); 721 | var a = document.createElement('a'); 722 | a.setAttribute('href', url); 723 | a.setAttribute('download', args.filename); 724 | document.body.appendChild(a); 725 | a.click(); 726 | document.body.removeChild(a); 727 | } 728 | 729 | args = Object.assign({ 730 | id: null, 731 | mime: null, 732 | filename: null, 733 | attachmentId: null 734 | }, args); 735 | 736 | console.log('GoogleMail::attachment()', args); 737 | if ( !checkArguments(args, ['id', 'mime', 'filename', 'attachmentId'], cb) ) { 738 | return; 739 | } 740 | 741 | var request = gapi.client.gmail.users.messages.attachments.get({ 742 | 'userId': 'me', 743 | 'messageId': args.id, 744 | 'id': args.attachmentId 745 | }); 746 | 747 | request.execute(function(resp) { 748 | if ( resp.data ) { 749 | _download(FS.atobUrlsafe(resp.data)); 750 | 751 | cb(false, true); 752 | return; 753 | } 754 | 755 | cb('Failed to fetch attachment'); 756 | }); 757 | } 758 | 759 | // 760 | // MESSAGES 761 | // 762 | 763 | /** 764 | * Gets all messages in a folder 765 | */ 766 | getMessages(args, callback) { 767 | callback = callback || function() {}; 768 | var self = this; 769 | 770 | if ( !this._checkConnection(callback) ) { 771 | return; 772 | } 773 | 774 | args = Object.assign({ 775 | force: false, 776 | folder: null, 777 | onPageAdvance: function() {}, 778 | onMessageQueue: function() {}, 779 | onStart: function() {}, 780 | onEnd: function() {} 781 | }, args); 782 | 783 | if ( this.aborting ) { 784 | console.warn('I AM BUSY ABORTING. YOU SHOULD MAKE YOUR UI DISABLE WHEN THIS HAPPENS'); 785 | callback('Cannot perform this opertaion'); 786 | return; 787 | } 788 | 789 | var aborted = false; 790 | var refresh = this.lastFolderId && this.lastFolderId === args.folder; 791 | 792 | function resumeNext() { 793 | if ( aborted ) { 794 | return; 795 | } 796 | 797 | aborted = true; 798 | self.args.onAbortEnd(); 799 | 800 | self.busy = false; 801 | self.aborting = false; 802 | self.getMessages(args, callback); 803 | } 804 | 805 | if ( this.busy ) { 806 | console.warn('Looks like GoogleMail is busy... I have to abort and reschedule'); 807 | this.args.onAbortStart(); 808 | 809 | this.aborting = function() { 810 | //self.busy = false; 811 | 812 | if ( !self.aborting ) { 813 | resumeNext(); 814 | } else { 815 | var tmp = setInterval(function() { 816 | if ( !self.aborting ) { 817 | clearInterval(tmp); 818 | resumeNext(); 819 | } 820 | }, 100); 821 | } 822 | }; 823 | 824 | callback('Delaying action...'); 825 | return; 826 | } 827 | 828 | function cb(err, messageList) { 829 | console.groupEnd(); 830 | 831 | if ( ENABLE_NOTIFICATIONS && !self.aborting && !err && messageList ) { 832 | var unreadCount = 0; 833 | messageList.forEach(function(m) { 834 | if ( m && m.state & STATE_UNREAD ) { 835 | unreadCount++; 836 | } 837 | }); 838 | createInboxNotification(args.folder, unreadCount); 839 | } 840 | 841 | args.onEnd(); 842 | callback.apply(null, arguments); 843 | } 844 | 845 | console.group('GoogleMail::getMessages()', args); 846 | if ( !checkArguments(args, ['folder'], cb) ) { 847 | return; 848 | } 849 | 850 | /* 851 | var now = new Date(); 852 | if ( this.lastMessageSync[args.folder] && (now - this.lastMessageSync[args.folder]) < INTERVAL_MESSAGES ) { 853 | console.log('GoogleMail::getMessages()', args); 854 | console.warn('GoogleMail::getMessages() aborted due to timeout'); 855 | callback('You have to wait ' + INTERVAL_MESSAGES/1000 + ' seconds between folder sync'); 856 | return; 857 | } 858 | */ 859 | 860 | var maxPages = this.args.maxPages; 861 | var pageCurrent = 0; 862 | var pagesTotal = 0; 863 | var messagesCaught = 0; 864 | var messagesTotal = 0; 865 | var resultList = []; 866 | 867 | function checkAbortion(done) { 868 | if ( self.aborting ) { 869 | self.aborting(); 870 | 871 | done('Cancelled due to abortion...'); 872 | return true; 873 | } 874 | return false; 875 | } 876 | 877 | function runMessageQueue(queue, nextPage, finished) { 878 | var current = 0; 879 | var messages = []; 880 | 881 | function _next() { 882 | if ( checkAbortion(finished) ) { 883 | return; 884 | } 885 | 886 | if ( current >= queue.length ) { 887 | finished(false, messages, nextPage); 888 | return; 889 | } 890 | 891 | args.onMessageQueue({ 892 | refresh: refresh, 893 | force: args.force, 894 | pageCurrent: pageCurrent + 1, 895 | pagesTotal: pagesTotal, 896 | messageIndex: current, 897 | messageCurrent: messagesCaught, 898 | messagesTotal: messagesTotal, 899 | folder: args.folder 900 | }); 901 | 902 | self._getMessage(queue[current], function(error, result, fromCache) { 903 | if ( checkAbortion(finished) ) { 904 | return; 905 | } 906 | 907 | //console.debug(' =', fromCache); 908 | messagesCaught++; 909 | 910 | if ( result ) { 911 | messages.push(result); 912 | } 913 | _next(); 914 | }, args.force); 915 | 916 | current++; 917 | } 918 | 919 | _next(); 920 | } 921 | 922 | function fetchPage(folder, nextPage, done) { 923 | function fetchPageMessages(folderId, pcb) { 924 | var rargs = { 925 | userId: 'me', 926 | labelIds: folderId 927 | }; 928 | 929 | if ( nextPage ) { 930 | rargs.pageToken = nextPage; 931 | } 932 | 933 | var request = gapi.client.gmail.users.messages.list(rargs); 934 | request.execute(function(resp) { 935 | pcb(false, { 936 | queue: parsePageResponse(resp), 937 | nextPageToken: resp.nextPageToken 938 | }); 939 | }); 940 | } 941 | 942 | if ( nextPage ) { 943 | pageCurrent++; 944 | } 945 | 946 | if ( maxPages > 0 && pageCurrent >= maxPages ) { 947 | console.warn('fetchPage()', 'We hit the limit, sir', pageCurrent, maxPages); 948 | done(false); 949 | return; 950 | } 951 | 952 | console.debug('GoogleMail::getMessages()->fetchPage()', nextPage, pageCurrent); 953 | 954 | args.onPageAdvance({ 955 | refresh: refresh, 956 | force: args.force, 957 | pageCurrent: pageCurrent + 1, 958 | pagesTotal: pagesTotal, 959 | messageCurrent: messagesCaught, 960 | messagesTotal: messagesTotal, 961 | messages: resultList 962 | }); 963 | 964 | if ( checkAbortion(done) ) { 965 | return; 966 | } 967 | 968 | fetchPageMessages(args.folder, function(err, result) { 969 | if ( checkAbortion(done) ) { 970 | return; 971 | } 972 | 973 | runMessageQueue(result.queue, result.nextPageToken, function(error, messages, nextPage) { 974 | if ( checkAbortion(done) ) { 975 | return; 976 | } 977 | 978 | if ( messages ) { 979 | resultList = resultList.concat(messages); 980 | } 981 | 982 | if ( nextPage ) { 983 | console.debug('...advancing to next page...', nextPage); 984 | fetchPage(folder, nextPage, done); 985 | return; 986 | } 987 | 988 | done(error); 989 | }); 990 | }); 991 | } 992 | 993 | this.busy = true; 994 | this.aborting = false; 995 | this.lastFolderId = args.folder; 996 | args.onStart(); 997 | 998 | this._connect(function(error) { 999 | if ( error ) { 1000 | cb(error); 1001 | return; 1002 | } 1003 | 1004 | self._getFolder({id: args.folder}, function(error, folder) { 1005 | if ( checkAbortion(cb) ) { 1006 | return; 1007 | } 1008 | 1009 | if ( !folder ) { 1010 | cb('Cannot get messages: ' + error); 1011 | return; 1012 | } 1013 | 1014 | messagesTotal = folder.messagesTotal || 0; 1015 | pagesTotal = Math.ceil(messagesTotal / 100); 1016 | if ( maxPages > 0 && pagesTotal > maxPages ) { 1017 | pagesTotal = maxPages; 1018 | messagesTotal = maxPages * 100; 1019 | } 1020 | 1021 | fetchPage(folder, null, function(error) { 1022 | self.lastMessageSync[args.folder] = new Date(); 1023 | self.busy = false; 1024 | self.aborting = false; 1025 | 1026 | console.log('Finished getting messages. %d in total', resultList.length); 1027 | 1028 | cb(false, resultList); 1029 | }); 1030 | }); 1031 | }); 1032 | } 1033 | 1034 | /** 1035 | * Gets a message (either from cache or google) 1036 | */ 1037 | _getMessage(args, cb, forceFetch) { 1038 | cb = cb || function() {}; 1039 | 1040 | args = Object.assign({ 1041 | id: null 1042 | }, args); 1043 | 1044 | //console.group('GoogleMail::_getMessage()', args); 1045 | 1046 | if ( this.aborting ) { 1047 | cb('Cancelled due to abortion...'); 1048 | return; 1049 | } 1050 | 1051 | if ( !checkArguments(args, ['id'], cb) ) { 1052 | return; 1053 | } 1054 | 1055 | var self = this; 1056 | function fetch(fetched) { 1057 | var request = gapi.client.gmail.users.messages.get({ 1058 | userId: 'me', 1059 | id: args.id, 1060 | format: 'metadata' 1061 | }); 1062 | 1063 | request.execute(function(resp) { 1064 | fetched(false, parseMessageResponse(resp)); 1065 | }); 1066 | } 1067 | 1068 | function finished() { 1069 | //console.groupEnd(); 1070 | cb.apply(null, arguments); 1071 | } 1072 | 1073 | this.storage.getMessage(args.id, function(err, res) { 1074 | var fromCache = !err && res; 1075 | if ( forceFetch || !fromCache ) { 1076 | console.debug('GoogleMail::_getMessage()', 'Message was not found in cache, downloading'); 1077 | 1078 | fetch(function(error, message) { 1079 | if ( error ) { 1080 | console.warn('Failed to fetch message from google', error); 1081 | 1082 | finished('Failed to fetch message from google'); 1083 | return; 1084 | } 1085 | 1086 | self.storage.addMessage(message, function(serr, sres) { 1087 | /* 1088 | if ( serr ) { 1089 | console.warn('Failed to add message to cache', serr); 1090 | } 1091 | */ 1092 | finished(serr, message, fromCache); 1093 | }, forceFetch ? true : false); 1094 | }); 1095 | return; 1096 | } 1097 | 1098 | finished(res ? false : 'Failed to fetch message from cache', res, fromCache); 1099 | }); 1100 | } 1101 | 1102 | /** 1103 | * Send a Message 1104 | */ 1105 | sendMessage(args, cb) { 1106 | cb = cb || function() {}; 1107 | var self = this; 1108 | 1109 | args = Object.assign({ 1110 | to: null, 1111 | subject: null, 1112 | message: null 1113 | }, args); 1114 | 1115 | console.log('GoogleMail::send()', args); 1116 | if ( !checkArguments(args, ['to', 'subject', 'message'], cb) ) { 1117 | return; 1118 | } 1119 | 1120 | var request = gapi.client.gmail.users.messages.send({ 1121 | userId: 'me', 1122 | raw: generateRawData(self.user, args.to, args.subject, args.message) 1123 | }); 1124 | 1125 | request.execute(function(resp) { 1126 | var result = resp && resp.id ? resp : false; 1127 | cb(!result ? 'Failed to send' : false, result); 1128 | }); 1129 | } 1130 | 1131 | /** 1132 | * Mark a message 1133 | */ 1134 | markMessage(args, cb) { 1135 | var self = this; 1136 | cb = cb || function() {}; 1137 | 1138 | args = Object.assign({ 1139 | markAs: null, 1140 | id: null 1141 | }, args); 1142 | 1143 | console.log('GoogleMail::mark()', args); 1144 | if ( !checkArguments(args, ['id', 'markAs'], cb) ) { 1145 | return; 1146 | } 1147 | 1148 | var rargs = { 1149 | userId: 'me', 1150 | id: args.id 1151 | }; 1152 | 1153 | if ( args.markAs === 'read' ) { 1154 | rargs.removeLabelIds = ['UNREAD']; 1155 | } else if ( args.markAs === 'unread' ) { 1156 | rargs.addLabelIds = ['UNREAD']; 1157 | } else if ( args.markAs === 'starred' ) { 1158 | rargs.addLabelIds = ['STARRED']; 1159 | } else if ( args.markAs === 'unstar' ) { 1160 | rargs.removeLabelIds = ['STARRED']; 1161 | } 1162 | 1163 | var request = gapi.client.gmail.users.messages.modify(rargs); 1164 | request.execute(function(resp) { 1165 | if ( !resp || !resp.result ) { 1166 | cb(false, true); 1167 | return; 1168 | } 1169 | 1170 | self.storage.getMessage(args.id, function(err, msg) { 1171 | if ( err || !msg ) { 1172 | cb(false, true, args.id); 1173 | return; 1174 | } 1175 | 1176 | if ( args.markAs === 'read' ) { 1177 | if ( msg.state & STATE_UNREAD ) { 1178 | msg.state = msg.state ^ STATE_UNREAD; 1179 | } 1180 | } else if ( args.markAs === 'unread' ) { 1181 | if ( !(msg.state & STATE_UNREAD) ) { 1182 | msg.state = msg.state | STATE_UNREAD; 1183 | } 1184 | } else if ( args.markAs === 'starred' ) { 1185 | if ( !(msg.state & STATE_STARRED) ) { 1186 | msg.state = msg.state | STATE_STARRED; 1187 | } 1188 | } else if ( args.markAs === 'unstar' ) { 1189 | if ( msg.state & STATE_STARRED ) { 1190 | msg.state = msg.state ^ STATE_STARRED; 1191 | } 1192 | } 1193 | 1194 | self.storage.addMessage(msg, function(err) { 1195 | cb(err, true, args.id, msg.state); 1196 | }, true); 1197 | }); 1198 | }); 1199 | } 1200 | 1201 | /** 1202 | * Recieve a Message 1203 | */ 1204 | recieveMessage(args, cb) { 1205 | var self = this; 1206 | cb = cb || function() {}; 1207 | args = Object.assign({ 1208 | id: null, 1209 | returnFull: false, 1210 | markRead: false 1211 | }, args); 1212 | 1213 | console.log('GoogleMail::recieve()', args); 1214 | if ( !checkArguments(args, ['id'], cb) ) { 1215 | return; 1216 | } 1217 | 1218 | var request = gapi.client.gmail.users.messages.get({ 1219 | userId: 'me', 1220 | id: args.id 1221 | }); 1222 | 1223 | request.execute(function(resp) { 1224 | var message = parseMessageDataResponse(args.id, resp.payload); 1225 | if ( args.returnFull ) { 1226 | message = { 1227 | message: message, 1228 | data: parseMessageResponse(resp) 1229 | }; 1230 | } 1231 | 1232 | if ( args.markRead ) { 1233 | args = {id: args.id, markAs: 'read'}; 1234 | self.markMessage(args, function() { 1235 | cb(message ? false : 'Failed to fetch message data', message); 1236 | }); 1237 | } else { 1238 | cb(message ? false : 'Failed to fetch message data', message); 1239 | } 1240 | }); 1241 | } 1242 | 1243 | /** 1244 | * Move a Message 1245 | */ 1246 | moveMessage(args, cb) { 1247 | cb = cb || function() {}; 1248 | args = Object.assign({ 1249 | id: null, 1250 | from: null, 1251 | to: null 1252 | }, args); 1253 | 1254 | console.log('GoogleMail::move()', args); 1255 | if ( !checkArguments(args, ['id', 'from', 'to'], cb) ) { 1256 | return; 1257 | } 1258 | 1259 | if ( this.busy || this.aborting ) { 1260 | console.warn('Cannot perform this operation while busy or aborting'); 1261 | cb('Cannot perform this operation while busy or aborting'); 1262 | return; 1263 | } 1264 | 1265 | var request = gapi.client.gmail.users.messages.modify({ 1266 | userId: 'me', 1267 | id: args.id, 1268 | addLabelIds: [args.to], 1269 | removeLabelIds: [args.from] 1270 | }); 1271 | 1272 | request.execute(function(resp) { 1273 | if ( !resp || !resp.result ) { 1274 | console.warn('Failed to move google message'); 1275 | cb('Failed to move google message'); 1276 | return; 1277 | } 1278 | cb(false, true); 1279 | }); 1280 | } 1281 | 1282 | /** 1283 | * Delete a Message 1284 | */ 1285 | removeMessage(args, cb) { 1286 | var self = this; 1287 | 1288 | cb = cb || function() {}; 1289 | args = Object.assign({ 1290 | id: null 1291 | }, args); 1292 | 1293 | console.log('GoogleMail::remove()', args); 1294 | if ( !checkArguments(args, ['id'], cb) ) { 1295 | return; 1296 | } 1297 | 1298 | if ( this.busy || this.aborting ) { 1299 | console.warn('Cannot perform this operation while busy or aborting'); 1300 | cb('Cannot perform this operation while busy or aborting'); 1301 | return; 1302 | } 1303 | 1304 | var request = gapi.client.gmail.users.messages.delete({ 1305 | userId: 'me', 1306 | id: args.id 1307 | }); 1308 | 1309 | request.execute(function(resp) { 1310 | var error = !resp || Object.keys(resp).length > 0; 1311 | if ( !error ) { 1312 | self.storage.removeMessage(args.id, function(err, result) { 1313 | if ( err ) { 1314 | console.warn('Failed to remove cached message', err); 1315 | cb('Failed to remove cached message: ' + err); 1316 | return; 1317 | } 1318 | cb(false, true); 1319 | }); 1320 | return; 1321 | } 1322 | 1323 | console.warn('Failed to delete google message'); 1324 | cb('Failed to delete google message'); 1325 | }); 1326 | } 1327 | 1328 | setMaxPages(p) { 1329 | this.args.maxPages = parseInt(p, 10) || MAX_PAGES; 1330 | } 1331 | } 1332 | 1333 | export const MessageStates = { 1334 | EMPTY: STATE_EMPTY, 1335 | CREATED: STATE_CREATED, 1336 | UNREAD: STATE_UNREAD, 1337 | STARRED: STATE_STARRED, 1338 | TRASHED: STATE_TRASHED 1339 | }; 1340 | 1341 | --------------------------------------------------------------------------------