├── .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 | 
40 | 
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 |
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 |
14 |
15 |
18 | {% if hideLogout == true %}{% else %}
19 |
22 | {% endif %}
23 |
24 |
25 |
26 |
27 |
28 |
29 | {% block breadcrumb %}
30 | {% endblock %}
31 |
32 |
33 |
34 |
35 | {% block content %}
36 | {% endblock %}
37 |
38 |
39 |
44 |
45 | {% block js %}
46 |
47 |
48 |
53 | {% endblock %}
54 |
55 |
56 |