Add Files
parent
84a216a2e6
commit
758b72f4c4
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint unicorn/no-process-exit: 0 */
|
||||||
|
|
||||||
|
const config = require('./application/config')
|
||||||
|
|
||||||
|
// Until node 11 adds flatmap, we use this:
|
||||||
|
require('array.prototype.flatmap').shim()
|
||||||
|
|
||||||
|
const {app, io, server} = require('./infrastructure/web/web')
|
||||||
|
const ClientNotification = require('./infrastructure/web/client-notification')
|
||||||
|
const ImapService = require('./application/imap-service')
|
||||||
|
const MailProcessingService = require('./application/mail-processing-service')
|
||||||
|
const MailRepository = require('./domain/mail-repository')
|
||||||
|
|
||||||
|
const clientNotification = new ClientNotification()
|
||||||
|
clientNotification.use(io)
|
||||||
|
|
||||||
|
const imapService = new ImapService(config)
|
||||||
|
const mailProcessingService = new MailProcessingService(
|
||||||
|
new MailRepository(),
|
||||||
|
imapService,
|
||||||
|
clientNotification,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
|
||||||
|
// Put everything together:
|
||||||
|
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
|
||||||
|
mailProcessingService.onNewMail(mail)
|
||||||
|
)
|
||||||
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||||
|
mailProcessingService.onInitialLoadDone()
|
||||||
|
)
|
||||||
|
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
|
||||||
|
mailProcessingService.onMailDeleted(mail)
|
||||||
|
)
|
||||||
|
|
||||||
|
mailProcessingService.on('error', err => {
|
||||||
|
console.error('error from mailProcessingService, stopping.', err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
imapService.on(ImapService.EVENT_ERROR, error => {
|
||||||
|
console.error('fatal error from imap service', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.set('mailProcessingService', mailProcessingService)
|
||||||
|
|
||||||
|
imapService.connectAndLoadMessages().catch(error => {
|
||||||
|
console.error('fatal error from imap service', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
server.on('error', error => {
|
||||||
|
if (error.syscall !== 'listen') {
|
||||||
|
console.error('fatal web server error', error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle specific listen errors with friendly messages
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
console.error(
|
||||||
|
'Port ' + config.http.port + ' requires elevated privileges'
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
console.error('Port ' + config.http.port + ' is already in use')
|
||||||
|
process.exit(1)
|
||||||
|
default:
|
||||||
|
console.error('fatal web server error', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "48hr.email | disposable email",
|
||||||
|
"description": "a simple and fast disposable mail service that works directly with your imap server. No database required.",
|
||||||
|
"repository": "https://github.com/Crazyco-xyz/48hr.email",
|
||||||
|
"logo": "https://github.com/Crazyco-xyz/48hr.email/blobl/main/infrastructure/web/public/images/logo.gif",
|
||||||
|
"keywords": [
|
||||||
|
"node",
|
||||||
|
"disposable-mail"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"DOMAINS": {
|
||||||
|
"description": "Email domains."
|
||||||
|
},
|
||||||
|
"IMAP_SERVER": {
|
||||||
|
"description": "Hostname of the server (usually imap.example.com)"
|
||||||
|
},
|
||||||
|
"IMAP_USER": {
|
||||||
|
"description": "Username to login to the imap server"
|
||||||
|
},
|
||||||
|
"IMAP_PASSWORD": {
|
||||||
|
"description": "Password to login to the imap server"
|
||||||
|
},
|
||||||
|
"IMAP_REFRESH_INTERVAL_SECONDS": {
|
||||||
|
"description": "How often to refresh the imap messages manually"
|
||||||
|
},
|
||||||
|
"DELETE_MAILS_OLDER_THAN_DAYS": {
|
||||||
|
"description": "How many days to to wait before deleting messages. (default: `30`)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Note: Also update app.json and README.md!
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
email: {
|
||||||
|
domains: process.env.DOMAINS,
|
||||||
|
deleteMailsOlderThanDays: process.env.DELETE_MAILS_OLDER_THAN_DAYS
|
||||||
|
},
|
||||||
|
imap: {
|
||||||
|
user: process.env.IMAP_USER,
|
||||||
|
password: process.env.IMAP_PASSWORD,
|
||||||
|
host: process.env.IMAP_SERVER,
|
||||||
|
port: 993,
|
||||||
|
tls: true,
|
||||||
|
authTimeout: 3000,
|
||||||
|
refreshIntervalSeconds: process.env.IMAP_REFRESH_INTERVAL_SECONDS
|
||||||
|
},
|
||||||
|
http: {port: normalizePort(process.env.PORT || '3000')}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.imap.user || !config.imap.password || !config.imap.host) {
|
||||||
|
throw new Error('IMAP is not configured. Use IMAP_* ENV vars.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.email.domains) {
|
||||||
|
throw new Error('DOMAINS is not configured. Use ENV vars.')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a port into a number, string, or false.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizePort(val) {
|
||||||
|
const port = parseInt(val, 10)
|
||||||
|
|
||||||
|
if (isNaN(port)) {
|
||||||
|
// Named pipe
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port >= 0) {
|
||||||
|
// Port number
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = config
|
|
@ -0,0 +1,319 @@
|
||||||
|
const EventEmitter = require('events')
|
||||||
|
const imaps = require('imap-simple')
|
||||||
|
const {simpleParser} = require('mailparser')
|
||||||
|
const addressparser = require('nodemailer/lib/addressparser')
|
||||||
|
const pSeries = require('p-series')
|
||||||
|
const retry = require('async-retry')
|
||||||
|
const debug = require('debug')('48hr-email:imap')
|
||||||
|
const _ = require('lodash')
|
||||||
|
const Mail = require('../domain/mail')
|
||||||
|
|
||||||
|
|
||||||
|
// Just adding some missing functions to imap-simple... :-)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the specified message(s).
|
||||||
|
*
|
||||||
|
* @param {string|Array} uid The uid or array of uids indicating the messages to be deleted
|
||||||
|
* @param {function} [callback] Optional callback, receiving signature (err)
|
||||||
|
* @returns {undefined|Promise} Returns a promise when no callback is specified, resolving when the action succeeds.
|
||||||
|
* @memberof ImapSimple
|
||||||
|
*/
|
||||||
|
imaps.ImapSimple.prototype.deleteMessage = function (uid, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
return nodeify(self.deleteMessage(uid), callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
self.imap.addFlags(uid, '\\Deleted', function (err) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.imap.expunge( function (err) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a mailbox
|
||||||
|
*
|
||||||
|
* @param {boolean} [autoExpunge=true] If autoExpunge is true, any messages marked as Deleted in the currently open mailbox will be remove
|
||||||
|
* @param {function} [callback] Optional callback, receiving signature (err)
|
||||||
|
* @returns {undefined|Promise} Returns a promise when no callback is specified, resolving to `boxName`
|
||||||
|
* @memberof ImapSimple
|
||||||
|
*/
|
||||||
|
imaps.ImapSimple.prototype.closeBox = function (autoExpunge=true, callback) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if (typeof(autoExpunge) == 'function'){
|
||||||
|
callback = autoExpunge;
|
||||||
|
autoExpunge = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
return nodeify(this.closeBox(autoExpunge), callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
|
||||||
|
self.imap.closeBox(autoExpunge, function (err, result) {
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches emails from the imap server. It is a facade against the more complicated imap-simple api. It keeps the connection
|
||||||
|
* as a member field.
|
||||||
|
*
|
||||||
|
* With this abstraction it would be easy to replace this with any inbound mail service like mailgun.com.
|
||||||
|
*/
|
||||||
|
class ImapService extends EventEmitter {
|
||||||
|
constructor(config) {
|
||||||
|
super()
|
||||||
|
this.config = config
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of emitted UIDs. Listeners should get each email only once.
|
||||||
|
* @type {Set<any>}
|
||||||
|
*/
|
||||||
|
this.loadedUids = new Set()
|
||||||
|
|
||||||
|
this.connection = null
|
||||||
|
this.initialLoadDone = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectAndLoadMessages() {
|
||||||
|
const configWithListener = {
|
||||||
|
...this.config,
|
||||||
|
// 'onmail' adds a callback when new mails arrive. With this we can keep the imap refresh interval very low (or even disable it).
|
||||||
|
onmail: () => this._doOnNewMail()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||||
|
this._doAfterInitialLoad()
|
||||||
|
)
|
||||||
|
|
||||||
|
await this._connectWithRetry(configWithListener)
|
||||||
|
|
||||||
|
// Load all messages in the background. (ASYNC)
|
||||||
|
this._loadMailSummariesAndEmitAsEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
async _connectWithRetry(configWithListener) {
|
||||||
|
try {
|
||||||
|
await retry(
|
||||||
|
async _bail => {
|
||||||
|
// If anything throws, we retry
|
||||||
|
this.connection = await imaps.connect(configWithListener)
|
||||||
|
|
||||||
|
this.connection.on('error', err => {
|
||||||
|
// We assume that the app will be restarted after a crash.
|
||||||
|
console.error(
|
||||||
|
'got fatal error during imap operation, stop app.',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
this.emit('error', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.connection.openBox('INBOX')
|
||||||
|
debug('connected to imap')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retries: 5
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('can not connect, even with retry, stop app', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_doOnNewMail() {
|
||||||
|
// Only react to new mails after the initial load, otherwise it might load the same mails twice.
|
||||||
|
if (this.initialLoadDone) {
|
||||||
|
this._loadMailSummariesAndEmitAsEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_doAfterInitialLoad() {
|
||||||
|
// During initial load we ignored new incoming emails. In order to catch up with those, we have to refresh
|
||||||
|
// the mails once after the initial load. (async)
|
||||||
|
this._loadMailSummariesAndEmitAsEvents()
|
||||||
|
|
||||||
|
// If the above trigger on new mails does not work reliable, we have to regularly check
|
||||||
|
// for new mails on the server. This is done only after all the mails have been loaded for the
|
||||||
|
// first time. (Note: set the refresh higher than the time it takes to download the mails).
|
||||||
|
if (this.config.imap.refreshIntervalSeconds) {
|
||||||
|
setInterval(
|
||||||
|
() => this._loadMailSummariesAndEmitAsEvents(),
|
||||||
|
this.config.imap.refreshIntervalSeconds * 1000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _loadMailSummariesAndEmitAsEvents() {
|
||||||
|
// UID: Unique id of a message.
|
||||||
|
|
||||||
|
const uids = await this._getAllUids()
|
||||||
|
const newUids = uids.filter(uid => !this.loadedUids.has(uid))
|
||||||
|
|
||||||
|
// Optimize by fetching several messages (but not all) with one 'search' call.
|
||||||
|
// fetching all at once might be more efficient, but then it takes long until we see any messages
|
||||||
|
// in the frontend. With a small chunk size we ensure that we see the newest emails after a few seconds after
|
||||||
|
// restart.
|
||||||
|
const uidChunks = _.chunk(newUids, 20)
|
||||||
|
|
||||||
|
// Creates an array of functions. We do not start the search now, we just create the function.
|
||||||
|
const fetchFunctions = uidChunks.map(uidChunk => () =>
|
||||||
|
this._getMailHeadersAndEmitAsEvents(uidChunk)
|
||||||
|
)
|
||||||
|
|
||||||
|
await pSeries(fetchFunctions)
|
||||||
|
|
||||||
|
if (!this.initialLoadDone) {
|
||||||
|
this.initialLoadDone = true
|
||||||
|
this.emit(ImapService.EVENT_INITIAL_LOAD_DONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Date} deleteMailsBefore delete mails before this date instance
|
||||||
|
*/
|
||||||
|
async deleteOldMails(deleteMailsBefore) {
|
||||||
|
debug(`deleting mails before ${deleteMailsBefore}`)
|
||||||
|
const uids = await this._searchWithoutFetch([
|
||||||
|
['!DELETED'],
|
||||||
|
['BEFORE', deleteMailsBefore]
|
||||||
|
])
|
||||||
|
if (uids.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`deleting mails ${uids}`)
|
||||||
|
await this.connection.deleteMessage(uids)
|
||||||
|
console.log(`deleted ${uids.length} old messages.`)
|
||||||
|
|
||||||
|
uids.forEach(uid => this.emit(ImapService.EVENT_DELETED_MAIL, uid))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method because ImapSimple#search also fetches each message. We just need the uids here.
|
||||||
|
*
|
||||||
|
* @param {Object} searchCriteria (see ImapSimple#search)
|
||||||
|
* @returns {Promise<Array<Int>>} Array of UIDs
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async _searchWithoutFetch(searchCriteria) {
|
||||||
|
const imapUnderlying = this.connection.imap
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
imapUnderlying.search(searchCriteria, (err, uids) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
} else {
|
||||||
|
resolve(uids || [])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_createMailSummary(message) {
|
||||||
|
const headerPart = message.parts[0].body
|
||||||
|
const to = headerPart.to
|
||||||
|
.flatMap(to => addressparser(to))
|
||||||
|
// The address also contains the name, just keep the email
|
||||||
|
.map(addressObj => addressObj.address)
|
||||||
|
|
||||||
|
const from = headerPart.from.flatMap(from => addressparser(from))
|
||||||
|
|
||||||
|
const subject = headerPart.subject[0]
|
||||||
|
const date = headerPart.date[0]
|
||||||
|
const {uid} = message.attributes
|
||||||
|
|
||||||
|
return Mail.create(to, from, date, subject, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchOneFullMail(to, uid) {
|
||||||
|
if (!this.connection) {
|
||||||
|
// Here we 'fail fast' instead of waiting for the connection.
|
||||||
|
throw new Error('imap connection not ready')
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`fetching full message ${uid}`)
|
||||||
|
|
||||||
|
// For security we also filter TO, so it is harder to just enumerate all messages.
|
||||||
|
const searchCriteria = [['UID', uid], ['TO', to]]
|
||||||
|
const fetchOptions = {
|
||||||
|
bodies: ['HEADER', ''], // Empty string means full body
|
||||||
|
markSeen: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await this.connection.search(searchCriteria, fetchOptions)
|
||||||
|
if (messages.length === 0) {
|
||||||
|
throw new Error('email not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullBody = _.find(messages[0].parts, {which: ''})
|
||||||
|
return simpleParser(fullBody.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getAllUids() {
|
||||||
|
// We ignore mails that are flagged as DELETED, but have not been removed (expunged) yet.
|
||||||
|
const uids = await this._searchWithoutFetch([['!DELETED']])
|
||||||
|
// Create copy to not mutate the original array. Sort with newest first (DESC).
|
||||||
|
return [...uids].sort().reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getMailHeadersAndEmitAsEvents(uids) {
|
||||||
|
try {
|
||||||
|
const mails = await this._getMailHeaders(uids)
|
||||||
|
mails.forEach(mail => {
|
||||||
|
this.loadedUids.add(mail.attributes.uid)
|
||||||
|
// Some broadcast messages have no TO field. We have to ignore those messages.
|
||||||
|
if (mail.parts[0].body.to) {
|
||||||
|
this.emit(ImapService.EVENT_NEW_MAIL, this._createMailSummary(mail))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
debug('can not fetch', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getMailHeaders(uids) {
|
||||||
|
const fetchOptions = {
|
||||||
|
bodies: ['HEADER.FIELDS (FROM TO SUBJECT DATE)'],
|
||||||
|
struct: false
|
||||||
|
}
|
||||||
|
const searchCriteria = [['UID', ...uids]]
|
||||||
|
return this.connection.search(searchCriteria, fetchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consumers should use these constants:
|
||||||
|
ImapService.EVENT_NEW_MAIL = 'mail'
|
||||||
|
ImapService.EVENT_DELETED_MAIL = 'mailDeleted'
|
||||||
|
ImapService.EVENT_INITIAL_LOAD_DONE = 'initial load done'
|
||||||
|
ImapService.EVENT_ERROR = 'error'
|
||||||
|
|
||||||
|
module.exports = ImapService
|
|
@ -0,0 +1,89 @@
|
||||||
|
const EventEmitter = require('events')
|
||||||
|
const debug = require('debug')('48hr-email:imap-manager')
|
||||||
|
const mem = require('mem')
|
||||||
|
const moment = require('moment')
|
||||||
|
const ImapService = require('./imap-service')
|
||||||
|
|
||||||
|
class MailProcessingService extends EventEmitter {
|
||||||
|
constructor(mailRepository, imapService, clientNotification, config) {
|
||||||
|
super()
|
||||||
|
this.mailRepository = mailRepository
|
||||||
|
this.clientNotification = clientNotification
|
||||||
|
this.imapService = imapService
|
||||||
|
this.config = config
|
||||||
|
|
||||||
|
// Cached methods:
|
||||||
|
this.cachedFetchFullMail = mem(
|
||||||
|
this.imapService.fetchOneFullMail.bind(this.imapService),
|
||||||
|
{maxAge: 10 * 60 * 1000}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.initialLoadDone = false
|
||||||
|
|
||||||
|
// Delete old messages now and every few hours
|
||||||
|
this.imapService.once(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||||
|
this._deleteOldMails()
|
||||||
|
)
|
||||||
|
setInterval(() => this._deleteOldMails(), 1000 * 3600 * 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
getMailSummaries(address) {
|
||||||
|
return this.mailRepository.getForRecipient(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
getOneFullMail(address, uid) {
|
||||||
|
return this.cachedFetchFullMail(address, uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllMailSummaries() {
|
||||||
|
return this.mailRepository.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
onInitialLoadDone() {
|
||||||
|
this.initialLoadDone = true
|
||||||
|
console.log(
|
||||||
|
`initial load done, got ${this.mailRepository.mailCount()} mails`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewMail(mail) {
|
||||||
|
if (this.initialLoadDone) {
|
||||||
|
// For now, only log messages if they arrive after the initial load
|
||||||
|
debug('new mail for', mail.to[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
mail.to.forEach(to => {
|
||||||
|
this.mailRepository.add(to, mail)
|
||||||
|
return this.clientNotification.emit(to)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMailDeleted(uid) {
|
||||||
|
debug('mail deleted with uid', uid)
|
||||||
|
this.mailRepository.removeUid(uid)
|
||||||
|
// No client notification required, as nobody can cold a connection for 30+ days.
|
||||||
|
}
|
||||||
|
|
||||||
|
async _deleteOldMails() {
|
||||||
|
try {
|
||||||
|
await this.imapService.deleteOldMails(
|
||||||
|
moment()
|
||||||
|
.subtract(this.config.email.deleteMailsOlderThanDays, 'days')
|
||||||
|
.toDate()
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.log('can not delete old messages', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_saveToFile(mails, filename) {
|
||||||
|
const fs = require('fs')
|
||||||
|
fs.writeFile(filename, JSON.stringify(mails), err => {
|
||||||
|
if (err) {
|
||||||
|
console.error('can not save mails to file', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MailProcessingService
|
|
@ -0,0 +1,42 @@
|
||||||
|
const debug = require('debug')('48hr-email:mail-summary-store')
|
||||||
|
const MultiMap = require('mnemonist/multi-map')
|
||||||
|
const _ = require('lodash')
|
||||||
|
|
||||||
|
class MailRepository {
|
||||||
|
constructor() {
|
||||||
|
// MultiMap docs: https://yomguithereal.github.io/mnemonist/multi-map
|
||||||
|
this.mailSummaries = new MultiMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
getForRecipient(address) {
|
||||||
|
const mails = this.mailSummaries.get(address) || []
|
||||||
|
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll() {
|
||||||
|
const mails = [...this.mailSummaries.values()]
|
||||||
|
return _.orderBy(mails, mail => Date.parse(mail.date), ['desc'])
|
||||||
|
}
|
||||||
|
|
||||||
|
add(to, mailSummary) {
|
||||||
|
this.mailSummaries.set(to.toLowerCase(), mailSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUid(uid) {
|
||||||
|
// TODO: make this more efficient, looping through each email is not cool.
|
||||||
|
this.mailSummaries.forEachAssociation((mails, to) => {
|
||||||
|
mails
|
||||||
|
.filter(mail => mail.uid === uid)
|
||||||
|
.forEach(mail => {
|
||||||
|
this.mailSummaries.remove(to, mail)
|
||||||
|
debug('removed ', mail.date, to, mail.subject)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mailCount() {
|
||||||
|
return this.mailSummaries.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MailRepository
|
|
@ -0,0 +1,15 @@
|
||||||
|
class Mail {
|
||||||
|
constructor(to, from, date, subject, uid) {
|
||||||
|
this.to = to
|
||||||
|
this.from = from
|
||||||
|
this.date = date
|
||||||
|
this.subject = subject
|
||||||
|
this.uid = uid
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(to, from, date, subject, uid) {
|
||||||
|
return new Mail(to, from, date, subject, uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Mail
|
|
@ -0,0 +1,31 @@
|
||||||
|
const EventEmitter = require('events')
|
||||||
|
const debug = require('debug')('48hr-email:notification')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives sign-ins from users and notifies them when new mails are available.
|
||||||
|
*/
|
||||||
|
class ClientNotification extends EventEmitter {
|
||||||
|
use(io) {
|
||||||
|
io.on('connection', socket => {
|
||||||
|
socket.on('sign in', address => this._signIn(socket, address))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_signIn(socket, address) {
|
||||||
|
debug(`socketio signed in: ${address}`)
|
||||||
|
|
||||||
|
const newMailListener = () => {
|
||||||
|
debug(`${address} has new messages, sending notification`)
|
||||||
|
socket.emit('new emails')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on(address, newMailListener)
|
||||||
|
|
||||||
|
socket.on('disconnect', reason => {
|
||||||
|
debug(`client disconnect: ${address} (${reason})`)
|
||||||
|
this.removeListener(address, newMailListener)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ClientNotification
|
|
@ -0,0 +1,602 @@
|
||||||
|
/*!
|
||||||
|
* Milligram v1.3.0
|
||||||
|
* https://milligram.github.io
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017 CJ Patoilo
|
||||||
|
* Licensed under the MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:after,
|
||||||
|
*:before {
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #606c76;
|
||||||
|
font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: .01em;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 0.3rem solid #d1d1d1;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
input[type='button'],
|
||||||
|
input[type='reset'],
|
||||||
|
input[type='submit'] {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border: 0.1rem solid #9b4dca;
|
||||||
|
border-radius: .4rem;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
height: 3.8rem;
|
||||||
|
letter-spacing: .1rem;
|
||||||
|
line-height: 3.8rem;
|
||||||
|
padding: 0 3.0rem;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:focus, .button:hover,
|
||||||
|
button:focus,
|
||||||
|
button:hover,
|
||||||
|
input[type='button']:focus,
|
||||||
|
input[type='button']:hover,
|
||||||
|
input[type='reset']:focus,
|
||||||
|
input[type='reset']:hover,
|
||||||
|
input[type='submit']:focus,
|
||||||
|
input[type='submit']:hover {
|
||||||
|
background-color: #606c76;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #fff;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled],
|
||||||
|
button[disabled],
|
||||||
|
input[type='button'][disabled],
|
||||||
|
input[type='reset'][disabled],
|
||||||
|
input[type='submit'][disabled] {
|
||||||
|
cursor: default;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled]:focus, .button[disabled]:hover,
|
||||||
|
button[disabled]:focus,
|
||||||
|
button[disabled]:hover,
|
||||||
|
input[type='button'][disabled]:focus,
|
||||||
|
input[type='button'][disabled]:hover,
|
||||||
|
input[type='reset'][disabled]:focus,
|
||||||
|
input[type='reset'][disabled]:hover,
|
||||||
|
input[type='submit'][disabled]:focus,
|
||||||
|
input[type='submit'][disabled]:hover {
|
||||||
|
background-color: #9b4dca;
|
||||||
|
border-color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline,
|
||||||
|
button.button-outline,
|
||||||
|
input[type='button'].button-outline,
|
||||||
|
input[type='reset'].button-outline,
|
||||||
|
input[type='submit'].button-outline {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline:focus, .button.button-outline:hover,
|
||||||
|
button.button-outline:focus,
|
||||||
|
button.button-outline:hover,
|
||||||
|
input[type='button'].button-outline:focus,
|
||||||
|
input[type='button'].button-outline:hover,
|
||||||
|
input[type='reset'].button-outline:focus,
|
||||||
|
input[type='reset'].button-outline:hover,
|
||||||
|
input[type='submit'].button-outline:focus,
|
||||||
|
input[type='submit'].button-outline:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: #606c76;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-outline[disabled]:focus, .button.button-outline[disabled]:hover,
|
||||||
|
button.button-outline[disabled]:focus,
|
||||||
|
button.button-outline[disabled]:hover,
|
||||||
|
input[type='button'].button-outline[disabled]:focus,
|
||||||
|
input[type='button'].button-outline[disabled]:hover,
|
||||||
|
input[type='reset'].button-outline[disabled]:focus,
|
||||||
|
input[type='reset'].button-outline[disabled]:hover,
|
||||||
|
input[type='submit'].button-outline[disabled]:focus,
|
||||||
|
input[type='submit'].button-outline[disabled]:hover {
|
||||||
|
border-color: inherit;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear,
|
||||||
|
button.button-clear,
|
||||||
|
input[type='button'].button-clear,
|
||||||
|
input[type='reset'].button-clear,
|
||||||
|
input[type='submit'].button-clear {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear:focus, .button.button-clear:hover,
|
||||||
|
button.button-clear:focus,
|
||||||
|
button.button-clear:hover,
|
||||||
|
input[type='button'].button-clear:focus,
|
||||||
|
input[type='button'].button-clear:hover,
|
||||||
|
input[type='reset'].button-clear:focus,
|
||||||
|
input[type='reset'].button-clear:hover,
|
||||||
|
input[type='submit'].button-clear:focus,
|
||||||
|
input[type='submit'].button-clear:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.button-clear[disabled]:focus, .button.button-clear[disabled]:hover,
|
||||||
|
button.button-clear[disabled]:focus,
|
||||||
|
button.button-clear[disabled]:hover,
|
||||||
|
input[type='button'].button-clear[disabled]:focus,
|
||||||
|
input[type='button'].button-clear[disabled]:hover,
|
||||||
|
input[type='reset'].button-clear[disabled]:focus,
|
||||||
|
input[type='reset'].button-clear[disabled]:hover,
|
||||||
|
input[type='submit'].button-clear[disabled]:focus,
|
||||||
|
input[type='submit'].button-clear[disabled]:hover {
|
||||||
|
color: #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-radius: .4rem;
|
||||||
|
font-size: 86%;
|
||||||
|
margin: 0 .2rem;
|
||||||
|
padding: .2rem .5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f4f5f6;
|
||||||
|
border-left: 0.3rem solid #9b4dca;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code {
|
||||||
|
border-radius: 0;
|
||||||
|
display: block;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: 0;
|
||||||
|
border-top: 0.1rem solid #f4f5f6;
|
||||||
|
margin: 3.0rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='email'],
|
||||||
|
input[type='number'],
|
||||||
|
input[type='password'],
|
||||||
|
input[type='search'],
|
||||||
|
input[type='tel'],
|
||||||
|
input[type='text'],
|
||||||
|
input[type='url'],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0.1rem solid #d1d1d1;
|
||||||
|
border-radius: .4rem;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: inherit;
|
||||||
|
height: 3.8rem;
|
||||||
|
padding: .6rem 1.0rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='email']:focus,
|
||||||
|
input[type='number']:focus,
|
||||||
|
input[type='password']:focus,
|
||||||
|
input[type='search']:focus,
|
||||||
|
input[type='tel']:focus,
|
||||||
|
input[type='text']:focus,
|
||||||
|
input[type='url']:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
border-color: #9b4dca;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#d1d1d1" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>') center right no-repeat;
|
||||||
|
padding-right: 3.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="14" viewBox="0 0 29 14" width="29"><path fill="#9b4dca" d="M9.37727 3.625l5.08154 6.93523L19.54036 3.625"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
min-height: 6.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label,
|
||||||
|
legend {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'],
|
||||||
|
input[type='radio'] {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-inline {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 112.0rem;
|
||||||
|
padding: 0 2.0rem;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-no-padding {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-no-padding > .column {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-top {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-bottom {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-stretch {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row.row-baseline {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column {
|
||||||
|
display: block;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-left: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-10 {
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-20 {
|
||||||
|
margin-left: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-25 {
|
||||||
|
margin-left: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-33, .row .column.column-offset-34 {
|
||||||
|
margin-left: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-50 {
|
||||||
|
margin-left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-66, .row .column.column-offset-67 {
|
||||||
|
margin-left: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-75 {
|
||||||
|
margin-left: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-80 {
|
||||||
|
margin-left: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-offset-90 {
|
||||||
|
margin-left: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-10 {
|
||||||
|
flex: 0 0 10%;
|
||||||
|
max-width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-20 {
|
||||||
|
flex: 0 0 20%;
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-25 {
|
||||||
|
flex: 0 0 25%;
|
||||||
|
max-width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-33, .row .column.column-34 {
|
||||||
|
flex: 0 0 33.3333%;
|
||||||
|
max-width: 33.3333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-40 {
|
||||||
|
flex: 0 0 40%;
|
||||||
|
max-width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-50 {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-60 {
|
||||||
|
flex: 0 0 60%;
|
||||||
|
max-width: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-66, .row .column.column-67 {
|
||||||
|
flex: 0 0 66.6666%;
|
||||||
|
max-width: 66.6666%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-75 {
|
||||||
|
flex: 0 0 75%;
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-80 {
|
||||||
|
flex: 0 0 80%;
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column.column-90 {
|
||||||
|
flex: 0 0 90%;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-top {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-bottom {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .column .column-center {
|
||||||
|
-ms-grid-row-align: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40rem) {
|
||||||
|
.row {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-left: -1.0rem;
|
||||||
|
width: calc(100% + 2.0rem);
|
||||||
|
}
|
||||||
|
.row .column {
|
||||||
|
margin-bottom: inherit;
|
||||||
|
padding: 0 1.0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #9b4dca;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus, a:hover {
|
||||||
|
color: #606c76;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dl,
|
||||||
|
dl ol,
|
||||||
|
dl ul,
|
||||||
|
ol dl,
|
||||||
|
ol ol,
|
||||||
|
ol ul,
|
||||||
|
ul dl,
|
||||||
|
ul ol,
|
||||||
|
ul ul {
|
||||||
|
font-size: 90%;
|
||||||
|
margin: 1.5rem 0 1.5rem 3.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style: decimal inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: circle inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
button,
|
||||||
|
dd,
|
||||||
|
dt,
|
||||||
|
li {
|
||||||
|
margin-bottom: 1.0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
figure,
|
||||||
|
form,
|
||||||
|
ol,
|
||||||
|
p,
|
||||||
|
pre,
|
||||||
|
table,
|
||||||
|
ul {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-bottom: 0.1rem solid #e1e1e1;
|
||||||
|
padding: 1.2rem 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child,
|
||||||
|
th:first-child {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:last-child,
|
||||||
|
th:last-child {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: 300;
|
||||||
|
letter-spacing: -.1rem;
|
||||||
|
margin-bottom: 2.0rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 4.6rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 3.6rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
letter-spacing: -.08rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
letter-spacing: -.05rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix:after {
|
||||||
|
clear: both;
|
||||||
|
content: ' ';
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=milligram.css.map */
|
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
|
@ -0,0 +1,57 @@
|
||||||
|
/* eslint no-unused-vars: 0 */
|
||||||
|
/* eslint no-undef: 0 */
|
||||||
|
|
||||||
|
function showNewMailsNotification(address, reloadPage) {
|
||||||
|
// We want the page to be reloaded. But then when clicking the notification, it can not find the tab and will open a new one.
|
||||||
|
|
||||||
|
const notification = new Notification(address, {
|
||||||
|
body: 'You have new messages',
|
||||||
|
icon: '/images/logo.gif',
|
||||||
|
tag: 'voidmail-replace-notification',
|
||||||
|
renotify: true
|
||||||
|
})
|
||||||
|
notification.addEventListener('click', event => {
|
||||||
|
// TODO: does not work after reloading the page, see #1
|
||||||
|
event.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (reloadPage) {
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableNewMessageNotifications(address, reloadPage) {
|
||||||
|
enableNotifications()
|
||||||
|
const socket = io()
|
||||||
|
socket.emit('sign in', address)
|
||||||
|
|
||||||
|
socket.on('reconnect', () => {
|
||||||
|
socket.emit('sign in', address)
|
||||||
|
})
|
||||||
|
socket.on('new emails', () => {
|
||||||
|
showNewMailsNotification(address, reloadPage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableNotifications() {
|
||||||
|
// Let's check if the browser supports notifications
|
||||||
|
if (!('Notification' in window)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's check whether notification permissions have already been granted
|
||||||
|
if (Notification.permission === 'granted') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we need to ask the user for permission
|
||||||
|
if (Notification.permission !== 'denied') {
|
||||||
|
Notification.requestPermission(permission => {
|
||||||
|
// If the user accepts, let's create a notification
|
||||||
|
return permission === 'granted'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, if the user has denied notifications and you
|
||||||
|
// want to be respectful there is no need to bother them any more.
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 10px 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #131516;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
flex: 1; /* keep footer at the bottom */
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.no-link-color {
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email:hover {
|
||||||
|
color: black;
|
||||||
|
background-color: #9b4cda;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 80%;
|
||||||
|
height: 60vh;
|
||||||
|
border: 1px dotted black;
|
||||||
|
margin-left: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: .3rem solid #9b4dca;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote.warning {
|
||||||
|
border-left: .3rem solid #ca5414;
|
||||||
|
}
|
||||||
|
|
||||||
|
text-muted {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset apple form styles */
|
||||||
|
input, textarea, select, select:active, select:focus, select:hover {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none; border-radius: 0;
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
padding-top: 15vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login h1, #login h4 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login fieldset label {
|
||||||
|
position: relative;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
width: fit-content;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
background-color: #131516;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login input[type="text"], #login select {
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
height: 4.2rem;
|
||||||
|
padding: 0 1.4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login select option {
|
||||||
|
background-color: #131516;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login .dropdown {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login .dropdown::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "\2193";
|
||||||
|
top: 15%;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login .buttons {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login .buttons > * {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
const router = new express.Router()
|
||||||
|
const {sanitizeParam} = require('express-validator/filter')
|
||||||
|
|
||||||
|
const sanitizeAddress = sanitizeParam('address').customSanitizer(
|
||||||
|
(value, {req}) => {
|
||||||
|
return req.params.address
|
||||||
|
.replace(/[^A-Za-z0-9_.+@-]/g, '') // Remove special characters
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, (req, res, _next) => {
|
||||||
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
res.render('inbox', {
|
||||||
|
title: "48hr.email | " + req.params.address,
|
||||||
|
address: req.params.address,
|
||||||
|
mailSummaries: mailProcessingService.getMailSummaries(req.params.address)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'^/:address/:uid([0-9]+$)',
|
||||||
|
sanitizeAddress,
|
||||||
|
async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
const mail = await mailProcessingService.getOneFullMail(
|
||||||
|
req.params.address,
|
||||||
|
req.params.uid
|
||||||
|
)
|
||||||
|
if (mail) {
|
||||||
|
// Emails are immutable, cache if found
|
||||||
|
res.set('Cache-Control', 'private, max-age=600')
|
||||||
|
res.render('mail', {
|
||||||
|
title: req.params.address,
|
||||||
|
address: req.params.address,
|
||||||
|
mail
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
next({message: 'email not found', status: 404})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error while fetching one email', error)
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -0,0 +1,41 @@
|
||||||
|
const express = require('express')
|
||||||
|
|
||||||
|
const router = new express.Router()
|
||||||
|
const randomWord = require('random-word')
|
||||||
|
const {check, validationResult} = require('express-validator/check')
|
||||||
|
const config = require('../../../application/config')
|
||||||
|
|
||||||
|
router.get('/', (req, res, _next) => {
|
||||||
|
res.render('login', {
|
||||||
|
title: '48hr.email | Your temporary Inbox',
|
||||||
|
username: randomWord(),
|
||||||
|
domains: config.email.domains
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/random', (req, res, _next) => {
|
||||||
|
res.redirect(`/${randomWord()}@${config.email.domains[Math.floor(Math.random() * config.email.domains.length)]}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
[
|
||||||
|
check('username').isLength({min: 1}),
|
||||||
|
check('domain').isIn(config.email.domains)
|
||||||
|
],
|
||||||
|
(req, res) => {
|
||||||
|
const errors = validationResult(req)
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.render('login', {
|
||||||
|
title: 'Login',
|
||||||
|
username: req.body.username,
|
||||||
|
domain: req.body.domain,
|
||||||
|
userInputError: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(`/${req.body.username}@${req.body.domain}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
module.exports = router
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div style="float: right"><a href="/login"> Logout</a></div>
|
||||||
|
|
||||||
|
<h1>{{message}}</h1>
|
||||||
|
<h2>{{error.status}}</h2>
|
||||||
|
<pre>{{error.stack}}</pre>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,35 @@
|
||||||
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
enableNewMessageNotifications('{{ address }}', true)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="float: right"><a href="/login"> Logout</a></div>
|
||||||
|
<h1>{{ address }}</h1>
|
||||||
|
{% for mail in mailSummaries %}
|
||||||
|
<a href="{{ mail.to[0] }}/{{ mail.uid }}" class="no-link-color">
|
||||||
|
<blockquote class="email">
|
||||||
|
<h6 class="list-group-item-heading">
|
||||||
|
{{ mail.from[0].name }}
|
||||||
|
<span class="text-muted">{{ mail.from[0].address }}</span>
|
||||||
|
<small class="float-right">{{ mail.date |date }}</small>
|
||||||
|
</h6>
|
||||||
|
<p class="list-group-item-text text-truncate" style="width: 75%">
|
||||||
|
{{ mail.subject }} </p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</blockquote>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if not mailSummaries %}
|
||||||
|
<blockquote>
|
||||||
|
There are no mails yet.
|
||||||
|
</blockquote>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
|
||||||
|
<meta name="darkreader" content="stfu">
|
||||||
|
|
||||||
|
<meta name="description" content="Dont give shady companies your real email. Use 48hr.email to protect your privacy!">
|
||||||
|
<meta property="og:image" content="images/logo.gif">
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href="/images/icon.ico">
|
||||||
|
<link rel='stylesheet' href='/dependencies/milligram.css' />
|
||||||
|
<link rel='stylesheet' href='/stylesheets/custom.css' />
|
||||||
|
|
||||||
|
<script src="/socket.io/socket.io.js" defer="true"></script>
|
||||||
|
<script src="/javascripts/notifications.js" defer="true"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<a href="https://48hr.email/">
|
||||||
|
<img src="/images/logo.gif" class="logo" style="max-width: 75px">
|
||||||
|
</a>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
<section class="container footer">
|
||||||
|
<hr>
|
||||||
|
<h4>48hr.email offered by <a href="https://crazyco.xyz/" style="text-decoration:underline">CrazyCo</a> | All Emails will be deleted after 48hrs</h4>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1 @@
|
||||||
|
2023/11/01 09:58:03 Micro started
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<div id="login">
|
||||||
|
<h1>Welcome!</h1>
|
||||||
|
<h4>Here you can either create a new Inbox, or access your old one</h4>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{% if userInputError %}
|
||||||
|
<blockquote class="warning">
|
||||||
|
Your input was invalid. Please try other values.
|
||||||
|
</blockquote>
|
||||||
|
{% endif %}
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<fieldset>
|
||||||
|
<label for="nameField">Name</label>
|
||||||
|
<input type="text" id="nameField" name="username" value="{{ username }}">
|
||||||
|
<label for="commentField">Domain</label>
|
||||||
|
<div class="dropdown">
|
||||||
|
<select id="commentField" name="domain">
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{ domain }}">{{ domain }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="buttons">
|
||||||
|
<input class="button" type="submit" value="Access This Inbox">
|
||||||
|
<a class="button" href="/login/random">Create Random Inbox</a>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
|
||||||
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<script>
|
||||||
|
enableNewMessageNotifications('{{ address }}', false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div style="float: right; text-align: end;">
|
||||||
|
<a href="/{{ address }}">
|
||||||
|
← Return to inbox</a>
|
||||||
|
<br>
|
||||||
|
<a href="/login">
|
||||||
|
Logout</a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<h1>{{ mail.subject }}</h1>
|
||||||
|
</div>
|
||||||
|
{% if mail.html %}
|
||||||
|
<div>
|
||||||
|
<iframe srcdoc='{{ mail.html }}'></iframe>
|
||||||
|
</div>
|
||||||
|
{% elseif mail.textAsHtml %}
|
||||||
|
<div class="mail_body">
|
||||||
|
{{ mail.textAsHtml|raw }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="mail_body"></div>
|
||||||
|
{% endif %}
|
||||||
|
<h3 style="text-align: center;display: block;">{{ mail.from.text }} | {{ mail.date| date }}</h3>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
{% extends 'layout.twig' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<script>
|
||||||
|
enableNewMessageNotifications('{{ address }}', false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href="/{{ address }}">
|
||||||
|
← Return to inbox</a>
|
||||||
|
<div style="float: right">
|
||||||
|
<a href="/login">
|
||||||
|
Logout</a>
|
||||||
|
</div>
|
||||||
|
<h1>{{ mail.subject }}</h1>
|
||||||
|
<h3>{{ mail.from.text }}</h3>
|
||||||
|
<h3>{{ mail.date| date }}</h3>
|
||||||
|
<hr>
|
||||||
|
{% if mail.html %}
|
||||||
|
<iframe>
|
||||||
|
<div class="mail_body" style="overflow: auto;">
|
||||||
|
{{ mail.html|raw }}
|
||||||
|
</div>
|
||||||
|
</iframe>
|
||||||
|
{% elseif mail.textAsHtml %}
|
||||||
|
<div class="mail_body">
|
||||||
|
{{ mail.textAsHtml|raw }}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="mail_body"></div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,27 @@
|
||||||
|
const sanitizeHtml = require('sanitize-html')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes <a> tags to always use "noreferrer noopener" and open in a new window.
|
||||||
|
* @param {Object} value the dom before transformation
|
||||||
|
* @returns {*} dom after transformation
|
||||||
|
*/
|
||||||
|
exports.sanitizeHtmlTwigFilter = function(value) {
|
||||||
|
return sanitizeHtml(value, {
|
||||||
|
allowedAttributes: {
|
||||||
|
a: ['href', 'target', 'rel']
|
||||||
|
},
|
||||||
|
|
||||||
|
transformTags: {
|
||||||
|
a(tagName, attribs) {
|
||||||
|
return {
|
||||||
|
tagName,
|
||||||
|
attribs: {
|
||||||
|
rel: 'noreferrer noopener',
|
||||||
|
href: attribs.href,
|
||||||
|
target: '_blank'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
const path = require('path')
|
||||||
|
const http = require('http')
|
||||||
|
const debug = require('debug')('voidmail:server')
|
||||||
|
const express = require('express')
|
||||||
|
const logger = require('morgan')
|
||||||
|
const Twig = require('twig')
|
||||||
|
const compression = require('compression')
|
||||||
|
const helmet = require('helmet')
|
||||||
|
const socketio = require('socket.io')
|
||||||
|
|
||||||
|
const config = require('../../application/config')
|
||||||
|
const inboxRouter = require('./routes/inbox')
|
||||||
|
const loginRouter = require('./routes/login')
|
||||||
|
const {sanitizeHtmlTwigFilter} = require('./views/twig-filters')
|
||||||
|
|
||||||
|
// Init express middleware
|
||||||
|
const app = express()
|
||||||
|
app.use(helmet())
|
||||||
|
app.use(compression())
|
||||||
|
app.set('config', config)
|
||||||
|
const server = http.createServer(app)
|
||||||
|
const io = socketio(server)
|
||||||
|
|
||||||
|
app.set('socketio', io)
|
||||||
|
app.use(logger('dev'))
|
||||||
|
app.use(express.json())
|
||||||
|
app.use(express.urlencoded({extended: false}))
|
||||||
|
// View engine setup
|
||||||
|
app.set('views', path.join(__dirname, 'views'))
|
||||||
|
app.set('view engine', 'twig')
|
||||||
|
app.set('twig options', {
|
||||||
|
autoescape: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Application code:
|
||||||
|
app.use(
|
||||||
|
express.static(path.join(__dirname, 'public'), {
|
||||||
|
immutable: true,
|
||||||
|
maxAge: '1h'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
Twig.extendFilter('sanitizeHtml', sanitizeHtmlTwigFilter)
|
||||||
|
|
||||||
|
app.get('/', (req, res, _next) => {
|
||||||
|
res.redirect('/login')
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/login', loginRouter)
|
||||||
|
app.use('/', inboxRouter)
|
||||||
|
|
||||||
|
// Catch 404 and forward to error handler
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
next({message: 'page not found', status: 404})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
app.use((err, req, res, _next) => {
|
||||||
|
// Set locals, only providing error in development
|
||||||
|
res.locals.message = err.message
|
||||||
|
res.locals.error = req.app.get('env') === 'development' ? err : {}
|
||||||
|
|
||||||
|
// Render the error page
|
||||||
|
res.status(err.status || 500)
|
||||||
|
res.render('error')
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get port from environment and store in Express.
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.set('port', config.http.port)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen on provided port, on all network interfaces.
|
||||||
|
*/
|
||||||
|
server.listen(config.http.port)
|
||||||
|
server.on('listening', () => {
|
||||||
|
const addr = server.address()
|
||||||
|
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
|
||||||
|
debug('Listening on ' + bind)
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = {app, io, server}
|
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
"name": "48hr.email",
|
||||||
|
"version": "1.0",
|
||||||
|
"private": false,
|
||||||
|
"scripts": {
|
||||||
|
"start": "node ./app.js",
|
||||||
|
"test": "xo",
|
||||||
|
"debug": "node --nolazy --inspect-brk=9229 ./app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"array.prototype.flatmap": "^1.2.1",
|
||||||
|
"async-retry": "^1.2.3",
|
||||||
|
"compression": "^1.7.3",
|
||||||
|
"debug": "^2.6.9",
|
||||||
|
"express": "~4.16.0",
|
||||||
|
"express-validator": "^5.3.1",
|
||||||
|
"helmet": "^3.16.0",
|
||||||
|
"http-errors": "~1.6.2",
|
||||||
|
"imap-simple": "^4.3.0",
|
||||||
|
"lodash": "^4.17.13",
|
||||||
|
"mailparser": "^2.4.3",
|
||||||
|
"mem": "^4.2.0",
|
||||||
|
"mnemonist": "^0.27.2",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"morgan": "~1.9.0",
|
||||||
|
"nodemailer": "^5.1.1",
|
||||||
|
"p-series": "^2.0.0",
|
||||||
|
"random-word": "^2.0.0",
|
||||||
|
"sanitize-html": "^1.20.0",
|
||||||
|
"socket.io": "^2.2.0",
|
||||||
|
"twig": "~0.10.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"xo": "^0.24.0"
|
||||||
|
},
|
||||||
|
"xo": {
|
||||||
|
"semicolon": false,
|
||||||
|
"prettier": true,
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "public/javascripts/*.js",
|
||||||
|
"esnext": false,
|
||||||
|
"env": [
|
||||||
|
"browser"
|
||||||
|
],
|
||||||
|
"globals": [
|
||||||
|
"io"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "10.x"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue