├── .gitignore ├── LICENSE ├── README.md ├── config.json.dist ├── index.js ├── package-lock.json ├── package.json ├── public ├── css │ ├── bootstrap.min.css │ └── screen.css └── js │ ├── bootstrap.min.js │ ├── jquery-3.6.0.min.js │ └── main.js ├── screenshots ├── image1.png └── image2.png ├── services ├── ConfigParser.js └── MlmmjWrapper.js └── views ├── archive.html ├── archives.html ├── base.html ├── control.html ├── group.html ├── list.html ├── login.html └── subscribers.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Dependency directory 6 | # Commenting this out is preferred by some people, see 7 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 8 | node_modules 9 | 10 | config.json 11 | data -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 tchap 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mlmmj Simple Web Interface 2 | A very simple web frontend in node for [mlmmj](http://mlmmj.org/). 3 | 4 | > **Attention : run as `list`** : 5 | 6 | > When you run the app, you have to run it as the `list` user if you want to have access to mlmmj's files : 7 | ``` 8 | sudo su list -c "node index.js" 9 | ``` 10 | 11 | ## Structure 12 | 13 | This is a Node.js app, that uses : 14 | 15 | * [Express](http://expressjs.com/) as a global framework, with sessions and flash messages 16 | * [Consolidate](https://github.com/tj/consolidate.js) & [Nunjucks](https://mozilla.github.io/nunjucks) as the template engine 17 | * [Passportjs](http://passportjs.org) for the authentification 18 | * [Bootstrap](http://getbootstrap.com) for the frontend 19 | 20 | And some local modules like `ConfigParser` (_cf. source code if you want to see more_) 21 | 22 | ## Functionalities 23 | 24 | * Parameters 25 | 26 | The app allows to modify all tunables (files present in /control) : values, lists, texts and booleans 27 | 28 | * Subscribers 29 | 30 | The app allows to modify all tunables (files present in /control) : values, lists, texts and booleans 31 | 32 | * Archives 33 | 34 | There is also a very simplist archives browser that can display archives by year, month, day, and individual files. No thread yet, though (see TODO) 35 | 36 | 37 | ## Screenshots 38 | 39 | ![list actions](https://raw.githubusercontent.com/tchapi/mlmmj-simple-web-interface/master/screenshots/image1.png "Mailing list actions") 40 | ![control parameters](https://raw.githubusercontent.com/tchapi/mlmmj-simple-web-interface/master/screenshots/image2.png "Control parameters") 41 | 42 | ## Configuration 43 | 44 | Some options are available in the `config.json` file. Before launching the app,you must copy `config.json.dist` to `config.json` and change the desired settings : 45 | 46 | * **Server** 47 | 48 | The settings allows you to change the port that the app use if you want to reverse-proxy it afterwards (_and you should_) 49 | 50 | * **App name** 51 | 52 | The app name is displayed in the navbar, on the left 53 | 54 | * **Mlmmj directory path** 55 | 56 | This path is the one that mlmmj uses to store all the content and parameters. Should be the default `/var/spool/mlmmj` unless you manually changed it 57 | 58 | ## Authentification 59 | 60 | The whole app needs authentication. 61 | 62 | Authentification is managed by [passportjs](http://passportjs.org). The username and password of users are stored in the `config.json` file : 63 | 64 | ```json 65 | { 66 | //... 67 | "users": [ 68 | { "username": "admin", "password": "secret"}, 69 | { "username": "jon", "password": "pass"} 70 | ], 71 | //... 72 | } 73 | ``` 74 | 75 | ## Licence 76 | 77 | MIT ! See the [licence](https://github.com/tchapi/mlmmj-simple-web-interface/blob/master/LICENSE) file. 78 | 79 | ## To Do 80 | 81 | * Implement other functions such as : 82 | - i18n texts editing 83 | * Browse Archives by thread 84 | * Improve Archives altogether (subject of mail in lists, etc) 85 | * Add a button to restart exim / postfix ? 86 | -------------------------------------------------------------------------------- /config.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "address": "localhost", 4 | "port": "4792" 5 | }, 6 | "app": { 7 | "name": "Mlmmj Web Interface" 8 | }, 9 | "users": [ 10 | { "username": "admin", "password": "secret"} 11 | ], 12 | "mlmmj": { 13 | "path": "/var/spool/mlmmj" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var bodyParser = require('body-parser') 3 | var flash = require('connect-flash') 4 | var session = require('express-session') 5 | var passport = require('passport') 6 | , LocalStrategy = require('passport-local').Strategy 7 | 8 | // Add config module 9 | var CONFIG = require('./services/ConfigParser') 10 | , config = new CONFIG() 11 | 12 | // Consolidate, to make beautiful templates in nunjucks 13 | var cons = require('consolidate') 14 | , nunjucks = require('nunjucks') 15 | 16 | // Add mlmmj service wrapper module 17 | var Mlmmj = require('./services/MlmmjWrapper') 18 | 19 | 20 | var app = express() 21 | 22 | /* Configuration is done here 23 | */ 24 | // add nunjucks to requires so filters can be 25 | // added and the same instance will be used inside the render method 26 | cons.requires.nunjucks = nunjucks.configure(); 27 | cons.requires.nunjucks.addGlobal('website_name', config.get('app').name); 28 | 29 | // Handy flash messages 30 | app.use(flash()) 31 | 32 | // Use sessions 33 | app.use(session({ name: "mlmmj.web.session.id", secret: 'mlmmjNotSoSecretPhrase', cookie: { maxAge: null }, resave: true, saveUninitialized: true })) 34 | 35 | // Use passport-local auth system with persistent sessions 36 | app.use(passport.initialize()) 37 | app.use(passport.session()) 38 | 39 | // Static content 40 | app.use(express.static(__dirname + '/public')) 41 | 42 | // .. parse application/x-www-form-urlencoded 43 | app.use(bodyParser.urlencoded({ extended: false })) 44 | 45 | // .. parse application/json 46 | app.use(bodyParser.json()) 47 | 48 | // Assign the swig engine to .html files 49 | app.engine('html', cons.nunjucks) 50 | 51 | // Set .html as the default extension 52 | app.set('view engine', 'html') 53 | app.set('views', __dirname + '/views') 54 | 55 | /* End Express Configuration 56 | */ 57 | 58 | // Passport users setup. 59 | function findByUsername(username, fn) { 60 | for (var i = 0, len = config.get('users').length; i < len; i++) { 61 | var user = config.get('users')[i] 62 | if (user.username === username) { 63 | return fn(null, user) 64 | } 65 | } 66 | return fn(null, null) 67 | } 68 | 69 | // Passport session setup. 70 | // To support persistent login sessions, Passport needs to be able to 71 | // serialize users into and deserialize users out of the session. Typically, 72 | // this will be as simple as storing the user ID when serializing, and finding 73 | // the user by ID when deserializing. 74 | passport.serializeUser(function(user, done) { 75 | done(null, user.username) 76 | }); 77 | 78 | passport.deserializeUser(function(username, done) { 79 | findByUsername(username, function (err, user) { 80 | done(err, user) 81 | }); 82 | }); 83 | 84 | // Login form / handler 85 | passport.use(new LocalStrategy( 86 | function(username, password, done) { 87 | // asynchronous verification, for effect... 88 | process.nextTick(function () { 89 | 90 | // Find the user by username. If there is no user with the given 91 | // username, or the password is not correct, set the user to `false` to 92 | // indicate failure and set a flash message. Otherwise, return the 93 | // authenticated `user`. 94 | findByUsername(username, function(err, user) { 95 | if (err) { return done(err) } 96 | if (!user) { return done(null, false, { message: 'Unknown user ' + username }) } 97 | if (user.password != password) { return done(null, false, { message: 'Invalid password' }) } 98 | return done(null, user) 99 | }) 100 | }); 101 | } 102 | )); 103 | 104 | // Simple route middleware to ensure user is authenticated. 105 | // Use this route middleware on any resource that needs to be protected. If 106 | // the request is authenticated (typically via a persistent login session), 107 | // the request will proceed. Otherwise, the user will be redirected to the 108 | // login page. 109 | function ensureAuthenticated(req, res, next) { 110 | if (req.isAuthenticated()) { return next(); } 111 | req.session.returnTo = req.path; // Save redirect path in session 112 | res.redirect('/login') 113 | } 114 | 115 | // POST /login 116 | // Use passport.authenticate() as route middleware to authenticate the 117 | // request. If authentication fails, the user will be redirected back to the 118 | // login page. Otherwise, the primary route function function will be called, 119 | // which, in this example, will redirect the user to the home page. 120 | // 121 | // curl -v -d "username=bob&password=secret" http://127.0.0.1:3000/login 122 | app.post('/login', 123 | passport.authenticate('local', { failureRedirect: '/login', failureFlash: true }), 124 | function(req, res) { 125 | res.redirect('/'); 126 | }); 127 | 128 | app.get('/login', function(req, res){ 129 | res.render('login', { hideLogout: true, title: "Login", message: req.flash('error') }) 130 | }); 131 | 132 | app.get('/logout', function(req, res){ 133 | req.logout() 134 | res.redirect('/login') 135 | }); 136 | 137 | 138 | app.param('name', function(req, res, next, name) { 139 | 140 | var group = null 141 | 142 | try { 143 | req.group = new Mlmmj(config.get('mlmmj').path, name) 144 | req.name = name 145 | } catch (err) { 146 | res.status(404).send(err.name + " : " + err.message) 147 | return 148 | } 149 | 150 | next() 151 | }) 152 | 153 | app.param('mail_id', function(req, res, next, mail_id){ 154 | 155 | req.group.getArchive(mail_id, function(mail_object){ 156 | req.mail = mail_object 157 | req.mail_id = mail_id 158 | next() 159 | }) 160 | 161 | }) 162 | 163 | app.get('/', ensureAuthenticated, function (req, res) { 164 | 165 | try { 166 | groups = Mlmmj.listGroups(config.get('mlmmj').path) 167 | } catch (err) { 168 | res.status(500).send(err.name + " : " + err.message) 169 | return 170 | } 171 | 172 | res.render('list', { 173 | title: 'List', 174 | directory: config.get('mlmmj').path, 175 | groups: groups 176 | }) 177 | 178 | }) 179 | 180 | app.get('/group/:name', ensureAuthenticated, function(req, res){ 181 | res.render('group', { 182 | title: 'Mailing list ' + req.name, 183 | name: req.name 184 | }) 185 | }) 186 | 187 | app.get('/group/:name/control', ensureAuthenticated, function(req, res){ 188 | res.render('control', { 189 | title: 'Mailing list ' + req.name, 190 | name: req.name, 191 | availables : Mlmmj.getAllAvailables(), 192 | flags: req.group.getFlags(), 193 | texts: req.group.getTexts(), 194 | values: req.group.getValues(), 195 | lists: req.group.getLists() 196 | }) 197 | }) 198 | 199 | app.get('/group/:name/subscribers', ensureAuthenticated, function(req, res){ 200 | res.render('subscribers', { 201 | title: 'Mailing list ' + req.name, 202 | name: req.name, 203 | subscribers: req.group.getSubscribers() 204 | }) 205 | }) 206 | 207 | app.post('/group/:name/save/:key', ensureAuthenticated, function(req, res){ 208 | 209 | var key = req.params.key 210 | 211 | if (key == 'flags') { 212 | for (key in req.body) { 213 | req.group.setFlag(key, req.body[key]) 214 | } 215 | } else if (key == 'texts') { 216 | for (key in req.body) { 217 | req.group.setText(key, req.body[key]) 218 | } 219 | } else if (key == 'lists') { 220 | for (key in req.body) { 221 | req.group.setList(key, req.body[key].split("\n")) 222 | } 223 | } else if (key == 'values') { 224 | for (key in req.body) { 225 | req.group.setValue(key, req.body[key]) 226 | } 227 | } else if (key == 'subscribers') { 228 | for (key in req.body) { 229 | req.group.setSubscribers(key, req.body[key]) 230 | } 231 | } 232 | 233 | // Save to disk 234 | req.group.saveAll() 235 | res.status(200).send("1") 236 | 237 | }) 238 | 239 | app.post('/group/:name/remove/:key', ensureAuthenticated, function(req, res){ 240 | 241 | var key = req.params.key 242 | 243 | if (key == 'subscribers') { 244 | req.group.removeSubscriber(req.body.element) 245 | } 246 | 247 | // Save to disk 248 | req.group.saveAll() 249 | res.status(200).send("1") 250 | }) 251 | 252 | app.post('/group/:name/add/:key', ensureAuthenticated, function(req, res){ 253 | 254 | var key = req.params.key 255 | 256 | if (key == 'subscribers') { 257 | req.group.addSubscriber(req.body.element) 258 | } 259 | 260 | // Save to disk 261 | req.group.saveAll() 262 | res.status(200).send("1") 263 | }) 264 | 265 | app.get('/group/:name/archives/:year?/:month?/:day?', ensureAuthenticated, function(req, res){ 266 | 267 | try { 268 | archives = req.group.listArchives(req.params.year, req.params.month) 269 | } catch (err) { 270 | res.status(500).send(err.name + " : " + err.message) 271 | return 272 | } 273 | 274 | res.render('archives', { 275 | title: 'Mailing list ' + req.name, 276 | name: req.name, 277 | year: req.params.year, 278 | month: req.params.month, 279 | day: req.params.day, 280 | archives: archives 281 | }) 282 | }) 283 | 284 | app.get('/group/:name/archive/:mail_id', ensureAuthenticated, function(req, res){ 285 | res.render('archive', { 286 | title: 'Mailing list ' + req.name, 287 | id: req.mail_id, 288 | name: req.name, 289 | mail: req.mail 290 | }) 291 | }) 292 | 293 | // Start application 294 | var server = app.listen(config.get('server').port, config.get('server').address, function () { 295 | 296 | var host = server.address().address 297 | var port = server.address().port 298 | 299 | console.log('Starting mlmmj groups management interface at http://%s:%s', host, port) 300 | 301 | }) 302 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mlmmj-simple-web-interface", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "a-sync-waterfall": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", 10 | "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==" 11 | }, 12 | "accepts": { 13 | "version": "1.3.7", 14 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 15 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 16 | "requires": { 17 | "mime-types": "~2.1.24", 18 | "negotiator": "0.6.2" 19 | } 20 | }, 21 | "array-flatten": { 22 | "version": "1.1.1", 23 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 24 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 25 | }, 26 | "asap": { 27 | "version": "2.0.6", 28 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 29 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" 30 | }, 31 | "bluebird": { 32 | "version": "3.7.2", 33 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 34 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 35 | }, 36 | "body-parser": { 37 | "version": "1.19.0", 38 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 39 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 40 | "requires": { 41 | "bytes": "3.1.0", 42 | "content-type": "~1.0.4", 43 | "debug": "2.6.9", 44 | "depd": "~1.1.2", 45 | "http-errors": "1.7.2", 46 | "iconv-lite": "0.4.24", 47 | "on-finished": "~2.3.0", 48 | "qs": "6.7.0", 49 | "raw-body": "2.4.0", 50 | "type-is": "~1.6.17" 51 | } 52 | }, 53 | "bytes": { 54 | "version": "3.1.0", 55 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 56 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 57 | }, 58 | "commander": { 59 | "version": "5.1.0", 60 | "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", 61 | "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" 62 | }, 63 | "connect-flash": { 64 | "version": "0.1.1", 65 | "resolved": "https://registry.npmjs.org/connect-flash/-/connect-flash-0.1.1.tgz", 66 | "integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA=" 67 | }, 68 | "consolidate": { 69 | "version": "0.14.0", 70 | "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.0.tgz", 71 | "integrity": "sha1-sDrNVmolZcqW6Z9E/RQXSGtN+I0=", 72 | "requires": { 73 | "bluebird": "^3.1.1" 74 | } 75 | }, 76 | "content-disposition": { 77 | "version": "0.5.3", 78 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 79 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 80 | "requires": { 81 | "safe-buffer": "5.1.2" 82 | } 83 | }, 84 | "content-type": { 85 | "version": "1.0.4", 86 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 87 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 88 | }, 89 | "cookie": { 90 | "version": "0.4.0", 91 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 92 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 93 | }, 94 | "cookie-signature": { 95 | "version": "1.0.6", 96 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 97 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 98 | }, 99 | "debug": { 100 | "version": "2.6.9", 101 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 102 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 103 | "requires": { 104 | "ms": "2.0.0" 105 | } 106 | }, 107 | "deepmerge": { 108 | "version": "4.2.2", 109 | "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", 110 | "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" 111 | }, 112 | "depd": { 113 | "version": "1.1.2", 114 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 115 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 116 | }, 117 | "destroy": { 118 | "version": "1.0.4", 119 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 120 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 121 | }, 122 | "dom-serializer": { 123 | "version": "1.2.0", 124 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz", 125 | "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==", 126 | "requires": { 127 | "domelementtype": "^2.0.1", 128 | "domhandler": "^4.0.0", 129 | "entities": "^2.0.0" 130 | } 131 | }, 132 | "domelementtype": { 133 | "version": "2.1.0", 134 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", 135 | "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==" 136 | }, 137 | "domhandler": { 138 | "version": "4.0.0", 139 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", 140 | "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", 141 | "requires": { 142 | "domelementtype": "^2.1.0" 143 | } 144 | }, 145 | "domutils": { 146 | "version": "2.4.4", 147 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz", 148 | "integrity": "sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==", 149 | "requires": { 150 | "dom-serializer": "^1.0.1", 151 | "domelementtype": "^2.0.1", 152 | "domhandler": "^4.0.0" 153 | } 154 | }, 155 | "ee-first": { 156 | "version": "1.1.1", 157 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 158 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 159 | }, 160 | "encodeurl": { 161 | "version": "1.0.2", 162 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 163 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 164 | }, 165 | "encoding-japanese": { 166 | "version": "1.0.30", 167 | "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-1.0.30.tgz", 168 | "integrity": "sha512-bd/DFLAoJetvv7ar/KIpE3CNO8wEuyrt9Xuw6nSMiZ+Vrz/Q21BPsMHvARL2Wz6IKHKXgb+DWZqtRg1vql9cBg==" 169 | }, 170 | "entities": { 171 | "version": "2.2.0", 172 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", 173 | "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" 174 | }, 175 | "escape-html": { 176 | "version": "1.0.3", 177 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 178 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 179 | }, 180 | "etag": { 181 | "version": "1.8.1", 182 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 183 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 184 | }, 185 | "express": { 186 | "version": "4.17.1", 187 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 188 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 189 | "requires": { 190 | "accepts": "~1.3.7", 191 | "array-flatten": "1.1.1", 192 | "body-parser": "1.19.0", 193 | "content-disposition": "0.5.3", 194 | "content-type": "~1.0.4", 195 | "cookie": "0.4.0", 196 | "cookie-signature": "1.0.6", 197 | "debug": "2.6.9", 198 | "depd": "~1.1.2", 199 | "encodeurl": "~1.0.2", 200 | "escape-html": "~1.0.3", 201 | "etag": "~1.8.1", 202 | "finalhandler": "~1.1.2", 203 | "fresh": "0.5.2", 204 | "merge-descriptors": "1.0.1", 205 | "methods": "~1.1.2", 206 | "on-finished": "~2.3.0", 207 | "parseurl": "~1.3.3", 208 | "path-to-regexp": "0.1.7", 209 | "proxy-addr": "~2.0.5", 210 | "qs": "6.7.0", 211 | "range-parser": "~1.2.1", 212 | "safe-buffer": "5.1.2", 213 | "send": "0.17.1", 214 | "serve-static": "1.14.1", 215 | "setprototypeof": "1.1.1", 216 | "statuses": "~1.5.0", 217 | "type-is": "~1.6.18", 218 | "utils-merge": "1.0.1", 219 | "vary": "~1.1.2" 220 | } 221 | }, 222 | "express-session": { 223 | "version": "1.17.1", 224 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz", 225 | "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==", 226 | "requires": { 227 | "cookie": "0.4.0", 228 | "cookie-signature": "1.0.6", 229 | "debug": "2.6.9", 230 | "depd": "~2.0.0", 231 | "on-headers": "~1.0.2", 232 | "parseurl": "~1.3.3", 233 | "safe-buffer": "5.2.0", 234 | "uid-safe": "~2.1.5" 235 | }, 236 | "dependencies": { 237 | "depd": { 238 | "version": "2.0.0", 239 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 240 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 241 | }, 242 | "safe-buffer": { 243 | "version": "5.2.0", 244 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 245 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 246 | } 247 | } 248 | }, 249 | "finalhandler": { 250 | "version": "1.1.2", 251 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 252 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 253 | "requires": { 254 | "debug": "2.6.9", 255 | "encodeurl": "~1.0.2", 256 | "escape-html": "~1.0.3", 257 | "on-finished": "~2.3.0", 258 | "parseurl": "~1.3.3", 259 | "statuses": "~1.5.0", 260 | "unpipe": "~1.0.0" 261 | } 262 | }, 263 | "forwarded": { 264 | "version": "0.1.2", 265 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 266 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 267 | }, 268 | "fresh": { 269 | "version": "0.5.2", 270 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 271 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 272 | }, 273 | "he": { 274 | "version": "1.2.0", 275 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 276 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" 277 | }, 278 | "html-to-text": { 279 | "version": "7.0.0", 280 | "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-7.0.0.tgz", 281 | "integrity": "sha512-UR/WMSHRN8m+L7qQUhbSoxylwBovNPS+xURn/pHeJvbnemhyMiuPYBTBGqB6s8ajAARN5jzKfF0d3CY86VANpA==", 282 | "requires": { 283 | "deepmerge": "^4.2.2", 284 | "he": "^1.2.0", 285 | "htmlparser2": "^6.0.0", 286 | "minimist": "^1.2.5" 287 | } 288 | }, 289 | "htmlparser2": { 290 | "version": "6.0.0", 291 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz", 292 | "integrity": "sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==", 293 | "requires": { 294 | "domelementtype": "^2.0.1", 295 | "domhandler": "^4.0.0", 296 | "domutils": "^2.4.4", 297 | "entities": "^2.0.0" 298 | } 299 | }, 300 | "http-errors": { 301 | "version": "1.7.2", 302 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 303 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 304 | "requires": { 305 | "depd": "~1.1.2", 306 | "inherits": "2.0.3", 307 | "setprototypeof": "1.1.1", 308 | "statuses": ">= 1.5.0 < 2", 309 | "toidentifier": "1.0.0" 310 | } 311 | }, 312 | "iconv-lite": { 313 | "version": "0.4.24", 314 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 315 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 316 | "requires": { 317 | "safer-buffer": ">= 2.1.2 < 3" 318 | } 319 | }, 320 | "inherits": { 321 | "version": "2.0.3", 322 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 323 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 324 | }, 325 | "ipaddr.js": { 326 | "version": "1.9.1", 327 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 328 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 329 | }, 330 | "libbase64": { 331 | "version": "1.2.1", 332 | "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.2.1.tgz", 333 | "integrity": "sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==" 334 | }, 335 | "libmime": { 336 | "version": "5.0.0", 337 | "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.0.0.tgz", 338 | "integrity": "sha512-2Bm96d5ktnE217Ib1FldvUaPAaOst6GtZrsxJCwnJgi9lnsoAKIHyU0sae8rNx6DNYbjdqqh8lv5/b9poD8qOg==", 339 | "requires": { 340 | "encoding-japanese": "1.0.30", 341 | "iconv-lite": "0.6.2", 342 | "libbase64": "1.2.1", 343 | "libqp": "1.1.0" 344 | }, 345 | "dependencies": { 346 | "iconv-lite": { 347 | "version": "0.6.2", 348 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", 349 | "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", 350 | "requires": { 351 | "safer-buffer": ">= 2.1.2 < 3.0.0" 352 | } 353 | } 354 | } 355 | }, 356 | "libqp": { 357 | "version": "1.1.0", 358 | "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz", 359 | "integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=" 360 | }, 361 | "linkify-it": { 362 | "version": "3.0.2", 363 | "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz", 364 | "integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==", 365 | "requires": { 366 | "uc.micro": "^1.0.1" 367 | } 368 | }, 369 | "mailparser": { 370 | "version": "3.1.0", 371 | "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.1.0.tgz", 372 | "integrity": "sha512-XW8aZ649hdgIxWIiHVsgaX7hUwf3eD4KJvtYOonssDuJHQpFJSqKWvTO5XjclNBF5ARWPFDq5OzBPTYH2i57fg==", 373 | "requires": { 374 | "encoding-japanese": "1.0.30", 375 | "he": "1.2.0", 376 | "html-to-text": "7.0.0", 377 | "iconv-lite": "0.6.2", 378 | "libmime": "5.0.0", 379 | "linkify-it": "3.0.2", 380 | "mailsplit": "5.0.1", 381 | "nodemailer": "6.4.18", 382 | "tlds": "1.217.0" 383 | }, 384 | "dependencies": { 385 | "iconv-lite": { 386 | "version": "0.6.2", 387 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", 388 | "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", 389 | "requires": { 390 | "safer-buffer": ">= 2.1.2 < 3.0.0" 391 | } 392 | } 393 | } 394 | }, 395 | "mailsplit": { 396 | "version": "5.0.1", 397 | "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.0.1.tgz", 398 | "integrity": "sha512-CcGy1sv8j9jdjKiNIuMZYIKhq4s47nUj9Q98BZfptabH/whmiQX7EvrHx36O4DcyPEsnG152GVNyvqPi9FNIew==", 399 | "requires": { 400 | "libbase64": "1.2.1", 401 | "libmime": "5.0.0", 402 | "libqp": "1.1.0" 403 | } 404 | }, 405 | "media-typer": { 406 | "version": "0.3.0", 407 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 408 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 409 | }, 410 | "merge-descriptors": { 411 | "version": "1.0.1", 412 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 413 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 414 | }, 415 | "methods": { 416 | "version": "1.1.2", 417 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 418 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 419 | }, 420 | "mime": { 421 | "version": "1.6.0", 422 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 423 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 424 | }, 425 | "mime-db": { 426 | "version": "1.46.0", 427 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", 428 | "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" 429 | }, 430 | "mime-types": { 431 | "version": "2.1.29", 432 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", 433 | "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", 434 | "requires": { 435 | "mime-db": "1.46.0" 436 | } 437 | }, 438 | "minimist": { 439 | "version": "1.2.5", 440 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 441 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 442 | }, 443 | "ms": { 444 | "version": "2.0.0", 445 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 446 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 447 | }, 448 | "negotiator": { 449 | "version": "0.6.2", 450 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 451 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 452 | }, 453 | "nodemailer": { 454 | "version": "6.4.18", 455 | "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.18.tgz", 456 | "integrity": "sha512-ht9cXxQ+lTC+t00vkSIpKHIyM4aXIsQ1tcbQCn5IOnxYHi81W2XOaU66EQBFFpbtzLEBTC94gmkbD4mGZQzVpA==" 457 | }, 458 | "nunjucks": { 459 | "version": "3.2.3", 460 | "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.3.tgz", 461 | "integrity": "sha512-psb6xjLj47+fE76JdZwskvwG4MYsQKXUtMsPh6U0YMvmyjRtKRFcxnlXGWglNybtNTNVmGdp94K62/+NjF5FDQ==", 462 | "requires": { 463 | "a-sync-waterfall": "^1.0.0", 464 | "asap": "^2.0.3", 465 | "commander": "^5.1.0" 466 | } 467 | }, 468 | "on-finished": { 469 | "version": "2.3.0", 470 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 471 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 472 | "requires": { 473 | "ee-first": "1.1.1" 474 | } 475 | }, 476 | "on-headers": { 477 | "version": "1.0.2", 478 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 479 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" 480 | }, 481 | "parseurl": { 482 | "version": "1.3.3", 483 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 484 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 485 | }, 486 | "passport": { 487 | "version": "0.4.1", 488 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", 489 | "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", 490 | "requires": { 491 | "passport-strategy": "1.x.x", 492 | "pause": "0.0.1" 493 | } 494 | }, 495 | "passport-local": { 496 | "version": "1.0.0", 497 | "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", 498 | "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", 499 | "requires": { 500 | "passport-strategy": "1.x.x" 501 | } 502 | }, 503 | "passport-strategy": { 504 | "version": "1.0.0", 505 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 506 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 507 | }, 508 | "path-to-regexp": { 509 | "version": "0.1.7", 510 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 511 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 512 | }, 513 | "pause": { 514 | "version": "0.0.1", 515 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", 516 | "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" 517 | }, 518 | "proxy-addr": { 519 | "version": "2.0.6", 520 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 521 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 522 | "requires": { 523 | "forwarded": "~0.1.2", 524 | "ipaddr.js": "1.9.1" 525 | } 526 | }, 527 | "qs": { 528 | "version": "6.7.0", 529 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 530 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 531 | }, 532 | "random-bytes": { 533 | "version": "1.0.0", 534 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", 535 | "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" 536 | }, 537 | "range-parser": { 538 | "version": "1.2.1", 539 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 540 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 541 | }, 542 | "raw-body": { 543 | "version": "2.4.0", 544 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 545 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 546 | "requires": { 547 | "bytes": "3.1.0", 548 | "http-errors": "1.7.2", 549 | "iconv-lite": "0.4.24", 550 | "unpipe": "1.0.0" 551 | } 552 | }, 553 | "safe-buffer": { 554 | "version": "5.1.2", 555 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 556 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 557 | }, 558 | "safer-buffer": { 559 | "version": "2.1.2", 560 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 561 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 562 | }, 563 | "send": { 564 | "version": "0.17.1", 565 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 566 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 567 | "requires": { 568 | "debug": "2.6.9", 569 | "depd": "~1.1.2", 570 | "destroy": "~1.0.4", 571 | "encodeurl": "~1.0.2", 572 | "escape-html": "~1.0.3", 573 | "etag": "~1.8.1", 574 | "fresh": "0.5.2", 575 | "http-errors": "~1.7.2", 576 | "mime": "1.6.0", 577 | "ms": "2.1.1", 578 | "on-finished": "~2.3.0", 579 | "range-parser": "~1.2.1", 580 | "statuses": "~1.5.0" 581 | }, 582 | "dependencies": { 583 | "ms": { 584 | "version": "2.1.1", 585 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 586 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 587 | } 588 | } 589 | }, 590 | "serve-static": { 591 | "version": "1.14.1", 592 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 593 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 594 | "requires": { 595 | "encodeurl": "~1.0.2", 596 | "escape-html": "~1.0.3", 597 | "parseurl": "~1.3.3", 598 | "send": "0.17.1" 599 | } 600 | }, 601 | "setprototypeof": { 602 | "version": "1.1.1", 603 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 604 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 605 | }, 606 | "statuses": { 607 | "version": "1.5.0", 608 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 609 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 610 | }, 611 | "tlds": { 612 | "version": "1.217.0", 613 | "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.217.0.tgz", 614 | "integrity": "sha512-iRVizGqUFSBRwScghTSJyRkkEXqLAO17nFwlVcmsNHPDdpE+owH91wDUmZXZfJ4UdBYuVSm7kyAXZo0c4X7GFQ==" 615 | }, 616 | "toidentifier": { 617 | "version": "1.0.0", 618 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 619 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 620 | }, 621 | "type-is": { 622 | "version": "1.6.18", 623 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 624 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 625 | "requires": { 626 | "media-typer": "0.3.0", 627 | "mime-types": "~2.1.24" 628 | } 629 | }, 630 | "uc.micro": { 631 | "version": "1.0.6", 632 | "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", 633 | "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" 634 | }, 635 | "uid-safe": { 636 | "version": "2.1.5", 637 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", 638 | "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", 639 | "requires": { 640 | "random-bytes": "~1.0.0" 641 | } 642 | }, 643 | "unpipe": { 644 | "version": "1.0.0", 645 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 646 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 647 | }, 648 | "utils-merge": { 649 | "version": "1.0.1", 650 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 651 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 652 | }, 653 | "vary": { 654 | "version": "1.1.2", 655 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 656 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 657 | } 658 | } 659 | } 660 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mlmmj-simple-web-interface", 3 | "version": "2.0.0", 4 | "description": "A very simple web frontend in node for mlmmj (http://mlmmj.org)", 5 | "main": "index.js", 6 | "dependencies": { 7 | "body-parser": "^1.19.0", 8 | "connect-flash": "^0.1.1", 9 | "consolidate": "0.14.0", 10 | "express": "^4.17.1", 11 | "express-session": "^1.17.1", 12 | "mailparser": "^3.1.0", 13 | "nunjucks": "^3.2.3", 14 | "passport": "^0.4.1", 15 | "passport-local": "^1.0.0" 16 | }, 17 | "scripts": { 18 | "start": "node index.js" 19 | }, 20 | "author": "tchapi", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /public/css/screen.css: -------------------------------------------------------------------------------- 1 | html { 2 | position: relative; 3 | min-height: 100%; 4 | } 5 | body { 6 | /* For the navbar height */ 7 | padding-top: 70px; 8 | padding-bottom: 50px; 9 | /* Margin bottom by footer height */ 10 | margin-bottom: 60px; 11 | } 12 | 13 | /* reduce margin under breadcrumb */ 14 | .breadcrumb { 15 | margin-bottom: 0; 16 | } 17 | 18 | /* Give some headroom to the intro

