- BREAKING CHANGE from v0.9.35 and eariler: Move accounts into subfolder of SessionStore - viewergen and viewer updates from @GraspYonOx - new homepage theme inspired by @GraspYonOx - connect setup also mostly by @GraspYonOx - numerous bug fixes - nntp-server-zefie is now on npmjs - implement proper nodejs debugging - will start to phase out using console.log for actual debugging - existing "debug" (verbose) messages will stay as is - future code debugging will use debug() to debug WIP, therefore not showing to most users if it is accidently left in
551 lines
22 KiB
JavaScript
551 lines
22 KiB
JavaScript
class WTVMail {
|
|
|
|
fs = require('fs');
|
|
path = require('path');
|
|
uuid = require('uuid');
|
|
ssid = null;
|
|
unread_mail = 0;
|
|
inbox_store = null;
|
|
sent_store = null;
|
|
saved_store = null;
|
|
minisrv_config = [];
|
|
wtvshared = null;
|
|
wtvmime = null;
|
|
wtvclient = null;
|
|
WTVClientSessionData = null;
|
|
mailstore_dir = null;
|
|
mailboxes = null;
|
|
msgFileExt = ".zmsg";
|
|
trashMailboxName = "Trash";
|
|
defaultColors = {};
|
|
sendmailDefaultBGColor = "#1F2033"
|
|
|
|
constructor(minisrv_config, wtvclient) {
|
|
if (!minisrv_config) throw ("minisrv_config required");
|
|
var WTVShared = require("./WTVShared.js")['WTVShared'];
|
|
var WTVMime = require("./WTVMime.js");
|
|
this.WTVClientSessionData = require("./WTVClientSessionData.js");
|
|
this.minisrv_config = minisrv_config;
|
|
this.wtvshared = new WTVShared(minisrv_config);
|
|
this.wtvmime = new WTVMime(minisrv_config);
|
|
this.wtvclient = wtvclient;
|
|
this.ssid = this.wtvclient.ssid;
|
|
this.unread_mail = this.wtvclient.getSessionData("subscriber_unread_mail") ? this.wtvclient.getSessionData("subscriber_unread_mail") : 0;
|
|
this.mailboxes = [
|
|
// referenced by id, so order is important!
|
|
"Inbox",
|
|
"Sent",
|
|
"Saved",
|
|
this.trashMailboxName
|
|
];
|
|
this.defaultColors = {
|
|
bgcolor: "#191919",
|
|
text: "#82A9D9",
|
|
link: "#BDA73A",
|
|
vlink: "#62B362"
|
|
};
|
|
}
|
|
|
|
checkMailIntroSeen() {
|
|
return (this.wtvclient.getSessionData("subscriber_mail_intro_seen")) ? this.wtvclient.getSessionData("subscriber_mail_intro_seen") : false;
|
|
}
|
|
|
|
setMailIntroSeen(seen) {
|
|
this.wtvclient.setSessionData("subscriber_mail_intro_seen", (seen) ? true : false);
|
|
}
|
|
|
|
|
|
mailstoreExists() {
|
|
if (!this.isguest) {
|
|
if (this.mailstore_dir === null) {
|
|
// set mailstore directory local var so we don't call the function every time
|
|
var userstore_dir = this.wtvclient.getUserStoreDirectory();
|
|
|
|
// MailStore
|
|
var store_dir = "MailStore" + this.path.sep;
|
|
this.mailstore_dir = userstore_dir + store_dir;
|
|
}
|
|
return this.fs.existsSync(this.mailstore_dir);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getSignatureColors(signature = null, sendmail = true) {
|
|
var colors = Object.assign({}, this.defaultColors); // start with default colors
|
|
if (sendmail) colors.bgcolor = this.sendmailDefaultBGColor;
|
|
|
|
if (signature) {
|
|
if (signature.length > 0) {
|
|
if (signature.indexOf('<html>') >= 0) {
|
|
if (signature.indexOf('<body') >= 0) {
|
|
// parse <body> tag of html signature to get colors
|
|
const htmlparser2 = require("htmlparser2");
|
|
const dom = htmlparser2.parseDocument(signature);
|
|
const body = htmlparser2.DomUtils.getElementsByTagName('body', dom)[0];
|
|
if (body.attribs) {
|
|
for (const [key, value] of Object.entries(body.attribs)) {
|
|
colors[key] = value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!colors.cursor) colors.cursor = colors.link;
|
|
return colors;
|
|
}
|
|
|
|
mailboxExists(mailboxid) {
|
|
if (mailboxid >= this.mailboxes.length) return null;
|
|
var mailbox_dir = null;
|
|
if (this.mailstoreExists()) {
|
|
var mailbox_name = this.getMailboxById(mailboxid);
|
|
if (!mailbox_name) return null;
|
|
|
|
var mailbox_dir = mailbox_name + this.path.sep;
|
|
var store_dir = this.mailstore_dir + mailbox_dir;
|
|
}
|
|
return (store_dir !== null) ? this.fs.existsSync(store_dir) : false;
|
|
}
|
|
|
|
createMailstore() {
|
|
if (this.mailstoreExists() === false) {
|
|
if (!this.fs.existsSync(this.mailstore_dir)) this.fs.mkdirSync(this.mailstore_dir, { recursive: true });
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
getMailboxById(mailboxid) {
|
|
return (mailboxid < this.mailboxes.length) ? this.mailboxes[mailboxid] : false;
|
|
}
|
|
|
|
getMailboxByName(mailbox_name) {
|
|
var mailbox_id = false;
|
|
this.mailboxes.every(function (v, k) {
|
|
if (v.toLowerCase() == mailbox_name.toLowerCase()) {
|
|
mailbox_id = k;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return mailbox_id;
|
|
}
|
|
|
|
getMailboxStoreDir(mailboxid) {
|
|
if (this.mailboxExists(mailboxid)) {
|
|
var mailbox_name = this.getMailboxById(mailboxid);
|
|
return this.mailstore_dir + mailbox_name + this.path.sep;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
createMailbox(mailboxid) {
|
|
var mailbox_exists = this.mailboxExists(mailboxid);
|
|
if (mailbox_exists === false) {
|
|
var mailbox_name = this.getMailboxById(mailboxid);
|
|
var mailbox_dir = mailbox_name + this.path.sep;
|
|
var store_dir = this.mailstore_dir + mailbox_dir;
|
|
if (!this.fs.existsSync(store_dir)) this.fs.mkdirSync(store_dir, { recursive: true });
|
|
return true;
|
|
}
|
|
return mailbox_exists;
|
|
}
|
|
|
|
createMessageID() {
|
|
return this.uuid.v1();
|
|
}
|
|
|
|
createMessage(mailboxid, from_addr, to_addr, msgbody, subject = null, from_name = null, to_name = null, signature = null, date = null, known_sender = false, attachments = [], url = null, url_title = null, allow_html = false) {
|
|
if (this.createMailbox(mailboxid)) {
|
|
if (!date) date = Math.floor(Date.now() / 1000);
|
|
|
|
var mailbox_path = this.getMailboxStoreDir(mailboxid);
|
|
var message_id = this.createMessageID();
|
|
var message_file = message_id + this.msgFileExt;
|
|
var message_file_out = mailbox_path + message_file;
|
|
var message_data = {
|
|
"from_addr": from_addr,
|
|
"from_name": from_name,
|
|
"to_addr": to_addr,
|
|
"to_name": to_name,
|
|
"date": date,
|
|
"subject": subject,
|
|
"body": msgbody,
|
|
"known_sender": known_sender,
|
|
"signature": signature,
|
|
"unread": true,
|
|
"attachments": attachments,
|
|
"url": url,
|
|
"url_title": url_title,
|
|
"allow_html": allow_html
|
|
}
|
|
try {
|
|
if (this.fs.existsSync(message_file_out)) {
|
|
console.error(" * ERROR: Message with this UUID (" + messageid + ") already exists (should never happen). Message lost.");
|
|
return false;
|
|
}
|
|
|
|
// encode message into json
|
|
var result = this.fs.writeFileSync(message_file_out, JSON.stringify(message_data));
|
|
if (!result) return false;
|
|
|
|
// rely on filesystem times for sorting as it is quicker then reading every file
|
|
var file_timestamp = new Date(date * 1000);
|
|
fs.utimesSync(message_file, Date.now(), file_timestamp);
|
|
if (!result) console.error(" WARNING: Setting timestamp on " + message_file + " failed, mail dates will be inaccurate.");
|
|
|
|
} catch (e) {
|
|
console.error(" # MailErr: Mail Store failed\n", e, "\n", message_file_out, "\n", message_data ,"\n");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
createWelcomeMessage() {
|
|
var welcomeTemplate = this.wtvshared.getTemplate("wtv-mail", "welcomeMail.txt").toString('ascii');
|
|
var end_of_headers = false;
|
|
var msg = "";
|
|
var self = this;
|
|
var to_addr = this.wtvclient.getSessionData("subscriber_username") + "@" + this.minisrv_config.config.service_name;
|
|
var to_name = this.wtvclient.getSessionData("subscriber_name");
|
|
var available_tags = {
|
|
...this.minisrv_config.config,
|
|
"user_address": to_addr,
|
|
"user_name": to_name
|
|
}
|
|
var from_name, from_addr, subj = null;
|
|
var lines = welcomeTemplate.replace(/\r/g, '').split("\n");
|
|
lines.forEach((line) => {
|
|
if (line.indexOf(": ") > 1 && !end_of_headers) {
|
|
var header = [line.slice(0, line.indexOf(':')), line.slice(line.indexOf(':') + 2).trim()];
|
|
switch (header[0].toLowerCase()) {
|
|
case "from":
|
|
if (header[1].indexOf("<") >= 0) {
|
|
var email = header[1].match(/(.+) \<(.+)\>/);
|
|
if (email) {
|
|
from_name = email[1];
|
|
from_addr = email[2];
|
|
} else {
|
|
var email = header[1].match(/\<(.+)\>/);
|
|
from_addr = email[1];
|
|
}
|
|
} else if (header[1].indexOf('@') >= 0) {
|
|
from_addr = header[1];
|
|
}
|
|
break;
|
|
|
|
case "subject":
|
|
subj = header[1];
|
|
break;
|
|
}
|
|
} else if (line == '') end_of_headers = true;
|
|
else {
|
|
msg += line.replace(/\$\{(\w{1,})\}/g, function (x) {
|
|
var out = '';
|
|
var tag = x.replace("${", '').replace('}', '');
|
|
if (available_tags[tag]) out = available_tags[tag];
|
|
return out
|
|
}) + "\n";
|
|
}
|
|
});
|
|
return this.createMessage(0, from_addr, to_addr, msg, subj, from_name, to_name, null, null, true, [], null, null, true);
|
|
}
|
|
|
|
getMessage(mailboxid, messageid) {
|
|
if (this.createMailbox(mailboxid)) {
|
|
var mailbox_path = this.getMailboxStoreDir(mailboxid);
|
|
var message_file = messageid + this.msgFileExt;
|
|
var message_file_in = mailbox_path + this.path.sep + message_file;
|
|
var message_data_raw = null;
|
|
|
|
if (this.fs.existsSync(message_file_in)) message_data_raw = this.fs.readFileSync(message_file_in);
|
|
else console.error(" # MailErr: could not find ", message_file_in);
|
|
|
|
if (message_data_raw) {
|
|
var message_data = JSON.parse(message_data_raw);
|
|
message_data.mailbox_path = mailbox_path;
|
|
message_data.message_file = message_file;
|
|
if (message_data) {
|
|
message_data.id = messageid;
|
|
// backwards compat
|
|
if (!message_data.attachments) message_data.attachments = [];
|
|
|
|
return message_data;
|
|
}
|
|
else console.error(" # MailErr: could not parse json in ", message_file_in);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
updateMessage(message_data) {
|
|
// encode message into json
|
|
var message_out = new Object();
|
|
Object.assign(message_out, message_data);
|
|
delete message_out.mailbox_path;
|
|
delete message_out.message_file;
|
|
var result = this.fs.writeFileSync(message_data.mailbox_path + this.path.sep + message_data.message_file, JSON.stringify(message_out));
|
|
if (!result) return false;
|
|
|
|
// rely on filesystem times for sorting as it is quicker then reading every file
|
|
var file_timestamp = new Date(message_data.date * 1000);
|
|
fs.utimesSync(message_file, Date.now(), file_timestamp);
|
|
if (!result) console.error(" WARNING: Setting timestamp on " + message_file + " failed, mail dates will be inaccurate.");
|
|
}
|
|
|
|
checkMessageIdSanity(messageid) {
|
|
return /^[A-Za-z0-9\-]{36}$/.test(messageid);
|
|
}
|
|
|
|
|
|
listMessages(mailboxid, limit, reverse_sort = false, offset = 0) {
|
|
if (this.createMailbox(mailboxid)) {
|
|
var mailbox_path = this.getMailboxStoreDir(mailboxid);
|
|
var self = this;
|
|
var files = this.fs.readdirSync(mailbox_path)
|
|
.map(function (v) {
|
|
var message_data_raw = null;
|
|
var message_date = null;
|
|
var message_path = mailbox_path + self.path.sep + v;
|
|
if (self.fs.existsSync(message_path)) message_data_raw = self.fs.readFileSync(message_path);
|
|
if (message_data_raw) {
|
|
var message_data = JSON.parse(message_data_raw);
|
|
if (message_data) message_date = message_data.date;
|
|
}
|
|
var message_date_ret = (message_date) ? message_date : self.fs.statSync(mailbox_path + self.path.sep + v).mtime.getTime();
|
|
self.fs.statSync(mailbox_path + self.path.sep + v).mtime.getTime()
|
|
return {
|
|
name: v,
|
|
time: message_date_ret
|
|
};
|
|
})
|
|
.sort(function (a, b) {
|
|
if (!reverse_sort) return b.time - a.time;
|
|
else return a.time - b.time;
|
|
})
|
|
.map(function (v) {
|
|
if (v.name.substring((v.name.length - self.msgFileExt.length)) === self.msgFileExt) return v.name.substring(0, (v.name.length - 5));
|
|
});
|
|
|
|
if (files.length == 0) return false; // no messages
|
|
else {
|
|
// todo filter previous results when offset
|
|
var messagelist_out = new Array();
|
|
Object.keys(files).forEach(function (k) {
|
|
var message = self.getMessage(mailboxid, files[k]);
|
|
if (message) messagelist_out.push(mailboxid, message);
|
|
else console.error(" # MailErr: reading message ID: ", files[k]);
|
|
})
|
|
return messagelist_out.filter(function (n) { return n; });
|
|
}
|
|
}
|
|
return null; // error
|
|
}
|
|
|
|
countMessages(mailboxid) {
|
|
var messages = this.listMessages(mailboxid, 100, false);
|
|
var message_count = Object.keys(messages).length;
|
|
return (message_count) ? message_count : 0;
|
|
}
|
|
|
|
countUnreadMessages(mailboxid) {
|
|
var messages = this.listMessages(mailboxid, 100, false);
|
|
var unread = 0;
|
|
Object.keys(messages).forEach(function (k) {
|
|
if (messages[k].unread) unread++;
|
|
});
|
|
return unread;
|
|
}
|
|
|
|
getMailboxIcon() {
|
|
var icon_image = null;
|
|
switch (this.countMessages(0)) {
|
|
case 0:
|
|
icon_image = "OpenMailbox0.gif";
|
|
break;
|
|
case 1:
|
|
icon_image = "OpenMailbox1.gif";
|
|
break;
|
|
default:
|
|
icon_image = "OpenMailbox2.gif";
|
|
break;
|
|
}
|
|
return icon_image;
|
|
}
|
|
|
|
checkUserExists(username, directory = null) {
|
|
// returns the user's ssid, and user_id and userid in an array if true, false if not
|
|
var search_dir = this.minisrv_config.config.SessionStore;
|
|
var return_val = false;
|
|
var self = this;
|
|
if (directory) search_dir = directory;
|
|
this.fs.readdirSync(search_dir).forEach(file => {
|
|
if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory() && !return_val) {
|
|
return_val = self.checkUserExists(username, search_dir + self.path.sep + file);
|
|
}
|
|
if (!file.match(/.*\.json/ig)) return;
|
|
try {
|
|
var temp_session_data_file = self.fs.readFileSync(search_dir + self.path.sep + file, 'Utf8');
|
|
var temp_session_data = JSON.parse(temp_session_data_file);
|
|
if (temp_session_data.subscriber_username.toLowerCase() == username.toLowerCase()) {
|
|
return_val = search_dir.replace(this.minisrv_config.config.SessionStore + self.path.sep + "accounts" + self.path.sep, '').replace("user", '').split(self.path.sep);
|
|
return_val.push(temp_session_data.subscriber_name);
|
|
}
|
|
} catch (e) {
|
|
console.error(" # Error parsing Session Data JSON", file, e);
|
|
}
|
|
});
|
|
return return_val;
|
|
}
|
|
|
|
getUserMailstore(username) {
|
|
var user_data = this.checkUserExists(username);
|
|
if (user_data) {
|
|
var user_wtvsession = new this.WTVClientSessionData(this.minisrv_config, user_data[0]);
|
|
user_wtvsession.user_id = user_data[1];
|
|
var user_mailstore = new WTVMail(this.minisrv_config, user_wtvsession)
|
|
return user_mailstore;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
sendMessageToAddr(from_addr, to_addr, msgbody, subject = null, from_name = null, to_name = null, signature = null, attachments = [], url = null, url_title = null) {
|
|
if (!to_addr) return "Your message could not be sent.<p>You must specify an addressee in the <blackface>To:</blackface> area.";
|
|
|
|
|
|
if (to_addr.indexOf('@') === -1) to_addr += "@"+this.minisrv_config.config.service_name;
|
|
var username = to_addr.split("@")[0];
|
|
var dest_minisrv = to_addr.split("@")[1] || this.minisrv_config.config.service_name;
|
|
|
|
// local only for now
|
|
if (dest_minisrv.toLowerCase() !== this.minisrv_config.config.service_name.toLowerCase()) {
|
|
return "The m-mail address <strong>" + to_addr + "</strong> is not supported by this MiniSrv.";
|
|
}
|
|
|
|
// find user if local
|
|
if (dest_minisrv.toLowerCase() === this.minisrv_config.config.service_name.toLowerCase()) {
|
|
var dest_user_mailstore = this.getUserMailstore(username);
|
|
// user does not exist
|
|
if (!dest_user_mailstore) return "The user <strong>" + username + "</strong> does not exist on MiniSrv <strong>" + dest_minisrv + "</strong>";
|
|
|
|
if (!to_name) {
|
|
var userExistsData = this.checkUserExists(username);
|
|
to_name = userExistsData[2];
|
|
}
|
|
|
|
// check if the destination user's Inbox exists yet
|
|
if (!dest_user_mailstore.mailboxExists(0)) {
|
|
// mailbox does not yet exist, create it
|
|
var mailbox_exists = dest_user_mailstore.createMailbox(0);
|
|
// Just created Inbox for the first time, so create the welcome message
|
|
if (mailbox_exists) dest_user_mailstore.createWelcomeMessage();
|
|
}
|
|
// if the mailbox exists, deliver the message
|
|
if (dest_user_mailstore.mailboxExists(0)) dest_user_mailstore.createMessage(0, from_addr, to_addr, msgbody, subject, from_name, to_name, signature, null, this.isInUserAddressBook(to_addr, from_addr), attachments, url, url_title);
|
|
else return "There was an internal error sending the message to <strong>" + to_addr + "</strong>. Please try again later";
|
|
|
|
// clean up
|
|
dest_user_mailstore = null;
|
|
return true;
|
|
}
|
|
return "Unknown error";
|
|
}
|
|
|
|
isInUserAddressBook(address_to_check, address_to_look_for) {
|
|
// unimplemented
|
|
return false;
|
|
}
|
|
|
|
getMessageMailboxName(messageid) {
|
|
// returns the mailbox id of which the message was found for the current user
|
|
var self = this;
|
|
var mailbox_name = false;
|
|
if (this.checkMessageIdSanity(messageid)) {
|
|
if (this.mailstoreExists()) {
|
|
this.fs.readdirSync(this.mailstore_dir).every(mailbox => {
|
|
if (mailbox_name) return false;
|
|
self.fs.readdirSync(self.mailstore_dir + mailbox).every(file => {
|
|
var regexSearch = messageid + self.msgFileExt;
|
|
var re = new RegExp(regexSearch, "ig");
|
|
if (!file.match(re)) return true;
|
|
mailbox_name = mailbox;
|
|
return false;
|
|
});
|
|
return true;
|
|
});
|
|
}
|
|
}
|
|
return mailbox_name;
|
|
}
|
|
|
|
getMessageMailboxID(messageid) {
|
|
var mailbox_name = this.getMessageMailboxName(messageid);
|
|
if (!mailbox_name) return false;
|
|
return this.getMailboxByName(mailbox_name);
|
|
}
|
|
|
|
getMessageByID(messageid) {
|
|
var mailbox_name = this.getMessageMailboxName(messageid);
|
|
if (!mailbox_name) return false;
|
|
|
|
var mailboxid = this.mailboxes.findIndex((value) => value == mailbox_name);
|
|
|
|
if (mailboxid !== false) return this.getMessage(mailboxid, messageid);
|
|
return null;
|
|
}
|
|
|
|
moveMailMessage(messageid, dest_mailbox_id) {
|
|
// returns true if successful, false if failed.
|
|
var currentMailbox = this.getMessageMailboxID(messageid);
|
|
// Same mailbox
|
|
if (dest_mailbox_id == currentMailbox) return false;
|
|
|
|
// Invalid destination mailbox ID
|
|
if (dest_mailbox_id > (this.mailboxes.length - 1) || dest_mailbox_id < 0) return false;
|
|
|
|
if (!this.mailboxExists(dest_mailbox_id)) this.createMailbox(dest_mailbox_id);
|
|
|
|
var currentMailStoreDir = this.getMailboxStoreDir(currentMailbox);
|
|
if (!currentMailStoreDir) return false;
|
|
|
|
var destMailStoreDir = this.getMailboxStoreDir(dest_mailbox_id);
|
|
if (!destMailStoreDir) return false;
|
|
|
|
var currentMailFile = currentMailStoreDir + this.path.sep + messageid + this.msgFileExt;
|
|
var destMailFile = destMailStoreDir + this.path.sep + messageid + this.msgFileExt;
|
|
|
|
// File exists
|
|
if (this.fs.existsSync(destMailFile)) return false;
|
|
|
|
return this.fs.renameSync(currentMailFile, destMailFile);
|
|
}
|
|
|
|
deleteMessage(messageid) {
|
|
var currentMailbox = this.getMessageMailboxName(messageid);
|
|
var trashMailbox = this.getMailboxByName(this.trashMailboxName);
|
|
if (currentMailbox != trashMailbox) {
|
|
// if not in the trash, move it to trash
|
|
return this.moveMailMessage(messageid, trashMailbox);
|
|
} else {
|
|
// if its already in the trash, delete it forever
|
|
var currentMailFile = this.getMailboxStoreDir(trashMailbox) + this.path.sep + messageid + this.msgFileExt;
|
|
if (this.fs.fileExistsSync(currentMailFile))
|
|
return this.fs.unlink(currentMailFile);
|
|
else
|
|
return false;
|
|
}
|
|
}
|
|
|
|
setMessageReadStatus(messageid, read = true) {
|
|
var message = this.getMessageByID(messageid);
|
|
if (!message) return false;
|
|
|
|
message.unread = !read;
|
|
this.updateMessage(message);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
module.exports = WTVMail;
|