├── www
├── views
│ ├── error.ejs
│ ├── about.ejs
│ ├── mainmenu.ejs
│ ├── frontpage.ejs
│ ├── feed.ejs
│ ├── index.ejs
│ ├── mailbox.ejs
│ └── message.ejs
└── static
│ ├── style
│ ├── iframe.css
│ ├── plaintext.css
│ └── custom.css
│ └── bootstrap
│ ├── img
│ ├── glyphicons-halflings.png
│ └── glyphicons-halflings-white.png
│ ├── css
│ ├── bootstrap-responsive.min.css
│ └── bootstrap-responsive.css
│ └── js
│ ├── bootstrap.min.js
│ └── bootstrap.js
├── .gitignore
├── setup
├── disposable.monit
└── disposable
├── package.json
├── config
└── development.json
├── index.js
├── server.js
├── lib
├── web.js
├── smtp.js
├── pop3.js
├── routes.js
└── api.js
└── README.md
/www/views/error.ejs:
--------------------------------------------------------------------------------
1 |
Error
2 |
3 |
4 | <%= message %>
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | .node_env
4 | npm-debug.log
5 | config/production.json
6 |
--------------------------------------------------------------------------------
/www/static/style/iframe.css:
--------------------------------------------------------------------------------
1 | body{
2 | font-family: Helvetica, Arial, Sans-serif;
3 | color: #333;
4 | font-size: 13px;
5 | }
--------------------------------------------------------------------------------
/www/static/bootstrap/img/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andris9/disposable/master/www/static/bootstrap/img/glyphicons-halflings.png
--------------------------------------------------------------------------------
/www/static/bootstrap/img/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andris9/disposable/master/www/static/bootstrap/img/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/www/views/about.ejs:
--------------------------------------------------------------------------------
1 | About
2 |
3 |
4 | <%= title %> is free software licensed under extremely permissive MIT license.
5 |
--------------------------------------------------------------------------------
/www/views/mainmenu.ejs:
--------------------------------------------------------------------------------
1 | class="active"<%}%>>Home
2 | class="active"<%}%>>About
--------------------------------------------------------------------------------
/www/static/style/plaintext.css:
--------------------------------------------------------------------------------
1 | body{
2 | font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
3 | color: #333;
4 | font-size: 12px;
5 | }
--------------------------------------------------------------------------------
/setup/disposable.monit:
--------------------------------------------------------------------------------
1 | check process disposable with pidfile /var/run/disposable.pid
2 | start program = "/etc/init.d/disposable start"
3 | stop program = "/etc/init.d/disposable stop"
4 | if 5 restarts within 5 cycles then timeout
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "disposable",
3 | "private": true,
4 | "version": "0.1.5",
5 | "description": "Disposable inboxes",
6 | "main": "index.js",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "repository": "",
11 | "author": "Andris Reinman",
12 | "license": "BSD",
13 | "dependencies": {
14 | "express": "*",
15 | "ejs": "*",
16 | "moment": "*",
17 | "mongodb": "*",
18 | "simplesmtp": "*",
19 | "mailparser": "*",
20 | "mailpurify": "*",
21 | "pop3-n3": "*"
22 | },
23 | "scripts":{
24 | "start": "node index.js"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/config/development.json:
--------------------------------------------------------------------------------
1 | {
2 | "workerCount": 1,
3 |
4 | "loggerInterface": "dev",
5 | "hostname": "localhost",
6 | "title": "Disposable",
7 |
8 | "mailbox":{
9 | "maxMessages": 100,
10 | "dateFormat": "dddd, YYYY-MM-DD H:mm:ss"
11 | },
12 |
13 | "web":{
14 | "port": 80
15 | },
16 |
17 | "smtp":{
18 | "banner": "My Disposable E-mail service",
19 | "port": 25,
20 | "maxSize": 1048576
21 | },
22 |
23 | "pop3":{
24 | "port": 110
25 | },
26 |
27 | "mongo":{
28 | "host": "localhost",
29 | "name": "mailbox",
30 | "collection": "messages",
31 | "ttl": 604800
32 | }
33 | }
--------------------------------------------------------------------------------
/www/static/style/custom.css:
--------------------------------------------------------------------------------
1 | /* Footer
2 | -------------------------------------------------- */
3 |
4 | .footer {
5 | text-align: center;
6 | padding: 30px 0;
7 | margin-top: 70px;
8 | border-top: 1px solid #e5e5e5;
9 | background-color: #f5f5f5;
10 | }
11 | .footer p {
12 | margin-bottom: 0;
13 | color: #777;
14 | }
15 | .footer-links {
16 | margin: 10px 0;
17 | }
18 | .footer-links li {
19 | display: inline;
20 | padding: 0 2px;
21 | }
22 | .footer-links li:first-child {
23 | padding-left: 0;
24 | }
25 |
26 | /* Message
27 | -------------------------------------------------- */
28 |
29 | ul.message-tabs{
30 | margin-bottom: 0;
31 | }
32 |
33 | div.message-border{
34 | border-left: 1px solid #DDD;
35 | border-right: 1px solid #DDD;
36 | border-bottom: 1px solid #DDD;
37 | }
38 |
39 | div.message-padding{
40 | padding: 10px;
41 | background: white
42 | }
43 |
44 | iframe.seamless{
45 | background-color: transparent;
46 | border: 0px none transparent;
47 | padding: 0px;
48 | overflow: auto;
49 | width: 100%;
50 | height: 600px;
51 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var config = require("./config/" + (process.env.NODE_ENV || "development") + ".json"),
2 | cluster = require('cluster'),
3 | numCPUs = config.workerCount || require('os').cpus().length;
4 |
5 | if(cluster.isMaster){
6 |
7 | console.log("Starting Disposable server");
8 |
9 | // Fork workers.
10 | for (var i = 0; i < numCPUs; i++) {
11 | cluster.fork();
12 | }
13 |
14 | cluster.on('exit', function(worker, code, signal) {
15 | console.log('Worker ' + worker.process.pid + ' died, restarting');
16 | cluster.fork();
17 | });
18 |
19 | // Handle error conditions
20 | process.on("SIGTERM", function(){
21 | console.log("Exited on SIGTERM");
22 | process.exit(0);
23 | });
24 |
25 | process.on("SIGINT", function(){
26 | console.log("Exited on SIGINT");
27 | process.exit(0);
28 | });
29 |
30 | }else{
31 | console.log("Starting worker "+process.pid);
32 | require("./server");
33 | }
34 |
35 | process.on('uncaughtException', function(err) {
36 | console.log("uncaughtException");
37 | console.log(err.stack);
38 | process.exit(1);
39 | });
--------------------------------------------------------------------------------
/www/views/frontpage.ejs:
--------------------------------------------------------------------------------
1 |
5 |
6 | Free disposable mailboxes, hassle free!
7 |
8 | Whenever in the need of a temporary e-mail address, enter <your_desired_username>@<%= hostname %> as your e-mail address and check back here. If there's any mail sent to then you can easily check it with the form above.
9 |
10 | All received messages are stored up to 7 days and will be deleted after that.
11 |
12 | Well, what about privacy?
13 |
14 | There is none. Anyone can check any inbox at will. Use unguessable usernames, if you do not want someone else to see the e-mails sent to you and delete the messages after you've read them.
15 |
16 | So, what are the features?
17 |
18 | Every mailbox can be accessed through web, POP3, JSON and RSS. Original design of the e-mails and attachments are preserved (great for newsletters) and attachments can be downloaded.
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var api = require("./lib/api"),
2 | web = require("./lib/web"),
3 | smtp = require("./lib/smtp"),
4 | pop3 = require("./lib/pop3"),
5 | ready = 0;
6 |
7 | // Open DB connection and on success start listening to HTTP, POP3 and SMTP
8 | api.initDB(function(err){
9 | if(err){
10 | throw err;
11 | }
12 |
13 | web.listen(listener.bind(this, "Web"));
14 | smtp.listen(listener.bind(this, "SMTP"));
15 | pop3.listen(listener.bind(this, "POP3"));
16 | });
17 |
18 | /**
19 | * Handles port binding callback.
20 | *
21 | * @param {String} service Indicator of which service was binded
22 | * @param {Error} error Error object if binding failed
23 | */
24 | function listener(service, error){
25 | if(error){
26 | console.log("Starting " + service + " server failed for the following error:");
27 | console.log(error);
28 | return process.exit(1);
29 | }
30 | console.log(service + " server started successfully");
31 | ready++;
32 |
33 | // if all services are binded, release root privilieges
34 | if(ready == 3){
35 | ready++;
36 | console.log("All servers started, downgrading from root to nobody");
37 | process.setgid("nobody");
38 | process.setuid("nobody");
39 | }
40 | }
--------------------------------------------------------------------------------
/www/views/feed.ejs:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | feed at <%= title %>]]>
11 |
12 | /mailbox/<%= mailbox %>]]>
13 | - hassle free disposable mailboxes]]>
14 |
15 | http://<%= hostname %>/
16 | en
17 |
18 | <% docs.forEach(function(doc){ %>
19 |
20 | -
21 |
/message/<%= doc._id %>]]>
22 | ]]>
23 | /message/<%= doc._id %>]]>
24 | ]]>
25 | ]]>
26 | ]]>
27 |
28 |
29 | <% }) %>
30 |
31 |
32 |
--------------------------------------------------------------------------------
/lib/web.js:
--------------------------------------------------------------------------------
1 | var config = require("../config/" + (process.env.NODE_ENV || "development") + ".json"),
2 | pathlib = require("path"),
3 | express = require("express"),
4 | http = require("http"),
5 | app = express(),
6 | routes = require("./routes");
7 |
8 | // Express.js configuration
9 | app.configure(function(){
10 | // HTTP port to listen
11 | app.set("port", config.web.port);
12 |
13 | // Define path to EJS templates
14 | app.set("views", pathlib.join(__dirname, "..", "www", "views"));
15 |
16 | // Use EJS template engine
17 | app.set("view engine", "ejs");
18 |
19 | // Use gzip compression
20 | app.use(express.compress());
21 |
22 | // Parse POST requests
23 | app.use(express.bodyParser());
24 |
25 | // Use default Espress.js favicon
26 | app.use(express.favicon());
27 |
28 | // Log requests to console
29 | app.use(express.logger(config.loggerInterface));
30 |
31 | app.use(app.router);
32 |
33 | // Define static content path
34 | app.use(express["static"](pathlib.join(__dirname, "..", "www", "static")));
35 |
36 | //Show error traces
37 | app.use(express.errorHandler());
38 | });
39 |
40 | // Use routes from routes.js
41 | routes(app);
42 |
43 | /**
44 | * Starts listening HTTP port
45 | *
46 | * @param {Function} callback Callback function to run once the binding has been succeeded or failed
47 | */
48 | module.exports.listen = function(callback){
49 | app.listen(app.get("port"), callback);
50 | };
--------------------------------------------------------------------------------
/setup/disposable:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ### BEGIN INIT INFO
3 | # Provides: disposable_autostart
4 | # Required-Start: $local_fs $remote_fs $network $syslog $netdaemons
5 | # Required-Stop: $local_fs $remote_fs
6 | # Default-Start: 2 3 4 5
7 | # Default-Stop: 0 1 6
8 | # Short-Description: disposable
9 | # Description: disposable
10 | ### END INIT INFO
11 |
12 | PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
13 | NODE_PATH=/usr/local/lib/node_modules
14 | NODE=/usr/local/bin/node
15 | NAME=disposable
16 |
17 | SOURCE="${BASH_SOURCE[0]}"
18 | DIR="$( dirname "$SOURCE" )"
19 | while [ -h "$SOURCE" ]
20 | do
21 | SOURCE="$(readlink "$SOURCE")"
22 | [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
23 | DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
24 | done
25 | DIR="$( cd -P "$( dirname "$SOURCE" )" && cd .. && pwd )"
26 |
27 | if [ "$NODE_ENV" == "" ]; then
28 | NODE_ENV=`cat $DIR/.node_env 2>/dev/null`
29 | fi
30 |
31 | if [ "$NODE_ENV" == "" ]; then
32 | NODE_ENV=production
33 | fi
34 |
35 | test -x $NODE || exit 0
36 |
37 | function start_app {
38 | echo "start"
39 | NODE_ENV=$NODE_ENV nohup "$NODE" "$DIR/index.js" 1>>"/var/log/$NAME.log" 2>&1 &
40 | echo $! > "/var/run/$NAME.pid"
41 | }
42 |
43 | function stop_app {
44 | echo "stop"
45 | kill `cat /var/run/$NAME.pid`
46 | }
47 |
48 | case $1 in
49 | start)
50 | start_app ;;
51 | stop)
52 | stop_app ;;
53 | restart)
54 | stop_app
55 | start_app
56 | ;;
57 | *)
58 | echo "usage: $NAME {start|stop}" ;;
59 | esac
60 | exit 0
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # disposable
2 |
3 | Web app for creating your own hassle free disposable mailbox service like Mailinator or TrashMail.
4 |
5 | ## DEMO
6 |
7 | Demo app can be seen at [disposebox.com](http://disposebox.com)
8 |
9 | ## Setup DNS
10 |
11 | Before you can receive any e-mails you need to set up at least one MX record for your domain.
12 |
13 | For example if you want to receive e-mails for `usename@example.com` and the hostname of the actual server where the SMTP daemon is running is `smtp.example.com` then you need to set up the following MX record:
14 |
15 | * host: **example.com** (derives from username@ `example.com`, most DNS admin interfaces allow or require to use `@` as the host placeholder, some do not allow you to set this value by yourself at all)
16 | * priority: **10** (any positive number is ok - if multiple MX servers are listed, the one with the lower number is preferred when connecting)
17 | * mx/hostname/points to: **smtp.example.com** (hostname or IP where the SMTP daemon is running)
18 |
19 | You can check if the record is correct with the `dig` command - be patient though when checking, since DNS propagation usually takes some time.
20 |
21 | > dig MX example.com
22 | ...
23 | ;; ANSWER SECTION:
24 | example.com. 3600 IN MX 10 smtp.example.com.
25 |
26 | ## Installation
27 |
28 | ### Requiremenets
29 |
30 | * **Node.js** (min v0.8)
31 | * **MongoDB** (min. v2.2)
32 |
33 | ### Install
34 |
35 | cd /path/to/install
36 | git clone git://github.com/andris9/disposable.git
37 | cd disposable
38 | npm install
39 | cp config/development.json config/production.json
40 |
41 | Edit the values in `config/production.json` - you probably want to keep everything except `hostname` and `title` and probably `loggerInterface` (set to an empty string to get more conventional logging, see all available options for the logger [here](http://www.senchalabs.org/connect/logger.html)).
42 |
43 | ### Pre run
44 |
45 | Check that nothing is already using port 25. Usually there might me a sendmail daemon or such already installed. Try to connect to localhost port 25 and if you receive an answer uninstall or kill the task that is using this port.
46 |
47 | ### Run
48 |
49 | Run from the console
50 |
51 | sudo NODE_ENV=production node index.js
52 |
53 | Or alternatively add an init script (you can tail the log file from /var/log/disposable.log)
54 |
55 | cd /etc/init.d
56 | sudo ln -s /path/to/disposable/setup/disposable
57 | service disposable start
58 |
59 | **NB!** the app needs to be run as root - there are ports under 1000 to bind. Root privileges are released shortly after binding the ports though.
60 |
61 | You can also setup a monit script to ensure that the app keeps runnings
62 |
63 | cd /path/to/monit/conf.d/
64 | sudo ln -s /path/to/disposable/setup/disposable.monit
65 | sudo service monit restart
66 |
67 | To ensure that the app runs on reboot you can add it to startup list
68 |
69 | In CentOS
70 |
71 | sudo chkconfig disposable on
72 |
73 | In Ubuntu
74 |
75 | sudo update-rc.d disposable defaults
76 |
77 | ## License
78 |
79 | **MIT**
80 |
--------------------------------------------------------------------------------
/lib/smtp.js:
--------------------------------------------------------------------------------
1 | var config = require("../config/" + (process.env.NODE_ENV || "development") + ".json"),
2 | api = require("./api"),
3 | simplesmtp = require("simplesmtp"),
4 |
5 | // Define Simple SMTP server
6 | server = simplesmtp.createSimpleServer({
7 | name: config.hostname, // Hostname reported to the client
8 | SMTPBanner: config.smtp.banner, // Server greeting
9 | maxSize: config.smtp.maxSize, // Maximum allowed message size in bytes
10 | // (soft limit, reported to the client but not used in
11 | // any other way by the smtp server instance)
12 | ignoreTLS: true, // Do not require STARTTLS
13 | disableDNSValidation: true // do not validate sender DNS
14 | }, requestListener);
15 |
16 | /**
17 | * Starts listening SMTP port
18 | *
19 | * @param {Function} callback Callback function to run once the binding has been succeeded or failed
20 | */
21 | module.exports.listen = function(callback){
22 | server.listen(config.smtp.port, callback);
23 | };
24 |
25 | /**
26 | * SMTP session handler. Processes incoming message
27 | *
28 | * @param {Object} req SMTP request object
29 | */
30 | function requestListener(req){
31 | var messageBodyArr = [],
32 | messageBodyLength = 0,
33 | reject = false;
34 |
35 | // Keep buffering incoming data until maxSize length is reached
36 | req.on("data", function(chunk){
37 | if(chunk.length + messageBodyLength <= config.smtp.maxSize){
38 | messageBodyArr.push(chunk);
39 | messageBodyLength += chunk.length;
40 | }else{
41 | reject = true;
42 | }
43 | });
44 |
45 | req.on("end", function(){
46 | // if message reached maxSize, reject it
47 | if(reject){
48 | return req.reject("Message size larger than allowed " + config.smtp.maxSize + " bytes");
49 | }
50 |
51 | var rawMessage = new Buffer(Buffer.concat(messageBodyArr, messageBodyLength).toString("binary").replace(/^\.\./mg, "."), "binary"),
52 | idList = [],
53 |
54 | // store the received message for every recipient separately
55 | processRecipients = function(){
56 | if(!req.to.length){
57 | // in case of several recipients there should also be several message id values
58 | req.accept(idList.join(", "));
59 | return;
60 | }
61 | var recipient = req.to.shift();
62 | api.storeRawMessage(recipient, req.from, rawMessage, function(err, id){
63 | if(err){
64 | console.log("Error storing message for " + recipient);
65 | console.log(err);
66 | }
67 | if(id){
68 | idList.push(id);
69 | }
70 | process.nextTick(processRecipients);
71 | });
72 | };
73 |
74 | // store message for every recipient separately
75 | processRecipients();
76 | });
77 | }
--------------------------------------------------------------------------------
/www/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= title %><% if(pageTitle){%> » <%= pageTitle %><%}%>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
<%= title %>
39 |
40 |
41 | <% include mainmenu %>
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
<%= title %>
51 |
52 |
Welcome to <%= hostname %>, the home of hassle free disposable mailboxes
53 |
54 | <% if(!page || page == "/"){ %>
55 | <% include frontpage %>
56 | <% } %>
57 |
58 | <% if(page == "/error"){ %>
59 | <% include error %>
60 | <% } %>
61 |
62 | <% if(page == "/about"){ %>
63 | <% include about %>
64 | <% } %>
65 |
66 | <% if(page == "/mailbox"){ %>
67 | <% include mailbox %>
68 | <% } %>
69 |
70 | <% if(page == "/message"){ %>
71 | <% include message %>
72 | <% } %>
73 |
74 |
75 |
76 |
78 |
84 |
85 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/www/views/mailbox.ejs:
--------------------------------------------------------------------------------
1 |
2 | Home /
3 | <%= mailbox %>
4 |
5 |
6 |
7 |
8 |
9 |
<%= mailbox %>
10 |
11 | <% if(message == "deleted"){%>
12 |
13 | ×
14 | Message deleted
15 |
16 | <%}%>
17 |
18 | <% if(message == "uploaded"){%>
19 |
20 | ×
21 | Message uploaded
22 |
23 | <%}%>
24 |
25 | <% if(error){%>
26 |
27 | ×
28 | <%= error %>
29 |
30 | <%}%>
31 |
32 |
33 |
34 |
35 |
36 |
82 |
83 |
84 |
94 |
95 |
96 |
97 |
98 |
99 |
Access <%= mailbox %> :
100 |
101 |
1. JSON
102 |
JSON link
103 |
104 |
2. RSS
105 |
RSS link
106 |
107 |
3. POP3 desktop client
108 |
109 | Hostname: <%= hostname %>
110 | Port: <%= pop3port %>
111 | Username: <%= mailbox %>
112 | Password: <%= mailbox %>
113 | Secure: No
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/www/views/message.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | Home /
4 |
5 |
6 | <%= doc.envelope.to %> /
7 |
8 |
9 | <%= (doc.subject || "").length <= 40 ? doc.subject || "[untitled]" : doc.subject.substr(0,20) + "..." %>
10 |
11 |
12 |
13 |
14 |
15 | <%= doc.from && doc.from[0] && ((doc.from[0].name?doc.from[0].name+" ":"")+"<"+doc.from[0].address+">") || doc.envelope.from %>
16 |
17 | <% if(doc.replyTo && doc.replyTo.length){%>
18 | Reply-To:
19 | <% doc.replyTo.map(function(replyTo){%>
20 | <%= (replyTo.name?replyTo.name+" ":"") + "<" + replyTo.address + ">" %>
21 | <%}).join(", ")%>
22 |
23 | <% } %>
24 |
25 | <% if(!doc.to || !doc.to.length){%>
26 | To: <%= "<"+doc.envelope.to+">"%>
27 | <% } %>
28 |
29 | <% if(doc.to && doc.to.length){%>
30 | To:
31 | <% doc.to.map(function(to){%>
32 | <%= (to.name?to.name+" ":"") + "<" + to.address + ">" %>
33 | <%}).join(", ")%>
34 |
35 | <% } %>
36 |
37 | <% if(doc.cc && doc.cc.length){%>
38 | Cc:
39 | <% doc.cc.map(function(cc){%>
40 | <%= (cc.name?cc.name+" ":"") + "<" + cc.address + ">" %>
41 | <%}).join(", ")%>
42 |
43 | <% } %>
44 |
45 |
46 |
47 | Delete message
48 |
49 | <%= doc.subject || "[untitled]"%>
50 |
51 |
69 |
70 |
71 |
72 |
79 |
80 | <% if(doc.text){%>
81 |
88 | <% } %>
89 |
90 |
97 |
98 | <% if(doc.attachments && doc.attachments.length){%>
99 |
100 |
101 |
102 |
103 |
104 |
105 | #
106 | Filename
107 | MIME type
108 | Size
109 |
110 |
111 |
112 |
113 | <%doc.attachments.forEach(function(attachment, i){%>
114 |
115 |
116 | <%= i+1 %>
117 |
118 |
119 | <%= attachment.generatedFileName %>
120 |
121 |
122 | <%= attachment.contentType %>
123 |
124 |
125 | <%= attachment.length %> B
126 |
127 |
128 | Download attachment
129 |
130 |
131 | <%})%>
132 |
133 |
134 |
135 |
136 |
137 | <% } %>
138 |
--------------------------------------------------------------------------------
/lib/pop3.js:
--------------------------------------------------------------------------------
1 | var config = require("../config/" + (process.env.NODE_ENV || "development") + ".json"),
2 | N3 = require("pop3-n3").N3,
3 | api = require("./api");
4 |
5 | /**
6 | * Starts listening POP3 port by creating a new N3 instance
7 | *
8 | * @param {Function} callback Callback function to run once the binding has been succeeded or failed
9 | */
10 | module.exports.listen = function(callback){
11 | N3.startServer(config.pop3.port, config.hostname, AuthStore, MessageStore, callback);
12 | };
13 |
14 | /**
15 | * POP3 authentication function, always returns username as the password for an user
16 | *
17 | * @param {String} user Username
18 | * @param {Function} auth Authentication function to run with the password of the user
19 | */
20 | function AuthStore(user, auth){
21 | // report username as the password
22 | return auth(user);
23 | }
24 |
25 | /**
26 | * POP3 message store. Handles all message listing and deleting requests
27 | *
28 | * @constructor
29 | * @param {String} user Authenticated username
30 | */
31 | function MessageStore(user){
32 | this.user = (user || "").toString().toLowerCase().trim();
33 | if(!this.user.match(/@/)){
34 | this.user += "@" + config.hostname;
35 | }
36 |
37 | this.messages = [];
38 | this.size = 0;
39 | this.length = 0;
40 | this.loaded = false;
41 | }
42 |
43 | /**
44 | * Handles POP3 STAT command. This function is forced to run before any other
45 | * message related function (listing or deleting). Current message list is buffered
46 | * and the buffer is used for later calls. POP3 uses sequential message numbers
47 | * and if the message list is modified outside the POP3 session it would break the
48 | * protocol if the list is reloaded.
49 | *
50 | * STAT calculates message count and total size of the mailbox in bytes. Response looks
51 | * like the following (first number is the total number of messages and the second one
52 | * indicates the total size in bytes of the all messages)
53 | *
54 | * 3 12596
55 | *
56 | * @param {Function} callback Callback function to run with the mailbox data
57 | */
58 | MessageStore.prototype.stat = function(callback){
59 | if(this.loaded){
60 | return callback(null, this.length, this.size);
61 | }
62 |
63 | this.size = 0;
64 | this.length = 0;
65 |
66 | api.loadMailbox(this.user, (function(err, docs){
67 | if(err){
68 | console.log("POP3 Error: STAT for " + this.user);
69 | console.log(err);
70 | return callback(err);
71 | }
72 |
73 | // force to an array
74 | docs = [].concat(docs || []);
75 |
76 | this.messages = docs.map((function(elm){
77 | this.size += elm.size || 0;
78 | this.length ++;
79 | return {
80 | uid: (elm.id || "").toString(),
81 | deleteFlag: false,
82 | size: elm.size || 0
83 | };
84 | }).bind(this));
85 |
86 | this.loaded = true;
87 |
88 | return callback(null, this.length, this.size);
89 |
90 | }).bind(this));
91 | };
92 |
93 | /**
94 | * Handles POP3 LIST command. LIST retrieves a list of message sequence numbers and message sizes in bytes.
95 | * Returned list looks like the following (first number is the message sequence number and the second one is its size).
96 | *
97 | * 1 1879
98 | * 2 6518
99 | * 3 4199
100 | *
101 | * This function uses always cached values (if the cache is not set yet, force run the STAT command).
102 | *
103 | * @param {Number} msg Message sequence number if a size of a specific message needs to be known.
104 | * If not set, all messages will be listed
105 | * @param {Function} callback Callback function with the message list
106 | */
107 | MessageStore.prototype.list = function(msg, callback){
108 | if(!this.loaded){
109 | return this.stat((function(){
110 | if(!this.loaded){
111 | console.log("POP3 Error: LIST for " + this.user);
112 | console.log("Failed listing messages");
113 | return callback(new Error("Failed"));
114 | }
115 | this.list(msg, callback);
116 | }).bind(this));
117 | }
118 |
119 | var result = [];
120 | if(msg){
121 | if(isNaN(msg) || msg<1 || msg>this.messages.length || this.messages[msg-1].deleteFlag){
122 | callback(null, false);
123 | }
124 | return msg+" "+this.messages[msg-1].size;
125 | }
126 | for(var i=0, len = this.messages.length;ithis.messages.length || this.messages[msg-1].deleteFlag){
163 | callback(null, false);
164 | }
165 | callback(null, msg+" "+this.messages[msg-1].uid);
166 | }
167 | for(var i=0, len = this.messages.length;ithis.messages.length || this.messages[msg-1].deleteFlag){
196 | return callback(null, false);
197 | }
198 |
199 | if(this.messages[msg-1].uid.length != 12 && this.messages[msg-1].uid.length != 24){
200 | return callback(null, false);
201 | }
202 |
203 | api.loadRawMessage(this.messages[msg-1].uid, (function(err, message){
204 | if(err){
205 | console.log("POP3 Error: RETR for " + this.user);
206 | console.log(err);
207 | return callback(err);
208 | }
209 | if(!message){
210 | return callback(null, false);
211 | }
212 | return callback(null, message);
213 | }).bind(this));
214 | };
215 |
216 | /**
217 | * Handles POP3 DELE command which is meant for marking messages as deleted. Marked message
218 | * is not deleted yet, as the state can be reset with RSET command. Deletion usually
219 | * occurs on exit and is performed by removeDeleted function
220 | *
221 | * @param {Number} msg Message sequence number
222 | * @param {Function} callback Callback function to run
223 | */
224 | MessageStore.prototype.dele = function(msg, callback){
225 | if(!this.loaded){
226 | return this.stat((function(){
227 | if(!this.loaded){
228 | console.log("POP3 Error: DELE for " + this.user);
229 | console.log("Failed listing messages");
230 | return callback(new Error("Failed"));
231 | }
232 | this.list(msg, callback);
233 | }).bind(this));
234 | }
235 |
236 | if(!msg || isNaN(msg) || msg<1 || msg>this.messages.length || this.messages[msg-1].deleteFlag){
237 | return callback(null, false);
238 | }
239 | this.messages[msg-1].deleteFlag = true;
240 | this.length--;
241 | this.size -= this.messages[msg-1].size;
242 | return callback(null, true);
243 | };
244 |
245 | /**
246 | * Handles POP3 RSET command which resets the state of messages marked for deletion
247 | */
248 | MessageStore.prototype.rset = function(){
249 | for(var i=0, len = this.messages.length; i=this.messages.length){
268 | return;
269 | }
270 | var message = this.messages[i++];
271 | if(!message || !message.deleteFlag){
272 | return process.nextTick(deleteMessages);
273 | }
274 | api.deleteMessage(message.uid, deleteMessages);
275 | }).bind(this));
276 |
277 | deleteMessages();
278 | };
--------------------------------------------------------------------------------
/lib/routes.js:
--------------------------------------------------------------------------------
1 | var config = require("../config/" + (process.env.NODE_ENV || "development") + ".json"),
2 | api = require("./api"),
3 | crypto = require("crypto"),
4 | fs = require("fs");
5 |
6 | // Main router function
7 | module.exports = function(app){
8 | app.get("/", serveFrontpage);
9 | app.get('/about', serveAbout);
10 | app.post('/redir', serveRedirect);
11 | app.get('/mailbox/:mailbox/json', serveMailboxJSON);
12 | app.get('/mailbox/:mailbox/rss', serveMailboxRSS);
13 | app.get('/message/:message/json', serveMessageJSON);
14 | app.get('/message/:message/html', serveMessageHTML);
15 | app.get('/message/:message/text', serveMessagePlain);
16 | app.get('/message/:message/eml', serveMessageRaw);
17 | app.get('/attachment/:message/:nr/:filename', serveAttachment);
18 | app.get('/mailbox/:mailbox', serveMailbox);
19 | app.get('/message/:message', serveMessage);
20 | app.get('/delete/:mailbox/:message', deleteMessage);
21 | app.post('/mailbox/:mailbox/upload', uploadMessage);
22 | };
23 |
24 | /**
25 | * Serves frontpage (/) of the website
26 | *
27 | * @param {Object} req HTTP Request object
28 | * @param {Object} req HTTP Response object
29 | */
30 | function serveFrontpage(req, res){
31 | res.setHeader("Content-Type", "text/html");
32 | res.render("index", {
33 | title: config.title,
34 | hostname: config.hostname,
35 | pageTitle: false,
36 | page: "/"
37 | });
38 | }
39 |
40 | /**
41 | * Serves about page (/about) of the website
42 | *
43 | * @param {Object} req HTTP Request object
44 | * @param {Object} req HTTP Response object
45 | */
46 | function serveAbout(req, res){
47 | res.setHeader("Content-Type", "text/html");
48 | res.render("index", {
49 | title: config.title,
50 | hostname: config.hostname,
51 | pageTitle: "About",
52 | page: "/about"
53 | });
54 | }
55 |
56 | /**
57 | * Redirects frontpage form to an actual mailbox URL
58 | *
59 | * @param {Object} req HTTP Request object
60 | * @param {Object} req HTTP Response object
61 | */
62 | function serveRedirect(req, res){
63 | var mailbox = (req.body.mailbox || "").toString().trim().toLowerCase() ||
64 | (crypto.randomBytes(4).toString("hex") + "@" + config.hostname);
65 |
66 | if(!mailbox.match(/@/)){
67 | mailbox += "@" + config.hostname;
68 | }
69 |
70 | return res.redirect("/mailbox/" + encodeURIComponent(mailbox));
71 | }
72 |
73 | /**
74 | * Serves a selected mailbox page with a table of received messages
75 | *
76 | * @param {Object} req HTTP Request object
77 | * @param {Object} req HTTP Response object
78 | */
79 | function serveMailbox(req, res){
80 | var mailbox = (req.params.mailbox || "").toLowerCase().trim(),
81 | errors = {
82 | "missing-file": "Upload failed: Empty or missing file",
83 | "too-large-file": "Upload failed: message file was larger than allowed " + config.smtp.maxSize + " Bytes"
84 | };
85 |
86 | if(!mailbox.match(/@/)){
87 | mailbox += "@" + config.hostname;
88 | }
89 |
90 | api.loadMailbox(mailbox, function(err, docs){
91 | if(err){
92 | console.log("WEB Error Mailbox for " + mailbox);
93 | console.log(err);
94 | res.render("index", {
95 | title: config.title,
96 | hostname: config.hostname,
97 | pageTitle: "Mailbox for " + mailbox,
98 | page: "/error",
99 | message: err.message
100 | });
101 | return;
102 | }
103 | res.render("index", {
104 | title: config.title,
105 | hostname: config.hostname,
106 | pageTitle: "Mailbox for " + mailbox,
107 | page: "/mailbox",
108 | mailbox: mailbox,
109 | message: req.query.message,
110 | error: req.query.error && errors[req.query.error],
111 | pop3port: config.pop3.port,
112 | docs: docs
113 | });
114 | });
115 | }
116 |
117 | /**
118 | * Serves a selected mailbox in the form of a JSON string
119 | *
120 | * @param {Object} req HTTP Request object
121 | * @param {Object} req HTTP Response object
122 | */
123 | function serveMailboxJSON(req, res){
124 | var mailbox = (req.params.mailbox || "").toLowerCase().trim();
125 |
126 | if(!mailbox.match(/@/)){
127 | mailbox += "@" + config.hostname;
128 | }
129 |
130 | api.loadMailbox(mailbox, function(err, docs){
131 | if(err){
132 | console.log("WEB Error Mailbox JSON for " + mailbox);
133 | console.log(err);
134 | res.set('Content-Type', "application/json; Charset=utf-8");
135 | res.send(500, JSON.stringify({success: false, error: err.message}));
136 | return;
137 | }
138 | res.set('Content-Type', "application/json; Charset=utf-8");
139 | res.send(JSON.stringify({success: true, data: docs}));
140 | });
141 | }
142 |
143 | /**
144 | * Serves a selected mailbox in the form of a RSS feed
145 | *
146 | * @param {Object} req HTTP Request object
147 | * @param {Object} req HTTP Response object
148 | */
149 | function serveMailboxRSS(req, res){
150 | var mailbox = (req.params.mailbox || "").toLowerCase().trim();
151 |
152 | if(!mailbox.match(/@/)){
153 | mailbox += "@" + config.hostname;
154 | }
155 |
156 | api.loadMailbox(mailbox, true, function(err, docs){
157 | if(err){
158 | console.log("WEB Error Mailbox RSS for " + mailbox);
159 | console.log(err);
160 | res.set('Content-Type', "application/json; Charset=utf-8");
161 | res.send(500, JSON.stringify({success: false, error: err.message}));
162 | return;
163 | }
164 | res.set('Content-Type', "application/rss+xml; Charset=utf-8");
165 | res.render("feed",{
166 | title: config.title,
167 | mailbox: mailbox,
168 | hostname: config.hostname + ([80, 443].indexOf(config.web.port)<0?":"+config.web.port:""),
169 | docs: docs
170 | });
171 | });
172 | }
173 |
174 | /**
175 | * Serves a selected message page
176 | *
177 | * @param {Object} req HTTP Request object
178 | * @param {Object} req HTTP Response object
179 | */
180 | function serveMessage(req, res){
181 | var message = (req.params.message || "").trim();
182 |
183 | api.loadMessage(message, function(err, doc){
184 | if(err){
185 | console.log("WEB Error Message JSON for " + message);
186 | console.log(err);
187 | res.render("index", {
188 | title: config.title,
189 | hostname: config.hostname,
190 | pageTitle: "",
191 | page: "/error",
192 | message: err.message
193 | });
194 | return;
195 | }
196 | res.render("index", {
197 | title: config.title,
198 | hostname: config.hostname,
199 | pageTitle: "",
200 | page: "/message",
201 | doc: doc
202 | });
203 | });
204 | }
205 |
206 | /**
207 | * Serves a selected message in the form of a JSON string
208 | *
209 | * @param {Object} req HTTP Request object
210 | * @param {Object} req HTTP Response object
211 | */
212 | function serveMessageJSON(req, res){
213 | var message = (req.params.message || "").trim();
214 |
215 | api.loadMessage(message, function(err, doc){
216 | if(err){
217 | console.log("WEB Error Message JSON for " + message);
218 | console.log(err);
219 | res.set('Content-Type', "application/json; Charset=utf-8");
220 | res.send(500, JSON.stringify({success: false, error: err.message}));
221 | return;
222 | }
223 | res.set('Content-Type', "application/json; Charset=utf-8");
224 | res.send(JSON.stringify({success: true, data: doc}));
225 | });
226 | }
227 |
228 | /**
229 | * Serves a selected message in the RFC2822 format
230 | *
231 | * @param {Object} req HTTP Request object
232 | * @param {Object} req HTTP Response object
233 | */
234 | function serveMessageRaw(req, res){
235 | var message = (req.params.message || "").trim();
236 |
237 | api.loadRawMessage(message, function(err, raw){
238 | if(err){
239 | console.log("WEB Error Message Raw for " + message);
240 | console.log(err);
241 | res.set('Content-Type', "text/plain");
242 | res.send(500, err.message);
243 | return;
244 | }
245 | res.set('Content-Type', "text/plain");
246 | res.send(raw && raw.length && raw || "Error: Selected message not found - message is either expired or deleted");
247 | });
248 | }
249 |
250 | /**
251 | * Serves a selected message as a standalone HTML page (displayed in an iframe)
252 | *
253 | * @param {Object} req HTTP Request object
254 | * @param {Object} req HTTP Response object
255 | */
256 | function serveMessageHTML(req, res){
257 | var message = (req.params.message || "").trim();
258 |
259 | api.loadMessage(message, function(err, doc){
260 | if(err){
261 | console.log("WEB Error Message JSON for " + message);
262 | console.log(err);
263 | res.set('Content-Type', "text/plain; Charset=utf-8");
264 | res.send(500, err.message);
265 | return;
266 | }
267 |
268 | var base = req.protocol + '://' + req.host + "/",
269 | html = doc?doc.html || "":"Error: Selected message not found - message is either expired or deleted ";
270 |
271 | if(!html.match(/]*>/i)){
272 | html = " " + html + "";
273 | }else{
274 | html = html.replace(/(]*>)/i, "$1\n \n");
275 | }
276 |
277 | res.send(html);
278 | });
279 | }
280 |
281 | /**
282 | * Serves plaintext property of a selected message as a standalone HTML page (displayed in an iframe)
283 | *
284 | * @param {Object} req HTTP Request object
285 | * @param {Object} req HTTP Response object
286 | */
287 | function serveMessagePlain(req, res){
288 | var message = (req.params.message || "").trim();
289 |
290 | api.loadMessage(message, function(err, doc){
291 | if(err){
292 | console.log("WEB Error Message JSON for " + message);
293 | console.log(err);
294 | res.set('Content-Type', "text/plain; Charset=utf-8");
295 | res.send(500, err.message);
296 | return;
297 | }
298 |
299 | var base = req.protocol + '://' + req.host + "/",
300 | text = doc?doc.text || "":"Error: Selected message not found - message is either expired or deleted",
301 | html = "" + text.replace(/>/g, ">").replace(/
").replace(/[ \t]*\r?\n[ \t]*/g," \n") + "
";
302 |
303 | html = " " + html + "";
304 |
305 | res.send(html);
306 | });
307 | }
308 |
309 | /**
310 | * Serves a an attachment for a message
311 | *
312 | * @param {Object} req HTTP Request object
313 | * @param {Object} req HTTP Response object
314 | */
315 | function serveAttachment(req, res){
316 | var message = (req.params.message || "").trim(),
317 | nr = Number(req.params.nr) || 0;
318 |
319 | api.loadAttachments(message, function(err, attachments){
320 | if(err){
321 | console.log("WEB Error Message Attachments for " + message);
322 | console.log(err);
323 | res.set('Content-Type', "text/plain; Charset=utf-8");
324 | res.send(500, err.message);
325 | return;
326 | }
327 | if(!attachments || !attachments[nr]){
328 | res.set('Content-Type', "text/plain; Charset=utf-8");
329 | res.send(404, "Not found");
330 | return;
331 | }
332 | res.set('Content-Type', attachments[nr].contentType);
333 | res.send(attachments[nr].content);
334 | });
335 | }
336 |
337 | /**
338 | * Handles delete request for a message and redirects accordingly
339 | *
340 | * @param {Object} req HTTP Request object
341 | * @param {Object} req HTTP Response object
342 | */
343 | function deleteMessage(req, res){
344 | var mailbox = (req.params.mailbox || "").toLowerCase().trim(),
345 | message = (req.params.message || "").trim();
346 |
347 | if(!mailbox.match(/@/)){
348 | mailbox += "@" + config.hostname;
349 | }
350 |
351 | api.deleteMessage(message, function(err, success){
352 | if(err){
353 | console.log("WEB Error Message Delete for " + message);
354 | console.log(err);
355 | res.render("index", {
356 | title: config.title,
357 | hostname: config.hostname,
358 | pageTitle: "",
359 | page: "/error",
360 | message: err.message
361 | });
362 | return;
363 | }
364 | res.redirect("/mailbox/"+encodeURIComponent(mailbox)+"?message=deleted");
365 | });
366 | }
367 |
368 | /**
369 | * Handles a POST request for uploading a RFC2822 message source to the selected mailbox
370 | *
371 | * @param {Object} req HTTP Request object
372 | * @param {Object} req HTTP Response object
373 | */
374 | function uploadMessage(req, res){
375 | var mailbox = (req.params.mailbox || "").toLowerCase().trim();
376 | if(!req.files || !req.files.eml || !req.files.eml.size){
377 | res.redirect("/mailbox/"+encodeURIComponent(mailbox)+"?error=missing-file");
378 | }else if(req.files.eml.size > config.smtp.maxSize){
379 | res.redirect("/mailbox/"+encodeURIComponent(mailbox)+"?error=too-large-file");
380 | }else{
381 | fs.readFile(req.files.eml.path, function(err, body){
382 | fs.unlink(req.files.eml.path);
383 | if(err){
384 | console.log("WEB Error Message upload for " + mailbox);
385 | console.log(err);
386 | res.render("index", {
387 | title: config.title,
388 | hostname: config.hostname,
389 | pageTitle: "Mailbox for " + mailbox,
390 | page: "/error",
391 | message: err.message
392 | });
393 | return;
394 | }
395 | api.storeRawMessage(mailbox, mailbox, body, function(err, mid){
396 | if(err){
397 | console.log("WEB Error Message upload for " + mailbox);
398 | console.log(err);
399 | res.render("index", {
400 | title: config.title,
401 | hostname: config.hostname,
402 | pageTitle: "Mailbox for " + mailbox,
403 | page: "/error",
404 | message: err.message
405 | });
406 | return;
407 | }
408 | if(mid){
409 | res.redirect("/mailbox/"+encodeURIComponent(mailbox)+"?message=uploaded&mid="+mid);
410 | }else{
411 | res.set('Content-Type', "text/plain; Charset=utf-8");
412 | res.send(500, "Upload failed for reasons not so well known");
413 | }
414 | });
415 | });
416 | return;
417 | }
418 |
419 | if(req.files && req.files.eml && req.files.eml.path){
420 | fs.unlink(req.files.eml.path);
421 | }
422 | }
--------------------------------------------------------------------------------
/www/static/bootstrap/css/bootstrap-responsive.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Responsive v2.3.1
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}}
10 |
--------------------------------------------------------------------------------
/lib/api.js:
--------------------------------------------------------------------------------
1 | var config = require("../config/" + (process.env.NODE_ENV || "development") + ".json"),
2 | MailParser = require("mailparser").MailParser,
3 | indentText = require("mailpurify").indentText,
4 | zlib = require('zlib'),
5 | Stream = require("stream").Stream,
6 | ObjectID = require('mongodb').ObjectID,
7 | mongodb = require("mongodb"),
8 | mongoserver = new mongodb.Server(config.mongo.host, config.mongo.port || 27017, {auto_reconnect: true}),
9 | db_connector = new mongodb.Db(config.mongo.name, mongoserver, {safe: false}),
10 | dbObj,
11 | moment = require("moment");
12 |
13 | module.exports.initDB = initDB;
14 | module.exports.loadMailbox = loadMailbox;
15 | module.exports.loadMessage = loadMessage;
16 | module.exports.loadRawMessage = loadRawMessage;
17 | module.exports.loadAttachments = loadAttachments;
18 | module.exports.storeRawMessage = storeRawMessage;
19 | module.exports.deleteMessage = deleteMessage;
20 |
21 | /**
22 | * Open MongoDB connection, ensure indexes and store database connection pointer to module level variable
23 | * This should be the first thing to run, everything else should be run after the callback fires.
24 | *
25 | * @param {Function} callback Callback function to run after connection to database has been established or failed
26 | */
27 | function initDB(callback){
28 | db_connector.open(function(err, db){
29 | if(err){
30 | return callback(err);
31 | }
32 | dbObj = db;
33 |
34 | db.ensureIndex(config.mongo.collection, {received: 1}, { expireAfterSeconds: config.mongo.ttl }, function(err){
35 | if(err){
36 | return callback(err);
37 | }
38 | db.ensureIndex(config.mongo.collection, {received: -1}, function(err){
39 | if(err){
40 | return callback(err);
41 | }
42 | db.ensureIndex(config.mongo.collection, {mailbox: 1}, function(err){
43 | if(err){
44 | return callback(err);
45 | }
46 | return callback(null, db);
47 | });
48 | });
49 | });
50 | });
51 | }
52 |
53 | /**
54 | * Parses a raw RFC2822 message and stores it to database. Callback function gets the db entity id
55 | *
56 | * @param {String} mailbox The mailbox (e-mail address) the message should be stored into
57 | * @param {String} from Sender address
58 | * @param {Buffer} rawMessage RFC2822 or mboxrd format e-mail
59 | * @param {Function} callback Callback function to run after the message is stored
60 | */
61 | function storeRawMessage(mailbox, from, rawMessage, callback){
62 | var mailparser = new MailParser({showAttachmentLinks: true});
63 | mailparser.on("end", function(message){
64 | indentText(message.text || "", function(err, textHTML){
65 | message.html = message.html || textHTML;
66 | if(message.attachments){
67 | for(var i=0, len = message.attachments.length; i /g,">");
219 | }
220 | }
221 | }
222 | return prefix+"cid:"+cid;
223 | }).
224 | replace(/\r?\n|\r/g, "\u0000").
225 | replace(/