mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 03:09:36 +01:00
[Chore]: Add extensive debug logging and improve config clarity
Introduces detailed debug logging throughout the application to aid troubleshooting and monitoring, unifying the debug namespace usage. Refactors configuration files for clarity, adds missing environment variables, and updates example values and documentation. Enhances screenshots management by hosting assets locally. Updates scripts for better development and production workflows. Improves comments for maintainability and adjusts minor UI meta tags.
This commit is contained in:
parent
ba6d97c7fe
commit
994142fc29
21 changed files with 9034 additions and 8985 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# --- EMAIL CONFIGURATION ---
|
||||
EMAIL_DOMAINS=["example.com","example.net"] # List of domains your service handles (list)
|
||||
EMAIL_DOMAINS=["example.com","example.net"] # List of domains your service handles ['example.com', 'example.net']
|
||||
|
||||
# --- Purge configuration ---
|
||||
EMAIL_PURGE_TIME=48 # Time value for when to purge
|
||||
|
|
@ -24,7 +24,7 @@ IMAP_CONCURRENCY=6 # Number of conc
|
|||
|
||||
# --- HTTP / WEB CONFIGURATION ---
|
||||
HTTP_PORT=3000 # Port
|
||||
HTTP_BRANDING=["48hr.email","CrazyCo","https://crazyco.xyz"] # [service_title, company_name, company_url]
|
||||
HTTP_BRANDING=["48hr.email","CrazyCo","https://crazyco.xyz"] # ['service_title', 'company_name', 'company_url']
|
||||
HTTP_DISPLAY_SORT=2 # Domain display sorting:
|
||||
# 0 = no change,
|
||||
# 1 = alphabetical,
|
||||
|
|
|
|||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -3,7 +3,7 @@
|
|||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: crazyco
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
ko_fi: crazyco
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
|
|
|
|||
BIN
.github/assets/html.png
vendored
Normal file
BIN
.github/assets/html.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
.github/assets/inbox.png
vendored
Normal file
BIN
.github/assets/inbox.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
.github/assets/raw.png
vendored
Normal file
BIN
.github/assets/raw.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,4 +5,3 @@
|
|||
copilot-instructions.md
|
||||
node_modules
|
||||
db/*
|
||||
package-lock.json
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -71,7 +71,7 @@ User=user
|
|||
Group=user
|
||||
|
||||
WorkingDirectory=/opt/48hr-email
|
||||
ExecStart=npm run start
|
||||
ExecStart=npm run prod
|
||||
|
||||
Restart=on-failure
|
||||
TimeoutStartSec=0
|
||||
|
|
@ -113,13 +113,13 @@ WantedBy=multi-user.target
|
|||
### Screenshots:
|
||||
|
||||
- #### Inbox:
|
||||
<img align="center" src="https://i.imgur.com/JJmSe7S.png">
|
||||
<img align="center" src=".github/assets/inbox.png">
|
||||
|
||||
- #### Email with CSS:
|
||||
<img align="center" src="https://i.imgur.com/x8OBoI7.png">
|
||||
- #### Email using HTML and CSS:
|
||||
<img align="center" src=".github/assets/html.png">
|
||||
|
||||
- #### Email without CSS:
|
||||
<img align="center" src="https://i.imgur.com/VPZ8IG6.png">
|
||||
<img align="center" src=".github/assets/raw.png">
|
||||
|
||||
<br><br>
|
||||
|
||||
|
|
|
|||
11
app.js
11
app.js
|
|
@ -3,6 +3,7 @@
|
|||
/* eslint unicorn/no-process-exit: 0 */
|
||||
|
||||
const config = require('./application/config')
|
||||
const debug = require('debug')('48hr-email:app')
|
||||
|
||||
// Until node 11 adds flatmap, we use this:
|
||||
require('array.prototype.flatmap').shim()
|
||||
|
|
@ -14,40 +15,50 @@ const MailProcessingService = require('./application/mail-processing-service')
|
|||
const MailRepository = require('./domain/mail-repository')
|
||||
|
||||
const clientNotification = new ClientNotification()
|
||||
debug('Client notification service initialized')
|
||||
clientNotification.use(io)
|
||||
|
||||
const imapService = new ImapService(config)
|
||||
debug('IMAP service initialized')
|
||||
const mailProcessingService = new MailProcessingService(
|
||||
new MailRepository(),
|
||||
imapService,
|
||||
clientNotification,
|
||||
config
|
||||
)
|
||||
debug('Mail processing service initialized')
|
||||
|
||||
// Put everything together:
|
||||
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
|
||||
mailProcessingService.onNewMail(mail)
|
||||
)
|
||||
debug('Bound IMAP new mail event handler')
|
||||
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||
mailProcessingService.onInitialLoadDone()
|
||||
)
|
||||
debug('Bound IMAP initial load done event handler')
|
||||
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
|
||||
mailProcessingService.onMailDeleted(mail)
|
||||
)
|
||||
debug('Bound IMAP deleted mail event handler')
|
||||
|
||||
mailProcessingService.on('error', err => {
|
||||
debug('Fatal error from mail processing service:', err.message)
|
||||
console.error('Error from mailProcessingService, stopping.', err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
imapService.on(ImapService.EVENT_ERROR, error => {
|
||||
debug('Fatal error from IMAP service:', error.message)
|
||||
console.error('Fatal error from IMAP service', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
app.set('mailProcessingService', mailProcessingService)
|
||||
|
||||
debug('Starting IMAP connection and message loading')
|
||||
imapService.connectAndLoadMessages().catch(error => {
|
||||
debug('Failed to connect to IMAP:', error.message)
|
||||
console.error('Fatal error from IMAP service', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
|||
151
app.json
151
app.json
|
|
@ -1,71 +1,84 @@
|
|||
{
|
||||
"name": "48hr.email | Disposable email",
|
||||
"description": "A simple and fast disposable mail service that works directly with your already existing imap server. No database required.",
|
||||
"repository": "https://github.com/Crazyco-xyz/48hr.email",
|
||||
"logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png",
|
||||
"keywords": [
|
||||
"node",
|
||||
"disposable-mail"
|
||||
],
|
||||
"env": {
|
||||
"EMAIL_DOMAINS": {
|
||||
"description": "Email domains"
|
||||
},
|
||||
"EMAIL_PURGE_TIME": {
|
||||
"description": "Config for when to purge the emails",
|
||||
"value": {
|
||||
"time": 48,
|
||||
"unit": "hours",
|
||||
"convert": true
|
||||
}
|
||||
},
|
||||
"EMAIL_EXAMPLES": {
|
||||
"description": "Examples of the domains",
|
||||
"value": {
|
||||
"account": "example@48hr.email",
|
||||
"uids": [1, 2, 3]
|
||||
}
|
||||
},
|
||||
"IMAP_USER": {
|
||||
"description": "Username to login to the imap server"
|
||||
},
|
||||
"IMAP_PASSWORD": {
|
||||
"description": "Password to login to the imap server"
|
||||
},
|
||||
"IMAP_SERVER": {
|
||||
"description": "Hostname of the server (usually imap.example.com or mx.example.com)"
|
||||
},
|
||||
"IMAP_PORT": {
|
||||
"description": "Port of the server (usually 993)",
|
||||
"value": 993
|
||||
},
|
||||
"IMAP_TLS": {
|
||||
"description": "Use tls or not",
|
||||
"value": true
|
||||
},
|
||||
"IMAP_AUTHTIMEOUT": {
|
||||
"description": "Timeout for the auth",
|
||||
"value": 3000
|
||||
},
|
||||
"IMAP_REFRESH_INTERVAL_SECONDS": {
|
||||
"description": "How often to refresh the imap messages manually",
|
||||
"value": 60
|
||||
},
|
||||
"HTTP_PORT": {
|
||||
"description": "Port to listen on",
|
||||
"value": 3000
|
||||
},
|
||||
"HTTP_BRANDING": {
|
||||
"description": "The branding of the site",
|
||||
"value": ["48hr.email", "Crazyco", "https://crazyco.xyz"]
|
||||
},
|
||||
"HTTP_DISPLAY_SORT": {
|
||||
"description": "Sort the emails for use",
|
||||
"value": 0
|
||||
},
|
||||
"HTTP_HIDE_OTHER": {
|
||||
"description": "Hide other emails from the list besides the first",
|
||||
"value": false
|
||||
"name": "48hr.email | Disposable email",
|
||||
"description": "A simple and fast disposable mail service that works directly with your already existing imap server. No database required.",
|
||||
"repository": "https://github.com/Crazyco-xyz/48hr.email",
|
||||
"logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png",
|
||||
"keywords": [
|
||||
"node",
|
||||
"disposable-mail"
|
||||
],
|
||||
"env": {
|
||||
"EMAIL_DOMAINS": {
|
||||
"description": "List of domains your service handles"
|
||||
},
|
||||
"EMAIL_PURGE_TIME": {
|
||||
"description": "Time value for when to purge",
|
||||
"value": 48
|
||||
},
|
||||
"EMAIL_PURGE_UNIT": {
|
||||
"description": "Time unit for purging (minutes, hours, days)",
|
||||
"value": "hours"
|
||||
},
|
||||
"EMAIL_PURGE_CONVERT": {
|
||||
"description": "Convert to highest sensible unit and round",
|
||||
"value": true
|
||||
},
|
||||
"EMAIL_EXAMPLE_ACCOUNT": {
|
||||
"description": "Example email account to preserve",
|
||||
"value": "example@48hr.email"
|
||||
},
|
||||
"EMAIL_EXAMPLE_UIDS": {
|
||||
"description": "Example UIDs to preserve",
|
||||
"value": [1, 2, 3]
|
||||
},
|
||||
"IMAP_USER": {
|
||||
"description": "Username to login to the imap server"
|
||||
},
|
||||
"IMAP_PASSWORD": {
|
||||
"description": "Password to login to the imap server"
|
||||
},
|
||||
"IMAP_SERVER": {
|
||||
"description": "Hostname of the server (usually imap.example.com or mx.example.com)"
|
||||
},
|
||||
"IMAP_PORT": {
|
||||
"description": "Port of the server (usually 993)",
|
||||
"value": 993
|
||||
},
|
||||
"IMAP_TLS": {
|
||||
"description": "Use tls or not",
|
||||
"value": true
|
||||
},
|
||||
"IMAP_AUTH_TIMEOUT": {
|
||||
"description": "Timeout for the auth in milliseconds",
|
||||
"value": 3000
|
||||
},
|
||||
"IMAP_REFRESH_INTERVAL_SECONDS": {
|
||||
"description": "How often to refresh the imap messages manually",
|
||||
"value": 60
|
||||
},
|
||||
"IMAP_FETCH_CHUNK": {
|
||||
"description": "Number of UIDs per fetch chunk during initial load",
|
||||
"value": 200
|
||||
},
|
||||
"IMAP_CONCURRENCY": {
|
||||
"description": "Number of concurrent fetch workers during initial load",
|
||||
"value": 6
|
||||
},
|
||||
"HTTP_PORT": {
|
||||
"description": "Port to listen on",
|
||||
"value": 3000
|
||||
},
|
||||
"HTTP_BRANDING": {
|
||||
"description": "The branding of the site",
|
||||
"value": ["48hr.email", "Crazyco", "https://crazyco.xyz"]
|
||||
},
|
||||
"HTTP_DISPLAY_SORT": {
|
||||
"description": "Domain display sorting: 0 = no change, 1 = alphabetical, 2 = alphabetical + first item shuffled, 3 = shuffle all",
|
||||
"value": 2
|
||||
},
|
||||
"HTTP_HIDE_OTHER": {
|
||||
"description": "Hide other emails from the list besides the first",
|
||||
"value": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
// config.js
|
||||
require("dotenv").config({ quiet: true });
|
||||
const debug = require('debug')('48hr-email:config')
|
||||
|
||||
/**
|
||||
* Safely parse a value from env.
|
||||
|
|
@ -63,12 +64,17 @@ const config = {
|
|||
};
|
||||
|
||||
// validation
|
||||
debug('Validating configuration...')
|
||||
if (!config.imap.user || !config.imap.password || !config.imap.host) {
|
||||
debug('IMAP configuration validation failed: missing user, password, or host')
|
||||
throw new Error("IMAP is not configured. Check IMAP_* env vars.");
|
||||
}
|
||||
|
||||
if (!config.email.domains.length) {
|
||||
debug('Email domains validation failed: no domains configured')
|
||||
throw new Error("No EMAIL_DOMAINS configured.");
|
||||
}
|
||||
|
||||
debug(`Configuration validated successfully: ${config.email.domains.length} domains, IMAP host: ${config.imap.host}`)
|
||||
|
||||
module.exports = config;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
const config = require('./config')
|
||||
const moment = require('moment')
|
||||
const debug = require('debug')('48hr-email:helper')
|
||||
|
||||
class Helper {
|
||||
|
||||
|
|
@ -8,9 +9,11 @@ class Helper {
|
|||
* @returns {Date}
|
||||
*/
|
||||
purgeTimeStamp() {
|
||||
return moment()
|
||||
const cutoff = moment()
|
||||
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
|
||||
.toDate()
|
||||
debug(`Purge cutoff calculated: ${cutoff} (${config.email.purgeTime.time} ${config.email.purgeTime.unit} ago)`)
|
||||
return cutoff
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -25,10 +28,12 @@ class Helper {
|
|||
const nowMs = now instanceof Date ? now.getTime() : now;
|
||||
const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime();
|
||||
|
||||
return (nowMs - pastMs) >= DAY_IN_MS;
|
||||
const diffMs = nowMs - pastMs;
|
||||
const result = diffMs >= DAY_IN_MS;
|
||||
debug(`Time difference check: ${diffMs}ms >= ${DAY_IN_MS}ms = ${result}`)
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert time to highest possible unit (minutes → hours → days),
|
||||
* rounding if necessary and prefixing "~" when rounded.
|
||||
|
|
@ -76,9 +81,8 @@ class Helper {
|
|||
}
|
||||
|
||||
const footer = `<label title="${Tooltip}">
|
||||
<h4 style="display: inline;"><u><i>${time}</i></u></h4>
|
||||
</Label>`
|
||||
|
||||
<h4 style="display: inline;"><u><i>${time}</i></u></h4>
|
||||
</Label>`
|
||||
return footer
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +91,6 @@ class Helper {
|
|||
* @param {Array} array
|
||||
* @returns {Array}
|
||||
*/
|
||||
|
||||
shuffleArray(array) {
|
||||
for (let i = array.length - 1; i >= 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
|
|
@ -101,7 +104,6 @@ class Helper {
|
|||
* @param {Array} array
|
||||
* @returns {Array}
|
||||
*/
|
||||
|
||||
shuffleFirstItem(array) {
|
||||
let first = array[Math.floor(Math.random() * array.length)]
|
||||
array = array.filter((value) => value != first);
|
||||
|
|
@ -126,17 +128,26 @@ class Helper {
|
|||
* Get a domain list from config for use
|
||||
* @returns {Array}
|
||||
*/
|
||||
|
||||
getDomains() {
|
||||
debug(`Getting domains with displaySort: ${config.http.displaySort}`)
|
||||
let result;
|
||||
switch (config.http.displaySort) {
|
||||
case 0:
|
||||
return this.hideOther(config.email.domains) // No modification
|
||||
result = this.hideOther(config.email.domains) // No modification
|
||||
debug(`Domain sort 0: no modification, ${result.length} domains`)
|
||||
return result
|
||||
case 1:
|
||||
return this.hideOther(config.email.domains.sort()) // Sort alphabetically
|
||||
result = this.hideOther(config.email.domains.sort()) // Sort alphabetically
|
||||
debug(`Domain sort 1: alphabetical sort, ${result.length} domains`)
|
||||
return result
|
||||
case 2:
|
||||
return this.hideOther(this.shuffleFirstItem(config.email.domains.sort())) // Sort alphabetically and shuffle first item
|
||||
result = this.hideOther(this.shuffleFirstItem(config.email.domains.sort())) // Sort alphabetically and shuffle first item
|
||||
debug(`Domain sort 2: alphabetical + shuffle first, ${result.length} domains`)
|
||||
return result
|
||||
case 3:
|
||||
return this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
|
||||
result = this.hideOther(this.shuffleArray(config.email.domains)) // Shuffle all
|
||||
debug(`Domain sort 3: shuffle all, ${result.length} domains`)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ 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 debug = require('debug')('48hr-email:imap-manager')
|
||||
const _ = require('lodash')
|
||||
const moment = require('moment')
|
||||
const Mail = require('../domain/mail')
|
||||
|
|
@ -133,7 +133,7 @@ class ImapService extends EventEmitter {
|
|||
})
|
||||
|
||||
await this.connection.openBox('INBOX')
|
||||
debug('Connected to imap')
|
||||
debug('Connected to imap Server at ' + this.config.imap.host)
|
||||
}, {
|
||||
retries: 5
|
||||
}
|
||||
|
|
@ -173,8 +173,14 @@ class ImapService extends EventEmitter {
|
|||
debug('Load skipped: another load already in progress')
|
||||
return
|
||||
}
|
||||
|
||||
this.loadingInProgress = true
|
||||
debug('Starting load of mail summaries')
|
||||
if (this.initialLoadDone) {
|
||||
debug('Updating mail summaries from server...')
|
||||
} else {
|
||||
debug('Fetching mail summaries from server...')
|
||||
}
|
||||
|
||||
const uids = await this._getAllUids()
|
||||
const newUids = uids.filter(uid => !this.loadedUids.has(uid))
|
||||
debug(`UIDs on server: ${uids.length}, new UIDs to fetch: ${newUids.length}, already loaded: ${this.loadedUids.size}`)
|
||||
|
|
@ -219,7 +225,7 @@ class ImapService extends EventEmitter {
|
|||
}
|
||||
|
||||
this.loadingInProgress = false
|
||||
debug('Load finished')
|
||||
debug('Finished updating mail summary list')
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const EventEmitter = require('events')
|
||||
const debug = require('debug')('48hr-email:imap-manager')
|
||||
const debug = require('debug')('48hr-email:imap-processor')
|
||||
const mem = require('mem')
|
||||
const ImapService = require('./imap-service')
|
||||
const Helper = require('./helper')
|
||||
|
|
@ -33,29 +33,36 @@ class MailProcessingService extends EventEmitter {
|
|||
}
|
||||
|
||||
getMailSummaries(address) {
|
||||
debug('Getting mail summaries for', address)
|
||||
return this.mailRepository.getForRecipient(address)
|
||||
}
|
||||
|
||||
deleteSpecificEmail(adress, uid) {
|
||||
debug('Deleting specific email', adress, uid)
|
||||
if (this.mailRepository.removeUid(uid, adress) == true) {
|
||||
this.imapService.deleteSpecificEmail(uid)
|
||||
}
|
||||
}
|
||||
|
||||
getOneFullMail(address, uid, raw = false) {
|
||||
debug('Cache lookup for', address + ':' + uid, raw ? '(raw)' : '(parsed)')
|
||||
return this.cachedFetchFullMail(address, uid, raw)
|
||||
}
|
||||
|
||||
getAllMailSummaries() {
|
||||
debug('Getting all mail summaries')
|
||||
return this.mailRepository.getAll()
|
||||
}
|
||||
|
||||
getCount() {
|
||||
return this.mailRepository.mailCount()
|
||||
const count = this.mailRepository.mailCount()
|
||||
debug('Mail count requested:', count)
|
||||
return count
|
||||
}
|
||||
|
||||
onInitialLoadDone() {
|
||||
this.initialLoadDone = true
|
||||
debug('Initial load completed, total mails:', this.mailRepository.mailCount())
|
||||
console.log(`Initial load done, got ${this.mailRepository.mailCount()} mails`)
|
||||
console.log(`Fetching and deleting mails every ${this.config.imap.refreshIntervalSeconds} seconds`)
|
||||
console.log(`Mails older than ${this.config.email.purgeTime.time} ${this.config.email.purgeTime.unit} will be deleted`)
|
||||
|
|
@ -69,7 +76,9 @@ class MailProcessingService extends EventEmitter {
|
|||
}
|
||||
|
||||
mail.to.forEach(to => {
|
||||
debug('Adding mail to repository for recipient:', to)
|
||||
this.mailRepository.add(to, mail)
|
||||
debug('Emitting notification for:', to)
|
||||
return this.clientNotification.emit(to)
|
||||
})
|
||||
}
|
||||
|
|
@ -81,8 +90,11 @@ class MailProcessingService extends EventEmitter {
|
|||
|
||||
async _deleteOldMails() {
|
||||
try {
|
||||
debug('Starting deletion of old mails')
|
||||
await this.imapService.deleteOldMails(helper.purgeTimeStamp())
|
||||
debug('Completed deletion of old mails')
|
||||
} catch (error) {
|
||||
debug('Error deleting old messages:', error.message)
|
||||
console.log('Cant delete old messages', error)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,26 +4,38 @@ const router = new express.Router()
|
|||
const config = require('../../../application/config')
|
||||
const Helper = require('../../../application/helper')
|
||||
const helper = new(Helper)
|
||||
const debug = require('debug')('48hr-email:routes')
|
||||
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
|
||||
router.get('/:address/:errorCode', async(req, res) => {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const count = await mailProcessingService.getCount()
|
||||
router.get('/:address/:errorCode', async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
if (!mailProcessingService) {
|
||||
throw new Error('Mail processing service not available')
|
||||
}
|
||||
debug(`Error page requested: ${req.params.errorCode} for ${req.params.address}`)
|
||||
const count = await mailProcessingService.getCount()
|
||||
const errorCode = parseInt(req.params.errorCode) || 404
|
||||
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
|
||||
|
||||
const errorCode = parseInt(req.params.errorCode) || 404
|
||||
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
|
||||
|
||||
res.status(errorCode)
|
||||
res.render('error', {
|
||||
title: `${config.http.branding[0]} | ${errorCode}`,
|
||||
purgeTime: purgeTime,
|
||||
address: req.params.address,
|
||||
count: count,
|
||||
message: message,
|
||||
status: errorCode,
|
||||
branding: config.http.branding
|
||||
})
|
||||
debug(`Rendering error page ${errorCode} with message: ${message}`)
|
||||
res.status(errorCode)
|
||||
res.render('error', {
|
||||
title: `${config.http.branding[0]} | ${errorCode}`,
|
||||
purgeTime: purgeTime,
|
||||
address: req.params.address,
|
||||
count: count,
|
||||
message: message,
|
||||
status: errorCode,
|
||||
branding: config.http.branding
|
||||
})
|
||||
} catch (error) {
|
||||
debug('Error loading error page:', error.message)
|
||||
console.error('Error while loading error page', error)
|
||||
// For error pages, we should still try to render something basic
|
||||
res.status(500).send('Internal Server Error')
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
module.exports = router
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
const express = require('express')
|
||||
const router = new express.Router()
|
||||
const { param } = require('express-validator')
|
||||
const debug = require('debug')('48hr-email:routes')
|
||||
|
||||
const config = require('../../../application/config')
|
||||
const Helper = require('../../../application/helper')
|
||||
|
|
@ -16,17 +17,28 @@ const sanitizeAddress = param('address').customSanitizer(
|
|||
}
|
||||
)
|
||||
|
||||
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, _next) => {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const count = await mailProcessingService.getCount()
|
||||
res.render('inbox', {
|
||||
title: `${config.http.branding[0]} | ` + req.params.address,
|
||||
purgeTime: purgeTime,
|
||||
address: req.params.address,
|
||||
count: count,
|
||||
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
|
||||
branding: config.http.branding,
|
||||
})
|
||||
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
if (!mailProcessingService) {
|
||||
throw new Error('Mail processing service not available')
|
||||
}
|
||||
debug(`Inbox request for ${req.params.address}`)
|
||||
const count = await mailProcessingService.getCount()
|
||||
debug(`Rendering inbox with ${count} total mails`)
|
||||
res.render('inbox', {
|
||||
title: `${config.http.branding[0]} | ` + req.params.address,
|
||||
purgeTime: purgeTime,
|
||||
address: req.params.address,
|
||||
count: count,
|
||||
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
|
||||
branding: config.http.branding,
|
||||
})
|
||||
} catch (error) {
|
||||
debug(`Error loading inbox for ${req.params.address}:`, error.message)
|
||||
console.error('Error while loading inbox', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
router.get(
|
||||
|
|
@ -35,6 +47,7 @@ router.get(
|
|||
async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
debug(`Viewing email ${req.params.uid} for ${req.params.address}`)
|
||||
const count = await mailProcessingService.getCount()
|
||||
const mail = await mailProcessingService.getOneFullMail(
|
||||
req.params.address,
|
||||
|
|
@ -48,6 +61,7 @@ router.get(
|
|||
|
||||
// Emails are immutable, cache if found
|
||||
res.set('Cache-Control', 'private, max-age=600')
|
||||
debug(`Rendering email view for UID ${req.params.uid}`)
|
||||
res.render('mail', {
|
||||
title: mail.subject + " | " + req.params.address,
|
||||
purgeTime: purgeTime,
|
||||
|
|
@ -58,10 +72,12 @@ router.get(
|
|||
branding: config.http.branding,
|
||||
})
|
||||
} else {
|
||||
debug(`Email ${req.params.uid} not found for ${req.params.address}`)
|
||||
req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!'
|
||||
res.redirect(`/error/${req.params.address}/404`)
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Error fetching email ${req.params.uid} for ${req.params.address}:`, error.message)
|
||||
console.error('Error while fetching email', error)
|
||||
next(error)
|
||||
}
|
||||
|
|
@ -75,12 +91,15 @@ router.get(
|
|||
async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
debug(`Deleting all emails for ${req.params.address}`)
|
||||
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
|
||||
for (mail in mailSummaries) {
|
||||
await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid)
|
||||
}
|
||||
debug(`Deleted all emails for ${req.params.address}`)
|
||||
res.redirect(`/inbox/${req.params.address}`)
|
||||
} catch (error) {
|
||||
debug(`Error deleting all emails for ${req.params.address}:`, error.message)
|
||||
console.error('Error while deleting email', error)
|
||||
next(error)
|
||||
}
|
||||
|
|
@ -95,9 +114,12 @@ router.get(
|
|||
async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
debug(`Deleting email ${req.params.uid} for ${req.params.address}`)
|
||||
await mailProcessingService.deleteSpecificEmail(req.params.address, req.params.uid)
|
||||
debug(`Successfully deleted email ${req.params.uid} for ${req.params.address}`)
|
||||
res.redirect(`/inbox/${req.params.address}`)
|
||||
} catch (error) {
|
||||
debug(`Error deleting email ${req.params.uid} for ${req.params.address}:`, error.message)
|
||||
console.error('Error while deleting email', error)
|
||||
next(error)
|
||||
}
|
||||
|
|
@ -110,11 +132,13 @@ router.get(
|
|||
async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
debug(`Fetching attachment ${req.params.checksum} for email ${req.params.uid} (${req.params.address})`)
|
||||
const uid = parseInt(req.params.uid, 10)
|
||||
const count = await mailProcessingService.getCount()
|
||||
|
||||
// Validate UID is a valid integer
|
||||
if (isNaN(uid) || uid <= 0) {
|
||||
debug(`Invalid UID provided: ${req.params.uid}`)
|
||||
req.session.errorMessage = 'Invalid/Malformed UID provided.'
|
||||
return res.redirect(`/error/${req.params.address}/400`)
|
||||
}
|
||||
|
|
@ -125,6 +149,7 @@ router.get(
|
|||
)
|
||||
|
||||
if (!mail || !mail.attachments) {
|
||||
debug(`Email ${uid} or attachments not found for ${req.params.address}`)
|
||||
req.session.errorMessage = 'This email could not be found. It either does not exist or has been deleted from our servers!'
|
||||
return res.redirect(`/error/${req.params.address}/404`)
|
||||
}
|
||||
|
|
@ -134,20 +159,24 @@ router.get(
|
|||
|
||||
if (attachment) {
|
||||
try {
|
||||
debug(`Serving attachment: ${attachment.filename}`)
|
||||
res.set('Content-Disposition', `attachment; filename=${attachment.filename}`);
|
||||
res.set('Content-Type', attachment.contentType);
|
||||
res.send(attachment.content);
|
||||
return;
|
||||
} catch (error) {
|
||||
debug(`Error serving attachment: ${error.message}`)
|
||||
console.error('Error while fetching attachment', error);
|
||||
next(error);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
debug(`Attachment ${req.params.checksum} not found in email ${uid}`)
|
||||
req.session.errorMessage = 'This attachment could not be found. It either does not exist or has been deleted from our servers!'
|
||||
return res.redirect(`/error/${req.params.address}/404`)
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Error fetching attachment: ${error.message}`)
|
||||
console.error('Error while fetching attachment', error)
|
||||
next(error)
|
||||
}
|
||||
|
|
@ -162,11 +191,13 @@ router.get(
|
|||
async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
debug(`Fetching raw email ${req.params.uid} for ${req.params.address}`)
|
||||
const uid = parseInt(req.params.uid, 10)
|
||||
const count = await mailProcessingService.getCount()
|
||||
|
||||
// Validate UID is a valid integer
|
||||
if (isNaN(uid) || uid <= 0) {
|
||||
debug(`Invalid UID provided for raw view: ${req.params.uid}`)
|
||||
req.session.errorMessage = 'Invalid/Malformed UID provided.'
|
||||
return res.redirect(`/error/${req.params.address}/400`)
|
||||
}
|
||||
|
|
@ -180,15 +211,18 @@ router.get(
|
|||
mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
||||
// Emails are immutable, cache if found
|
||||
res.set('Cache-Control', 'private, max-age=600')
|
||||
debug(`Rendering raw email view for UID ${req.params.uid}`)
|
||||
res.render('raw', {
|
||||
title: req.params.uid + " | raw | " + req.params.address,
|
||||
mail
|
||||
})
|
||||
} else {
|
||||
debug(`Raw email ${uid} not found for ${req.params.address}`)
|
||||
req.session.errorMessage = 'This mail could not be found. It either does not exist or has been deleted from our servers!'
|
||||
res.redirect(`/error/${req.params.address}/404`)
|
||||
}
|
||||
} catch (error) {
|
||||
debug(`Error fetching raw email ${req.params.uid}: ${error.message}`)
|
||||
console.error('Error while fetching raw email', error)
|
||||
next(error)
|
||||
}
|
||||
|
|
@ -206,4 +240,4 @@ router.get(
|
|||
)
|
||||
|
||||
|
||||
module.exports = router
|
||||
module.exports = router
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
const express = require('express')
|
||||
const router = new express.Router()
|
||||
const { check, validationResult } = require('express-validator')
|
||||
const debug = require('debug')('48hr-email:routes')
|
||||
|
||||
const randomWord = require('random-word')
|
||||
const config = require('../../../application/config')
|
||||
|
|
@ -9,21 +10,36 @@ const helper = new(Helper)
|
|||
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
|
||||
router.get('/', async(req, res, _next) => {
|
||||
const count = await req.app.get('mailProcessingService').getCount()
|
||||
res.render('login', {
|
||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||
username: randomWord(),
|
||||
purgeTime: purgeTime,
|
||||
domains: helper.getDomains(),
|
||||
count: count,
|
||||
branding: config.http.branding,
|
||||
example: config.email.examples.account,
|
||||
})
|
||||
router.get('/', async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
if (!mailProcessingService) {
|
||||
throw new Error('Mail processing service not available')
|
||||
}
|
||||
debug('Login page requested')
|
||||
const count = await mailProcessingService.getCount()
|
||||
debug(`Rendering login page with ${count} total mails`)
|
||||
res.render('login', {
|
||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||
username: randomWord(),
|
||||
purgeTime: purgeTime,
|
||||
domains: helper.getDomains(),
|
||||
count: count,
|
||||
branding: config.http.branding,
|
||||
example: config.email.examples.account,
|
||||
})
|
||||
} catch (error) {
|
||||
debug('Error loading login page:', error.message)
|
||||
console.error('Error while loading login page', error)
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/inbox/random', (req, res, _next) => {
|
||||
res.redirect(`/inbox/${randomWord()}@${config.email.domains[Math.floor(Math.random() * config.email.domains.length)]}`)
|
||||
const randomDomain = config.email.domains[Math.floor(Math.random() * config.email.domains.length)]
|
||||
const inbox = `${randomWord()}@${randomDomain}`
|
||||
debug(`Generated random inbox: ${inbox}`)
|
||||
res.redirect(`/inbox/${inbox}`)
|
||||
})
|
||||
|
||||
router.get('/logout', (req, res, _next) => {
|
||||
|
|
@ -40,22 +56,35 @@ router.post(
|
|||
check('username').isLength({ min: 1 }),
|
||||
check('domain').isIn(config.email.domains)
|
||||
],
|
||||
async(req, res) => {
|
||||
const errors = validationResult(req)
|
||||
const count = await req.app.get('mailProcessingService').getCount()
|
||||
if (!errors.isEmpty()) {
|
||||
return res.render('login', {
|
||||
userInputError: true,
|
||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||
purgeTime: purgeTime,
|
||||
username: randomWord(),
|
||||
domains: helper.getDomains(),
|
||||
count: count,
|
||||
branding: config.http.branding,
|
||||
})
|
||||
}
|
||||
async(req, res, next) => {
|
||||
try {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
if (!mailProcessingService) {
|
||||
throw new Error('Mail processing service not available')
|
||||
}
|
||||
const errors = validationResult(req)
|
||||
const count = await mailProcessingService.getCount()
|
||||
if (!errors.isEmpty()) {
|
||||
debug(`Login validation failed for ${req.body.username}@${req.body.domain}: ${errors.array().map(e => e.msg).join(', ')}`)
|
||||
return res.render('login', {
|
||||
userInputError: true,
|
||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
||||
purgeTime: purgeTime,
|
||||
username: randomWord(),
|
||||
domains: helper.getDomains(),
|
||||
count: count,
|
||||
branding: config.http.branding,
|
||||
})
|
||||
}
|
||||
|
||||
res.redirect(`/inbox/${req.body.username}@${req.body.domain}`)
|
||||
const inbox = `${req.body.username}@${req.body.domain}`
|
||||
debug(`Login successful, redirecting to inbox: ${inbox}`)
|
||||
res.redirect(`/inbox/${inbox}`)
|
||||
} catch (error) {
|
||||
debug('Error processing login:', error.message)
|
||||
console.error('Error while processing login', error)
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
<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="darkreader-lock">
|
||||
<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.png">
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
<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="darkreader-lock">
|
||||
<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.png">
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ const Helper = require('../../application/helper')
|
|||
const helper = new(Helper)
|
||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||
|
||||
// Utility function for consistent error handling in routes
|
||||
const handleRouteError = (error, req, res, next, context = 'route') => {
|
||||
debug(`Error in ${context}:`, error.message)
|
||||
console.error(`Error in ${context}`, error)
|
||||
next(error)
|
||||
}
|
||||
|
||||
// Init express middleware
|
||||
const app = express()
|
||||
app.use(helmet())
|
||||
|
|
@ -34,7 +41,7 @@ app.use(express.urlencoded({ extended: false }))
|
|||
|
||||
// Session middleware
|
||||
app.use(session({
|
||||
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this
|
||||
secret: '1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', // They will hate me for this, its temporary tho, I swear!
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours
|
||||
|
|
@ -82,22 +89,29 @@ app.use((req, res, next) => {
|
|||
|
||||
// Error handler
|
||||
app.use(async(err, req, res, _next) => {
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const count = await mailProcessingService.getCount()
|
||||
try {
|
||||
debug('Error handler triggered:', err.message)
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
const count = await mailProcessingService.getCount()
|
||||
|
||||
// Set locals, only providing error in development
|
||||
res.locals.message = err.message
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {}
|
||||
// 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', {
|
||||
purgeTime: purgeTime,
|
||||
address: req.params.address,
|
||||
count: count,
|
||||
branding: config.http.branding
|
||||
|
||||
})
|
||||
// Render the error page
|
||||
res.status(err.status || 500)
|
||||
res.render('error', {
|
||||
purgeTime: purgeTime,
|
||||
address: req.params && req.params.address,
|
||||
count: count,
|
||||
branding: config.http.branding
|
||||
})
|
||||
} catch (renderError) {
|
||||
debug('Error in error handler:', renderError.message)
|
||||
console.error('Critical error in error handler', renderError)
|
||||
// Fallback: send plain text error if rendering fails
|
||||
res.status(500).send('Internal Server Error')
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -116,4 +130,4 @@ server.on('listening', () => {
|
|||
debug('Listening on ' + bind)
|
||||
})
|
||||
|
||||
module.exports = { app, io, server }
|
||||
module.exports = { app, io, server }
|
||||
17364
package-lock.json
generated
17364
package-lock.json
generated
File diff suppressed because it is too large
Load diff
164
package.json
164
package.json
|
|
@ -1,85 +1,83 @@
|
|||
{
|
||||
"name": "48hr.email",
|
||||
"version": "1.6.2",
|
||||
"private": false,
|
||||
"description": "48hr.email is your favorite open-source tempmail client. ",
|
||||
"keywords": [
|
||||
"tempmail",
|
||||
"48hr.email",
|
||||
"disposable-email"
|
||||
],
|
||||
"homepage": "https://48hr.email/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Crazyco-xyz/48hr.email/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"author": "ClaraCrazy",
|
||||
"type": "commonjs",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node --trace-warnings ./app.js",
|
||||
"test": "xo",
|
||||
"debug": "node --nolazy --inspect-brk=9229 ./app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"array.prototype.flatmap": "^1.3.2",
|
||||
"async-retry": "^1.3.3",
|
||||
"compression": "^1.7.4",
|
||||
"debug": "^2.6.9",
|
||||
"dotenv": "^17.2.3",
|
||||
"encodings": "^1.0.0",
|
||||
"express": "^4.21.1",
|
||||
"express-session": "^1.18.2",
|
||||
"express-validator": "^7.2.0",
|
||||
"helmet": "^3.23.3",
|
||||
"http-errors": "~1.6.2",
|
||||
"imap-simple": "^1.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mailparser": "^3.7.1",
|
||||
"mem": "^4.3.0",
|
||||
"mnemonist": "^0.27.2",
|
||||
"moment": "^2.30.1",
|
||||
"morgan": "^1.10.1",
|
||||
"nodemailer": "^7.0.11",
|
||||
"p-series": "^2.1.0",
|
||||
"random-word": "^2.0.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"semver": "^7.6.3",
|
||||
"socket.io": "^4.8.0",
|
||||
"twig": "^0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"xo": "^0.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "22.x"
|
||||
},
|
||||
"xo": {
|
||||
"semicolon": false,
|
||||
"prettier": true,
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
"name": "48hr.email",
|
||||
"version": "1.6.3",
|
||||
"private": false,
|
||||
"description": "48hr.email is your favorite open-source tempmail client. ",
|
||||
"keywords": [
|
||||
"tempmail",
|
||||
"48hr.email",
|
||||
"disposable-email"
|
||||
],
|
||||
"homepage": "https://48hr.email/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Crazyco-xyz/48hr.email/issues"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": "public/javascripts/*.js",
|
||||
"esnext": false,
|
||||
"env": [
|
||||
"browser"
|
||||
],
|
||||
"globals": [
|
||||
"io"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"author": "ClaraCrazy",
|
||||
"type": "commonjs",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node --trace-warnings ./app.js",
|
||||
"debug": "DEBUG=48hr-email:* node --nolazy --inspect-brk=9229 --trace-warnings ./app.js",
|
||||
"test": "xo"
|
||||
},
|
||||
"dependencies": {
|
||||
"array.prototype.flatmap": "^1.3.2",
|
||||
"async-retry": "^1.3.3",
|
||||
"compression": "^1.7.4",
|
||||
"debug": "^4.4.3",
|
||||
"dotenv": "^17.2.3",
|
||||
"encodings": "^1.0.0",
|
||||
"express": "^4.21.1",
|
||||
"express-session": "^1.18.2",
|
||||
"express-validator": "^7.2.0",
|
||||
"helmet": "^3.23.3",
|
||||
"http-errors": "~1.6.2",
|
||||
"imap-simple": "^1.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mailparser": "^3.7.1",
|
||||
"mem": "^4.3.0",
|
||||
"mnemonist": "^0.27.2",
|
||||
"moment": "^2.30.1",
|
||||
"morgan": "^1.10.1",
|
||||
"nodemailer": "^7.0.11",
|
||||
"p-series": "^2.1.0",
|
||||
"random-word": "^2.0.0",
|
||||
"sanitize-html": "^2.13.0",
|
||||
"semver": "^7.6.3",
|
||||
"socket.io": "^4.8.0",
|
||||
"twig": "^0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"xo": "^0.59.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "22.x"
|
||||
},
|
||||
"xo": {
|
||||
"semicolon": false,
|
||||
"prettier": true,
|
||||
"rules": {
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [{
|
||||
"files": "public/javascripts/*.js",
|
||||
"esnext": false,
|
||||
"env": [
|
||||
"browser"
|
||||
],
|
||||
"globals": [
|
||||
"io"
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue