├── .travis.yml ├── logo.png ├── api ├── routes │ ├── methods │ │ ├── get.access.js │ │ ├── delete.email.js │ │ ├── delete.attachment.js │ │ ├── post.attachment.js │ │ ├── patch.email.js │ │ ├── get.emails.js │ │ ├── post.email.js │ │ ├── get.attachment.js │ │ └── post.access.js │ ├── access.js │ └── api.js ├── server.js └── middleware │ └── authentication.js ├── bin ├── tasks │ ├── install.js │ ├── clean.js │ ├── list.js │ ├── remove.js │ ├── changePassword.js │ ├── add.js │ ├── status.js │ ├── config.js │ ├── restore.js │ ├── process.js │ ├── setup_ssl.js │ ├── start.js │ ├── setup.js │ └── _questionnaire.js ├── galleon.js ├── configFile.js ├── modulator.js └── startup ├── query ├── delete.js ├── mark.js ├── attachment.js ├── unlink.attachment.js ├── get.attachment.js ├── clean.js ├── get.js ├── link.attachment.js ├── restore.js └── get.build.js ├── fleet ├── models │ ├── Users.js │ ├── Sessions.js │ ├── Queue.js │ └── Mail.js ├── connection.js ├── outgoing │ ├── outgoing.js │ ├── outbound.js │ └── queue.js ├── incoming │ ├── attachment.js │ ├── create.js │ ├── processor.js │ └── incoming.js └── bootstrap.js ├── tutorials ├── AUTHBIND.md ├── SPAMASSASIN.md └── INSTALLATION.md ├── tests ├── A_Init_Tests.js ├── C_SMTP_Incoming_Tests.js ├── B_USER_Management_Tests.js └── D_API_User_Auth.js ├── test.js ├── .gitignore ├── LICENSE ├── package.json ├── README.md └── Galleon.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7.4.0" 4 | - "6.9.4" -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schahriar/Galleon/HEAD/logo.png -------------------------------------------------------------------------------- /api/routes/methods/get.access.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res) { 2 | req.getCredentials(function (error, credentials) { 3 | if (error) res.json({ authenticated: false }); 4 | else res.json({ authenticated: credentials }); 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /api/routes/methods/delete.email.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res) { 2 | req.galleon.query('delete', { eID: req.param('eID'), email: req.credentials.email }, function (error) { 3 | if (error) return res.status(500).json({ error: error }); 4 | res.json({ success: true }) 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /api/routes/methods/delete.attachment.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res) { 2 | req.galleon.query('unlinkAttachment', { eID: req.params.eID, email: req.credentials.email, ref: req.params.ref }, function (error) { 3 | if (error) res.json({ error: error }); 4 | else res.json({ success: true }); 5 | }) 6 | } -------------------------------------------------------------------------------- /bin/tasks/install.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Modulator = require('../modulator'); 3 | 4 | module.exports = function (Galleon, argv) { 5 | var modulator = new Modulator(); 6 | // Remove install from argv 7 | argv._.shift(); 8 | _.each(argv._, function (Module) { 9 | modulator.install(Module); 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /api/routes/methods/post.attachment.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res) { 2 | req.galleon.query('linkAttachment', { eID: req.params.eID, email: req.credentials.email, file: req.file, body: req.body }, function (error, result) { 3 | if (error) res.json({ error: error }); 4 | else res.json({ success: true, ref: (!!result) ? result.ref : undefined }); 5 | }) 6 | } -------------------------------------------------------------------------------- /api/routes/access.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv; 2 | var express = require('express'); 3 | var router = express.Router(); 4 | 5 | router.post('/login', require("./methods/post.access.js").login); 6 | router.post('/logout', require("./methods/post.access.js").logout); 7 | router.post('/changepassword', require("./methods/post.access.js").changePassword); 8 | router.get('/check', require("./methods/get.access.js")); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /bin/tasks/clean.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); // Better looking error handling 2 | 3 | module.exports = function (Galleon, argv) { 4 | var g = new Galleon({ noCheck: true, verbose: false, safemode: true }); 5 | g.on('ready', function () { 6 | g.query("clean", {}, function (error) { 7 | if (error) { 8 | ; 9 | console.error(error); 10 | process.exit(1); 11 | } 12 | console.log("CLEAN WAS SUCCESSFUL"); 13 | process.exit(0); 14 | }); 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /query/delete.js: -------------------------------------------------------------------------------- 1 | module.exports = function (Galleon, query, callback) { 2 | if (!Galleon.connection.collections.queue) return callback(new Error('Collection Not Found!')); 3 | if (!Galleon.connection.collections.mail) return callback(new Error('Collection Not Found!')); 4 | if (query.eID.substring(0, 1) === 'O') 5 | Galleon.connection.collections.queue.destroy({ association: query.email, eID: query.eID.substring(1) }).exec(callback); 6 | else 7 | Galleon.connection.collections.mail.destroy({ association: query.email, eID: query.eID.substring(1) }).exec(callback); 8 | } 9 | -------------------------------------------------------------------------------- /bin/tasks/list.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var colors = require('colors'); // Better looking error handling 3 | 4 | module.exports = function (Galleon, argv) { 5 | var g = new Galleon({ noCheck: true, verbose: false, safemode: true }); 6 | g.on('ready', function () { 7 | g.listUsers({ limit: argv.limit || 20 }, function (error, users) { 8 | /* USE ASYNC HERE LATER */ 9 | _.each(users, function (user, count) { 10 | console.log((count + 1 + ":").red, user.email.magenta, '\t', user.name.blue); 11 | }) 12 | process.exit(0); 13 | }) 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /bin/tasks/remove.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); // Better looking error handling 2 | 3 | module.exports = function (Galleon, argv) { 4 | console.warn("WARNING:".red, "REMOVE COMMAND ONLY REMOVES THE USER & NOT THE EMAILS (FOR NOW)\nYOU CAN REMOVE THE EMAILS MANUALLY.".yellow); 5 | var g = new Galleon({ noCheck: true, verbose: false, safemode: true }); 6 | g.on('ready', function () { 7 | g.removeUser({ email: argv.email || argv._[1] }, function (error, user) { 8 | if (error) throw error; 9 | console.log("USER".cyan, user[0].email || user, "SUCCESSFULLY REMOVED!".green); 10 | process.exit(0); 11 | }) 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /bin/tasks/changePassword.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); // Better looking error handling 2 | 3 | module.exports = function (Galleon, argv) { 4 | var g = new Galleon({ noCheck: true, verbose: false, safemode: true }); 5 | g.on('ready', function () { 6 | g.changePassword({ email: argv._[1] }, argv.password || argv.p, null, function (error, user) { 7 | if (error) { 8 | console.log("ERROR -> PASSWORD NOT CHANGED".bgRed); 9 | console.error(error); 10 | process.exit(1); 11 | } 12 | console.log("USER".cyan, argv._[1], "SUCCESSFULLY CHANGED!".green); 13 | process.exit(0); 14 | }, true); 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /bin/tasks/add.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); // Better looking error handling 2 | 3 | module.exports = function (Galleon, argv) { 4 | var g = new Galleon({ noCheck: true, verbose: false, safemode: true }); 5 | g.on('ready', function () { 6 | g.createUser({ email: argv._[1], name: argv.name || argv.n, password: argv.password || argv.p }, function (error, user) { 7 | if (error) { 8 | console.log("ERROR -> USER NOT REGISTERED".bgRed); 9 | console.error(error); 10 | process.exit(1); 11 | } 12 | console.log("USER".cyan, argv._[1], "SUCCESSFULLY ADDED!".green); 13 | process.exit(0); 14 | }); 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /api/routes/methods/patch.email.js: -------------------------------------------------------------------------------- 1 | module.exports = function (req, res) { 2 | var credentials = req.credentials; 3 | 4 | var apply = new Object; 5 | 6 | if (req.param('read') !== undefined) apply.read = (!!req.param('read')) || false; 7 | if (req.param('spam') !== undefined) apply.spam = (!!req.param('spam')) || false; 8 | if (req.param('trash') !== undefined) apply.trash = (!!req.param('trash')) || false; 9 | 10 | /* Add credentials check here */ 11 | req.galleon.query('mark', { eID: req.param('eID'), email: req.credentials.email, apply: apply }, function (error, model) { 12 | if (error) return res.status(500).json({ error: error }); 13 | res.json({ success: true }) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /api/routes/methods/get.emails.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (req, res) { 4 | req.galleon.query('get', 5 | { 6 | email: req.credentials.email, 7 | page: parseInt(req.param("page")) || 1, 8 | folder: (req.param("folder") != undefined) ? req.param("folder") : 'inbox' 9 | }, 10 | function (error, emails, stats) { 11 | if (error) res.status(500).json({ 12 | error: error 13 | }); 14 | 15 | res.json({ 16 | folder: stats.folder, 17 | total: stats.total, 18 | page: stats.page, 19 | pages: stats.total / stats.limit, 20 | showing: stats.limit, 21 | results: emails 22 | }); 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /bin/tasks/status.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var colors = require('colors'); // Better looking error handling 3 | var pm2 = require('pm2'); 4 | 5 | module.exports = function (Galleon, argv) { 6 | // Connect or launch PM2 7 | pm2.connect(function (err) { 8 | if (err) return new Error(err); 9 | // Get all processes running 10 | pm2.list(function (err, process_list) { 11 | if (err) throw err; 12 | /* USE ASYNC HERE LATER */ 13 | _.each(process_list, function (process, count) { 14 | console.log((count + 1 + ":").error, ("PID " + process.pid).yellow, '\t', process.name.magenta, '\t', process.pm2_env.status.green); 15 | }) 16 | process.exit(0); 17 | }); 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /fleet/models/Users.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Idenitity is a unique name for this model 3 | identity: 'users', 4 | connection: 'authentication', 5 | 6 | types: { 7 | 8 | }, 9 | 10 | attributes: { 11 | email: { 12 | type: 'string', 13 | required: true, 14 | unique: true 15 | }, 16 | 17 | name: { 18 | type: 'string', 19 | required: true, 20 | index: true 21 | }, 22 | 23 | isAdmin: { 24 | type: 'boolean', 25 | required: true 26 | }, 27 | 28 | password: { 29 | type: 'string', 30 | maxLength: 512, 31 | required: true 32 | }, 33 | 34 | lastLogin: { 35 | type: 'json', 36 | required: false 37 | } 38 | } 39 | }; -------------------------------------------------------------------------------- /tutorials/AUTHBIND.md: -------------------------------------------------------------------------------- 1 | # Authbind setup 2 | Galleon requires bind access to two ports (SMTP: 25, SMTPS: 587) and you will not be able to bind to ports lower than 1024 without root access. While it would be possible to run Galleon as root it is highly not recommended to run any node application via root but **authbind** provides this functionality. 3 | 4 | *Setting up authbind on most linux distribution would look like this:* 5 | ``` 6 | sudo apt-get install authbind 7 | ``` 8 | Once you have installed authbind run the following to configure port 25 (remember to replace with current OS user's username): 9 | ``` 10 | sudo touch /etc/authbind/byport/25 11 | sudo chown /etc/authbind/byport/25 12 | sudo chmod 755 /etc/authbind/byport/25 13 | -------------------------------------------------------------------------------- /bin/tasks/config.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var Modulator = require('../modulator'); 3 | 4 | module.exports = function (Galleon, argv) { 5 | // Format -> galleon config 6 | 7 | var modulator = new Modulator(); 8 | 9 | // Remove config from argv 10 | argv._.shift(); 11 | var Config = {}; 12 | try { 13 | Config[argv._[1]] = argv._[2]; 14 | } catch (e) { 15 | throw new Error("Failed to update Config. Try the following format \n galleon config "); 16 | } 17 | var SUCCESS = modulator.update(argv._[0], Config); 18 | if (SUCCESS) { 19 | console.log("UPDATED SUCCESSFULLY"); 20 | } else { 21 | console.log("MODULE NOT FOUND"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/A_Init_Tests.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var expect = chai.expect; 3 | var http = require('http'); 4 | 5 | describe('Initial Test Suite', function(){ 6 | this.timeout(14000); 7 | it("should create a new instance of Galleon", function(done) { 8 | global.galleon = new global.Galleon(global.options); 9 | global.galleon.on('ready', function(){ 10 | global.galleon.server(function(error, hasStarted){ 11 | if(error) throw error; 12 | if(hasStarted) { 13 | done(); 14 | } 15 | }); 16 | }) 17 | }) 18 | it("should connect to API server", function(done) { 19 | http.get('http://localhost:3080', function (res) { 20 | // Not Authenticated (403) 21 | expect(res.statusCode).to.equal(403); 22 | done(); 23 | }); 24 | }) 25 | }) -------------------------------------------------------------------------------- /bin/galleon.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv; 2 | var Stream = require("stream").Stream; 3 | var colors = require('colors'); // Better looking error handling 4 | 5 | colors.setTheme({ 6 | silly: 'rainbow', 7 | input: 'grey', 8 | verbose: 'cyan', 9 | prompt: 'grey', 10 | success: 'green', 11 | data: 'grey', 12 | help: 'cyan', 13 | warn: 'yellow', 14 | debug: 'grey', 15 | bgWhite: 'bgWhite', 16 | bold: 'bold', 17 | error: 'red' 18 | }); 19 | 20 | var Galleon = require("../Galleon"); 21 | 22 | var g = new Galleon({ port: argv.port || argv.p, dock: true }); 23 | g.on('ready', function () { 24 | g.server(function (error, hasStarted) { 25 | if (error) console.log(error.error); 26 | if (hasStarted) console.log("Server started...".help); 27 | }); 28 | }) 29 | -------------------------------------------------------------------------------- /tutorials/SPAMASSASIN.md: -------------------------------------------------------------------------------- 1 | # SpamAssasin setup 2 | Install [SpamAssasin](http://spamassassin.apache.org/downloads.cgi?update=201402111327) 3 | ``` 4 | sudo apt-get install spamassassin spamc 5 | ``` 6 | Edit **SpamAssasin** launch config using root privileges: 7 | ``` 8 | sudo nano /etc/default/spamassassin 9 | ``` 10 | Set ENABLED to 1 to activate daemon: 11 | ``` 12 | ENABLED=1 13 | ``` 14 | Add **-l** flag to options to enable Spam/Ham reporting and learning in Galleon: 15 | ``` 16 | OPTIONS="-l --create-prefs --max-children 5 --helper-home-dir" 17 | ``` 18 | Set CRON to 1 to *enable the cron job to automatically update SpamAssassin's rules on a nightly basis*: 19 | ``` 20 | CRON=1 21 | ``` 22 | 23 | Restart **SpamAssasin** for changes to take effect: 24 | ``` 25 | sudo service spamassassin start 26 | ``` 27 | -------------------------------------------------------------------------------- /bin/tasks/restore.js: -------------------------------------------------------------------------------- 1 | var colors = require('colors'); // Better looking error handling 2 | 3 | module.exports = function (Galleon, argv) { 4 | var g = new Galleon({ noCheck: true, verbose: false, safemode: true }); 5 | console.warn("WARNING: This process may take a long time...") 6 | g.on('ready', function () { 7 | console.log("READY") 8 | g.query("restore", {}, function (error, failed) { 9 | if (error) { 10 | ; 11 | console.error(error); 12 | process.exit(1); 13 | } 14 | var i; 15 | var errors = []; 16 | for (i = 0; i < failed.length; i++) { 17 | if (failed[i] !== undefined) error.push(failed[i]); 18 | } 19 | if (errors.length >= 1) console.log("FAILED TO PROCESS", errors.length, "ITEMS"); 20 | console.log("RESTORE WAS SUCCESSFUL"); 21 | process.exit(0); 22 | }); 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /api/routes/methods/post.email.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (req, res) { 4 | var email = { 5 | association: req.credentials.email, 6 | id: (req.param('id')) ? req.param('id').substring(1) : undefined, 7 | from: req.credentials.name + ' <' + req.credentials.email + '>', 8 | to: req.param('to'), 9 | subject: req.param('subject'), 10 | html: req.param('html'), 11 | draft: req.param('draft'), 12 | remove: req.param('remove') 13 | } 14 | 15 | _.defaults(email, { 16 | to: req.credentials.email, 17 | subject: "", 18 | text: "No Text", 19 | html: "", 20 | draft: false, 21 | }) 22 | 23 | // Dispatch Email 24 | req.galleon.dispatch(email, function (error, queue) { 25 | if (error) return res.json({ error: error }); 26 | // Add 'O' for outgoing to eID 27 | res.json({ success: true, state: queue.state, id: (queue.eID) ? ('O' + queue.eID) : undefined }); 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var inspect = require("util").inspect; 3 | var crypto = require("crypto"); 4 | var SMTPConnection = require('smtp-connection'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var PORT = 8800; 8 | 9 | // Actual Tests are located in the `tests` folder 10 | 11 | global.Galleon = require('./Galleon'); 12 | global.galleon; 13 | global.connection = new SMTPConnection({ 14 | port: PORT, 15 | ignoreTLS: true 16 | }); 17 | global.options = { 18 | verbose: false, 19 | ports: { 20 | incoming: PORT, 21 | server: 3080 22 | }, 23 | dock: true, 24 | connections: { 25 | storage: { adapter: 'sails-memory' }, 26 | authentication: { adapter: 'sails-memory' } 27 | }, 28 | modules: [], 29 | secret: crypto.randomBytes(20).toString('hex'), 30 | } 31 | 32 | var expect = chai.expect; 33 | 34 | fs.readdirSync('./tests').forEach(function(file) { 35 | // Require a test if available 36 | if(path.extname(file) === '.js') { 37 | require(path.resolve('./tests', file)); 38 | } 39 | }); -------------------------------------------------------------------------------- /api/routes/methods/get.attachment.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function (req, res) { 4 | req.galleon.query('getAttachment', { eID: req.params.eID, email: req.credentials.email, id: req.params.id.toString() }, function (error, attachment) { 5 | if (error) return res.status(400).json({ error: error.toString() }) 6 | if (attachment.cid) { 7 | console.log("CID ATTACHMENT", attachment) 8 | res.type(attachment.name); 9 | res.sendFile(path.basename(attachment.path), { 10 | root: path.dirname(attachment.path), 11 | dotfiles: 'deny', 12 | headers: { 13 | 'x-timestamp': Date.now(), 14 | 'x-sent': true 15 | } 16 | }, function (error) { 17 | console.log(error) 18 | if (error) res.status(error.status).end(); 19 | }) 20 | } 21 | else { 22 | res.download(attachment.path, attachment.name, function (error) { 23 | if (error) res.status(error.status).end(); 24 | }); 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /fleet/connection.js: -------------------------------------------------------------------------------- 1 | /// Database 2 | // Waterline 3 | var Waterline = require('waterline'); 4 | var Database = require('./bootstrap'); 5 | // ---------------------------------- 6 | /// 7 | /* -- ------- -- */ 8 | 9 | module.exports = function (connections, callback) { 10 | Database({ 11 | adapters: { 12 | 'sails-disk': require('sails-disk'), 13 | 'sails-memory': require('sails-memory'), 14 | 'sails-mysql': require('sails-mysql'), 15 | 'sails-mongo': require('sails-mongo'), 16 | 'sails-postgresql': require('sails-postgresql'), 17 | 'sails-redis': require('sails-redis'), 18 | 'sails-sqlserver': require('sails-sqlserver'), 19 | }, 20 | collections: { 21 | mail: require('./models/Mail'), 22 | queue: require('./models/Queue'), 23 | 24 | users: require('./models/Users'), 25 | sessions: require('./models/Sessions') 26 | }, 27 | connections: connections 28 | }, function waterlineReady(error, ontology) { 29 | callback(error, ontology); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | coverage.html 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # Compiled binary addons (http://nodejs.org/api/addons.html) 21 | build/Release 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | 31 | # _ Windows _ 32 | 33 | # Windows image file caches 34 | Thumbs.db 35 | ehthumbs.db 36 | 37 | # Folder config file 38 | Desktop.ini 39 | 40 | # Recycle Bin used on file shares 41 | $RECYCLE.BIN/ 42 | 43 | # Windows Installer files 44 | *.cab 45 | *.msi 46 | *.msm 47 | *.msp 48 | 49 | # Windows shortcuts 50 | *.lnk 51 | 52 | # Ignore Visual Studio Code settings 53 | .settings 54 | .vscode 55 | *.db 56 | -------------------------------------------------------------------------------- /query/mark.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | module.exports = function (Galleon, query, callback) { 5 | if (!Galleon.connection.collections.mail) return callback(new Error('Collection Not Found!')); 6 | Galleon.connection.collections.mail.update({ 7 | eID: query.eID.substring(1), 8 | association: query.email 9 | }, query.apply).exec(callback); 10 | 11 | // If Spam -> True/False applied 12 | if ((query.apply.spam) && (query.apply.spam.constructor === Boolean)) { 13 | // Find Raw Email for training 14 | fs.readFile(path.resolve(Galleon.environment.paths.raw, query.eID.substring(1)), 'utf8', function (error, buffer) { 15 | if (error) return console.log("SMAPC_RAW->ERROR", error); 16 | // Report or Revoke Spam 17 | try { 18 | Galleon.spamc[(query.apply.spam) ? 'tell' : 'revoke'](buffer, function (error) { 19 | throw error; 20 | console.log("SMAPC_REPORT->REPORTED", query.eID.substring(1), "AS", (query.apply.spam) ? 'SPAM' : 'HAM'); 21 | }); 22 | } catch (error) { 23 | if (error) console.log("SMAPC_REPORT->ERROR", error); 24 | } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /fleet/models/Sessions.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var shortId = require('shortid'); 3 | 4 | module.exports = { 5 | // Idenitity is a unique name for this model 6 | identity: 'sessions', 7 | connection: 'authentication', 8 | 9 | types: { 10 | stamp: function (time) { 11 | return time.opened && time.expires 12 | } 13 | }, 14 | 15 | attributes: { 16 | sessionID: { 17 | type: 'string', 18 | required: false, // Automatically created 19 | maxLength: 48, 20 | unique: true, 21 | }, 22 | 23 | email: { 24 | type: 'string', 25 | required: true, 26 | unique: true // This will disable dual sessions 27 | }, 28 | 29 | access: { 30 | type: 'string', 31 | enum: ['approved', 'provoked'] 32 | }, 33 | 34 | ipAddress: { 35 | type: 'string', 36 | required: true 37 | }, 38 | 39 | stamp: { 40 | type: 'json', 41 | stamp: true 42 | } 43 | }, 44 | 45 | beforeCreate: function (attributes, callback) { 46 | // Should round up about 14 + 2 + 32 = 48 characters at max 47 | // Hashsum enables email checking without exposing the email 48 | // to session token. 49 | attributes.sessionID = shortId.generate() + '__' + crypto.createHash('md5').update(attributes.email).digest('hex'); 50 | callback(); 51 | } 52 | }; -------------------------------------------------------------------------------- /fleet/outgoing/outgoing.js: -------------------------------------------------------------------------------- 1 | /* -- Modules -- */ 2 | // Essential 3 | var eventEmmiter = require('events').EventEmitter; 4 | var util = require("util"); 5 | 6 | // Foundations 7 | 8 | /* -- ------- -- */ 9 | 10 | // 11 | /* -------------- Module Human description -------------- */ 12 | /* 13 | 14 | Outgoing module will take care of outbound mails 15 | initiated through an inbound connection from mail 16 | clients or internal programs using the api. 17 | 18 | This will not only enable mail clients to 19 | connect to Galleon and send emails but also will 20 | enable the ability to build a REST Api where 21 | necessary. 22 | 23 | We use ~mailin~ in this module as well to catch 24 | messages sent to the specified protocol 25 | (587 by default or 465 when secured) and will forward 26 | it to ~outbound~ if autosend is enabled in options. 27 | 28 | */ 29 | /* -------------- ------------------------ -------------- */ 30 | // 31 | 32 | /* * Performance 33 | ---------------- 34 | NO TESTS YET 35 | ---------------- 36 | */ 37 | 38 | 39 | /* Start the Mailin server for Outgoing connections. */ 40 | var Outgoing = function (port) { 41 | eventEmmiter.call(this); 42 | 43 | this.port = port; 44 | } 45 | 46 | util.inherits(Outgoing, eventEmmiter); 47 | 48 | Outgoing.prototype.listen = function (port) { 49 | var _this = this; 50 | 51 | 52 | }; 53 | 54 | module.exports = Outgoing; -------------------------------------------------------------------------------- /api/routes/methods/post.access.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | - LOGIN METHOD - 4 | Accepts: 5 | @ email -> String 6 | @ password -> String 7 | */ 8 | login: function (req, res) { 9 | /* Improve code 10 | // Logout if already logged 11 | if(req.credentials){ 12 | req.signOut(req, res, function(error){ 13 | req.signIn(req, res, function(error){ 14 | if(error) return res.json({ error: error }); 15 | res.json({ success: true }); 16 | }) 17 | }) 18 | }*/ 19 | 20 | // Login 21 | req.signIn(req, res, function (error, token) { 22 | if (error) return res.json({ error: error, success: false }); 23 | res.json({ success: true, token: token }); 24 | }) 25 | }, 26 | logout: function (req, res) { 27 | req.signOut(req, res, function (error) { 28 | if (error) return res.json({ error: error, success: false }); 29 | res.json({ success: true }); 30 | }) 31 | }, 32 | changePassword: function (req, res) { 33 | req.changePassword(req, res, function (error) { 34 | if (error) return res.json({ error: error, success: false }); 35 | // Logout after password change 36 | req.signOut(req, res, function (error) { 37 | if (error) return res.json({ error: error, success: false }); 38 | res.json({ success: true }); 39 | }) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/C_SMTP_Incoming_Tests.js: -------------------------------------------------------------------------------- 1 | var chai = require("chai"); 2 | var expect = chai.expect; 3 | 4 | describe("SMTP Incoming Tests", function () { 5 | it("should receive connections to SMTP server", function (done) { 6 | global.connection.connect(function () { 7 | done(); 8 | }); 9 | }) 10 | it("should receive an email to the database", function (done) { 11 | global.connection.send({ 12 | from: "test@example.com", 13 | to: "info@example.com" 14 | }, "From: me@domain.com\nTo: you@sample.com\nSubject: Example Message\n\rSending a test message.", function (error) { 15 | if (error) throw error; 16 | done(); 17 | }); 18 | }) 19 | it("should process incoming emails ot the database", function (done) { 20 | process.nextTick(function () { 21 | global.galleon.query('get', { 22 | email: "info@example.com", 23 | folder: 'inbox', 24 | paginate: false 25 | }, function (error, results) { 26 | if (error) throw error; 27 | expect(results[0].text).to.equal('Sending a test message.\n'); 28 | done(); 29 | }); 30 | }); 31 | }) 32 | it("should handle bad email gracefully", function (done) { 33 | global.connection.send({ 34 | from: "test@example.com", 35 | to: "info@example.com" 36 | }, "From: me@domain.com\nTo: you@ubject: Example MessageSending a test message.", function (error) { 37 | expect(error.responseCode).to.equal(451); 38 | done(); 39 | }); 40 | }) 41 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Schahriar SaffarShargh 4 | Contact: info@schahriar.com (www.schahriar.com) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | Except as contained in this notice, the name(s) of the above copyright holders 17 | shall not be used in advertising or otherwise to promote the sale, use or other 18 | dealings in this Software without prior written authorization. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /query/attachment.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var _ = require('lodash'); 4 | 5 | module.exports = function (Galleon, query, callback) { 6 | if (!Galleon.connection.collections.mail) return callback(new Error('Collection Not Found!')); 7 | Galleon.connection.collections.mail.findOne({ 8 | association: query.email, 9 | eID: query.eID 10 | }).exec(function (error, mail) { 11 | if (error) callback(error); 12 | if (!mail) callback("Mail not found!"); 13 | 14 | // Determines if attachment is CID based (embedded) 15 | var attachment = _.findWhere(mail.attachments, (query.id.substring(0, 4) !== "cid=") ? { 16 | id: query.eID + "_" + query.id 17 | } : { 18 | cid: query.id.slice(4) 19 | }); 20 | 21 | if (attachment) { 22 | fs.exists(path.resolve(Galleon.environment.paths.attachments, _.result(attachment, 'id')), function (exists) { 23 | /* Escape filename */ 24 | if (exists) { 25 | callback(null, { 26 | path: path.resolve(Galleon.environment.paths.attachments, _.result(attachment, 'id')), 27 | name: _.result(attachment, 'fileName'), 28 | length: _.result(attachment, 'length') || 0, 29 | cid: (query.id.substring(0, 4) === "cid="), 30 | }) 31 | } else callback('Attachment not found!'); 32 | }); 33 | } else { 34 | callback('Attachment not found!'); 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /bin/tasks/process.js: -------------------------------------------------------------------------------- 1 | var pm2 = require('pm2'); 2 | var colors = require('colors'); 3 | 4 | var success = function (message) { 5 | return function (error, PM2Process) { 6 | if (error) console.error(error); 7 | else console.log(message.green); 8 | process.exit(0); 9 | } 10 | } 11 | 12 | var exitError = function () { 13 | console.log.apply(null, arguments); 14 | process.exit(0); 15 | } 16 | 17 | module.exports = { 18 | stop: function (Galleon, argv) { 19 | pm2.connect(function (error) { 20 | if (error) throw error; 21 | pm2.stop('galleon-instance' || argv._[1], success("GALLEON HALTED SUCCESSFULLY!")); 22 | }); 23 | }, 24 | delete: function (Galleon, argv) { 25 | pm2.connect(function (error) { 26 | if (error) throw error; 27 | pm2.delete('galleon-instance' || argv._[1], success("GALLEON DELETED SUCCESSFULLY!")); 28 | }); 29 | }, 30 | restart: function (Galleon, argv) { 31 | pm2.connect(function (error) { 32 | if (error) throw error; 33 | pm2.restart('galleon-instance' || argv._[1], success("GALLEON RESTARTED SUCCESSFULLY!")); 34 | }); 35 | }, 36 | startup: function (Galleon, argv) { 37 | pm2.connect(function (error) { 38 | if (error) throw error; 39 | if (!argv._[1]) return exitError("REQUIRES AN OS ARGUMENT".red, "\ngalleon startup ".magenta) 40 | 41 | pm2.startup(argv._[1], success("STARTUP SCRIPT IS SHOWN BELOW!")); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /query/unlink.attachment.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var _ = require('lodash'); 4 | 5 | module.exports = function (Galleon, query, callback) { 6 | if (!Galleon.connection.collections.queue) return callback(new Error('Collection Not Found!')); 7 | Galleon.connection.collections.queue.findOne({ 8 | association: query.email, 9 | eID: query.eID.substring(1) 10 | }).exec(function (error, mail) { 11 | if (error) return callback(error); 12 | if (!mail) return callback("Mail not found!"); 13 | 14 | // Make sure mail.attachments is an array 15 | if (!_.isArray(mail.attachments)) mail.attachments = []; 16 | 17 | if (mail.attachments.length > 0) { 18 | /* Implement a new method to break out of loop after the first element is found */ 19 | // Remove attachment 20 | _.remove(mail.attachments, function (attachment) { 21 | return (attachment.ref === query.ref); 22 | }) 23 | // Update Draft 24 | Galleon.connection.collections.queue.update({ 25 | association: query.email, 26 | eID: query.eID.substring(1) 27 | }, { 28 | attachments: mail.attachments 29 | }).exec(function (error, mail) { 30 | if (error) return callback(error); 31 | if (!mail) return callback("Mail not found!"); 32 | 33 | // Else 34 | callback(null); 35 | }) 36 | } else { 37 | callback(null); 38 | } 39 | 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /query/get.attachment.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var _ = require('lodash'); 4 | 5 | module.exports = function (Galleon, query, callback) { 6 | if (!Galleon.connection.collections.mail) return callback(new Error('Collection Not Found!')); 7 | Galleon.connection.collections.mail.findOne({ 8 | association: query.email, 9 | eID: query.eID.substring(1) 10 | }).exec(function (error, mail) { 11 | if (error) return callback(error); 12 | if (!mail) return callback("Mail not found!"); 13 | 14 | // Determines if attachment is CID based (embedded) 15 | var attachment = _.findWhere(mail.attachments, (query.id.substring(0, 4) !== "cid=") ? { 16 | id: query.eID.substring(1) + "_" + query.id 17 | } : { 18 | cid: query.id.slice(4) 19 | }); 20 | 21 | if (attachment) { 22 | fs.exists(path.resolve(Galleon.environment.paths.attachments, _.result(attachment, 'id')), function (exists) { 23 | /* Escape filename */ 24 | if (exists) { 25 | callback(null, { 26 | path: path.resolve(Galleon.environment.paths.attachments, _.result(attachment, 'id')), 27 | name: _.result(attachment, 'fileName'), 28 | length: _.result(attachment, 'length') || 0, 29 | cid: (query.id.substring(0, 4) === "cid="), 30 | }) 31 | } else callback('Attachment file not found!'); 32 | }); 33 | } else { 34 | callback('Attachment not found!'); 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /query/clean.js: -------------------------------------------------------------------------------- 1 | // Automatically Cleans Stored Files based on Database status 2 | var _ = require('lodash'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var async = require('async'); 6 | // Connection -> Galleon.connection.collections.mail 7 | module.exports = function (Galleon, query, callback) { 8 | /* Implement Attachment removal */ 9 | // Make sure raw path is retrievable from environment 10 | if (!_.has(Galleon.environment, "paths.raw")) return callback(new Error("Raw Path not found.")); 11 | // Resolve Path to raw emails 12 | var rawPath = path.resolve(Galleon.environment.paths.raw); 13 | // Read List of files 14 | fs.readdir(rawPath, function (error, files) { 15 | if (error) return callback(error); 16 | // Execution Array for Async Parallel 17 | var ParallelExecutionArray = []; 18 | _.each(files, function (eID) { 19 | // Push a check function per file to Exec Array 20 | ParallelExecutionArray.push(function UNLINK_IF_NO_RECORD(_callback) { 21 | Galleon.connection.collections.mail.findOne({ eID: eID }, function (error, model) { 22 | if (error) return _callback(error); 23 | // If record not found Unlink 24 | if (!model) { 25 | console.log("UNLINKING", path.resolve(rawPath, eID)); 26 | fs.unlink(path.resolve(rawPath, eID), _callback); 27 | } else { 28 | _callback(); 29 | } 30 | }) 31 | }) 32 | }) 33 | // Launch in parallel 34 | async.parallel(ParallelExecutionArray, callback); 35 | }) 36 | } -------------------------------------------------------------------------------- /query/get.js: -------------------------------------------------------------------------------- 1 | var build = require('./get.build'); 2 | 3 | module.exports = function (Galleon, query, callback) { 4 | var folder = build(query.email, query.page)[query.folder.toUpperCase()]; 5 | if (!folder) return callback('Folder not found!'); 6 | 7 | // Set Default Query Paginate to true 8 | if (query.paginate === undefined) query.paginate = true; 9 | 10 | if (!Galleon.connection.collections[folder.collection]) return callback(new Error('Collection Not Found!')); 11 | 12 | function GALLEON_QUERY_GET_EXEC(error, mails) { 13 | if (error) return callback("Not Authenticated"); 14 | 15 | if ((!mails) || (mails.length < 1)) mails = []; 16 | 17 | // Count total 18 | Galleon.connection.collections.mail.count().where(folder.where).exec(function (error, found) { 19 | if (error) return callback("Not Authenticated"); 20 | 21 | callback(null, 22 | ((folder.filter) && (folder.filter.constructor === Function)) 23 | ? folder.filter(mails) : mails, 24 | { 25 | folder: query.folder, 26 | page: query.page, 27 | total: parseInt(found), 28 | limit: parseInt(folder.paginate.limit), 29 | }) 30 | }) 31 | } 32 | if (query.paginate) { 33 | Galleon.connection.collections[folder.collection] 34 | .find() 35 | .where(folder.where) 36 | .sort(folder.sort) 37 | .paginate(folder.paginate) 38 | .exec(GALLEON_QUERY_GET_EXEC) 39 | } else { 40 | Galleon.connection.collections[folder.collection] 41 | .find() 42 | .where(folder.where) 43 | .sort(folder.sort) 44 | .exec(GALLEON_QUERY_GET_EXEC) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /api/routes/api.js: -------------------------------------------------------------------------------- 1 | var argv = require('yargs').argv; 2 | 3 | var express = require('express'); 4 | var router = express.Router(); 5 | // Multipart-Upload 6 | var multer = require('multer'); 7 | // Attachment ID 8 | var shortId = require('shortid'); 9 | 10 | // Multipart parser for attachment uploads 11 | var storage = multer.diskStorage({ 12 | destination: function (req, file, callback) { 13 | callback(null, req.environment.paths.attachments || "\tmp") 14 | }, 15 | filename: function (req, file, callback) { 16 | /* SECURITY -> Email ID Must be validated */ 17 | callback(null, "+" + req.param("eID") + "_" + shortId.generate()) 18 | } 19 | }) 20 | 21 | var upload = multer({ 22 | storage: storage, limits: { 23 | fields: 20, 24 | fileSize: 15000000, 25 | files: 20, 26 | parts: 20000, 27 | } 28 | }) 29 | 30 | router.use(function (req, res, next) { 31 | // runs for all HTTP verbs first 32 | req.getCredentials(function (error, credentials) { 33 | if (error) return res.status(403).json({ error: "Not Authenticated", definition: error }); 34 | req.credentials = credentials; 35 | next(); 36 | }); 37 | }) 38 | 39 | router.get('/', require("./methods/get.emails.js")); 40 | router.patch('/:eID', require("./methods/patch.email.js")); 41 | router.delete('/:eID', require("./methods/delete.email.js")); 42 | router.get('/:eID/attachment/:id', require("./methods/get.attachment.js")); 43 | router.post('/send', require("./methods/post.email.js")); 44 | router.post('/send/:eID/attachment', upload.single('attachment'), require("./methods/post.attachment.js")); 45 | router.delete('/send/:eID/attachment/:ref', require("./methods/delete.attachment.js")); 46 | 47 | module.exports = router; 48 | -------------------------------------------------------------------------------- /query/link.attachment.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var _ = require('lodash'); 4 | var crypto = require('crypto'); 5 | 6 | module.exports = function (Galleon, query, callback) { 7 | if (!Galleon.connection.collections.queue) return callback(new Error('Collection Not Found!')); 8 | Galleon.connection.collections.queue.findOne({ 9 | association: query.email, 10 | eID: query.eID.substring(1) 11 | }).exec(function (error, mail) { 12 | if (error) return callback(error); 13 | if (!mail) return callback("Mail not found!"); 14 | if (!query.file) return callback("No file received"); 15 | 16 | // Make sure mail.attachments is an array 17 | if (!_.isArray(mail.attachments)) mail.attachments = []; 18 | 19 | var reference = crypto.randomBytes(8).toString('hex'); 20 | 21 | mail.attachments.push({ 22 | id: (query.file.filename) ? (query.file.filename.split("_")[1] || null) : null, 23 | cid: null, /* Not implemented yet */ 24 | fileName: query.file.originalname, 25 | path: query.file.path, 26 | 27 | transferEncoding: query.file.encoding, 28 | contentType: query.file.mimetype, 29 | checksum: null, /* Not implemented yet */ 30 | length: query.file.size, 31 | 32 | ref: reference 33 | }) 34 | Galleon.connection.collections.queue.update({ 35 | association: query.email, 36 | eID: query.eID.substring(1) 37 | }, { 38 | attachments: mail.attachments 39 | }).exec(function (error, mail) { 40 | if (error) return callback(error); 41 | if (!mail) return callback("Mail not found!"); 42 | 43 | // Else 44 | callback(null, { ref: reference }); 45 | }) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /fleet/incoming/attachment.js: -------------------------------------------------------------------------------- 1 | // Essential 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | // Utilities 5 | var async = require('async'); 6 | var _ = require('lodash'); 7 | // ID Generation 8 | var crypto = require('crypto'); 9 | 10 | module.exports = { 11 | stream: function (_path_, eID, attachment) { 12 | attachment.id = eID + "_" 13 | + crypto.createHash('md5') 14 | .update(attachment.generatedFileName || attachment.fileName) 15 | .digest('hex'); 16 | attachment.path = path.resolve(_path_, attachment.id); 17 | 18 | var output = fs.createWriteStream(attachment.path); 19 | attachment.stream.pipe(output); 20 | }, 21 | save: function (_path_, databaseConnection, eID, attachments) { 22 | if (!attachments) return; 23 | 24 | var populatedAttachments = []; 25 | _.each(attachments || [], function (attachment) { 26 | attachment.id = eID + "_" 27 | + crypto.createHash('md5') 28 | .update(attachment.generatedFileName || attachment.fileName) 29 | .digest('hex'); 30 | attachment.path = path.resolve(_path_, attachment.id); 31 | populatedAttachments.push({ 32 | id: attachment.id, 33 | cid: attachment.contentId, 34 | fileName: attachment.fileName, 35 | path: attachment.path, 36 | 37 | transferEncoding: attachment.transferEncoding, 38 | contentType: attachment.contentType, 39 | checksum: attachment.checksum, 40 | length: attachment.length 41 | }); 42 | }) 43 | // Update Database 44 | databaseConnection.collections.mail.update({ 'eID': eID }, { 45 | attachments: populatedAttachments 46 | }, function (error, model) { 47 | /* Have this fall under verbose settings */ 48 | if (error) console.error("EMAIL BOUNCED", error); 49 | }); 50 | } 51 | } -------------------------------------------------------------------------------- /fleet/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('lodash') 6 | , Waterline = require('waterline'); 7 | 8 | 9 | /** 10 | * Set up Waterline with the specified 11 | * models, connections, and adapters. 12 | 13 | @param options 14 | :: {Object} adapters [i.e. a dictionary] 15 | :: {Object} connections [i.e. a dictionary] 16 | :: {Object} collections [i.e. a dictionary] 17 | 18 | @param {Function} cb 19 | () {Error} err 20 | () ontology 21 | :: {Object} collections 22 | :: {Object} connections 23 | 24 | @return {Waterline} 25 | */ 26 | 27 | module.exports = function bootstrap(options, cb) { 28 | 29 | var adapters = options.adapters || {}; 30 | var connections = options.connections || {}; 31 | var collections = options.collections || {}; 32 | 33 | _.each(adapters, function (def, identity) { 34 | // Make sure our adapter defs have `identity` properties 35 | def.identity = def.identity || identity; 36 | }); 37 | 38 | 39 | var extendedCollections = []; 40 | _.each(collections, function (def, identity) { 41 | 42 | // Make sure our collection defs have `identity` properties 43 | def.identity = def.identity || identity; 44 | 45 | // Fold object of collection definitions into an array 46 | // of extended Waterline collections. 47 | extendedCollections.push(Waterline.Collection.extend(def)); 48 | }); 49 | 50 | 51 | // Instantiate Waterline and load the already-extended 52 | // Waterline collections. 53 | var waterline = new Waterline(); 54 | extendedCollections.forEach(function (collection) { 55 | waterline.loadCollection(collection); 56 | }); 57 | 58 | 59 | // Initialize Waterline 60 | // (and tell it about our adapters) 61 | waterline.initialize({ 62 | adapters: adapters, 63 | connections: connections 64 | }, cb); 65 | 66 | return waterline; 67 | }; 68 | -------------------------------------------------------------------------------- /fleet/models/Queue.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var shortId = require('shortid'); 3 | 4 | module.exports = { 5 | // Idenitity is a unique name for this model 6 | identity: 'queue', 7 | connection: 'storage', 8 | 9 | types: { 10 | schedule: function (time) { 11 | return time.attempted && time.scheduled 12 | } 13 | }, 14 | 15 | attributes: { 16 | eID: { 17 | type: 'string', 18 | required: false, // Automatically created 19 | maxLength: 48, 20 | unique: true, 21 | }, 22 | 23 | association: { 24 | type: 'string', 25 | required: true, 26 | index: true, 27 | }, 28 | 29 | sender: { 30 | type: 'string', 31 | required: true, 32 | index: true, 33 | }, 34 | 35 | to: { 36 | type: 'json', 37 | required: true 38 | }, 39 | 40 | schedule: { 41 | type: 'json', 42 | required: true 43 | }, 44 | 45 | attempts: { 46 | type: 'integer', 47 | required: true 48 | }, 49 | 50 | subject: { 51 | type: 'string', 52 | maxLength: 998, // Refer to rfc5322#section-2.1.1 53 | required: false 54 | }, 55 | 56 | text: { 57 | type: 'string', 58 | required: false 59 | }, 60 | 61 | html: { 62 | type: 'string', 63 | required: false // Not required to allow for drafting 64 | }, 65 | 66 | attachments: { 67 | type: 'array' 68 | }, 69 | 70 | state: { 71 | type: 'string', 72 | enum: ['draft', 'pending', 'transit', 'sent', 'denied', 'failed'], 73 | required: true 74 | } 75 | }, 76 | 77 | beforeCreate: function (attributes, callback) { 78 | // Should round up about 14 + 2 + 32 = 48 characters at max 79 | // Hashsum enables content checking using a MD5 checksum 80 | attributes.eID = shortId.generate() + '&&' + crypto.createHash('md5').update(attributes.html).digest('hex'); 81 | callback(); 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /bin/tasks/setup_ssl.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | var herb = require('herb'); 6 | var colors = require('colors'); 7 | var osenv = require('osenv'); 8 | var fs = require('fs'); 9 | var path = require('path'); 10 | var inquirer = require('inquirer'); 11 | var askFor = require('./_questionnaire'); 12 | var crypto = require('crypto'); 13 | var configFile = require('../configFile'); 14 | 15 | var config = _.defaults(configFile.getSync(), { 16 | connections: { 17 | storage: new Object, 18 | authentication: new Object 19 | }, 20 | paths: new Object, 21 | modules: [], 22 | secret: crypto.randomBytes(20).toString('hex'), 23 | ssl: { 24 | use: false, 25 | incoming: { 26 | cert: undefined, 27 | key: undefined 28 | }, 29 | api: { 30 | cert: undefined, 31 | key: undefined 32 | } 33 | } 34 | }); 35 | 36 | var defaultDirectory = path.resolve(osenv.home(), '.galleon/'); 37 | 38 | module.exports = function (callback) { 39 | askFor.ssl().then(function (answers) { 40 | if (answers.shouldUseSSL) { 41 | config.ssl = { 42 | use: true, 43 | incoming: { 44 | cert: answers['ssl-smtp-cert'], 45 | key: answers['ssl-smtp-key'], 46 | ca: answers['ssl-smtp-ca'] 47 | }, 48 | api: { 49 | cert: answers['ssl-api-cert'], 50 | key: answers['ssl-api-key'], 51 | ca: answers['ssl-api-ca'] 52 | } 53 | } 54 | } 55 | 56 | // Log action 57 | herb.marker({ 58 | color: 'green' 59 | }).log('Updating config file ...'); 60 | 61 | // Write config to config file 62 | fs.writeFile(path.resolve(defaultDirectory, 'galleon.conf'), JSON.stringify(config, null, 2), function (error) { 63 | if (error) herb.error(error); 64 | herb.log('SSL CONFIG SUCCESSFUL!'); 65 | herb.log('Get yourself started by typing', colors.magenta('galleon restart'), 'in order to restart Galleon!'); 66 | process.exit(0); 67 | }); 68 | 69 | }).catch((error) => { 70 | throw error; 71 | }); 72 | } -------------------------------------------------------------------------------- /tutorials/INSTALLATION.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | ### Basic Requirements 3 | 1. A domain name (\*Fully qualified but we'll get to that) 4 | 2. A server running Node.JS 5 | 3. A local database (**Galleon** supports most [Waterline](https://github.com/balderdashy/waterline) modules) 6 | 7 | **Note:** Only NodeJS and Authbind is required for Galleon to function in a non-production environment. You should use a database and enable SpamAssasin by following the below directions to create a solid environment. 8 | ### OPTIONAL -> Install [MongoDB](http://docs.mongodb.org/manual/installation/) 9 | ``` 10 | sudo apt-get install -y mongodb-org 11 | sudo service mongod start 12 | ``` 13 | ### REQUIRED -> Install [NodeJS](http://nodejs.org/download/) 14 | ``` 15 | sudo apt-get update 16 | sudo apt-get install nodejs npm 17 | ``` 18 | ### OPTIONAL -> Install [SpamAssasin](http://spamassassin.apache.org/downloads.cgi?update=201402111327) 19 | Note that Spam detection is automatically available once the SpamAssassin Daemon **SPAMD** is online (after installation). For automatic training and reporting [Refer to the tutorial here!](https://github.com/schahriar/Galleon/blob/master/tutorials/SPAMASSASIN.md) 20 | ``` 21 | sudo apt-get install spamassassin spamc 22 | ``` 23 | ### Install [Galleon](https://github.com/schahriar/Galleon) 24 | Make sure to include the *-g* flag in order to enable CLI functions. 25 | ```javascript 26 | npm install -g Galleon 27 | ``` 28 | 29 | ------- 30 | ## Setup 31 | Run the following command to setup local directories and database connection: 32 | ``` 33 | galleon setup 34 | ``` 35 | 36 | ------- 37 | ## Authbind 38 | You'll need to setup **authbind** before running Galleon. [Check out the tutorial here!](https://github.com/schahriar/Galleon/blob/master/tutorials/AUTHBIND.md) 39 | 40 | **After setting up Authbind** you can run Galleon using: 41 | ``` 42 | authbind --deep galleon start 43 | ``` 44 | 45 | ------- 46 | ## Front-end interface 47 | Galleon no longer packages a front-end interface but rather provides an API. You can install [**Seascape** from NPM](https://npmjs.com/package/galleon-seascape) and serve as a front-facing interface. 48 | -------------------------------------------------------------------------------- /bin/tasks/start.js: -------------------------------------------------------------------------------- 1 | var pm2 = require('pm2'); 2 | var _ = require('lodash'); 3 | var path = require('path'); 4 | 5 | // Thanks to https://gist.github.com/timoxley/1689041 6 | var isPortTaken = function (port, fn) { 7 | var net = require('net') 8 | var tester = net.createServer() 9 | .once('error', function (err) { 10 | if (err.code != 'EADDRINUSE') return fn(err) 11 | fn(null, false) 12 | }) 13 | .once('listening', function () { 14 | tester.once('close', function () { fn(null, true) }) 15 | .close() 16 | }) 17 | .listen(port) 18 | } 19 | 20 | module.exports = function () { 21 | // Connect or launch PM2 22 | pm2.connect(function (error) { 23 | if (error) throw error; 24 | 25 | pm2.list(function (error, list) { 26 | if (error) throw error; 27 | if (_.findWhere(list, { name: 'galleon-instance' })) { 28 | console.error("Instance already exists!".red, "\nTRY", "galleon restart".magenta); 29 | process.exit(0); 30 | } 31 | 32 | isPortTaken(25, function (error, available) { 33 | if (error || !available) { 34 | console.log("PORT 25 is not available", "\nTRY", "authbind --deep galleon start".magenta, "\nFind More info about authbind -> https://github.com/schahriar/Galleon/blob/master/tutorials/AUTHBIND.md") 35 | process.exit(0); 36 | } 37 | 38 | // Start a script on the current folder 39 | /* BADPATCH -- There are significant issues with providing PM2 with a local script (https://github.com/schahriar/Galleon/issues/2). Start.JS should implement fallback methods and use a launch script inside the .galleon folder by default. */ 40 | pm2.start(path.resolve(__dirname, '../galleon.js'), { name: 'galleon-instance', force: true, scriptArgs: process.argv, nodeArgs: "--max_old_space_size=300" }, function (err, proc) { 41 | if (err) return new Error(err); 42 | 43 | // Get all processes running 44 | pm2.list(function (err, process_list) { 45 | console.log("Process Started".green); 46 | console.log("Type".cyan + " galleon status ".bold + "to show process status".cyan); 47 | 48 | // Disconnect to PM2 49 | pm2.disconnect(function () { process.exit(0) }); 50 | }); 51 | }); 52 | }) 53 | }) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /tests/B_USER_Management_Tests.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcryptjs'); 2 | var chai = require("chai"); 3 | var expect = chai.expect; 4 | 5 | describe('User Management Test Suite', function(){ 6 | this.timeout(8000); 7 | it("should create a user", function(done) { 8 | global.galleon.createUser({ 9 | email: "info@example.com", 10 | password: "bestpasswordever", 11 | name: "test" 12 | }, function(error, user) { 13 | if(error) throw error; 14 | expect(user.email).to.equal("info@example.com"); 15 | expect(user.name).to.equal("test"); 16 | done(); 17 | }) 18 | }) 19 | it("should hash user's password correctly", function(done) { 20 | global.galleon.createUser({ 21 | email: "hash@example.com", 22 | password: "okpassword", 23 | name: "hash" 24 | }, function(error, user) { 25 | if(error) throw error; 26 | expect(user.email).to.equal("hash@example.com"); 27 | expect(bcrypt.compareSync("okpassword", user.password)).to.equal(true); 28 | done(); 29 | }) 30 | }) 31 | it("should change user's password & hash it correctly", function(done) { 32 | global.galleon.changePassword("hash@example.com", "changetopass", "okpassword", function(error, user) { 33 | if(error) throw error; 34 | expect(user.email).to.equal("hash@example.com"); 35 | expect(bcrypt.compareSync("changetopass", user.password)).to.equal(true); 36 | done(); 37 | }) 38 | }) 39 | it("should remove a user", function(done) { 40 | global.galleon.removeUser("hash@example.com", function(error) { 41 | if(error) throw error; 42 | done(); 43 | }) 44 | }) 45 | it("should list users", function(done) { 46 | global.galleon.listUsers(function(error, users) { 47 | if(error) throw error; 48 | expect(users).to.have.length(1); 49 | expect(users[0].email).to.be.equal("info@example.com"); 50 | done(); 51 | }) 52 | }) 53 | it("should deny a short/bad password", function(done) { 54 | global.galleon.changePassword("info@example.com", "pass", "bestpasswordever", function(error, user) { 55 | expect(error).to.exist; 56 | done(); 57 | }) 58 | }) 59 | it("should deny a password change with wrong old password", function(done) { 60 | global.galleon.changePassword("info@example.com", "bestpasswordever", "wrongpass", function(error, user) { 61 | expect(error).to.exist; 62 | expect(error.message).to.equal("Current password does not match!"); 63 | done(); 64 | }) 65 | }) 66 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "galleon", 3 | "description": "A badass SMTP mail server built on Node to make your life simpler.", 4 | "version": "0.5.0", 5 | "main": "Galleon.js", 6 | "preferGlobal": true, 7 | "bin": { 8 | "galleon": "./bin/startup" 9 | }, 10 | "author": "Schahriar SaffarShargh ", 11 | "keywords": [ 12 | "Galleon", 13 | "SMTP", 14 | "MAIL", 15 | "SERVER", 16 | "EMAIL", 17 | "galleon.email" 18 | ], 19 | "license": "MIT", 20 | "dependencies": { 21 | "async": "^2.1.4", 22 | "bcryptjs": "^2.3.0", 23 | "blanket": "^1.2.3", 24 | "body-parser": "^1.15.2", 25 | "chai": "^3.5.0", 26 | "colors": "^1.1.2", 27 | "compression": "^1.6.2", 28 | "cookie-parser": "^1.4.3", 29 | "debug": "^2.3.3", 30 | "express": "^4.14.0", 31 | "herb": "^2.2.6", 32 | "inquirer": "^3.0.1", 33 | "istanbul": "^0.4.5", 34 | "jsonwebtoken": "^7.2.1", 35 | "lodash": "^3.10.1", 36 | "mailparser": "^0.6.1", 37 | "mocha": "^3.2.0", 38 | "moment": "^2.17.1", 39 | "morgan": "^1.7.0", 40 | "multer": "^1.2.0", 41 | "nodemailer": "^2.6.4", 42 | "osenv": "^0.1.3", 43 | "pm2": "^2.1.6", 44 | "portscanner": "^2.1.1", 45 | "request": "^2.79.0", 46 | "sails-disk": "^0.10.10", 47 | "sails-memory": "^0.10.7", 48 | "sails-mongo": "^0.12.1", 49 | "sails-mysql": "^0.11.5", 50 | "sails-postgresql": "^0.11.4", 51 | "sails-redis": "^0.10.7", 52 | "sails-sqlserver": "^0.10.8", 53 | "serve-favicon": "^2.3.2", 54 | "shelljs": "^0.7.5", 55 | "shortid": "^2.2.6", 56 | "smtp-connection": "^2.12.2", 57 | "smtp-server": "^1.16.1", 58 | "spamc-stream": "^1.0.2", 59 | "validator": "^6.2.0", 60 | "waterline": "^0.11.6", 61 | "yargs": "^6.6.0" 62 | }, 63 | "repository": { 64 | "type": "git", 65 | "url": "git://github.com/schahriar/Galleon.git" 66 | }, 67 | "scripts": { 68 | "start": "node ./seascape/server.js", 69 | "test": "node ./node_modules/mocha/bin/mocha test.js", 70 | "coverage": "mocha test.js -r blanket -R html-cov > coverage.html" 71 | }, 72 | "config": { 73 | "blanket": { 74 | "data-cover-never": [ 75 | "node_modules", 76 | "tests" 77 | ], 78 | "data-cover-reporter-options": { 79 | "shortnames": true 80 | } 81 | } 82 | }, 83 | "devDependencies": { 84 | "blanket": "^1.1.9", 85 | "chai": "^3.3.0", 86 | "istanbul": "^0.4.0", 87 | "mocha": "^2.3.3", 88 | "request": "^2.65.0", 89 | "smtp-connection": "^1.3.1" 90 | }, 91 | "bugs": { 92 | "url": "https://github.com/schahriar/Galleon/issues" 93 | }, 94 | "homepage": "https://github.com/schahriar/Galleon#readme", 95 | "directories": { 96 | "test": "tests" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /query/restore.js: -------------------------------------------------------------------------------- 1 | // Automatically Restores Raw Files to Database based on Database check 2 | var _ = require('lodash'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var async = require('async'); 6 | var Processor = require('../fleet/incoming/processor'); 7 | 8 | module.exports = function (Galleon, query, callback) { 9 | // Make sure raw path is retrievable from environment 10 | if (!_.has(Galleon.environment, "paths.raw")) return callback(new Error("Raw Path not found.")); 11 | // Resolve Path to raw emails 12 | var rawPath = path.resolve(Galleon.environment.paths.raw); 13 | 14 | var RestoreToDatabase = Processor(Galleon, Galleon.connection, Galleon.spamc); 15 | 16 | // Read List of files 17 | fs.readdir(rawPath, function (error, files) { 18 | if (error) return callback(error); 19 | // Execution Array for Async Parallel 20 | var ParallelExecutionArray = []; 21 | _.each(files, function (eID) { 22 | // Push a check function per file to Exec Array 23 | ParallelExecutionArray.push(function RESTORE_IF_NO_RECORD(_callback) { 24 | Galleon.connection.collections.mail.findOne({ eID: eID }, function (error, model) { 25 | if (error) return _callback(error); 26 | // If record not found restore 27 | // Match Legacy Raw records 28 | var OriginalID = eID; 29 | if (eID.substring(0, 5) === '_raw_') eID = eID.substring(5); 30 | if (!model) { 31 | var CALLBACK_CALLED = false; 32 | var FileStream = fs.createReadStream(path.resolve(rawPath, OriginalID)); 33 | // Ignore Stream errors 34 | FileStream.on('error', function (error) { 35 | if (!CALLBACK_CALLED) _callback(null, "ERROR:" + eID + "TIMED_OUT"); 36 | CALLBACK_CALLED = true; 37 | }); 38 | RestoreToDatabase(FileStream, { 39 | eID: eID, 40 | path: path.resolve(rawPath, OriginalID), 41 | store: false 42 | }, function (error) { 43 | // Call callback regardless of error 44 | if (error) { 45 | CALLBACK_CALLED = true; 46 | return _callback(null, "ERROR:" + eID + error); 47 | } 48 | console.log("RESTORED", eID); 49 | if (!CALLBACK_CALLED) _callback(); 50 | CALLBACK_CALLED = true; 51 | }); 52 | // Timeout Function 53 | setTimeout(function () { 54 | if (!CALLBACK_CALLED) { 55 | _callback(null, "ERROR:" + eID + "TIMED_OUT"); 56 | } 57 | CALLBACK_CALLED = true; 58 | }, 15000); 59 | } else { 60 | // EMAIL Exists -> Continue 61 | _callback(); 62 | } 63 | }) 64 | }) 65 | }) 66 | // Launch in parallel 67 | async.parallelLimit(ParallelExecutionArray, 5, callback); 68 | }) 69 | } -------------------------------------------------------------------------------- /tests/D_API_User_Auth.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcryptjs'); 2 | var chai = require("chai"); 3 | var expect = chai.expect; 4 | var request = require('request'); 5 | 6 | var CookieJar = request.jar(); 7 | var request = request.defaults({ jar: CookieJar }); 8 | 9 | describe('API Test Suite', function () { 10 | this.timeout(8000); 11 | it("should connect to API", function (done) { 12 | request.post({ 13 | url: 'http://localhost:3080/access/login/', 14 | form: { 15 | email: "info@example.com", 16 | password: "bestpasswordever" 17 | }, 18 | jar: true 19 | }, function (err, httpResponse, body) { 20 | if (err) throw err; 21 | expect(JSON.parse(body).success).to.equal(true); 22 | done() 23 | }); 24 | }) 25 | it("should remain connected with a cookie", function (done) { 26 | request.get({ 27 | url: "http://localhost:3080/access/check", 28 | jar: true 29 | }, function (err, httpResponse, body) { 30 | if (err) throw err; 31 | expect(JSON.parse(body).authenticated).to.not.equal(false); 32 | expect(JSON.parse(body).authenticated.email).to.equal("info@example.com"); 33 | done() 34 | }) 35 | }) 36 | it("should change password correctly", function(done) { 37 | request.post({ 38 | url: 'http://localhost:3080/access/changepassword/', 39 | form: { 40 | email: "info@example.com", 41 | cpassword: "bestpasswordever", 42 | password: "newpassword" 43 | }, 44 | jar: true 45 | }, function (err, httpResponse, body) { 46 | if (err) throw err; 47 | expect(JSON.parse(body).success).to.equal(true); 48 | done() 49 | }); 50 | }) 51 | it("should login with the new password", function (done) { 52 | request.post({ 53 | url: 'http://localhost:3080/access/login', 54 | form: { 55 | email: "info@example.com", 56 | password: "newpassword" 57 | }, 58 | jar: true 59 | }, function (err, httpResponse, body) { 60 | if (err) throw err; 61 | expect(JSON.parse(body).success).to.equal(true); 62 | done() 63 | }); 64 | }) 65 | it("should logout correctly", function(done) { 66 | request.post({ 67 | url: "http://localhost:3080/access/logout", 68 | jar: true 69 | }, function (err, httpResponse, body) { 70 | if (err) throw err; 71 | expect(JSON.parse(body).success).to.equal(true); 72 | // Double check 73 | request.get({ 74 | url: "http://localhost:3080/access/check", 75 | jar: true 76 | }, function (err, httpResponse, body) { 77 | if (err) throw err; 78 | expect(JSON.parse(body).authenticated).to.equal(false); 79 | done() 80 | }) 81 | }) 82 | }) 83 | it("should deny invalid password", function(done) { 84 | request.post({ 85 | url: 'http://localhost:3080/access/login', 86 | form: { 87 | email: "info@example.com", 88 | password: "bestpasswordever" 89 | }, 90 | jar: true 91 | }, function (err, httpResponse, body) { 92 | if (err) throw err; 93 | expect(JSON.parse(body).success).to.not.equal(true); 94 | done() 95 | }); 96 | }) 97 | }) -------------------------------------------------------------------------------- /fleet/models/Mail.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | var shortId = require('shortid'); 3 | 4 | module.exports = { 5 | // Idenitity is a unique name for this model 6 | identity: 'mail', 7 | connection: 'storage', 8 | 9 | types: { 10 | stamp: function (time) { 11 | return time.sent && time.received 12 | } 13 | }, 14 | 15 | attributes: { 16 | eID: { 17 | type: 'string', 18 | required: false, // Automatically created 19 | maxLength: 48, 20 | unique: true, 21 | }, 22 | 23 | // Should handle multiple associations 24 | // This would allow email sharing within organization and group associations 25 | // Unfortunately `contains` is not consistent across waterline adapters 26 | // Multiple Association is not possible at the time 27 | association: { 28 | type: 'string', 29 | required: true, 30 | index: true, 31 | }, 32 | 33 | sender: { 34 | type: 'string', 35 | required: true, 36 | index: true, 37 | }, 38 | 39 | receiver: { 40 | type: 'string', 41 | required: true, 42 | index: true 43 | }, 44 | 45 | to: { 46 | type: 'json', 47 | required: false 48 | }, 49 | 50 | stamp: { 51 | type: 'json', 52 | json: true 53 | }, 54 | 55 | subject: { 56 | type: 'string', 57 | maxLength: 998, // Refer to rfc5322#section-2.1.1 58 | required: false 59 | }, 60 | 61 | text: { 62 | type: 'string', 63 | required: false 64 | }, 65 | 66 | html: { 67 | type: 'string', 68 | required: false 69 | }, 70 | 71 | // Indicates if an email has been read 72 | read: { 73 | type: 'boolean', 74 | required: true 75 | }, 76 | 77 | // Indicates if an email has been trashed 78 | trash: { 79 | type: 'boolean', 80 | required: true 81 | }, 82 | 83 | // Indicates if an email is spam 84 | spam: { 85 | type: 'boolean', 86 | required: true 87 | }, 88 | 89 | // Indicates if an email is an outbox sent 90 | sent: { 91 | type: 'boolean', 92 | required: true, 93 | defaultsTo: false 94 | }, 95 | 96 | // DKIM Test 97 | dkim: { 98 | type: 'boolean', 99 | required: true, 100 | defaultsTo: false 101 | }, 102 | 103 | // spf Test 104 | spf: { 105 | type: 'boolean', 106 | required: true, 107 | defaultsTo: false 108 | }, 109 | 110 | // Ranges from 0 to 100 111 | spamScore: { 112 | type: 'integer', 113 | required: true 114 | }, 115 | 116 | attachments: { 117 | type: 'array' 118 | }, 119 | 120 | state: { 121 | type: 'string', 122 | enum: ['draft', 'pending', 'approved', 'denied', 'trashed'] 123 | } 124 | }, 125 | 126 | beforeCreate: function (attributes, callback) { 127 | // Should round up about 14 + 2 + 32 = 48 characters at max 128 | // Hashsum enables content checking using a MD5 checksum 129 | if (!attributes.html) attributes.html = attributes.text || "[NO_MESSAGE]"; 130 | if (!attributes.eID) attributes.eID = shortId.generate() + '&&' + crypto.createHash('md5').update(attributes.subject || "NONE").digest('hex'); 131 | callback(); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /fleet/incoming/create.js: -------------------------------------------------------------------------------- 1 | // Essentials 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | 6 | module.exports = function (_this, database, session, parsed, callback) { 7 | // Makes sure Email is parsed properly 8 | // Otherwise ignore 9 | if ( 10 | (!parsed) 11 | || 12 | (typeof (parsed) !== 'object') 13 | || 14 | (!parsed.envelopeTo) 15 | || 16 | (!_.isArray(parsed.envelopeTo)) 17 | || 18 | (!parsed.envelopeTo[0].address) 19 | || 20 | (!parsed.from) 21 | || 22 | (!_.isObject(parsed.from)) 23 | || 24 | (!session) 25 | || 26 | (!parsed.text && !parsed.html && !parsed.subject) /* Require at least a subject/text/html */ 27 | ) { 28 | _this.emit('ignored', session, parsed || {}, database); 29 | if (_this.environment.verbose) console.error("FAILED TO PARSE EMAIL"); 30 | return callback({ responseCode: 451, message: "local error in processing, E-MAIL is invalid." }); 31 | } 32 | 33 | // Formats from to -> Name 34 | if (_.isPlainObject(parsed.from)) 35 | parsed.from = parsed.from.name + ' <' + parsed.from.address + '>'; 36 | else if (_.isArray(parsed.from)) 37 | parsed.from = parsed.from[0].name + ' <' + parsed.from[0].address + '>'; 38 | 39 | // Sets association to envelope's receiver 40 | parsed.associtaion = parsed.envelopeTo[0].address; 41 | // --------------------- // 42 | 43 | var email = { 44 | eID: session.eID, 45 | association: parsed.associtaion, 46 | sender: parsed.from, 47 | receiver: parsed.headers.to || parsed.associtaion, 48 | to: parsed.toAll, 49 | stamp: { sent: (new Date(parsed.date)), received: (new Date()) }, 50 | subject: parsed.subject, 51 | text: parsed.text, 52 | html: parsed.html, 53 | 54 | read: false, 55 | trash: false, 56 | 57 | dkim: (parsed.dkim === "pass"), 58 | spf: (parsed.spf === "pass"), 59 | 60 | spam: false, 61 | spamScore: 0, 62 | 63 | // STRING ENUM: ['pending', 'approved', 'denied'] 64 | state: 'pending' 65 | } 66 | 67 | // Load incoming modules 68 | _this.environment.modulator.launch('incoming', parsed.associtaion, email, parsed, function (error, _email, _ignore) { 69 | if (_this.environment.verbose) console.log("INCOMING MODULES LAUNCHED".green, arguments); 70 | 71 | // Ignore email if requested 72 | if (_ignore === true) return _this.emit('ignored', session, parsed, database); 73 | 74 | // Assign modified ~email~ object if provided 75 | if (!_email) _email = email; 76 | // Create a new mail in the database 77 | database.collections.mail.create(_email, function (error, model) { 78 | if (error) { 79 | console.error(error, 'error'); 80 | 81 | // Emits 'mail' event with - SMTP Session, Mail object, Raw content, Database failure & Database object 82 | _this.emit('mail', error, session, parsed, database); 83 | callback(error, session, parsed, database); 84 | } else { 85 | // Add attachments to Mail 86 | _this.attach(database, model.eID, parsed.attachments); 87 | 88 | // Emits 'mail' event with - SMTP Session, Mail object, Raw content, Database model & Database object 89 | _this.emit('mail', null, session, parsed, database); 90 | callback(null, session, parsed, database); 91 | } 92 | }); 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /bin/tasks/setup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('lodash'); 4 | var async = require('async'); 5 | var herb = require('herb'); 6 | var colors = require('colors'); 7 | var osenv = require('osenv'); 8 | var fs = require('fs'); 9 | var path = require('path'); 10 | var inquirer = require('inquirer'); 11 | var askFor = require('./_questionnaire'); 12 | var Database = require('../../fleet/connection'); 13 | var crypto = require('crypto'); 14 | var configFile = require('../configFile'); 15 | 16 | var config = _.defaults(configFile.getSync(), { 17 | connections: { 18 | storage: new Object, 19 | authentication: new Object 20 | }, 21 | paths: new Object, 22 | modules: [], 23 | secret: crypto.randomBytes(20).toString('hex'), 24 | ssl: { 25 | use: false, 26 | incoming: { 27 | cert: undefined, 28 | key: undefined 29 | }, 30 | api: { 31 | cert: undefined, 32 | key: undefined 33 | } 34 | } 35 | }); 36 | 37 | var defaultDirectory = path.resolve(osenv.home(), '.galleon/'); 38 | 39 | function createDirectoryIfNotFound() { 40 | var dir = path.resolve.apply(null, arguments); 41 | if (!fs.existsSync(dir)) { 42 | fs.mkdirSync(dir); 43 | } 44 | return dir; 45 | } 46 | 47 | function checkDatabaseConnection(callback) { 48 | askFor.database().then(function (answers) { 49 | config.connections.storage = { 50 | adapter: answers.adapter, 51 | host: answers.host, 52 | port: answers.port, 53 | user: answers.user, 54 | password: answers.password, 55 | database: answers.database_name 56 | } 57 | config.connections.authentication = config.connections.storage; 58 | 59 | try { 60 | // Log next action 61 | herb.marker({ 62 | color: 'green' 63 | }).log('Checking database connection ...'); 64 | Database(config.connections, function (error, connection) { 65 | if (error) { 66 | herb.line('- -'); 67 | herb.warning("Database connection could not be made! Try again.") 68 | return checkDatabaseConnection(callback); 69 | } else callback(); 70 | }) 71 | } catch (error) { 72 | return callback(error); 73 | } 74 | }).catch((error) => { 75 | throw error; 76 | }); 77 | } 78 | 79 | module.exports = function (Galleon) { 80 | async.waterfall([ 81 | function (callback) { 82 | askFor.domain().then(function (answer) { 83 | config.domain = answer.domain; 84 | callback(); 85 | }).catch((error) => { 86 | throw error; 87 | }); 88 | }, 89 | function (callback) { 90 | createDirectoryIfNotFound(defaultDirectory); 91 | askFor.directory().then(function (answers) { 92 | if (answers.perform.indexOf('attachments') + 1) config.paths.attachments = answers.location_attachments || createDirectoryIfNotFound(defaultDirectory, 'attachments/'); 93 | if (answers.perform.indexOf('raw') + 1) config.paths.raw = answers.location_raw || createDirectoryIfNotFound(defaultDirectory, 'raw/'); 94 | callback(); 95 | }).catch((error) => { 96 | throw error; 97 | }); 98 | }, 99 | checkDatabaseConnection 100 | ], function (error, result) { 101 | if (error) return herb.error(error, "\nPlease retry again!"); 102 | 103 | // Log action 104 | herb.marker({ 105 | color: 'green' 106 | }).log('Creating config file ...'); 107 | 108 | // Write config to config file 109 | fs.writeFile(path.resolve(defaultDirectory, 'galleon.conf'), JSON.stringify(config, null, 2), function (error) { 110 | if (error) herb.error(error); 111 | herb.log('CONFIG SUCCESSFUL!'); 112 | herb.log('Get yourself started by typing', colors.magenta('galleon start'), 'in order to launch an instance of Galleon!'); 113 | process.exit(0); 114 | }); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /bin/configFile.js: -------------------------------------------------------------------------------- 1 | var osenv = require('osenv'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | 6 | function createDirectoryIfNotFound() { 7 | var dir = path.resolve.apply(null, arguments); 8 | if (!fs.existsSync(dir)) { 9 | fs.mkdirSync(dir); 10 | } 11 | return dir; 12 | } 13 | 14 | var defaultPath = path.resolve(osenv.home(), '.galleon/galleon.conf'); 15 | createDirectoryIfNotFound(path.resolve(osenv.home(), '.galleon')); 16 | 17 | var env = { 18 | watch: function (callback) { 19 | if (typeof callback === 'function') callback = _.noop(); 20 | // Listens to config file 21 | // and calls callback when changed 22 | fs.watchFile(defaultPath, function (curr, prev) { 23 | if (curr.mtime > prev.mtime) { 24 | env.get(function (error, data) { 25 | callback(error, data, curr, prev); 26 | }); 27 | } 28 | }); 29 | }, 30 | get: function (callback) { 31 | if (typeof callback !== 'function') callback = _.noop(); 32 | fs.exists(defaultPath, function (exists) { 33 | if (!exists) return callback("CONFIG FILE NOT FOUND!"); 34 | fs.readFile(defaultPath, 'utf8', function (error, data) { 35 | if (error) return callback(error); 36 | callback(null, JSON.parse(data)); 37 | }); 38 | }); 39 | }, 40 | getSync: function () { 41 | if (!fs.existsSync(defaultPath)) return {}; 42 | return JSON.parse(fs.readFileSync(defaultPath, 'utf8')); 43 | }, 44 | getModulesSync: function () { 45 | return this.getSync().modules; 46 | }, 47 | set: function (obj, callback) { 48 | if (typeof callback === 'function') callback = _.noop(); 49 | fs.writeFile(defaultPath, JSON.stringify(obj, null, 2), function (error) { 50 | if (error) return callback(error); 51 | if (callback) callback(); 52 | }); 53 | }, 54 | setSync: function (obj) { 55 | return fs.writeFileSync(defaultPath, JSON.stringify(obj, null, 2)); 56 | }, 57 | setModules: function (modules, callback) { 58 | if (typeof callback === 'function') callback = _.noop(); 59 | var self = this; 60 | modules = _.toArray(modules); 61 | self.get(function (error, data) { 62 | if (error) return callback(error); 63 | 64 | data.modules = modules; 65 | self.set(data, callback); 66 | }); 67 | }, 68 | updateModuleConfig: function (Module, Config, callback) { 69 | if (typeof callback === 'function') callback = _.noop(); 70 | var self = this; 71 | self.get(function (error, data) { 72 | if (error) return callback(error); 73 | var MODULE = _.findWhere(data.modules, { name: Module.name }); 74 | MODULE.config = _.merge(MODULE.config, Config); 75 | if (typeof (callback) === 'function') self.set(data, callback); 76 | else self.setSync(data); 77 | }); 78 | }, 79 | addModules: function (modules, callback) { 80 | if (typeof callback === 'function') callback = _.noop(); 81 | var self = this; 82 | modules = _.toArray(modules); 83 | self.get(function (error, data) { 84 | if (error) return callback(error); 85 | _.each(modules, function (MODULE) { 86 | if (!data.modules) data.modules = []; 87 | _.remove(data.modules, { name: MODULE.name }); 88 | data.modules.push(MODULE); 89 | }); 90 | self.set(data, callback); 91 | }); 92 | }, 93 | removeModules: function (modules, callback) { 94 | if (typeof callback === 'function') callback = _.noop(); 95 | var self = this; 96 | modules = _.toArray(modules); 97 | self.get(function (error, data) { 98 | if (error) return callback(error); 99 | 100 | _.each(modules, function (MODULE) { 101 | _.pull(data.modules, MODULE); 102 | }); 103 | self.set(data, callback); 104 | }); 105 | }, 106 | }; 107 | 108 | module.exports = env; 109 | -------------------------------------------------------------------------------- /fleet/incoming/processor.js: -------------------------------------------------------------------------------- 1 | // Essential 2 | var MailParser = require("mailparser").MailParser; 3 | var PassThrough = require('stream').PassThrough; 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | // Functions 8 | var create = require("./create"); 9 | var Attachment = require('./attachment'); 10 | 11 | // 12 | /* -------------- Module Human description -------------- */ 13 | /* 14 | 15 | Processor creates a function that handles 16 | processing of a stream and session through parsers 17 | and spam detectors. The final product is then 18 | recorded in the database. 19 | 20 | Note that this module is mostly stream based 21 | and any function placed here that does not involve 22 | direct database interaction should be entirely 23 | based on NodeJS streams. 24 | 25 | */ 26 | 27 | module.exports = function (context, databaseConnection, Spamc) { 28 | return function INCOMING_EMAIL_PROCESSOR(stream, session, callback) { 29 | var fileStream; 30 | /* Find/Create a Spamc module with streaming capability */ 31 | // Will not use SPAMASSASIN if the process is not available 32 | var mailparser = new MailParser({ 33 | showAttachmentLinks: true, 34 | streamAttachments: true 35 | }); 36 | 37 | mailparser.once("end", function (parsed) { 38 | /* Fix naming issues */ 39 | // Return an error if we don't know who the envelope is sent to 40 | if ((!session.envelope) && ((!parsed.to) || (!parsed.to[0]) || (!parsed.to[0].address))) { 41 | return callback({ 42 | responseCode: 451, 43 | message: "Failed to process Envelope headers" 44 | }); 45 | } 46 | parsed.envelopeTo = (session.envelope) ? session.envelope.rcptTo : parsed.to; 47 | 48 | if (!context.attach) context.attach = require('./attachment').save; 49 | 50 | create(context, databaseConnection, session, parsed, function (error) { 51 | // Respond to SMTP Connection (WITH OR WITHOUT ERROR) 52 | callback(error); 53 | 54 | if (session.store) { 55 | var reporter = Spamc.report(); 56 | var RawStream = fs.createReadStream(session.path); 57 | RawStream.pipe(reporter); 58 | 59 | // Once report is obtained 60 | reporter.once('report', function (report) { 61 | if (!report && context.environment.verbose) return console.error("SPAMC-STREAM-ERROR::NO_REPORT"); 62 | // Update Email from EID 63 | databaseConnection.collections.mail.update({ eID: session.eID }, { 64 | isSpam: report.isSpam || false, 65 | spamScore: report.spamScore || false, 66 | status: "approved" 67 | }, function (error, models) { 68 | if (error || (models.length < 1)) { 69 | if (context.environment.verbose) console.error("SPAMC-STREAM-ERROR::NO_RECORD"); 70 | return; 71 | } 72 | }) 73 | }); 74 | 75 | RawStream.once('error', function (error) { 76 | if (context.environment.verbose) console.error("RAW-STREAM-ERROR::", error) 77 | }) 78 | 79 | reporter.once('error', function (error) { 80 | if (context.environment.verbose) console.error("SPAMC-STREAM-ERROR::", error) 81 | }) 82 | } 83 | }); 84 | }); 85 | 86 | mailparser.on("attachment", function (attachment, mail) { 87 | if ((!context.environment.paths) || (!context.environment.paths.attachments)) return; 88 | Attachment.stream(context.environment.paths.attachments, session.eID, attachment); 89 | }); 90 | 91 | mailparser.once("error", function () { 92 | if (context.environment.verbose) console.log("PARSER-STREAM-ERROR", arguments) 93 | callback(new Error("FAILED TO STORE EMAIL")); 94 | }) 95 | 96 | // Set Stream Encoding 97 | stream.setEncoding('utf8'); 98 | // Create new FS Write stream 99 | if (session.store) fileStream = fs.createWriteStream(session.path); /* Add Error handling for FileStream */ 100 | // Pipe to FS Write Stream 101 | if (session.store) stream.pipe(fileStream); 102 | // Pipe to MailParser Stream 103 | stream.pipe(mailparser); 104 | } 105 | } -------------------------------------------------------------------------------- /bin/modulator.js: -------------------------------------------------------------------------------- 1 | var shell = require('shelljs'); 2 | var _ = require('lodash'); 3 | var async = require('async'); 4 | var herb = require('herb'); 5 | var colors = require('colors'); 6 | var configFile = require('./configFile'); 7 | 8 | var Modulator = function () { 9 | this.env = configFile; 10 | this.modules = this.env.getModulesSync(); 11 | this.container = {}; 12 | }; 13 | 14 | Modulator.prototype._getModules = function () { 15 | this.modules = this.env.getModulesSync(); 16 | }; 17 | 18 | Modulator.prototype._add = function (MODULE) { 19 | var m = require(MODULE); 20 | this.env.addModules([{ 21 | reference: MODULE, 22 | name: m.name, 23 | extends: m.extends, 24 | config: m.defaults 25 | }]); 26 | }; 27 | 28 | Modulator.prototype.load = function (modules) { 29 | var context = this; 30 | // Modules are likely required to be loaded on start thus will be loaded Synchronously 31 | _.each(modules || context.modules, function (MODULE) { 32 | // Fill context.modules according to 'extends' attribute 33 | 34 | // Assign array if key is undefined 35 | if (!context.container[MODULE.extends]) context.container[MODULE.extends] = {}; 36 | // Push current Module to the respective key 37 | context.container[MODULE.extends][MODULE.reference] = MODULE; 38 | }); 39 | return context.container; 40 | }; 41 | 42 | Modulator.prototype.launch = function () { 43 | var context = this; 44 | var args = _.toArray(arguments); 45 | var cat = args.shift(); 46 | var callback = args.pop(); 47 | var functions = []; 48 | 49 | if ((!context.container) || (!_.isPlainObject(context.container)) || (!context.container[cat])) return callback(); 50 | 51 | // Populate functions 52 | _.each(context.container[cat], function (MODULE) { 53 | functions.push(function (callback) { 54 | /* Slows down module execution but prevents unintended crashes */ 55 | // Prevents a bad module from corrupting the entire eco-system 56 | try { 57 | context.container[cat][MODULE.reference].__gcopy = require(MODULE.reference); 58 | context.container[cat][MODULE.reference].__gcopy.exec.apply(MODULE, args); 59 | } catch (error) { 60 | callback(error); 61 | } 62 | }); 63 | }); 64 | 65 | // Watch for config changes 66 | configFile.watch(function () { 67 | if (!context.container) return; 68 | context._getModules(); 69 | _.each(context.container[cat], function (MODULE, REF) { 70 | try { 71 | if (!_.isEqual(_.findWhere(context.modules, { reference: REF }).config, MODULE.config)) { 72 | if (typeof MODULE.__gcopy.update === 'function') { MODULE.__gcopy.update(_.findWhere(context.modules, { reference: REF }).config); } 73 | } 74 | } catch (e) { } 75 | }); 76 | context._getModules(); 77 | }); 78 | 79 | // Ignore if no modules are registered for the current task 80 | if (functions.length <= 0) return callback(); 81 | 82 | async.series(functions, callback); 83 | }; 84 | 85 | Modulator.prototype.install = function (moduleName) { 86 | var context = this; 87 | moduleName = moduleName.toLowerCase(); 88 | moduleName = (moduleName.substring(0, 8) !== 'galleon-') ? 'galleon-' + moduleName : moduleName; 89 | shell.exec('npm install -g ' + moduleName, function (code, output) { 90 | if (code === 0) { 91 | context._add(moduleName); 92 | herb.log(moduleName.toUpperCase().magenta, "SUCCESSFULLY INSTALLED!".green); 93 | herb.warn("Changes will only take affect after restart!"); 94 | } else { 95 | herb.log("INSTALLATION FAILED!".red, "\nCODE:", code); 96 | } 97 | }); 98 | }; 99 | 100 | Modulator.prototype.update = function (name, config) { 101 | var context = this; 102 | var MatchFound = false; 103 | // Modules are likely required to be loaded on start thus will be loaded Synchronously 104 | _.each(this.modules, function (MODULE) { 105 | // If Current Module Matches name 106 | if ((MODULE.name.toLowerCase() === name) || (MODULE.reference.toLowerCase() === name)) { 107 | // Update Config 108 | context.env.updateModuleConfig(MODULE, config); 109 | MatchFound = true; 110 | } 111 | }); 112 | return MatchFound; 113 | }; 114 | 115 | module.exports = Modulator; -------------------------------------------------------------------------------- /query/get.build.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = function (email, page) { 4 | var folders = new Object; 5 | 6 | /* 7 | HUGE MEMORY ALLOCATION PER REQUEST 8 | SELECTION SHOULD BE LIMITED TO A FOLDER 9 | & A SINGLE OBJECT IN FUTURE RELEASES. 10 | */ 11 | 12 | folders.OUTBOX = { 13 | collection: 'queue', 14 | find: new Object, 15 | where: { 16 | association: email, 17 | state: { '!=': 'draft' } /* MIGHT CAUSE POSTGRES ISSUES -> http://stackoverflow.com/a/22600564/804759 */ 18 | }, 19 | sort: { 20 | createdAt: 'desc' 21 | }, 22 | paginate: { 23 | page: page || 1, 24 | limit: 10 25 | }, 26 | filter: function (mails) { 27 | // Sort (Time asc) & Filter - Note: Sorting could be moved to Waterline native sorting 28 | return _.chain(mails) 29 | .map(function (t) { 30 | // Filter 31 | t = _.pick(t, ['eID', 'sender', 'to', 'schedule', 'subject', 'text', 'html', 'attachments']); 32 | // Remove path from Attachments 33 | t.attachments = _.map(t.attachments, function (attachment) { 34 | return _.pick(attachment, ['fileName', 'checksum', 'id', 'length']); 35 | }); 36 | // Prepend Indicator to the eID 37 | t.eID = 'O' + t.eID; 38 | // Set all Outbox emails to read/!spam/!trash 39 | t.read = true; 40 | t.spam = false; 41 | t.trash = false; 42 | // Stamp doesn't exist in QUEUE so we create it here 43 | t.stamp = { 44 | sent: (new Date(t.schedule.scheduled)), 45 | received: (new Date(t.schedule.attempted)) 46 | } 47 | // Pass to sort 48 | return t; 49 | }) 50 | .sortBy(function (t) { 51 | return (t.stamp.sent.getTime()) 52 | }) 53 | .value(); 54 | } 55 | } 56 | 57 | folders.DRAFT = _.extend(_.clone(folders.OUTBOX, true), { 58 | where: { 59 | association: email, 60 | state: 'draft' 61 | }, 62 | }); 63 | 64 | folders.INBOX = { 65 | collection: 'mail', 66 | find: new Object, 67 | where: { 68 | association: email, 69 | trash: false, 70 | spam: false, 71 | sent: false, 72 | spamScore: { 73 | '<=': 5 74 | } /* Spam filter */ 75 | }, 76 | sort: { 77 | createdAt: 'desc' 78 | }, 79 | paginate: { 80 | page: page, 81 | limit: 10 82 | }, 83 | filter: function (mails) { 84 | // Sort (Time asc) & Filter - Note: Sorting could be moved to Waterline native sorting 85 | return _.chain(mails) 86 | .map(function (t) { 87 | // Filter 88 | t = _.pick(t, ['eID', 'sender', 'receiver', 'to', 'stamp', 'subject', 'text', 'html', 'read', 'spam', 'trash', 'attachments', 'status']); 89 | // Remove path from Attachments 90 | t.attachments = _.map(t.attachments, function (attachment) { 91 | return _.pick(attachment, ['fileName', 'checksum', 'id', 'length']); 92 | }); 93 | // Prepend Indicator to the eID 94 | t.eID = 'I' + t.eID; 95 | // Covert stamps to JS Dates 96 | t.stamp.sent = (new Date(t.stamp.sent)); 97 | t.stamp.received = (new Date(t.stamp.received)); 98 | // Pass to sort 99 | return t; 100 | }) 101 | .sortBy(function (t) { 102 | return (t.stamp.sent.getTime()) 103 | }) 104 | .value(); 105 | } 106 | } 107 | 108 | folders.SENT = _.extend(_.clone(folders.INBOX, true), { 109 | where: { 110 | association: email, 111 | sent: true, 112 | spam: false, 113 | trash: false, 114 | }, 115 | }); 116 | 117 | folders.SPAM = _.extend(_.clone(folders.INBOX, true), { 118 | where: { 119 | association: email, 120 | trash: false, 121 | or: [{ 122 | spam: true 123 | }, { 124 | spamScore: { 125 | '>': 5 126 | } 127 | }] 128 | }, 129 | }); 130 | 131 | folders.TRASH = _.extend(_.clone(folders.INBOX, true), { 132 | where: { 133 | association: email, 134 | trash: true 135 | }, 136 | }); 137 | 138 | return folders; 139 | } 140 | -------------------------------------------------------------------------------- /fleet/outgoing/outbound.js: -------------------------------------------------------------------------------- 1 | /* -- Modules -- */ 2 | // Essential 3 | var eventEmmiter = require('events').EventEmitter; 4 | var util = require("util"); 5 | var fs = require("fs"); 6 | 7 | // Foundations 8 | var nodemailer = require('nodemailer'); 9 | var validator = require('validator'); 10 | 11 | // Utils 12 | var _ = require('lodash'); 13 | 14 | /* -- ------- -- */ 15 | 16 | // 17 | /* -------------- Module Human description -------------- */ 18 | /* 19 | 20 | Outbound module will take care of all outbound 21 | mails initiated through the API. 22 | 23 | Emails sent with proper header and DKIM will 24 | most likely be sent to the inbox but many other 25 | requirements are there to get around. This has to 26 | do with the nature of Mail Servers and while many 27 | advise to use a SMTP provider for outbound mails 28 | it would not be necessary if strict guidlines are 29 | followed. 30 | 31 | An example of blockage by GMail can be 32 | represented by a message indicating such and can 33 | be caught by the callback created using this 34 | module. 35 | 36 | Common best practices for sending emails can 37 | be found on most major providers such as GMail at 38 | 39 | http://support.google.com/mail/bin/answer.py?hl=en&answer=188131 40 | 41 | * Galleon will include a tutorial on how to 42 | create a legitimate server and will most likely 43 | include automation tools in the coming versions. 44 | The goal here is to enable anyone with a static 45 | IP and some storage to setup a mail server. 46 | 47 | */ 48 | /* -------------- ------------------------ -------------- */ 49 | // 50 | 51 | /* * Performance (Lightweight server - Tier 1 Connection - 0.5GB RAM) 52 | ---------------- 53 | * Mailforwarding (Gmail to Server to Gmail): 4-6 seconds 54 | * Outbound Mail (Server to Gmail): 2-3 seconds 55 | ---------------- 56 | */ 57 | 58 | /* Initiate an outbound transporter. */ 59 | var Outbound = function (environment, callback) { 60 | this.environment = environment; 61 | eventEmmiter.call(this); 62 | } 63 | 64 | util.inherits(Outbound, eventEmmiter); 65 | 66 | Outbound.prototype.createTransporter = function (transporter, callback) { 67 | try { 68 | if ((transporter == null) || (!transporter)) 69 | transporter = nodemailer.createTransport(); 70 | 71 | callback(undefined, transporter); 72 | } catch (error) { callback(error) }; 73 | } 74 | 75 | // Currently only sends to individual emails 76 | // #Revisit - Should add an array option to pass multiple senders and receivers 77 | Outbound.prototype.send = function (mail, options, callback) { 78 | var _this = this; 79 | 80 | // Humane programming 81 | if ((options.constructor !== Object) && (!callback)) { callback = options; options = {} } 82 | if (!options.transporter) transporter = nodemailer.createTransport(); 83 | else transporter = options.transporter; 84 | 85 | // Improve error handling here 86 | if (!mail) return _this.emit('failed', { error: "Mail Object is undefined!" }); 87 | 88 | /* -------------------------------------------------------------- */ 89 | 90 | // Load outbound modules 91 | _this.environment.modulator.launch('outbound', mail, function (error, _mail) { 92 | 93 | if (_mail !== undefined) mail = _mail; 94 | 95 | /* Better ways to do this | Rough setup for testing */ 96 | _.each(mail.attachments, function (attachment, index) { 97 | // Rename a few things to match nodemailer 98 | attachment.filename = attachment.fileName; 99 | attachment.encoding = attachment.transferEncoding; 100 | /* Bad practice, shouldn't trust database path -> Use environment instead */ 101 | /* Must test against checksum */ 102 | attachment.content = fs.createReadStream(attachment.path); 103 | attachment = _.omit(attachment, ["fileName", "path", "id", "checksum", "length"]); 104 | mail.attachments[index] = attachment; 105 | }) 106 | 107 | transporter.sendMail({ 108 | from: mail.from, 109 | to: mail.to, 110 | subject: mail.subject, 111 | text: mail.text, 112 | html: mail.html, 113 | attachments: mail.attachments 114 | }, function (error, response) { 115 | if (!!error) { 116 | callback(error, response); 117 | _this.emit('failed', error, response); 118 | } else { 119 | callback(error, response); 120 | _this.emit('sent', response); 121 | } 122 | }); 123 | }) 124 | }; 125 | 126 | module.exports = Outbound; 127 | -------------------------------------------------------------------------------- /api/server.js: -------------------------------------------------------------------------------- 1 | // HTTP/HTTPS 2 | var https = require('https'); 3 | var http = require('http'); 4 | // Express 5 | var express = require('express'); 6 | var inspect = require('util').inspect; 7 | var path = require('path'); 8 | var favicon = require('serve-favicon'); 9 | var logger = require('morgan'); 10 | var cookieParser = require('cookie-parser'); 11 | var bodyParser = require('body-parser'); 12 | var compress = require('compression'); 13 | var authentication = require('./middleware/authentication'); 14 | var crypto = require('crypto'); 15 | // File System 16 | var fs = require('fs'); 17 | 18 | var ACCESS = require('./routes/access'); 19 | var API = require('./routes/api'); 20 | 21 | // Make Database connection 22 | // & Start the server 23 | module.exports = function (environment, port, connection, instance) { 24 | var app = express(); 25 | 26 | if (!port) port = 3000; // Default port; 27 | 28 | app.set("models", connection.collections); 29 | app.set("connections", connection.connections); 30 | app.set("galleon", instance); 31 | app.set("environment", environment); 32 | app.set("secret", environment.secret || crypto.randomBytes(20).toString('hex')); 33 | 34 | // SSL Detection, Automatically switches between HTTP and HTTPS on start 35 | if (environment.ssl.use) { 36 | var SSL_CONFIG; 37 | try { 38 | SSL_CONFIG = { 39 | key: fs.readFileSync(environment.ssl.api.key, 'utf8'), 40 | cert: fs.readFileSync(environment.ssl.api.cert, 'utf8') 41 | } 42 | https.createServer(SSL_CONFIG, app).listen(port); 43 | } catch (e) { 44 | http.createServer(app).listen(port); 45 | } 46 | } else { 47 | http.createServer(app).listen(port); 48 | } 49 | 50 | // Allow API access outside origin (This is an API after all) 51 | app.use(function (req, res, next) { 52 | // Allow for Webmail interface 53 | // Echo Back Origin if header is provided (Equivalent to * but allows Credentials) 54 | if (req.get('origin') && (typeof req.get('origin') === 'string')) { 55 | res.header("Access-Control-Allow-Origin", req.get('origin')); 56 | } else res.header("Access-Control-Allow-Origin", req.protocol + '://' + environment.domain + ":2095"); 57 | 58 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,PATCH,DELETE'); 59 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 60 | res.header("Access-Control-Allow-Credentials", "true"); 61 | next(); 62 | }); 63 | 64 | 65 | // uncomment after placing your favicon in /public 66 | //app.use(favicon(__dirname + '/public/favicon.ico')); 67 | app.use(compress()); 68 | if (environment.verbose) app.use(logger('dev')); 69 | 70 | // If Environment secret is not set assign a random secret on every restart 71 | app.use(cookieParser(app.get('secret'))); 72 | // Secret middleware 73 | app.use((req, res, next) => { 74 | req.envSecret = app.get('secret'); 75 | next(); 76 | }); 77 | 78 | app.use(bodyParser.json()); 79 | app.use(bodyParser.urlencoded({ 80 | extended: false 81 | })); 82 | 83 | // Database middleware 84 | app.use(function (req, res, next) { 85 | req.galleon = app.get("galleon"); 86 | req.database = { 87 | models: app.get("models"), 88 | connections: app.get("connections") 89 | } 90 | req.environment = app.get("environment"); 91 | next(); 92 | }); 93 | 94 | app.use(authentication({ 95 | login: '/access/login', 96 | logout: '/access/logout' 97 | })); 98 | app.use('/access', ACCESS); 99 | app.use('/', API); 100 | 101 | // catch 404 and forward to error handler 102 | app.use(function (req, res, next) { 103 | var err = new Error('Not Found'); 104 | err.status = 404; 105 | next(err); 106 | }); 107 | 108 | // error handlers 109 | 110 | // development error handler 111 | // will print stacktrace 112 | if (app.get('env') === 'development') { 113 | app.use(function (err, req, res, next) { 114 | console.trace(err); 115 | res.status(err.status || 500); 116 | res.json({ 117 | message: err.message, 118 | error: inspect(err, { 119 | showHidden: true, 120 | depth: 5 121 | }) 122 | }) 123 | }); 124 | } 125 | 126 | // production error handler 127 | // no stacktraces leaked to user 128 | app.use(function (err, req, res, next) { 129 | res.status(err.status || 500); 130 | res.json({ 131 | message: err.message, 132 | error: JSON.stringify(err) 133 | }) 134 | }); 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Galleon Logo](logo.png) 2 | 3 | A badass SMTP mail server built on Node to make your life simpler. 4 | ====== 5 | 6 | **\*Galleon** is a super fast & efficient mail server powered by **Node.JS** and our favorite Document Database **MongoDB** or your own choice of Database. It will feature all the awesome stuff the big providers have, yet provides you with a powerful API to expand it on your own. 7 | 8 | Get ready to sail into a new world featuring: 9 | - Web based user interface [SEASCAPE](https://github.com/schahriar/Seascape) 10 | - Spam protection by default [(Follow the tutorial here!)](https://github.com/schahriar/Galleon/blob/master/tutorials/SPAMASSASIN.md) 11 | - Simple Mail Transfer Protocol **SMTP** (Listen, Process, Send) 12 | - ~~Connection control (ratelimiting, bandwith limiting and other terms that makes me sound professional)~~ *as a module* 13 | - Did I mention super fast? (Blame it on Node) 14 | 15 | [**\*Galleon**](http://en.wikipedia.org/wiki/Galleon) is named after multi-deck armed merchant ships dating back to 16th century. 16 | 17 | [![Build Status](https://travis-ci.org/schahriar/Galleon.svg)](https://travis-ci.org/schahriar/Galleon) 18 | 19 | # Installation 20 | [Installation](tutorials/INSTALLATION.md) can be as simple as this (but follow the [directions](tutorials/INSTALLATION.md)): 21 | ```javascript 22 | npm install -g galleon 23 | ``` 24 | `Note:` Galleon requires NodeJS v6.x.x and above. 25 | 26 | [Visit the tutorial for more info.](tutorials/INSTALLATION.md) 27 | 28 | # Why ditch your old Mail Servers? 29 | --------- 30 | > Are you tired of paying insane amounts of money for uselss services that come bundled with your email service subscription? 31 | 32 | > Are you tired of spending a ton of more money on a specialist to set up a mail server for you using ancient technology just because you can't get it up and running yourself? 33 | 34 | > Are you tired of setting up 3-5 different applications on your server to get be able to receive email? 35 | 36 | > Are you tired of seeing mediocre marketing questions? 37 | 38 | > ###### Are you tired? 39 | 40 | ---------- 41 | Well, **Galleon** is your solution. All you need is a server a domain name and a basic setup to get a complete mail server up which can serve a ton of other domains and users but guess what? We'll cover all the steps in this same repository. The goal is to make it easy and secure for all developers to have their own private domain running. 42 | 43 | # Launch An API Server 44 | You can easily run a Galleon server by installing the package globally and using the following command: 45 | 46 | `Note:` Use [Authbind](https://github.com/schahriar/Galleon/blob/master/tutorials/AUTHBIND.md) to run on port 25 47 | ```javascript 48 | galleon start 49 | ``` 50 | **BUT**, to get a complete solution running you'll need to follow a few steps. The best part is that the following command does most of the work: 51 | ``` 52 | galleon setup 53 | ``` 54 | You can install [Seascape](https://github.com/schahriar/Seascape) as your Webmail front-end *module* ... like this: 55 | ``` 56 | galleon install seascape 57 | galleon restart 58 | ``` 59 | And use it on your port 2095 60 | 61 | ## Features 62 | 63 | - API (port 3080) 64 | - Module support 65 | - Database and Raw storage 66 | - Database Adapter support for -> `MongoDB`, `Redis`, `MySQL`, `PostgreSQL`, `SQLServer`, etc. 67 | - Outbound Support (Send Emails) 68 | - Daemon Manager [PM2][https://github.com/Unitech/pm2] 69 | - Spam detection/reporting/learning etc. with **SPAMASSASIN** 70 | - XSS protection 71 | - Encryption & SSL support 72 | - CLI Automation 73 | - Session based auth with bcrypt 74 | - Built-in user management 75 | - Full attachment support (multipart upload, checksums etc.) 76 | - Automatic file/email/raw deletion based on email status on the database 77 | - & many more ... 78 | -------- 79 | VERSION: 0.3 [Golden Hind](https://en.wikipedia.org/wiki/Golden_Hind) -> Beta 2 80 | 81 | ## Upcoming 82 | These features are currently being tested and will be released in October 2015: 83 | 84 | - Tutorials and Documentation for creating modules and front-end interfaces 85 | 86 | ## What's next? 87 | - DoS protection 88 | - SPF & DKIM Support (broken in v0.3.x) 89 | - Raw Import & Deletion 90 | - Admin interface 91 | - Multiple Association 92 | 93 | ## NOTICE 94 | **GALLEON & SEASCAPE** are both in beta stages and may/will have critical bugs. These bugs will be fixed as we get closer to a release version. You can [report any issues with this repository here](https://github.com/schahriar/galleon/issues/new). 95 | 96 | ## License 97 | Who doesn't love a [MIT license](https://raw.githubusercontent.com/schahriar/Galleon/master/LICENSE)? 98 | Make sure you read the license and don't participate in any form of abuse (Spam, etc.) using any parts of this project. 99 | -------------------------------------------------------------------------------- /bin/startup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | process.title = 'galleon'; 6 | 7 | var _ = require('lodash'); 8 | var argv = require('yargs').argv; 9 | var colors = require('colors'); // Better looking error handling 10 | var herb = require('herb'); 11 | var Galleon = require("../Galleon"); 12 | 13 | var tasks = { 14 | setup: { 15 | description: "SETTING UP|SETS UP| GALLEON ENVIRONMENT", 16 | category: "installation", 17 | FUNC: require("./tasks/setup") 18 | }, 19 | ssl: { 20 | description: "SETTING UP|SETS UP| SSL FOR GALLEON", 21 | category: "installation", 22 | FUNC: require("./tasks/setup_ssl") 23 | }, 24 | start: { 25 | description: "STARTING|STARTS| A NEW GALLEON INSTANCE", 26 | category: "process management", 27 | FUNC: require("./tasks/start") 28 | }, 29 | stop: { 30 | description: "HALTING|HALTS| GALLEON INSTANCE", 31 | category: "process management", 32 | FUNC: require("./tasks/process").stop 33 | }, 34 | delete: { 35 | description: "DELETING|DELETES| GALLEON INSTANCE", 36 | category: "process management", 37 | FUNC: require("./tasks/process").delete 38 | }, 39 | restart: { 40 | description: "RESTARTING|RESTARTS| GALLEON INSTANCE", 41 | category: "process management", 42 | FUNC: require("./tasks/process").restart 43 | }, 44 | status: { 45 | description: "SHOWING|SHOWS| PROCESS STATUS", 46 | category: "process management", 47 | FUNC: require("./tasks/status") 48 | }, 49 | startup: { 50 | description: "GENERATING|GENERATES| AN OS DEPENDENT STARTUP SCRIPT", 51 | category: "process management", 52 | FUNC: require("./tasks/process").startup 53 | }, 54 | add: { 55 | description: "ADDING|ADDS| A NEW USER", 56 | category: "user management", 57 | FUNC: require("./tasks/add") 58 | }, 59 | list: { 60 | description: "LISTING|LISTS| USERS", 61 | category: "user management", 62 | FUNC: require("./tasks/list") 63 | }, 64 | changepass: { 65 | description: "CHANGING|CHANGES| USER PASSWORD", 66 | category: "user management", 67 | FUNC: require("./tasks/changePassword") 68 | }, 69 | remove: { 70 | description: "REMOVING|REMOVES| USER", 71 | category: "user management", 72 | FUNC: require("./tasks/remove") 73 | }, 74 | install: { 75 | description: "INSTALLING|INSTALLS| MODULES", 76 | category: "modules", 77 | FUNC: require("./tasks/install") 78 | }, 79 | config: { 80 | description: "CONFIGURING|CONFIGURES| A SETTING IN A GIVEN MODULE", 81 | category: "modules", 82 | FUNC: require("./tasks/config") 83 | }, 84 | clean: { 85 | description: "CLEANING|CLEANS| UNUSED/UNLINKED FILES (ATTACHMENTS OR RAW) BASED ON INDIVIDUAL EMAIL STATUS", 86 | category: "maintenance", 87 | FUNC: require("./tasks/clean") 88 | }, 89 | restore: { 90 | description: "RESTORING|RESTORES| RAW EMAILS TO DATABASE", 91 | category: "maintenance", 92 | FUNC: require("./tasks/restore") 93 | } 94 | } 95 | 96 | function verbalize(string, isVerb) { 97 | return (isVerb) ? string.split('|')[0] + string.split('|')[2] : string.split('|')[1] + string.split('|')[2] 98 | } 99 | 100 | function pad(width, string, padding) { 101 | return (width <= string.length) ? string : pad(width, string + padding, padding) 102 | } 103 | 104 | function capitalizeFirstLetter(string) { 105 | return string.replace(/\b./g, function (m) { return m.toUpperCase(); }); 106 | } 107 | 108 | if ( 109 | (argv._[0]) && 110 | (tasks[argv._[0].toLowerCase()] !== undefined) && 111 | (argv._[0].constructor === String) && 112 | (argv._[0].substring(0, 1) !== '-') && 113 | (argv._[0] !== 'help') && 114 | (tasks[argv._[0].toLowerCase()].FUNC.constructor === Function) 115 | ) { 116 | console.log("TASK INITIATED:".blue, verbalize(tasks[argv._[0].toLowerCase()].description, true) + " ...".blue); 117 | tasks[argv._[0].toLowerCase()].FUNC(Galleon, argv); 118 | } else if ((argv._[0] === '--help') || (argv._[0] === '-h') || (argv._[0] === 'help')) { 119 | var categories = new Object; 120 | // Sort tasks into their categories 121 | _.forEach(tasks, function (task, command) { 122 | if (!categories[task.category]) categories[task.category] = new Array; 123 | categories[task.category].push({ command: command, get: task }) 124 | }) 125 | 126 | _.forEach(categories, function (tasks, category) { 127 | herb.marker({ color: 'dim' }).line('- -'); 128 | herb.group(capitalizeFirstLetter(category)); 129 | _.each(tasks, function (task) { 130 | herb.log(pad(7 - task.command.length, task.command, ' ').cyan, "\t", "->".blue, verbalize(task.get.description, false)); 131 | }) 132 | herb.groupEnd(); 133 | }) 134 | 135 | } else { 136 | console.log("COMMAND NOT FOUND".yellow, "\r\nType".cyan, 'galleon help', 'for more info.'.cyan); 137 | } 138 | -------------------------------------------------------------------------------- /fleet/incoming/incoming.js: -------------------------------------------------------------------------------- 1 | /* -- Modules -- */ 2 | // Essential 3 | var eventEmmiter = require('events').EventEmitter; 4 | var util = require("util"); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var os = require('os'); 8 | 9 | // SMTP Mail Handling 10 | var SMTPServer = require('smtp-server').SMTPServer; 11 | var Processor = require('./processor'); 12 | var Attachment = require('./attachment'); 13 | 14 | // Utilities 15 | var async = require('async'); 16 | var _ = require('lodash'); 17 | 18 | // ID Generation 19 | var crypto = require('crypto'); 20 | var shortId = require('shortid'); 21 | 22 | /* -- ------- -- */ 23 | 24 | // 25 | /* -------------- Module Human description -------------- */ 26 | /* 27 | 28 | Incoming module will map all incoming 29 | connection to the specified port (SMTP - 25 Unless 30 | stated otherwise) and provide raw and processed 31 | data (in form of an object) through an event based 32 | API. 33 | 34 | Mails have three different states and events 35 | (connection, stream, mail) which can be programmed 36 | to do almost all the functions a mail server would 37 | require. There will be additions to functions of 38 | each in this module in each version but they will 39 | mostly be opt-in additions rather than opt-out. 40 | 41 | */ 42 | /* -------------- ------------------------ -------------- */ 43 | // 44 | 45 | // ** OLD DATA -> REQUIRES UPDATE 46 | /* * Performance (Lightweight server - Tier 1 Connection - 0.5GB RAM) 47 | ---------------- 48 | * Inbound Mail (Gmail to Server): 3-4 seconds 49 | * Inbound Mail (Hotmail to Server): 2-3 seconds 50 | * Inbound Mail (Google Apps Mail to Server): 2-3 seconds 51 | * Inbound Mail (Server to Server): TO BE TESTED - estimate: 3-4 seconds 52 | 53 | Note: Includes parsing time 54 | ---------------- 55 | */ 56 | 57 | /* Start the SMTP server. */ 58 | var Incoming = function (environment) { 59 | this.environment = environment; 60 | 61 | eventEmmiter.call(this); 62 | } 63 | 64 | util.inherits(Incoming, eventEmmiter); 65 | 66 | Incoming.prototype.listen = function (port, databaseConnection, Spamc) { 67 | var _this = this; 68 | 69 | var ProcessMail = Processor(this, databaseConnection, Spamc); 70 | 71 | var ServerConfig = { 72 | size: 20971520, // MAX 20MB Message 73 | banner: "Galleon MailServer <" + (_this.environment.domain || 'galleon.email') + ">", 74 | name: (_this.environment.domain), 75 | disabledCommands: ["AUTH"], // INCOMING SMTP is open to all without AUTH 76 | logger: false, // Disable Debug logs /* Add option for this in config */ 77 | onData: function (stream, session, callback) { 78 | // Create a new connection eID 79 | session.eID = shortId.generate() + '&&' + crypto.createHash('md5').update(session.id || "NONE").digest('hex'); 80 | session.path = undefined; 81 | 82 | _this.environment.modulator.launch('incoming-connection', session, function (error, _session, _block) { 83 | if (_this.environment.verbose) console.log("CONNECTION MODULES LAUNCHED".green, arguments); 84 | 85 | if (_.isObject(_session)) session = _session; 86 | 87 | // Ignore email if requested by 'incoming-connection' modules 88 | // Otherwise Process Stream 89 | if (_block === true) { 90 | _this.emit('blocked', session); 91 | callback({ responseCode: 451, message: "Request Blocked" }); 92 | } else { 93 | _this.emit('connection', session); 94 | /* Add FS EXISTS Check */ 95 | // Tell Processor to Store RAW if env path is set 96 | session.store = _.has(_this.environment, 'paths.raw'); 97 | // Set Connection path 98 | session.path = (_.has(_this.environment, 'paths.raw')) 99 | ? path.resolve(_this.environment.paths.raw, session.eID) 100 | : path.resolve(os.tmpdir(), session.eID); 101 | ProcessMail(stream, session, callback); 102 | } 103 | }); 104 | } 105 | }; 106 | 107 | if ((_this.environment.ssl.use) && (_this.environment.ssl.incoming) && (_this.environment.ssl.incoming.key) && (_this.environment.ssl.incoming.cert)) { 108 | try { 109 | ServerConfig.key = fs.readFileSync(_this.environment.ssl.incoming.key, 'utf8'); 110 | ServerConfig.cert = fs.readFileSync(_this.environment.ssl.incoming.cert, 'utf8'); 111 | ServerConfig.ca = fs.readFileSync(_this.environment.ssl.incoming.ca, 'utf8'); 112 | if (_this.environment.verbose) console.log("USING KEY", _this.environment.ssl.incoming); 113 | } catch (e) { 114 | if (_this.environment.verbose) console.log("FAILED TO START INCOMING SSL\nFALLING BACK."); 115 | ServerConfig.key = null; 116 | ServerConfig.cert = null; 117 | ServerConfig.ca = null; 118 | } 119 | } 120 | 121 | var server = new SMTPServer(ServerConfig); 122 | 123 | server.listen(port, null, function () { 124 | if (_this.environment.verbose) console.log("SMTP SERVER LISTENING ON PORT", port); 125 | }); 126 | server.on('error', function (error) { 127 | if (_this.environment.verbose) console.log('SMTP-SERVER-ERROR::%s', error.message); 128 | }) 129 | 130 | _this.emit("ready", server); 131 | }; 132 | 133 | Incoming.prototype.attach = function (databaseConnection, eID, attachments) { 134 | if ((!this.environment.paths) || (!this.environment.paths.attachments)) return; 135 | 136 | Attachment.save(this.environment.paths.attachments, databaseConnection, eID, attachments); 137 | } 138 | 139 | module.exports = Incoming; 140 | -------------------------------------------------------------------------------- /api/middleware/authentication.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const bcrypt = require('bcryptjs'); 3 | const jwt = require('jsonwebtoken'); 4 | const herb = require('herb'); 5 | 6 | exports = module.exports = function (urls) { 7 | return function authenticator(req, res, next) { 8 | 9 | // Use a REGEX like code here 10 | if ((req.path !== urls.login) || req.path !== urls.logout) { 11 | // Basic cookie/token based authentication 12 | let token = req.signedCookies.authentication; 13 | if ((!token) || (token == '') || (!token.sessionID)) { 14 | try { 15 | token = jwt.verify(req.headers.token || "", req.envSecret); 16 | } catch (error) { 17 | token = null; 18 | req.getCredentials = function (callback) { callback("NOT AUTHENTICATED") }; 19 | } 20 | } // :O No cookie! 21 | 22 | if (!token) { 23 | req.getCredentials = function (callback) { callback("NOT AUTHENTICATED") } 24 | } else { 25 | req.getCredentials = function (callback) { 26 | if (!token.sessionID) return res.redirect(urls.login); 27 | // 28 | /// Do a ton of cool security stuff here 29 | // 30 | req.database.models.sessions.findOne({ sessionID: token.sessionID }).exec(function (error, session) { 31 | if ((!session) || (!session.email)) return callback("Session Not Found"); 32 | 33 | if (moment(session.stamp.expires).isBefore(moment())) { 34 | req.database.models.sessions.destroy({ sessionID: token.sessionID }, function (error) { 35 | // Should do better logging here 36 | // An invalid sessionID would either 37 | // mean a broken secret key or 38 | // possibly an error in the token 39 | // system. 40 | if (error) console.log(error); 41 | res.clearCookie('authentication'); 42 | callback("Session expired"); 43 | }); 44 | } else { 45 | req.database.models.users.findOne({ email: session.email }).exec(function (error, user) { 46 | if ((!user) || (!user.email)) return callback("Email Not Found");; 47 | callback(error, { email: user.email, name: user.name }); 48 | }); 49 | } 50 | }); 51 | } 52 | } 53 | /// 54 | } 55 | 56 | req.signIn = function (req, res, callback) { 57 | var opened = moment(); 58 | var expires = opened.add(7, 'days'); 59 | 60 | herb.config({ verbose: (req.environment.verbose) ? 4 : 1 }); 61 | 62 | if (req.environment.verbose) herb.log('Login requested for', req.param('email')); 63 | 64 | req.database.models.users.findOne({ email: req.param('email') }).exec(function (error, user) { 65 | if (error) herb.error(error); 66 | 67 | if (error) return callback(error); 68 | if (!user) return callback('User not found.'); 69 | if (!user.id) return callback('Email does not match a record'); 70 | 71 | bcrypt.compare(req.param('password'), user.password, function (error, result) { 72 | if (error) herb.error(error); 73 | if (error) return callback(error); 74 | if (result) { 75 | herb.marker({ color: 'green' }).log('accessGranted to', user.email); 76 | req.database.models.sessions.destroy({ email: req.param('email') }, function (error) { 77 | // Log any errors here 78 | if (error) callback("DATABASE ERROR"); 79 | // Create a new session token 80 | req.database.models.sessions.create({ 81 | email: user.email, 82 | access: 'approved', 83 | ipAddress: req.ip, 84 | stamp: { opened: opened.toISOString(), expires: expires.toISOString() } 85 | }, function (error, session) { 86 | if (error) return callback(error); 87 | 88 | const payload = { sessionID: session.sessionID, opened: opened }; 89 | const token = jwt.sign(payload, req.envSecret); 90 | 91 | res.cookie('authentication', payload, { signed: true, httpOnly: true, secure: (req.protocol === 'https') }); 92 | return callback(error, token); 93 | }); 94 | }) 95 | } else { 96 | herb.marker({ color: 'red' }).log('accessDenied to', user.email); 97 | return callback("INCORRECT PASSWORD"); 98 | } 99 | }); 100 | }) 101 | } 102 | 103 | req.signOut = function (req, res, callback) { 104 | // If cookie/token does not exist then call it a success 105 | const token = req.signedCookies.authentication || jwt.verify(req.headers.token || "", req.envSecret); 106 | if (!token) callback(null); 107 | 108 | req.database.models.sessions.destroy({ sessionID: token.sessionID }, function (error) { 109 | // Should do better logging here 110 | // An invalid sessionID would either 111 | // mean a broken secret key or 112 | // possibly an error in the token 113 | // system. 114 | if (error) console.log(error); 115 | res.clearCookie('authentication'); 116 | return callback(error); 117 | }); 118 | } 119 | 120 | req.changePassword = function (req, res, callback) { 121 | req.getCredentials(function (error, credentials) { 122 | if (error) res.status(403).json({ error: "Not Authenticated" }); 123 | 124 | if (req.environment.verbose) herb.log('Password Change requested for', credentials.name + ' <' + credentials.email + '>'); 125 | req.database.models.users.findOne({ email: credentials.email }).exec(function (error, user) { 126 | if (error) herb.error(error); 127 | 128 | if (error) return callback(error); 129 | if (!user) return callback('User not found.'); 130 | if (!user.id) return callback('Email does not match a record'); 131 | 132 | req.galleon.changePassword(user, req.param('password'), req.param('cpassword'), function (error) { 133 | if (!error) if (req.environment.verbose) herb.log('Password Changed for', credentials.name + ' <' + credentials.email + '>'); 134 | callback(error); 135 | }); 136 | 137 | }) 138 | }); 139 | } 140 | 141 | next(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /bin/tasks/_questionnaire.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var inquirer = require('inquirer'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | function validateDirectory(input) { 8 | var done = this.async(); 9 | 10 | fs.access(path.resolve(input), fs.R_OK | fs.W_OK, function (err) { 11 | done(err ? 'NO ACCESS! USE ABSOLUTE PATHS OR CORRECT FILE PERMISSIONS' : true); 12 | }); 13 | } 14 | 15 | function validateFile(input) { 16 | var done = this.async(); 17 | 18 | fs.exists(path.resolve(input), function (exists) { 19 | done(!exists ? 'NO ACCESS! FILE NOT FOUND' : true); 20 | }) 21 | } 22 | 23 | module.exports = { 24 | domain: function () { 25 | return inquirer.prompt([{ 26 | type: "input", 27 | name: "domain", 28 | message: "Enter your FQDN (fully qualified domain name) -> IMPORTANT:" 29 | }]); 30 | }, 31 | directory: function () { 32 | return inquirer.prompt([{ 33 | type: "confirm", 34 | name: "auto", 35 | message: "Should Galleon automatically create and set permissions of required directories?", 36 | }, { 37 | type: "checkbox", 38 | name: "perform", 39 | message: "Should Galleon perform the following? (You can Multi-Select)", 40 | choices: [{ 41 | value: "attachments", 42 | name: "Save attachments", 43 | checked: true 44 | }, { 45 | value: "raw", 46 | name: "Store raw emails", 47 | checked: true 48 | }] 49 | }, { 50 | type: "input", 51 | name: "location_attachments", 52 | message: "Enter the location to store attachments in:", 53 | when: function (answers) { 54 | return !answers.auto && (answers.perform.indexOf('attachments') + 1); 55 | }, 56 | validate: validateDirectory 57 | }, { 58 | type: "input", 59 | name: "location_raw", 60 | message: "Enter the location to store raw emails in:", 61 | when: function (answers) { 62 | return !answers.auto && (answers.perform.indexOf('raw') + 1); 63 | }, 64 | validate: validateDirectory 65 | }]); 66 | }, 67 | ssl: function () { 68 | return inquirer.prompt([{ 69 | type: "confirm", 70 | name: "shouldUseSSL", 71 | message: "Do you want to setup SSL Certificates with Galleon? (You can use the same cert/key for both options | Certificates and Keys must be stored locally)", 72 | }, { 73 | type: "checkbox", 74 | name: "sslOpt", 75 | message: "Should Galleon use SSL Certificates for the following? (You can Multi-Select)", 76 | when: function (answers) { 77 | return answers.shouldUseSSL; 78 | }, 79 | choices: [{ 80 | value: "ssl-smtp", 81 | name: "SMTP Server", 82 | checked: true 83 | }, { 84 | value: "ssl-api", 85 | name: "Front-end Server & API", 86 | checked: true 87 | }] 88 | }, { 89 | type: "input", 90 | name: "ssl-smtp-cert", 91 | message: "Enter the location for *SSL Certificate* for (SMTP SERVER): (if any)", 92 | when: function (answers) { 93 | return answers.shouldUseSSL && (answers.sslOpt.indexOf('ssl-smtp') + 1); 94 | }, 95 | validate: validateFile 96 | }, { 97 | type: "input", 98 | name: "ssl-smtp-key", 99 | message: "Enter the location for *SSL Key* for (SMTP SERVER): (if any)", 100 | when: function (answers) { 101 | return answers.shouldUseSSL && (answers.sslOpt.indexOf('ssl-smtp') + 1); 102 | }, 103 | validate: validateFile 104 | }, { 105 | type: "input", 106 | name: "ssl-smtp-ca", 107 | message: "Enter the location for *SSL CA* for (SMTP SERVER): (if any)", 108 | when: function (answers) { 109 | return answers.shouldUseSSL && (answers.sslOpt.indexOf('ssl-smtp') + 1); 110 | }, 111 | validate: validateFile 112 | }, { 113 | type: "input", 114 | name: "ssl-api-cert", 115 | message: "Enter the location for *SSL Certificate* for (API/FRONTEND SERVER): (if any)", 116 | when: function (answers) { 117 | return answers.shouldUseSSL && (answers.sslOpt.indexOf('ssl-api') + 1); 118 | }, 119 | validate: validateFile 120 | }, { 121 | type: "input", 122 | name: "ssl-api-key", 123 | message: "Enter the location for *SSL Key* for (API/FRONTEND SERVER): (if any)", 124 | when: function (answers) { 125 | return answers.shouldUseSSL && (answers.sslOpt.indexOf('ssl-api') + 1); 126 | }, 127 | validate: validateFile 128 | }, { 129 | type: "input", 130 | name: "ssl-api-ca", 131 | message: "Enter the location for *SSL CA* for (API/FRONTEND SERVER): (if any)", 132 | when: function (answers) { 133 | return answers.shouldUseSSL && (answers.sslOpt.indexOf('ssl-api') + 1); 134 | }, 135 | validate: validateFile 136 | },]); 137 | }, 138 | database: function () { 139 | return inquirer.prompt([{ 140 | type: "list", 141 | name: "adapter", 142 | message: "Select Database Adapter", 143 | choices: [ 144 | new inquirer.Separator("Recommended:"), 145 | { name: "MongoDB", value: "sails-mongo" }, 146 | { name: "Redis", value: "sails-redis" }, 147 | new inquirer.Separator("Optional:"), 148 | { name: "PostgreSQL", value: "sails-postgresql" }, 149 | { name: "MySQL", value: "sails-mysql" }, 150 | { name: "Microsoft SQL Server", value: "sails-sqlserver" }, 151 | { name: "Disk", value: "sails-disk" }, 152 | { name: "Memory", value: "sails-memory" } 153 | ] 154 | }, { 155 | type: "input", 156 | name: "database_name", 157 | message: "Enter the database name for the selected adapter:", 158 | }, { 159 | type: "input", 160 | name: "host", 161 | message: "Enter the host location:", 162 | default: "localhost" 163 | }, { 164 | type: "input", 165 | name: "port", 166 | message: "Enter the port:", 167 | default: function (answers) { 168 | var crs = { 169 | "sails-mongo": 27017, 170 | "sails-redis": 6379, 171 | "sails-postgresql": 5432, 172 | "sails-mysql": 3306, 173 | "sails-sqlserver": 1433, 174 | "sails-disk": null, 175 | "sails-memory": null 176 | } 177 | return crs[answers.adapter]; 178 | } 179 | }, { 180 | type: "input", 181 | name: "username", 182 | message: "Enter database username (requires read/write/delete access):", 183 | }, { 184 | type: "password", 185 | name: "password", 186 | message: "Enter password:", 187 | }]); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /fleet/outgoing/queue.js: -------------------------------------------------------------------------------- 1 | /* -- Modules -- */ 2 | // Essential 3 | var eventEmmiter = require('events').EventEmitter; 4 | var util = require("util"); 5 | 6 | // Core 7 | var outbound = require('./outbound'); 8 | 9 | // Foundations 10 | var colors = require('colors'); // Better looking error handling 11 | var moment = require('moment'); 12 | var _ = require('lodash'); 13 | /* -- ------- -- */ 14 | 15 | // GLOBALS 16 | var queueUpdate, queueStart, queueAdd; 17 | 18 | colors.setTheme({ 19 | silly: 'rainbow', 20 | input: 'grey', 21 | verbose: 'cyan', 22 | prompt: 'grey', 23 | success: 'green', 24 | data: 'grey', 25 | info: 'cyan', 26 | warn: 'yellow', 27 | debug: 'grey', 28 | bgWhite: 'bgWhite', 29 | bold: 'bold', 30 | error: 'red' 31 | }); 32 | 33 | /* Initiate outbound queue. */ 34 | var Queue = function (environment, callback) { 35 | console.log("Queue created".success); 36 | this.environment = environment; 37 | eventEmmiter.call(this); 38 | } 39 | 40 | util.inherits(Queue, eventEmmiter); 41 | 42 | queueStart = function queueStart(environment, databaseConnection) { 43 | console.log("Queue started".success); 44 | 45 | var maxConcurrent = 50; 46 | var outbox = databaseConnection.collections.queue; 47 | 48 | outbox.count({ state: 'transit' }).exec(function (error, count) { 49 | if (error) return console.log(colors.error(error)); 50 | console.log(colors.info(count + " mails in transit")); 51 | // Bit of a callback hell here 52 | if ((count <= maxConcurrent) || (count === undefined)) { 53 | outbox.find().where({ or: [{ state: 'pending' }, { state: 'denied' }] }).limit(Math.abs(maxConcurrent - count)).exec(function (error, models) { 54 | if (error) return console.log(colors.error(error)); 55 | 56 | console.log(colors.info(models.length + " mails found in queue")); 57 | 58 | // TIMED FILTER FOR ATTEMPTED EMAILS 59 | models = _.filter(models, function (mail) { 60 | /* 61 | FILTERS OUT EMAILS BASED ON: 62 | STATE - WHEN STATE IS NOT DENIED EMAIL IS KEPT 63 | TIME - WHEN LAST ATTEMPT IS ( n+1 * 2 minutes ) IN THE PAST EMAIL IS KEPT 64 | OTHERWISE EMAIL IS REMOVED FROM THE ARRAY 65 | */ 66 | if (mail.state !== 'denied') return true; 67 | if (moment().isAfter(moment(mail.schedule.attempted).add((mail.attempts || 1) * 2, 'minutes'))) { 68 | return true; 69 | } else return false; 70 | }); 71 | 72 | _.forEach(models, function (mail) { 73 | outbox.update({ eID: mail.eID }, { state: 'transit' }).exec(function (error, mail) { 74 | if (error) console.log(error.error); 75 | 76 | var mail = mail[0]; // Update returns and Array 77 | 78 | var OUTBOUND = new outbound(environment); 79 | OUTBOUND.send({ 80 | from: mail.sender, 81 | to: mail.to, 82 | subject: mail.subject, 83 | text: mail.text, 84 | html: mail.html, 85 | attachments: mail.attachments || [] 86 | }, OUTBOUND_TRACKER(databaseConnection, outbox, mail, maxConcurrent)); 87 | }); 88 | }); 89 | }); 90 | } 91 | }); 92 | } 93 | 94 | // OUTBOUND_TRACKER Prevents clogs in the outbound process by implementing a timeout (essentially assuming a sent was failed after 60 seconds) 95 | /// Timeout should be assumed from the max number of concurrent sends (a ratio of 60seconds:10concurrent should be ideal) 96 | var OUTBOUND_TRACKER = function OUTBOUND_TRACKER(databaseConnection, outbox, mail, maxConcurrent) { 97 | var OUTBOUND_ERROR = function OUTBOUND_ERROR(error) { 98 | ///* SET MAX ATTEMPTS SET TO 5 AFTER QUEUE IS CRONNED 99 | var MAX_ATTEMPTS = 1; 100 | if (error) console.log("OUTBOUND_ERROR", error); 101 | // UPDATE DENIED/FAILED ITEM & INCREMENT ATTEMPTS 102 | outbox.update({ eID: mail.eID }, { 103 | state: (mail.attempts >= MAX_ATTEMPTS) ? 'failed' : 'denied', attempts: ++mail.attempts, schedule: { 104 | attempted: moment().toISOString(), 105 | scheduled: mail.scheduled || moment().toISOString() 106 | } 107 | }).exec(function (error, _mail) { 108 | if (error) console.log("OUTBOUND_ERROR->UPDATE", error); 109 | else console.log((mail.attempts > MAX_ATTEMPTS) ? "OUTBOUND_ERROR->FAILED" : "OUTBOUND_ERROR->DENIED", _.first(_mail).eID, _.first(_mail).subject); 110 | }); 111 | /// - FAILURE NOTICE - /// 112 | if (mail.attempts >= MAX_ATTEMPTS) { 113 | console.log("OUTBOUND_ERROR->FAILURE-NOTICE", mail.eID); 114 | var notice = "The following email has been denied by the receiver.
Attempts have been made to deliver this email but have failed in multiple occasions. Please try again.

Reason of denial: " + JSON.stringify(error) + "
Receiver: " + mail.to + "
Subject: " + mail.subject + "
Date: " + JSON.stringify(mail.stamp) + "


" + mail.html; 115 | databaseConnection.collections.mail.create({ 116 | association: mail.sender, 117 | sender: mail.sender, 118 | receiver: mail.to, 119 | to: mail.to, 120 | stamp: { sent: (new Date()), received: (new Date()) }, 121 | subject: "Failure Notice: " + mail.subject, 122 | text: notice, 123 | html: notice || "Failure Notice", 124 | 125 | read: false, 126 | trash: false, 127 | 128 | dkim: "pass", 129 | spf: "pass", 130 | 131 | spam: false, 132 | spamScore: 0, 133 | 134 | // STRING ENUM: ['pending', 'approved', 'denied'] 135 | state: 'approved' 136 | }, function (error, mail) { 137 | if (error) console.log("OUTBOUND_ERROR->FAILURE-NOTICE-DENIED", error); 138 | }) 139 | } 140 | /// - -------------- - /// 141 | } 142 | // ALIAS FOR DEBUGGING 143 | var OUTBOUND_TIMEDOUT = OUTBOUND_ERROR; 144 | // TIMEOUT* 145 | var local_timeout = setTimeout(OUTBOUND_TIMEDOUT, (maxConcurrent) ? Math.round(maxConcurrent * 6000) : 60000); 146 | /* REQUIRED IMPROVEMENTS */ 147 | return function OUTBOUND_SENT(error, response) { 148 | clearTimeout(local_timeout); 149 | if (error) { 150 | OUTBOUND_ERROR(error); 151 | } else { 152 | outbox.update({ eID: mail.eID }, { state: 'sent' }).limit(1).exec(function (error, mail) { 153 | var mail = _.first(mail); 154 | // Move from Queue to Mailbox 155 | databaseConnection.collections.mail.create({ 156 | association: /(?:"?([^"]*)"?\s)?(?:]+)>?)/.exec(mail.sender)[2], 157 | sender: mail.sender, 158 | receiver: mail.to, 159 | to: mail.to, 160 | stamp: { sent: new Date(), received: new Date() }, 161 | subject: mail.subject, 162 | text: mail.text, 163 | html: mail.html, 164 | 165 | read: false, 166 | trash: false, 167 | sent: true, 168 | 169 | spam: false, 170 | spamScore: 0, 171 | 172 | attachments: mail.attachments || [], 173 | 174 | // STRING ENUM: ['draft', 'pending', 'approved', 'denied'] 175 | state: 'approved' 176 | }, function (error, model) { 177 | if (error) console.log(error); 178 | if (!error) console.log("Message " + mail.subject + " sent"); 179 | }) 180 | }); 181 | } 182 | } 183 | } 184 | 185 | queueAdd = function queueAdd(databaseConnection, mail, options, callback) { 186 | var _this = this; 187 | 188 | // Humane programming 189 | if ((options.constructor !== Object) && (!callback)) callback = options; 190 | 191 | var queue = { 192 | association: mail.association, 193 | sender: mail.from, 194 | to: mail.to, 195 | schedule: { attempted: moment().toISOString(), scheduled: moment().toISOString() }, 196 | attempts: 0, 197 | subject: mail.subject, 198 | text: mail.text || "", 199 | html: mail.html || "", 200 | // !IMPORTANT: Attachments should not exists here since it will override previous uploads 201 | //attachments: mail.attachments || [], 202 | 203 | // STRING ENUM: ['draft', 'pending', 'transit', 'sent', 'denied', 'failed'] 204 | state: (mail.draft) ? 'draft' : 'pending' 205 | }; 206 | 207 | // Load queue modules 208 | _this.environment.modulator.launch('queue', queue, function (error, _queue) { 209 | if (_queue !== undefined) queue = _queue; 210 | 211 | queueUpdate = function queueUpdate(error, model) { 212 | if (error) console.log(colors.error(error)); 213 | 214 | // Start queue 215 | queueStart(_this.environment, databaseConnection); 216 | 217 | _this.emit('queued', error, model, databaseConnection); 218 | 219 | if (callback) callback(error, model); 220 | } 221 | 222 | // 223 | /// This section differentiates between deferred (draft) and immediate email 224 | if (mail.remove && mail.id) { 225 | // REMOVES DRAFT 226 | databaseConnection.collections.queue.destroy({ eID: mail.id, association: queue.association }); 227 | } else if (!!mail.id) { 228 | // UPDATES DRAFT (if ID is provided) 229 | databaseConnection.collections.queue.update({ eID: mail.id, association: queue.association }, queue, queueUpdate) 230 | } else { 231 | // CREATES DRAFT/PENDING EMAIL 232 | databaseConnection.collections.queue.create(queue, queueUpdate); 233 | } 234 | /// 235 | // 236 | }) 237 | }; 238 | 239 | Queue.prototype.start = queueStart; 240 | Queue.prototype.add = queueAdd; 241 | 242 | module.exports = Queue; 243 | -------------------------------------------------------------------------------- /Galleon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Runs Galleon Server. 3 | * 4 | * @param {Env} Object 5 | */ 6 | 7 | /* -- Modules -- */ 8 | // Core 9 | var incoming = require('./fleet/incoming/incoming'); 10 | //var outgoing = require('./fleet/outgoing/outgoing'); 11 | 12 | var outbound = require('./fleet/outgoing/outbound'); 13 | var queue = require('./fleet/outgoing/queue'); 14 | 15 | var Database = require('./fleet/connection'); 16 | var Modulator = require('./bin/modulator'); 17 | // ------------------------------------------- 18 | 19 | // Query 20 | var GalleonQuery = { 21 | delete: require('./query/delete'), 22 | mark: require('./query/mark'), 23 | get: require('./query/get'), 24 | getattachment: require('./query/get.attachment'), 25 | linkattachment: require('./query/link.attachment'), 26 | unlinkattachment: require('./query/unlink.attachment'), 27 | clean: require('./query/clean.js'), 28 | restore: require('./query/restore.js') 29 | }; 30 | 31 | // Essential 32 | var EventEmitter = require('events').EventEmitter; 33 | var util = require('util'); 34 | var path = require("path"); 35 | var fs = require('fs'); 36 | var osenv = require('osenv'); 37 | 38 | // Utilities 39 | const _ = require('lodash'); 40 | const portscanner = require('portscanner'); 41 | const validator = require('validator'); 42 | const bcrypt = require('bcryptjs'); 43 | const async = require('async'); 44 | const colors = require('colors'); // Better looking error handling 45 | const Spamc = require('spamc-stream'); 46 | /* -- ------- -- */ 47 | 48 | var pass = true, fail = false; 49 | 50 | var Defaults = { 51 | // Ports 52 | ports: { 53 | incoming: 25, 54 | outgoing: 587, 55 | server: 3080 56 | }, 57 | dock: false, 58 | noCheck: false, 59 | verbose: true, 60 | ssl: { 61 | use: false 62 | } 63 | }; 64 | 65 | class Galleon extends EventEmitter { 66 | constructor(env, callback) { 67 | super(); 68 | var environment = {}; 69 | 70 | if ((!callback) && (typeof (env) === 'function')) { 71 | callback = env; 72 | env = undefined; 73 | } 74 | if ((!callback) || (typeof (callback) !== 'function')) { 75 | callback = function () { }; 76 | } 77 | 78 | if ((typeof (env) !== 'object') || (!env.connections)) { 79 | try { 80 | environment = JSON.parse(fs.readFileSync(path.resolve(osenv.home(), '.galleon/', 'galleon.conf'), 'utf8')); 81 | } catch (e) { 82 | console.trace(e); 83 | if (e) throw new Error("Failed to resolve Environment. If you are using the API pass an environment object as the first parameter."); 84 | } 85 | } 86 | 87 | // Set Spamc 88 | this.spamc = new Spamc('localhost', 783, 60); 89 | 90 | // Defaults 91 | environment = _.defaultsDeep(environment || {}, env); 92 | environment = _.defaultsDeep(environment || {}, Defaults); 93 | // 94 | 95 | // Attach environment to Galleon Object 96 | this.environment = environment; 97 | 98 | // Assign module environment 99 | this.environment.modulator = new Modulator(); 100 | // Assign modules -> IF Environment is set to Safe Mode Ignore All Modules 101 | if (this.environment.safemode === true) { 102 | this.environment.modules = {}; 103 | } else { 104 | this.environment.modules = this.environment.modulator.load(); 105 | } 106 | 107 | Database(this.environment.connections, (error, connection) => { 108 | if (environment.verbose) console.log("Connection attempted".yellow); 109 | if (error) { 110 | console.error("Connection error!".red); 111 | if (callback) callback(error); 112 | else throw error; 113 | } 114 | 115 | if (environment.verbose) console.log("Database connection established".green); 116 | // Add database connection to `this` 117 | this.connection = connection; 118 | 119 | if (!environment.noCheck) { 120 | var ports = environment.ports; 121 | Galleon.checkPorts([ports.incoming, ports.server], (check) => { 122 | if (check && environment.verbose) console.log("All requested ports are free"); 123 | 124 | if (environment.dock) { 125 | this.dock((error, incoming) => { 126 | this.emit('ready', error, incoming); 127 | callback(error, incoming, connection); 128 | }); 129 | } else { 130 | // Emit -ready- event 131 | this.emit('ready'); 132 | callback(error, connection); 133 | } 134 | }); 135 | } else this.emit('ready'); 136 | }); 137 | 138 | // Load front-end modules 139 | this.environment.modulator.launch('frontend', osenv.tmpdir(), function () { 140 | if (environment.verbose) console.log("FRONTEND MODULES LAUNCHED".green, arguments); 141 | }); 142 | } 143 | 144 | dock(callback) { 145 | // Internal 146 | if (!callback) callback = function () { }; 147 | 148 | var INCOMING = new incoming(this.environment); 149 | INCOMING.listen(this.environment.ports.incoming, this.connection, this.spamc); // Start SMTP Incoming Server 150 | 151 | //var OUTGOING = new outgoing(); 152 | //OUTGOING.listen(587); // Start SMTP Incoming Server - Sets to default port for now 153 | 154 | // ERROR | INCOMING | OUTGOING // 155 | callback(undefined, INCOMING); 156 | } 157 | 158 | server(callback) { 159 | var Server = require('./api/server'); 160 | 161 | // Internal 162 | if (!callback) callback = function () { }; 163 | 164 | Server(this.environment, this.environment.ports.server, this.connection, this); 165 | callback(undefined, true); 166 | } 167 | 168 | query(method, query, callback) { 169 | // Check if a corresponding Function is available 170 | if (!GalleonQuery[method.toLowerCase()]) return callback(new Error("Method not found!")); 171 | if (GalleonQuery[method.toLowerCase()].constructor !== Function) return callback(new Error("Method not found!")); 172 | 173 | // Log Query 174 | if (this.environment.verbose) console.log(colors.green(method.toUpperCase()), query); 175 | 176 | // Execute Query 177 | GalleonQuery[method.toLowerCase()](this, query, callback); 178 | } 179 | 180 | static checkPorts(ports, callback) { 181 | var check = pass; 182 | 183 | async.mapSeries(ports, (port, callback) => { 184 | portscanner.checkPortStatus(port, '127.0.0.1', callback); 185 | }, (error, responses) => { 186 | if (error) return callback(error); 187 | 188 | for (let i = 0; i < responses.length; i++) if (responses[i] === false) { 189 | console.warn(`Port ${ports[i]} is occupied`); 190 | return callback(null, false); 191 | } 192 | 193 | callback(null, true); 194 | }); 195 | } 196 | }; 197 | 198 | /* - DISPATCH METHOD - */ 199 | Galleon.prototype.dispatch = function (mail, callback, connection) { 200 | connection = connection || this.connection; 201 | var QUEUE = new queue(this.environment); 202 | QUEUE.add(connection, mail, this.environment, callback); 203 | }; 204 | /* - ---------------- - */ 205 | 206 | /* - USER MANAGEMENT - */ 207 | Galleon.prototype.createUser = function (user, callback) { 208 | var _this = this; 209 | 210 | // Internal 211 | if (!callback) callback = function () { }; 212 | 213 | // REGEX to match: 214 | // * Between 4 to 64 characters 215 | // * Special characters allowed (_) 216 | // * Alphanumeric 217 | // * Must start with a letter 218 | if (!validator.isEmail(user.email)) 219 | return callback(new Error("Invalid email")); 220 | 221 | // REGEX to match: 222 | // * Between 2 to 256 characters 223 | // * Special characters allowed (&) 224 | // * Alpha 225 | if ((!validator.matches(user.name, /^([ \u00c0-\u01ffa-zA-Z-\&'\-])+$/)) && (validator.isLength(user.name, 2, 256))) 226 | return callback(new Error("Invalid name")); 227 | 228 | // Check if name is provided 229 | if (!user.name) 230 | return callback(new Error("No name provided\nTry -> --name=\"\"")); 231 | 232 | // REGEX to match: 233 | // * Between 6 to 20 characters 234 | // * Special characters allowed (@,$,!,%,*,?,&) 235 | // * Alphanumeric 236 | if (!validator.matches(user.password, /^(?=.*[a-zA-Z])[A-Za-z\d$@$!%*?&]{6,20}/)) 237 | return callback(new Error("Invalid password")); 238 | 239 | bcrypt.hash(user.password, 10, function (error, hash) { 240 | if (error) return callback(error); 241 | 242 | _this.connection.collections.users.create({ 243 | email: user.email, 244 | name: user.name, 245 | isAdmin: user.isAdmin || false, 246 | password: hash, 247 | }, function (error, user) { 248 | if (error) return callback(error); 249 | callback(null, user); 250 | }); 251 | }); 252 | }; 253 | 254 | Galleon.prototype.listUsers = function (options, callback) { 255 | // Internal 256 | if (typeof (options) === 'function') { 257 | callback = options; 258 | options = {}; 259 | } else if (!options) options = {}; 260 | if (!callback) callback = function () { }; 261 | 262 | this.connection.collections.users.find().limit(options.limit || 50).exec(callback); 263 | }; 264 | 265 | Galleon.prototype.removeUser = function (query, callback) { 266 | // Internal 267 | if (!callback) callback = function () { }; 268 | // IF Query is email 269 | if (typeof (query) === 'string') query = { email: query }; 270 | 271 | if (query) this.connection.collections.users.destroy(query).exec(callback); 272 | else callback(new Error("NO QUERY")); 273 | }; 274 | 275 | Galleon.prototype.changePassword = function (user, newPassword, oldPassword, callback, forceChange) { 276 | var self = this; 277 | // Internal 278 | if (!callback) callback = function () { }; 279 | if (!user) callback(new Error("No user argument (email) passed!")); 280 | if (user.email) user = user.email; 281 | 282 | // REGEX to match: 283 | // * Between 6 to 20 characters 284 | // * Special characters allowed (@,$,!,%,*,?,&) 285 | // * Alphanumeric 286 | if (!validator.matches(newPassword, /^(?=.*[a-zA-Z])[A-Za-z\d$@$!%*?&]{6,20}/)) 287 | return callback(new Error("Invalid password - Length must be between 6 to 20 characters")); 288 | 289 | if (!forceChange) { 290 | self.connection.collections.users.findOne({ email: user }).exec(function (error, user) { 291 | if (!user) callback(new Error("User Not found!")); 292 | bcrypt.compare(oldPassword, user.password, function (error, result) { 293 | if (error || !result) return callback(new Error("Current password does not match!")); 294 | bcrypt.hash(newPassword, 10, function (error, hash) { 295 | if (hash) 296 | self.connection.collections.users.update({ email: user.email }, { password: hash }).exec(function (error) { 297 | if (error) return callback(new Error("Password update failed")); 298 | self.connection.collections.users.findOne({ email: user.email }).exec(callback); 299 | }); 300 | else 301 | return callback(new Error("INCORRECT PASSWORD")); 302 | }); 303 | }); 304 | }); 305 | } else { 306 | bcrypt.hash(newPassword, 10, function (error, hash) { 307 | if (hash) 308 | self.connection.collections.users.update({ email: user }, { password: hash }).exec(callback); 309 | else 310 | return callback(new Error("INCORRECT PASSWORD")); 311 | }); 312 | } 313 | }; 314 | /* - --------------- - */ 315 | 316 | module.exports = Galleon; 317 | --------------------------------------------------------------------------------