*/ 19 | p.intro { 20 | margin-top: 20px; 21 | } 22 | 23 | /* Actions under each column */ 24 | .actions { 25 | margin-top: 20px; 26 | padding-top: 20px; 27 | border-top: 1px solid #CCC; 28 | } 29 | 30 | /* Border below navbar */ 31 | .navbar { 32 | border-bottom: 3px solid #337ab7; 33 | } 34 | 35 | /* Little (i) help buttons */ 36 | a.help { 37 | text-decoration: none; 38 | cursor: pointer; 39 | margin-left: 5px; 40 | font-weight: normal; 41 | } 42 | a.help:hover { 43 | color: black; 44 | } 45 | 46 | /* Mail headers on archive page */ 47 | .mailheaders { 48 | list-style-type: none; 49 | padding-left: 10px; 50 | border-left: 2px solid #AAA; 51 | } 52 | 53 | /* Separate attachments */ 54 | .attachments { 55 | border-top: 1px solid #CCC; 56 | padding-top: 10px; 57 | margin-top: 20px; 58 | } 59 | 60 | /* Popovers */ 61 | .popover { 62 | width: 200px; 63 | } 64 | 65 | /* Some space over the login row */ 66 | .login { 67 | margin-top: 20px; 68 | } 69 | 70 | /* Sticky footer */ 71 | .footer { 72 | position: absolute; 73 | bottom: 0; 74 | width: 100%; 75 | /* fixed height of the footer here */ 76 | height: 60px; 77 | background-color: #f5f5f5; 78 | } 79 | .container .text-muted { 80 | margin: 20px 0; 81 | } 82 | .footer > .container { 83 | padding-right: 15px; 84 | padding-left: 15px; 85 | } -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.4.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 Twitter, Inc. 4 | * Licensed under the MIT license 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");!function(t){"use strict";var e=jQuery.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||3this.$items.length-1||t<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){e.to(t)}):i==t?this.pause().cycle():this.slide(idocument.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&t?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!t?this.scrollbarWidth:""})},s.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},s.prototype.checkScrollbar=function(){var t=window.innerWidth;if(!t){var e=document.documentElement.getBoundingClientRect();t=e.right-Math.abs(e.left)}this.bodyIsOverflowing=document.body.clientWidth

',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0},sanitize:!0,sanitizeFn:null,whiteList:t},m.prototype.init=function(t,e,i){if(this.enabled=!0,this.type=t,this.$element=g(e),this.options=this.getOptions(i),this.$viewport=this.options.viewport&&g(document).find(g.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var o=this.options.trigger.split(" "),n=o.length;n--;){var s=o[n];if("click"==s)this.$element.on("click."+this.type,this.options.selector,g.proxy(this.toggle,this));else if("manual"!=s){var a="hover"==s?"mouseenter":"focusin",r="hover"==s?"mouseleave":"focusout";this.$element.on(a+"."+this.type,this.options.selector,g.proxy(this.enter,this)),this.$element.on(r+"."+this.type,this.options.selector,g.proxy(this.leave,this))}}this.options.selector?this._options=g.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},m.prototype.getDefaults=function(){return m.DEFAULTS},m.prototype.getOptions=function(t){var e=this.$element.data();for(var i in e)e.hasOwnProperty(i)&&-1!==g.inArray(i,o)&&delete e[i];return(t=g.extend({},this.getDefaults(),e,t)).delay&&"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),t.sanitize&&(t.template=n(t.template,t.whiteList,t.sanitizeFn)),t},m.prototype.getDelegateOptions=function(){var i={},o=this.getDefaults();return this._options&&g.each(this._options,function(t,e){o[t]!=e&&(i[t]=e)}),i},m.prototype.enter=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusin"==t.type?"focus":"hover"]=!0),e.tip().hasClass("in")||"in"==e.hoverState)e.hoverState="in";else{if(clearTimeout(e.timeout),e.hoverState="in",!e.options.delay||!e.options.delay.show)return e.show();e.timeout=setTimeout(function(){"in"==e.hoverState&&e.show()},e.options.delay.show)}},m.prototype.isInStateTrue=function(){for(var t in this.inState)if(this.inState[t])return!0;return!1},m.prototype.leave=function(t){var e=t instanceof this.constructor?t:g(t.currentTarget).data("bs."+this.type);if(e||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e)),t instanceof g.Event&&(e.inState["focusout"==t.type?"focus":"hover"]=!1),!e.isInStateTrue()){if(clearTimeout(e.timeout),e.hoverState="out",!e.options.delay||!e.options.delay.hide)return e.hide();e.timeout=setTimeout(function(){"out"==e.hoverState&&e.hide()},e.options.delay.hide)}},m.prototype.show=function(){var t=g.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(t);var e=g.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(t.isDefaultPrevented()||!e)return;var i=this,o=this.tip(),n=this.getUID(this.type);this.setContent(),o.attr("id",n),this.$element.attr("aria-describedby",n),this.options.animation&&o.addClass("fade");var s="function"==typeof this.options.placement?this.options.placement.call(this,o[0],this.$element[0]):this.options.placement,a=/\s?auto?\s?/i,r=a.test(s);r&&(s=s.replace(a,"")||"top"),o.detach().css({top:0,left:0,display:"block"}).addClass(s).data("bs."+this.type,this),this.options.container?o.appendTo(g(document).find(this.options.container)):o.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var l=this.getPosition(),h=o[0].offsetWidth,d=o[0].offsetHeight;if(r){var p=s,c=this.getPosition(this.$viewport);s="bottom"==s&&l.bottom+d>c.bottom?"top":"top"==s&&l.top-dc.width?"left":"left"==s&&l.left-ha.top+a.height&&(n.top=a.top+a.height-l)}else{var h=e.left-s,d=e.left+s+i;ha.right&&(n.left=a.left+a.width-d)}return n},m.prototype.getTitle=function(){var t=this.$element,e=this.options;return t.attr("data-original-title")||("function"==typeof e.title?e.title.call(t[0]):e.title)},m.prototype.getUID=function(t){for(;t+=~~(1e6*Math.random()),document.getElementById(t););return t},m.prototype.tip=function(){if(!this.$tip&&(this.$tip=g(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},m.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},m.prototype.enable=function(){this.enabled=!0},m.prototype.disable=function(){this.enabled=!1},m.prototype.toggleEnabled=function(){this.enabled=!this.enabled},m.prototype.toggle=function(t){var e=this;t&&((e=g(t.currentTarget).data("bs."+this.type))||(e=new this.constructor(t.currentTarget,this.getDelegateOptions()),g(t.currentTarget).data("bs."+this.type,e))),t?(e.inState.click=!e.inState.click,e.isInStateTrue()?e.enter(e):e.leave(e)):e.tip().hasClass("in")?e.leave(e):e.enter(e)},m.prototype.destroy=function(){var t=this;clearTimeout(this.timeout),this.hide(function(){t.$element.off("."+t.type).removeData("bs."+t.type),t.$tip&&t.$tip.detach(),t.$tip=null,t.$arrow=null,t.$viewport=null,t.$element=null})},m.prototype.sanitizeHtml=function(t){return n(t,this.options.whiteList,this.options.sanitizeFn)};var e=g.fn.tooltip;g.fn.tooltip=function i(o){return this.each(function(){var t=g(this),e=t.data("bs.tooltip"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.tooltip",e=new m(this,i)),"string"==typeof o&&e[o]())})},g.fn.tooltip.Constructor=m,g.fn.tooltip.noConflict=function(){return g.fn.tooltip=e,this}}(jQuery),function(n){"use strict";var s=function(t,e){this.init("popover",t,e)};if(!n.fn.tooltip)throw new Error("Popover requires tooltip.js");s.VERSION="3.4.1",s.DEFAULTS=n.extend({},n.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),((s.prototype=n.extend({},n.fn.tooltip.Constructor.prototype)).constructor=s).prototype.getDefaults=function(){return s.DEFAULTS},s.prototype.setContent=function(){var t=this.tip(),e=this.getTitle(),i=this.getContent();if(this.options.html){var o=typeof i;this.options.sanitize&&(e=this.sanitizeHtml(e),"string"===o&&(i=this.sanitizeHtml(i))),t.find(".popover-title").html(e),t.find(".popover-content").children().detach().end()["string"===o?"html":"append"](i)}else t.find(".popover-title").text(e),t.find(".popover-content").children().detach().end().text(i);t.removeClass("fade top bottom left right in"),t.find(".popover-title").html()||t.find(".popover-title").hide()},s.prototype.hasContent=function(){return this.getTitle()||this.getContent()},s.prototype.getContent=function(){var t=this.$element,e=this.options;return t.attr("data-content")||("function"==typeof e.content?e.content.call(t[0]):e.content)},s.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var t=n.fn.popover;n.fn.popover=function e(o){return this.each(function(){var t=n(this),e=t.data("bs.popover"),i="object"==typeof o&&o;!e&&/destroy|hide/.test(o)||(e||t.data("bs.popover",e=new s(this,i)),"string"==typeof o&&e[o]())})},n.fn.popover.Constructor=s,n.fn.popover.noConflict=function(){return n.fn.popover=t,this}}(jQuery),function(s){"use strict";function n(t,e){this.$body=s(document.body),this.$scrollElement=s(t).is(document.body)?s(window):s(t),this.options=s.extend({},n.DEFAULTS,e),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",s.proxy(this.process,this)),this.refresh(),this.process()}function e(o){return this.each(function(){var t=s(this),e=t.data("bs.scrollspy"),i="object"==typeof o&&o;e||t.data("bs.scrollspy",e=new n(this,i)),"string"==typeof o&&e[o]()})}n.VERSION="3.4.1",n.DEFAULTS={offset:10},n.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},n.prototype.refresh=function(){var t=this,o="offset",n=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),s.isWindow(this.$scrollElement[0])||(o="position",n=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var t=s(this),e=t.data("target")||t.attr("href"),i=/^#./.test(e)&&s(e);return i&&i.length&&i.is(":visible")&&[[i[o]().top+n,e]]||null}).sort(function(t,e){return t[0]-e[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},n.prototype.process=function(){var t,e=this.$scrollElement.scrollTop()+this.options.offset,i=this.getScrollHeight(),o=this.options.offset+i-this.$scrollElement.height(),n=this.offsets,s=this.targets,a=this.activeTarget;if(this.scrollHeight!=i&&this.refresh(),o<=e)return a!=(t=s[s.length-1])&&this.activate(t);if(a&&e=n[t]&&(n[t+1]===undefined||e .active"),n=i&&r.support.transition&&(o.length&&o.hasClass("fade")||!!e.find("> .fade").length);function s(){o.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),t.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),n?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu").length&&t.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),i&&i()}o.length&&n?o.one("bsTransitionEnd",s).emulateTransitionEnd(a.TRANSITION_DURATION):s(),o.removeClass("in")};var t=r.fn.tab;r.fn.tab=e,r.fn.tab.Constructor=a,r.fn.tab.noConflict=function(){return r.fn.tab=t,this};var i=function(t){t.preventDefault(),e.call(r(this),"show")};r(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',i).on("click.bs.tab.data-api",'[data-toggle="pill"]',i)}(jQuery),function(l){"use strict";var h=function(t,e){this.options=l.extend({},h.DEFAULTS,e);var i=this.options.target===h.DEFAULTS.target?l(this.options.target):l(document).find(this.options.target);this.$target=i.on("scroll.bs.affix.data-api",l.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",l.proxy(this.checkPositionWithEventLoop,this)),this.$element=l(t),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};function i(o){return this.each(function(){var t=l(this),e=t.data("bs.affix"),i="object"==typeof o&&o;e||t.data("bs.affix",e=new h(this,i)),"string"==typeof o&&e[o]()})}h.VERSION="3.4.1",h.RESET="affix affix-top affix-bottom",h.DEFAULTS={offset:0,target:window},h.prototype.getState=function(t,e,i,o){var n=this.$target.scrollTop(),s=this.$element.offset(),a=this.$target.height();if(null!=i&&"top"==this.affixed)return n+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 element */ 50 | var status = $(this).siblings('.status')[0] 51 | $(status).show() 52 | status.innerHTML = 'Saving ...' 53 | 54 | item = $(this).attr('data-key') 55 | data = (new get())[item]() 56 | 57 | $.post(this.href + '/' + item, data, function( data ) { 58 | if (data == 1) { 59 | console.log("saved") 60 | status.innerHTML = 'Saved.' 61 | $(status).fadeOut(3000) 62 | } else { 63 | console.log("error") 64 | status.innerHTML = 'Error.' 65 | } 66 | }.bind(this)) 67 | 68 | return false 69 | }) 70 | 71 | /* 72 | This function is called when users add an element from a group of elements of the group. 73 | This applies to subscribers, for instance. 74 | The clicked link contains a data-key and data-element attribute; The data-key 75 | indicates the type of element we are adding, and the data-element is the element we want to add. 76 | */ 77 | $('.add').on('click', function() { 78 | item = $(this).attr('data-key') 79 | group = $(this).attr('data-group') 80 | value = $("input[data-key=" + item + "]").val() 81 | data = { 'element' : value } 82 | 83 | if(value != "") { 84 | $.post(this.href + '/' + item, data, (function( data ) { 85 | if (data == 1) { 86 | console.log("added") 87 | $("#" + item).append('
  • ' + value + ' Remove
  • ') 88 | $(this).val("") // Empty field 89 | } else { 90 | console.log("error") 91 | } 92 | }).bind(this)) 93 | } 94 | 95 | return false 96 | }) 97 | 98 | /* Catch a Enter keypress in the email field */ 99 | if (document.getElementById('email')) { 100 | document.getElementById('email').onkeypress = function(e){ 101 | if (!e) e = window.event; 102 | var keyCode = e.keyCode || e.which; 103 | if (keyCode === 13){ 104 | // Enter pressed 105 | $('.add').trigger('click') 106 | e.target.value = "" // Empty field 107 | return false 108 | } 109 | } 110 | } 111 | 112 | /* 113 | This function is called when users remove an element from a group of elements of the group. 114 | This applies to subscribers, for instance. 115 | The clicked link contains a data-key and data-element attribute; The data-key 116 | indicates the type of element we are removing, and the data-element is the element we are removing. 117 | */ 118 | $(document).on('click', '.remove', function() { 119 | item = $(this).attr('data-key') 120 | data = { 'element' : $(this).attr('data-element') } 121 | 122 | $.post(this.href + '/' + item, data, (function( data ) { 123 | if (data == 1) { 124 | console.log("removed") 125 | $(this).parent().remove() 126 | } else { 127 | console.log("error") 128 | } 129 | }).bind(this)) 130 | 131 | return false 132 | }) 133 | 134 | }) 135 | -------------------------------------------------------------------------------- /screenshots/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/mlmmj-simple-web-interface/84fb14e88095c2281d7a876dd480ff0205fd8f79/screenshots/image1.png -------------------------------------------------------------------------------- /screenshots/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchapi/mlmmj-simple-web-interface/84fb14e88095c2281d7a876dd480ff0205fd8f79/screenshots/image2.png -------------------------------------------------------------------------------- /services/ConfigParser.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | ConfigParser = function(options) { 4 | 5 | try { 6 | 7 | // Read config file 8 | var data = fs.readFileSync('config.json') 9 | this.config = JSON.parse(data) 10 | 11 | } catch (err) { 12 | 13 | throw err 14 | 15 | } 16 | 17 | } 18 | 19 | var p = ConfigParser.prototype 20 | 21 | p.get = function(key) { 22 | // error catching ? 23 | return this.config[key] 24 | } 25 | 26 | module.exports = ConfigParser 27 | -------------------------------------------------------------------------------- /services/MlmmjWrapper.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | // Add mail parser module 4 | const simpleParser = require('mailparser').simpleParser; 5 | 6 | /* 7 | 8 | Mlmmj standard folders 9 | 10 | */ 11 | var control_folder = "control" 12 | var archive_folder = "archive" 13 | var subscribers_master_folder = "subscribers.d" // One file per letter 14 | var templates_folder = "text" 15 | 16 | /* 17 | 18 | These are the boolean flags you can set per mailing list, as per http://mlmmj.org/docs/tunables/ 19 | 20 | */ 21 | var available_flags = { 22 | "closedlist" : { 23 | name: "Closed list", 24 | description: "Is the list is open or closed. If it's closed subscription and unsubscription via mail is disabled." 25 | }, 26 | "closedlistsub" : { 27 | name: "Closed for subscription", 28 | description: "Closed for subscription. Unsubscription is possible." 29 | }, 30 | "moderated" : { 31 | name: "Moderated list", 32 | description: "If this file is present, the emailaddresses in the file listdir/control/moderators will act as moderators for the list." 33 | }, 34 | "submod" : { 35 | name: "Moderated subscription", 36 | description: "If this file is present, subscription will be moderated by owner(s). If there are emailaddresses in this file, then these will be used instead of owner." 37 | }, 38 | "tocc" : { 39 | name: "To/Cc optional", 40 | description: "If this file is present, the list address does not have to be in the To: or Cc: header of the email to the list." 41 | }, 42 | "subonlypost" : { 43 | name: "Closed posting", 44 | description: " When this file is present, only people who are subscribed to the list, are allowed to post to it. The check is made against the \"From:\" header." 45 | }, 46 | "modonlypost" : { 47 | name: "Moderator-only posting", 48 | description: " When this file is present, only people who are moderators of the list, are allowed to post to it. The check is made against the \"From:\" header." 49 | }, 50 | "modnonsubposts" : { 51 | name: "Moderated posts", 52 | description: "When this file is present, all postings from people who are not subscribed to the list will be moderated." 53 | }, 54 | "addtohdr" : { 55 | name: "Add To: header", 56 | description: "When this file is present, a To: header including the recipients emailaddress will be added to outgoing mail. Recommended usage is to remove existing To: headers with delheaders (see above) first." 57 | }, 58 | "notifysub" : { 59 | name: "Notify of subscription", 60 | description: "If this file is present, the owner(s) will get a mail with the address of someone sub/unsubscribing to a mailinglist." 61 | }, 62 | "notifymod" : { 63 | name: "Notify of moderation", 64 | description: "If this file is present, the poster (based on the envelope from) will get a mail when their post is being moderated." 65 | }, 66 | "noarchive" : { 67 | name: "No archive", 68 | description: "If this file exists, the mail won't be saved in the archive but simply deleted." 69 | }, 70 | "nosubconfirm" : { 71 | name: "No subscription confirmation", 72 | description: "If this file exists, no mail confirmation is needed to subscribe to the list. This should in principle never ever be used, but there is times on local lists etc. where this is useful. HANDLE WITH CARE!" 73 | }, 74 | "noget" : { 75 | name: "No mail post retrieval", 76 | description: "If this file exists, then retrieving old posts with +get-N is disabled" 77 | }, 78 | "subonlyget" : { 79 | name: "Closed retrieval", 80 | description: "If this file exists, then retrieving old posts with +get-N is only possible for subscribers. The above mentioned 'noget' have precedence." 81 | }, 82 | "notoccdenymails" : { 83 | name: "Do not notify of rejects", 84 | description: "These switches turns off whether mlmmj sends out notification about postings being denied due to the listaddress not being in To: or Cc: (see 'tocc'), when it was rejected due to an access rule (see 'access') or whether it's a subscribers only posting list (see 'subonlypost')." 85 | }, 86 | "noaccessdenymails" : { 87 | name: "Do not notify of access", 88 | description: "These switches turns off whether mlmmj sends out notification about postings being denied due to the listaddress not being in To: or Cc: (see 'tocc'), when it was rejected due to an access rule (see 'access') or whether it's a subscribers only posting list (see 'subonlypost')." 89 | }, 90 | "nosubonlydenymails" : { 91 | name: "Do not notify of closed list", 92 | description: "These switches turns off whether mlmmj sends out notification about postings being denied due to the listaddress not being in To: or Cc: (see 'tocc'), when it was rejected due to an access rule (see 'access') or whether it's a subscribers only posting list (see 'subonlypost')." 93 | }, 94 | "nosubmodmails" : { 95 | name: "Do not notify of subs moderation", 96 | description: "This switch turns off whether mlmmj sends out notification about subscription being moderated to the person requesting subscription (see 'submod')." 97 | }, 98 | "nodigesttext" : { 99 | name: "No digest text", 100 | description: "If this file exists, digest mails won't have a text part with a thread summary." 101 | }, 102 | "nodigestsub" : { 103 | name: "No digest subscription", 104 | description: "If this file exists, subscription to the digest version of the mailinglist will be denied. (Useful if you don't want to allow digests and notify users about it)." 105 | }, 106 | "nonomailsub" : { 107 | name: "No no-mail subscription", 108 | description: "If this file exists, subscription to the nomail version of the mailinglist will be denied. (Useful if you don't want to allow nomail and notify users about it)." 109 | }, 110 | "nomaxmailsizedenymails" : { 111 | name: "Do not notify of max size rejects", 112 | description: "If this is set, no reject notifications caused by violation of maxmailsize will be sent." 113 | }, 114 | "nolistsubsemail" : { 115 | name: "No +list functionality", 116 | description: "If this is set, the LISTNAME+list@ functionality for requesting an email with the subscribers for owner is disabled." 117 | }, 118 | "ifmodsendonlymodmoderate" : { 119 | name: "No replication of moderation mails", 120 | description: "If this file is present, then mlmmj in case of moderation checks the envelope from, to see if the sender is a moderator, and in that case only send the moderation mails to that address. In practice this means that a moderator sending mail to the list won't bother all the other moderators with his mail." 121 | }, 122 | "notmetoo" : { 123 | name: "Do not receive own posts", 124 | description: "If this file is present, mlmmj attempts to exclude the sender of a post from the distribution list for that post so people don't receive copies of their own posts." 125 | } 126 | } 127 | 128 | /* 129 | 130 | These are the editable files (normal, list, or text) you can set per mailing list, as per http://mlmmj.org/docs/tunables/ 131 | 132 | */ 133 | var available_lists = { 134 | "listaddress" : { 135 | name: "List addresses", 136 | default: "", 137 | description: "This file contains all addresses which mlmmj sees as listaddresses (see tocc below). The first one is the one used as the primary one, when mlmmj sends out mail." 138 | }, 139 | "owner" : { 140 | name: "List owner's email", 141 | default: "", 142 | description: "The emailaddresses in this file (1 pr. line) will get mails to listname+owner@listdomain.tld" 143 | }, 144 | "customheaders" : { 145 | name: "Custom headers", 146 | default: "", 147 | description: "These headers are added to every mail coming through. This is the place you want to add Reply-To: header in case you want such." 148 | }, 149 | "delheaders" : { 150 | name: "Deleted headers", 151 | default: "", 152 | description: "In this file is specified *ONE* header token to match per line. If the file consists of: Received: \n\r Message-ID: \n\r Then all occurences of these headers in incoming list mail will be deleted. \"From \" and \"Return-Path:\" are deleted no matter what." 153 | }, 154 | "access" : { 155 | name: "Access headers", 156 | default: "", 157 | description: "If this file exists, all headers of a post to the list is matched against the rules. The first rule to match wins." 158 | } 159 | } 160 | 161 | var available_texts = { 162 | "prefix" : { 163 | name: "Prefix Text", 164 | default: "", 165 | description: "The prefix for the Subject: line of mails to the list. This will alter the Subject: line, and add a prefix if it's not present elsewhere." 166 | }, 167 | "footer" : { 168 | name: "Footer text", 169 | default: "", 170 | description: "The content of this file is appended to mail sent to the list." 171 | } 172 | } 173 | 174 | var available_values = { 175 | "memorymailsize" : { 176 | name: "Memory mail size", 177 | default: "16384", 178 | description: "Here is specified in bytes how big a mail can be and still be prepared for sending in memory. It's greatly reducing the amount of write system calls to prepare it in memory before sending it, but can also lead to denial of service attacks. Default is 16k (16384 bytes)." 179 | }, 180 | "relayhost" : { 181 | name: "Relay host", 182 | default: "127.0.0.1", 183 | description: "The host specified (IP address or hostname, both works) in this file will be used for relaying the mail sent to the list. Defaults to 127.0.0.1." 184 | }, 185 | "digestinterval" : { 186 | name: "Digest interval", 187 | default: "604800", 188 | description: "This file specifies how many seconds will pass before the next digest is sent. Defaults to 604800 seconds, which is 7 days." 189 | }, 190 | "digestmaxmails" : { 191 | name: "Digest emails", 192 | default: "50", 193 | description: "This file specifies how many mails can accumulate before digest sending is triggered. Defaults to 50 mails, meaning that if 50 mails arrive to the list before digestinterval have passed, the digest is delivered." 194 | }, 195 | "bouncelife" : { 196 | name: "Bounce life time", 197 | default: "432000", 198 | description: "Here is specified for how long time in seconds an address can bounce before it's unsubscribed. Defaults to 432000 seconds, which is 5 days." 199 | }, 200 | "verp" : { 201 | name: "Verp control", 202 | default: "", 203 | description: "Control how Mlmmj does VERP (variable envelope return path). If this tunable does not exist, Mlmmj will send a message to the SMTP server for each recipient, with an appropriate envelope return path, i.e. it will handle VERP itself. If the tunable does exist, Mlmmj will instead divide the recipients into groups (the maximum number of recipients in a group can be controlled by the maxverprecips tunable) and send one message to the SMTP server per group. The content of this tunable allows VERP to be handled by the SMTP server. If the tunable contains \"postfix\", Mlmmj will make Postfix use VERP by adding XVERP=-= to the MAIL FROM: line. If it contains something else, that text will be appended to the MAIL FROM: line. If it contains nothing, VERP will effectively be disabled, as neither Mlmmj nor the SMTP server will do it." 204 | }, 205 | "maxverprecips" : { 206 | name: "Max recipients", 207 | default: "100", 208 | description: "How many recipients per mail delivered to the SMTP server. Defaults to 100." 209 | }, 210 | "smtpport" : { 211 | name: "SMTP port", 212 | default: "25", 213 | description: "In this file a port other than port 25 for connecting to the relayhost can be specified." 214 | }, 215 | "delimiter" : { 216 | name: "Delimiter", 217 | default: "+", 218 | description: "This specifies what to use as recipient delimiter for the list. Default is '+'." 219 | }, 220 | "maxmailsize" : { 221 | name: "Max mail size", 222 | default: "", 223 | description: "With this option the maximal allowed size of incoming mails can be specified." 224 | }, 225 | "staticbounceaddr" : { 226 | name: "Bounce address", 227 | default: "", 228 | description: "If this is set to something@example.org, the bounce address (Return-Path:) will be fixed to something+listname-bounces-and-so-on@example.org in case you need to disable automatic bounce handling." 229 | } 230 | } 231 | 232 | MlmmjWrapper = function(path, name) { 233 | 234 | this.path = path + "/" + name 235 | 236 | // Empty control dict to hold flag values 237 | this.flags = {} 238 | this.lists = {} 239 | this.texts = {} 240 | this.values = {} 241 | 242 | // Check that group exist 243 | try { 244 | fs.openSync(this.path, 'r') 245 | } catch (err) { 246 | throw new Error("Group " + name + " doesn't exist") 247 | } 248 | 249 | // Check flags & stuff 250 | this.retrieveAll() 251 | 252 | return this 253 | 254 | } 255 | 256 | // "Static" 257 | MlmmjWrapper.listGroups = function(path) { 258 | 259 | try { 260 | return fs.readdirSync(path) 261 | } catch (err) { 262 | throw new Error("Error opening base folder " + path + ". Are you sure mlmmj is installed ?") 263 | } 264 | 265 | } 266 | 267 | MlmmjWrapper.getAllAvailables = function() { 268 | 269 | return { 270 | flags: available_flags, 271 | lists: available_lists, 272 | values: available_values, 273 | texts: available_texts 274 | } 275 | 276 | } 277 | 278 | var p = MlmmjWrapper.prototype 279 | 280 | p.retrieveAll = function() { 281 | this.retrieveFlags() 282 | this.retrieveTexts() 283 | this.retrieveLists() 284 | this.retrieveValues() 285 | this.retrieveSubscribers() 286 | } 287 | 288 | p.saveAll = function() { 289 | this.saveFlags() 290 | this.saveTexts() 291 | this.saveLists() 292 | this.saveValues() 293 | this.saveSubscribers() 294 | } 295 | 296 | 297 | /* FLAGS (boolean) */ 298 | p.retrieveFlags = function() { 299 | for (var flag in available_flags) { 300 | try { 301 | fs.openSync(this.path + "/" + control_folder + "/" + flag, 'r') 302 | } catch (err) { 303 | this.clearFlag(flag) 304 | continue 305 | } 306 | this.raiseFlag(flag) 307 | } 308 | } 309 | 310 | p.raiseFlag = function(name) { 311 | if (name in available_flags){ 312 | this.flags[name] = true 313 | } else { 314 | throw new Error("'" + name + "' is not a valid flag") 315 | } 316 | } 317 | 318 | p.setFlag = function(name, bool) { 319 | if (name in available_flags){ 320 | this.flags[name] = (bool==true) 321 | } else { 322 | throw new Error("'" + name + "' is not a valid flag") 323 | } 324 | } 325 | 326 | p.clearFlag = function(name) { 327 | if (name in available_flags){ 328 | this.flags[name] = false 329 | } else { 330 | throw new Error("'" + name + "' is not a valid flag") 331 | } 332 | } 333 | 334 | p.getFlag = function(name) { 335 | if (name in available_flags){ 336 | return (this.flags[name] === true) 337 | } else { 338 | throw new Error("'" + name + "' is not a valid flag") 339 | } 340 | } 341 | 342 | p.getFlags = function() { 343 | return this.flags 344 | } 345 | 346 | p.saveFlags = function() { 347 | for (var flag in available_flags) { 348 | if (this.getFlag(flag) == true) { 349 | fd = fs.openSync(this.path + "/" + control_folder + "/" + flag, 'w') 350 | fs.closeSync(fd) 351 | } else { 352 | try { 353 | fs.unlinkSync(this.path + "/" + control_folder + "/" + flag) 354 | } catch(err){} 355 | } 356 | } 357 | } 358 | 359 | /* TEXTS */ 360 | p.retrieveTexts = function() { 361 | for (var name in available_texts) { 362 | try { 363 | text = fs.readFileSync(this.path + "/" + control_folder + "/" + name, { "encoding": 'utf8'}) 364 | } catch (err) { 365 | this.clearText(name) 366 | continue 367 | } 368 | this.setText(name, text) 369 | } 370 | } 371 | 372 | p.setText = function(name, text) { 373 | if (name in available_texts){ 374 | this.texts[name] = text 375 | } else { 376 | throw new Error("'" + name + "' is not a valid text") 377 | } 378 | } 379 | 380 | p.clearText = function(name) { 381 | if (name in available_texts){ 382 | this.texts[name] = "" 383 | } else { 384 | throw new Error("'" + name + "' is not a valid text") 385 | } 386 | } 387 | 388 | p.getText = function(name) { 389 | if (name in available_texts){ 390 | return this.texts[name] 391 | } else { 392 | throw new Error("'" + name + "' is not a valid text") 393 | } 394 | } 395 | 396 | p.getTexts = function() { 397 | return this.texts 398 | } 399 | 400 | p.saveTexts = function() { 401 | var text = null 402 | for (var name in available_texts) { 403 | text = this.getText(name) 404 | if (text != "") { 405 | fd = fs.openSync(this.path + "/" + control_folder + "/" + name, 'w') 406 | fs.writeSync(fd, text) 407 | fs.closeSync(fd) 408 | } else { 409 | try { 410 | fs.unlinkSync(this.path + "/" + control_folder + "/" + name) 411 | } catch(err){} 412 | } 413 | } 414 | } 415 | 416 | 417 | /* VALUES */ 418 | p.retrieveValues = function() { 419 | for (var name in available_values) { 420 | try { 421 | value = fs.readFileSync(this.path + "/" + control_folder + "/" + name, { "encoding": 'utf8'}) 422 | } catch (err) { 423 | this.clearValue(name) 424 | continue 425 | } 426 | this.setValue(name, value) 427 | } 428 | } 429 | 430 | p.setValue = function(name, value) { 431 | if (name in available_values){ 432 | this.values[name] = value 433 | } else { 434 | throw new Error("'" + name + "' is not a valid value") 435 | } 436 | } 437 | 438 | p.clearValue = function(name) { 439 | if (name in available_values){ 440 | this.values[name] = "" 441 | } else { 442 | throw new Error("'" + name + "' is not a valid value") 443 | } 444 | } 445 | 446 | p.getValue = function(name) { 447 | if (name in available_values){ 448 | return this.values[name] 449 | } else { 450 | throw new Error("'" + name + "' is not a valid value") 451 | } 452 | } 453 | 454 | p.getValues = function() { 455 | return this.values 456 | } 457 | 458 | p.saveValues = function() { 459 | var val = null 460 | for (var name in available_values) { 461 | val = this.getValue(name) 462 | if (val != "") { 463 | fd = fs.openSync(this.path + "/" + control_folder + "/" + name, 'w') 464 | fs.writeSync(fd, val) 465 | fs.closeSync(fd) 466 | } else { 467 | try { 468 | fs.unlinkSync(this.path + "/" + control_folder + "/" + name) 469 | } catch(err){} 470 | } 471 | 472 | } 473 | } 474 | 475 | /* LISTS */ 476 | p.retrieveLists = function() { 477 | for (var name in available_lists) { 478 | try { 479 | l = fs.readFileSync(this.path + "/" + control_folder + "/" + name, { "encoding": 'utf8'}) 480 | } catch (err) { 481 | this.clearList(name) 482 | continue 483 | } 484 | this.setList(name, l.split('\n\r')) 485 | } 486 | } 487 | 488 | p.setList = function(name, list) { 489 | if (name in available_lists){ 490 | this.lists[name] = list 491 | } else { 492 | throw new Error("'" + name + "' is not a valid list") 493 | } 494 | } 495 | 496 | p.clearList = function(name) { 497 | if (name in available_lists){ 498 | this.lists[name] = [] 499 | } else { 500 | throw new Error("'" + name + "' is not a valid list") 501 | } 502 | } 503 | 504 | p.getList = function(name) { 505 | if (name in available_lists){ 506 | return this.lists[name] 507 | } else { 508 | throw new Error("'" + name + "' is not a valid list") 509 | } 510 | } 511 | 512 | p.getLists = function() { 513 | return this.lists 514 | } 515 | 516 | p.saveLists = function() { 517 | var l = null 518 | for (var name in available_lists) { 519 | l = this.getList(name) 520 | if (l != "") { 521 | fd = fs.openSync(this.path + "/" + control_folder + "/" + name, 'w') 522 | fs.writeSync(fd,l.join('\n')) 523 | fs.closeSync(fd) 524 | } else { 525 | try { 526 | fs.unlinkSync(this.path + "/" + control_folder + "/" + name, 'w') 527 | } catch(err){} 528 | } 529 | } 530 | } 531 | 532 | /* Subscribers */ 533 | p.retrieveSubscribers = function() { 534 | this.subscribers = [] 535 | 536 | var path = this.path + "/" + subscribers_master_folder 537 | files = fs.readdirSync(path) 538 | 539 | for (var key in files) { 540 | subs = fs.readFileSync(path + "/" + files[key], { "encoding": 'utf8'}).split('\n') 541 | for (var item in subs) { 542 | if (subs[item] != "") { 543 | this.subscribers.push(subs[item]) 544 | } 545 | } 546 | } 547 | } 548 | 549 | p.removeSubscriber = function(element){ 550 | var index = this.subscribers.indexOf(element) 551 | this.subscribers.splice(index, 1) 552 | } 553 | 554 | p.addSubscriber = function(element){ 555 | this.subscribers.push(element) 556 | 557 | } 558 | 559 | p.getSubscribers = function() { 560 | return this.subscribers 561 | } 562 | 563 | p.saveSubscribers = function() { 564 | /* Empty directory first*/ 565 | var path = this.path + "/" + subscribers_master_folder 566 | files = fs.readdirSync(path) 567 | for (var key in files) { fs.unlinkSync(path + "/" + files[key]) } 568 | /* Then recreates on file per first email character */ 569 | for (var key in this.subscribers) { 570 | fd = fs.openSync(path + "/" + this.subscribers[key].charAt(0), 'a') 571 | fs.writeSync(fd, this.subscribers[key] + '\n') 572 | fs.closeSync(fd) 573 | } 574 | } 575 | 576 | p.listArchives = function(year, month) { 577 | var result = {} 578 | , dir = this.path + "/" + archive_folder 579 | try { 580 | var files = fs.readdirSync(dir) 581 | .map(function(v) { 582 | var timestamp = fs.statSync(dir + "/" + v).mtime 583 | 584 | if (month != null) { 585 | key = timestamp.getDate() 586 | if (timestamp.getYear() + 1900 == year && timestamp.getMonth() + 1 == month) { 587 | if ( !(result[key] instanceof Array)) { 588 | result[key] = [] 589 | } 590 | result[key].push(v); 591 | } 592 | } else if (year != null) { 593 | key = timestamp.getMonth() + 1 // Starts from 0 = Jan 594 | if (timestamp.getYear() + 1900 == year) { 595 | if ( !(result[key] instanceof Array)) { 596 | result[key] = [] 597 | } 598 | result[key].push(v); 599 | } 600 | } else { 601 | key = timestamp.getYear() + 1900 602 | if ( !(result[key] instanceof Array)) { 603 | result[key] = [] 604 | } 605 | result[key].push(v); 606 | } 607 | 608 | return null; 609 | }) 610 | } catch (err) { 611 | throw new Error("Error opening archives at " + this.path + "/" + archive_folder) 612 | } 613 | 614 | return result 615 | } 616 | 617 | 618 | p.getArchive = function(id, callback) { 619 | email = fs.readFileSync(this.path + "/" + archive_folder + "/" + id, { "encoding": 'utf8'}) 620 | simpleParser(email).then(callback) 621 | .catch(console.log) 622 | } 623 | 624 | module.exports = MlmmjWrapper 625 | -------------------------------------------------------------------------------- /views/archive.html: -------------------------------------------------------------------------------- 1 | {% extends "views/base.html" %} 2 | {% block breadcrumb %} 3 |
  • All mailing lists
  • 4 |
  • {{ name }}
  • 5 |
  • Archives
  • 6 |
  • Mail #{{ id }}
  • 7 | {% endblock %} 8 | 9 | {% block content %} 10 |

    Mail #{{ id }} : {{ mail.subject }}

    11 |

    12 | {% if id - 1 > 0 %} 13 | ← Prev 14 | {% endif %} 15 | Next → 16 |

    17 |
    18 |

    Received on {{ mail.date }}

    19 |
      20 |
    • from: {% for from_value in mail.from %}{{ from_value.address }}{% endfor %}
    • 21 |
    • to: {% for to_value in mail.to %}{{ to_value.address }}{% endfor %}
    • 22 | {% if mail.cc|length > 0 %} 23 |
    • cc: {% for cc_value in mail.cc %}{{ cc_value.address }}{% endfor %}
    • 24 | {% endif %} 25 |
    26 |
    27 | 28 | {% if mail.html %} 29 | {{ mail.html }} 30 | {% elif mail.text %} 31 | {{ mail.text }} 32 | {% else %} 33 | No content 34 | {% endif %} 35 | 36 |
    37 |

    Attachments : {{ mail.attachments|length }}

    38 |
      39 | {% for att in mail.attachments %} 40 |
    • {{ att.name }}
    • 41 | {% endfor %} 42 |
    43 |
    44 | 45 | {% endblock %} -------------------------------------------------------------------------------- /views/archives.html: -------------------------------------------------------------------------------- 1 | {% extends "views/base.html" %} 2 | {% block breadcrumb %} 3 |
  • All mailing lists
  • 4 |
  • {{ name }}
  • 5 | {% if year %} 6 |
  • Archives
  • 7 | {% if month %} 8 |
  • {{ year }}
  • 9 | {% if day %} 10 |
  • {{ month }}
  • 11 |
  • {{ day }}
  • 12 | {% else %} 13 |
  • {{ month }}
  • 14 | {% endif %} 15 | {% else %} 16 |
  • {{ year }}
  • 17 | {% endif %} 18 | {% else %} 19 |
  • Archives
  • 20 | {% endif %} 21 | {% endblock %} 22 | 23 | {% block content %} 24 |

    Archives

    25 | 26 |

    27 | This page lists all the archives of the mailing list. 28 |

    29 | 30 |
      31 | 32 | {% for key, mails in archives %} 33 | {% if day == null or key == day %} 34 |
    • 35 | {% if year %}{{ year }}{% else %}{{ key }}{% endif %}{% if month and year %}/{{ month }}/{{ key }}{% elif year and not month %}/{{ key }}{% endif %} 36 | : {{ mails|length }} mails 37 | (refine) : 38 | 43 |
    • 44 | {% endif %} 45 | {% endfor %} 46 | 47 |
    48 | {% endblock %} -------------------------------------------------------------------------------- /views/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ title }} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 26 | 27 |
    28 | 32 |
    33 | 34 |
    35 | {% block content %} 36 | {% endblock %} 37 |
    38 | 39 |
    40 |
    41 |

    mlmmj web interface made by tchapimlmmj, Mailing List Management Made Joyful, by Mads Martin Joergensen, Morten K. Poulsen & Ben Schmidt

    42 |
    43 |
    44 | 45 | {% block js %} 46 | 47 | 48 | 53 | {% endblock %} 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /views/control.html: -------------------------------------------------------------------------------- 1 | {% extends "views/base.html" %} 2 | 3 | {% block breadcrumb %} 4 |
  • All mailing lists
  • 5 |
  • {{ name }}
  • 6 |
  • Parameters
  • 7 | {% endblock %} 8 | 9 | {% block content %} 10 |

    Mailing list {{ name }} — Parameters

    11 | 12 |

    13 | From the documentation at ➡ http://mlmmj.org/docs/tunables/ : The following files can be used for changing the behaviour of a list. The 14 | filename is supposed to be below listdir/control. In the case it's a "boolean", 15 | the contents of a file does not matter, the mere presence of it, will set the 16 | variable to "true". If it's a "normal" file, the first line will be used as 17 | value, leaving line 2 and forward ready for commentary etc. If it's possible 18 | to specify several entries (one pr. line), it's marked "list". If the file's 19 | entire content is used as value, it's marked "text". 20 |

    21 | 22 |
    23 | 24 |
    25 |

    Control flags

    26 |
    27 | {% for key, flag in flags %} 28 |
    29 | 32 | 33 |
    34 | {% endfor %} 35 |
    36 |
    37 | 38 | Save 39 |
    40 |
    41 | 42 |
    43 |

    Values

    44 |
    45 | {% for key, value in values %} 46 |
    47 | 48 |
    49 | 50 |
    51 |
    52 | {% endfor %} 53 |
    54 |
    55 | 56 | Save 57 |
    58 |
    59 | 60 |
    61 |

    Texts

    62 |
    63 | {% for key, text in texts %} 64 |
    65 | 66 | 67 |
    68 | {% endfor %} 69 |
    70 |
    71 | 72 | Save 73 |
    74 |
    75 | 76 |
    77 |

    Lists

    78 |
    79 | {% for key, list in lists %} 80 |
    81 | 82 | 83 |
    84 | {% endfor %} 85 |
    86 |
    87 | 88 | Save 89 |
    90 |
    91 | 92 |
    93 | 94 | {% endblock %} 95 | 96 | {% block js %} 97 | {{ super() }} 98 | 99 | {% endblock %} 100 | -------------------------------------------------------------------------------- /views/group.html: -------------------------------------------------------------------------------- 1 | {% extends "views/base.html" %} 2 | 3 | {% block breadcrumb %} 4 |
  • All mailing lists
  • 5 |
  • {{ name }}
  • 6 | {% endblock %} 7 | 8 | {% block content %} 9 |

    Mailing list {{ name }} — Management

    10 | 11 |

    12 | For the mailing list, you can either change the tunables (parameters), add or remove subscribers and browse the mail archives 13 |

    14 | 15 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /views/list.html: -------------------------------------------------------------------------------- 1 | {% extends "views/base.html" %} 2 | {% block breadcrumb %} 3 |
  • All mailing lists
  • 4 | {% endblock %} 5 | 6 | {% block content %} 7 |

    Mailing lists

    8 | 9 |

    10 | This page lists the content of {{ directory }} that contains all the mailing lists managed by mlmmj. 11 |

    12 | 13 |
      14 | {% for group in groups %} 15 | 16 |
    • {{ group }}
    • 17 | 18 | {% endfor %} 19 |
    20 | {% endblock %} -------------------------------------------------------------------------------- /views/login.html: -------------------------------------------------------------------------------- 1 | {% extends "views/base.html" %} 2 | {% block breadcrumb %} 3 |
  • Please login to access the management pages
  • 4 | {% endblock %} 5 | 6 | {% block content %} 7 | 8 | 37 | 38 | {% endblock %} -------------------------------------------------------------------------------- /views/subscribers.html: -------------------------------------------------------------------------------- 1 | {% extends "views/base.html" %} 2 | 3 | {% block breadcrumb %} 4 |
  • All mailing lists
  • 5 |
  • {{ name }}
  • 6 |
  • Subscribers
  • 7 | {% endblock %} 8 | 9 | {% block content %} 10 |

    Mailing list {{ name }} — subscribers

    11 | 12 |

    13 | This page lists all the current subscribers to the list. 14 |

    15 | 16 |
    17 |
    18 |

    Suscribers list

    19 |
      20 | {% for subscriber in subscribers %} 21 |
    • {{ subscriber }} Remove
    • 22 | {% endfor %} 23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 30 | 31 |
    32 | Add a subscriber 33 |
    34 |
    35 |
    36 |
    37 | 38 | {% endblock %} 39 | 40 | {% block js %} 41 | {{ super() }} 42 | 43 | {% endblock %} 44 | --------------------------------------------------------------------------------