mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-02-14 17:19:35 +01:00
Compare commits
5 commits
63b30a3705
...
1f45db1886
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f45db1886 | ||
|
|
b4683e97a7 | ||
|
|
86347eb5ad | ||
|
|
994142fc29 | ||
|
|
ba6d97c7fe |
22 changed files with 9382 additions and 8969 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
# --- EMAIL CONFIGURATION ---
|
# --- 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 ---
|
# --- Purge configuration ---
|
||||||
EMAIL_PURGE_TIME=48 # Time value for when to purge
|
EMAIL_PURGE_TIME=48 # Time value for when to purge
|
||||||
|
|
@ -24,7 +24,7 @@ IMAP_CONCURRENCY=6 # Number of conc
|
||||||
|
|
||||||
# --- HTTP / WEB CONFIGURATION ---
|
# --- HTTP / WEB CONFIGURATION ---
|
||||||
HTTP_PORT=3000 # Port
|
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:
|
HTTP_DISPLAY_SORT=2 # Domain display sorting:
|
||||||
# 0 = no change,
|
# 0 = no change,
|
||||||
# 1 = alphabetical,
|
# 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]
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: crazyco
|
patreon: crazyco
|
||||||
open_collective: # Replace with a single Open Collective username
|
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
|
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
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
liberapay: # Replace with a single Liberapay username
|
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
|
copilot-instructions.md
|
||||||
node_modules
|
node_modules
|
||||||
db/*
|
db/*
|
||||||
package-lock.json
|
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -71,7 +71,7 @@ User=user
|
||||||
Group=user
|
Group=user
|
||||||
|
|
||||||
WorkingDirectory=/opt/48hr-email
|
WorkingDirectory=/opt/48hr-email
|
||||||
ExecStart=npm run start
|
ExecStart=npm run prod
|
||||||
|
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
TimeoutStartSec=0
|
TimeoutStartSec=0
|
||||||
|
|
@ -113,13 +113,13 @@ WantedBy=multi-user.target
|
||||||
### Screenshots:
|
### Screenshots:
|
||||||
|
|
||||||
- #### Inbox:
|
- #### Inbox:
|
||||||
<img align="center" src="https://i.imgur.com/JJmSe7S.png">
|
<img align="center" src=".github/assets/inbox.png">
|
||||||
|
|
||||||
- #### Email with CSS:
|
- #### Email using HTML and CSS:
|
||||||
<img align="center" src="https://i.imgur.com/x8OBoI7.png">
|
<img align="center" src=".github/assets/html.png">
|
||||||
|
|
||||||
- #### Email without CSS:
|
- #### Email without CSS:
|
||||||
<img align="center" src="https://i.imgur.com/VPZ8IG6.png">
|
<img align="center" src=".github/assets/raw.png">
|
||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
|
|
|
||||||
11
app.js
11
app.js
|
|
@ -3,6 +3,7 @@
|
||||||
/* eslint unicorn/no-process-exit: 0 */
|
/* eslint unicorn/no-process-exit: 0 */
|
||||||
|
|
||||||
const config = require('./application/config')
|
const config = require('./application/config')
|
||||||
|
const debug = require('debug')('48hr-email:app')
|
||||||
|
|
||||||
// Until node 11 adds flatmap, we use this:
|
// Until node 11 adds flatmap, we use this:
|
||||||
require('array.prototype.flatmap').shim()
|
require('array.prototype.flatmap').shim()
|
||||||
|
|
@ -14,40 +15,50 @@ const MailProcessingService = require('./application/mail-processing-service')
|
||||||
const MailRepository = require('./domain/mail-repository')
|
const MailRepository = require('./domain/mail-repository')
|
||||||
|
|
||||||
const clientNotification = new ClientNotification()
|
const clientNotification = new ClientNotification()
|
||||||
|
debug('Client notification service initialized')
|
||||||
clientNotification.use(io)
|
clientNotification.use(io)
|
||||||
|
|
||||||
const imapService = new ImapService(config)
|
const imapService = new ImapService(config)
|
||||||
|
debug('IMAP service initialized')
|
||||||
const mailProcessingService = new MailProcessingService(
|
const mailProcessingService = new MailProcessingService(
|
||||||
new MailRepository(),
|
new MailRepository(),
|
||||||
imapService,
|
imapService,
|
||||||
clientNotification,
|
clientNotification,
|
||||||
config
|
config
|
||||||
)
|
)
|
||||||
|
debug('Mail processing service initialized')
|
||||||
|
|
||||||
// Put everything together:
|
// Put everything together:
|
||||||
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
|
imapService.on(ImapService.EVENT_NEW_MAIL, mail =>
|
||||||
mailProcessingService.onNewMail(mail)
|
mailProcessingService.onNewMail(mail)
|
||||||
)
|
)
|
||||||
|
debug('Bound IMAP new mail event handler')
|
||||||
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () =>
|
||||||
mailProcessingService.onInitialLoadDone()
|
mailProcessingService.onInitialLoadDone()
|
||||||
)
|
)
|
||||||
|
debug('Bound IMAP initial load done event handler')
|
||||||
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
|
imapService.on(ImapService.EVENT_DELETED_MAIL, mail =>
|
||||||
mailProcessingService.onMailDeleted(mail)
|
mailProcessingService.onMailDeleted(mail)
|
||||||
)
|
)
|
||||||
|
debug('Bound IMAP deleted mail event handler')
|
||||||
|
|
||||||
mailProcessingService.on('error', err => {
|
mailProcessingService.on('error', err => {
|
||||||
|
debug('Fatal error from mail processing service:', err.message)
|
||||||
console.error('Error from mailProcessingService, stopping.', err)
|
console.error('Error from mailProcessingService, stopping.', err)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
imapService.on(ImapService.EVENT_ERROR, error => {
|
imapService.on(ImapService.EVENT_ERROR, error => {
|
||||||
|
debug('Fatal error from IMAP service:', error.message)
|
||||||
console.error('Fatal error from IMAP service', error)
|
console.error('Fatal error from IMAP service', error)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.set('mailProcessingService', mailProcessingService)
|
app.set('mailProcessingService', mailProcessingService)
|
||||||
|
|
||||||
|
debug('Starting IMAP connection and message loading')
|
||||||
imapService.connectAndLoadMessages().catch(error => {
|
imapService.connectAndLoadMessages().catch(error => {
|
||||||
|
debug('Failed to connect to IMAP:', error.message)
|
||||||
console.error('Fatal error from IMAP service', error)
|
console.error('Fatal error from IMAP service', error)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
151
app.json
151
app.json
|
|
@ -1,71 +1,84 @@
|
||||||
{
|
{
|
||||||
"name": "48hr.email | Disposable email",
|
"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.",
|
"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",
|
"repository": "https://github.com/Crazyco-xyz/48hr.email",
|
||||||
"logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png",
|
"logo": "https://github.com/Crazyco-xyz/48hr.email/blob/main/infrastructure/web/public/images/logo.png",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"node",
|
"node",
|
||||||
"disposable-mail"
|
"disposable-mail"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"EMAIL_DOMAINS": {
|
"EMAIL_DOMAINS": {
|
||||||
"description": "Email domains"
|
"description": "List of domains your service handles"
|
||||||
},
|
},
|
||||||
"EMAIL_PURGE_TIME": {
|
"EMAIL_PURGE_TIME": {
|
||||||
"description": "Config for when to purge the emails",
|
"description": "Time value for when to purge",
|
||||||
"value": {
|
"value": 48
|
||||||
"time": 48,
|
},
|
||||||
"unit": "hours",
|
"EMAIL_PURGE_UNIT": {
|
||||||
"convert": true
|
"description": "Time unit for purging (minutes, hours, days)",
|
||||||
}
|
"value": "hours"
|
||||||
},
|
},
|
||||||
"EMAIL_EXAMPLES": {
|
"EMAIL_PURGE_CONVERT": {
|
||||||
"description": "Examples of the domains",
|
"description": "Convert to highest sensible unit and round",
|
||||||
"value": {
|
"value": true
|
||||||
"account": "example@48hr.email",
|
},
|
||||||
"uids": [1, 2, 3]
|
"EMAIL_EXAMPLE_ACCOUNT": {
|
||||||
}
|
"description": "Example email account to preserve",
|
||||||
},
|
"value": "example@48hr.email"
|
||||||
"IMAP_USER": {
|
},
|
||||||
"description": "Username to login to the imap server"
|
"EMAIL_EXAMPLE_UIDS": {
|
||||||
},
|
"description": "Example UIDs to preserve",
|
||||||
"IMAP_PASSWORD": {
|
"value": [1, 2, 3]
|
||||||
"description": "Password to login to the imap server"
|
},
|
||||||
},
|
"IMAP_USER": {
|
||||||
"IMAP_SERVER": {
|
"description": "Username to login to the imap server"
|
||||||
"description": "Hostname of the server (usually imap.example.com or mx.example.com)"
|
},
|
||||||
},
|
"IMAP_PASSWORD": {
|
||||||
"IMAP_PORT": {
|
"description": "Password to login to the imap server"
|
||||||
"description": "Port of the server (usually 993)",
|
},
|
||||||
"value": 993
|
"IMAP_SERVER": {
|
||||||
},
|
"description": "Hostname of the server (usually imap.example.com or mx.example.com)"
|
||||||
"IMAP_TLS": {
|
},
|
||||||
"description": "Use tls or not",
|
"IMAP_PORT": {
|
||||||
"value": true
|
"description": "Port of the server (usually 993)",
|
||||||
},
|
"value": 993
|
||||||
"IMAP_AUTHTIMEOUT": {
|
},
|
||||||
"description": "Timeout for the auth",
|
"IMAP_TLS": {
|
||||||
"value": 3000
|
"description": "Use tls or not",
|
||||||
},
|
"value": true
|
||||||
"IMAP_REFRESH_INTERVAL_SECONDS": {
|
},
|
||||||
"description": "How often to refresh the imap messages manually",
|
"IMAP_AUTH_TIMEOUT": {
|
||||||
"value": 60
|
"description": "Timeout for the auth in milliseconds",
|
||||||
},
|
"value": 3000
|
||||||
"HTTP_PORT": {
|
},
|
||||||
"description": "Port to listen on",
|
"IMAP_REFRESH_INTERVAL_SECONDS": {
|
||||||
"value": 3000
|
"description": "How often to refresh the imap messages manually",
|
||||||
},
|
"value": 60
|
||||||
"HTTP_BRANDING": {
|
},
|
||||||
"description": "The branding of the site",
|
"IMAP_FETCH_CHUNK": {
|
||||||
"value": ["48hr.email", "Crazyco", "https://crazyco.xyz"]
|
"description": "Number of UIDs per fetch chunk during initial load",
|
||||||
},
|
"value": 200
|
||||||
"HTTP_DISPLAY_SORT": {
|
},
|
||||||
"description": "Sort the emails for use",
|
"IMAP_CONCURRENCY": {
|
||||||
"value": 0
|
"description": "Number of concurrent fetch workers during initial load",
|
||||||
},
|
"value": 6
|
||||||
"HTTP_HIDE_OTHER": {
|
},
|
||||||
"description": "Hide other emails from the list besides the first",
|
"HTTP_PORT": {
|
||||||
"value": false
|
"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
|
// config.js
|
||||||
require("dotenv").config({ quiet: true });
|
require("dotenv").config({ quiet: true });
|
||||||
|
const debug = require('debug')('48hr-email:config')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely parse a value from env.
|
* Safely parse a value from env.
|
||||||
|
|
@ -63,12 +64,17 @@ const config = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// validation
|
// validation
|
||||||
|
debug('Validating configuration...')
|
||||||
if (!config.imap.user || !config.imap.password || !config.imap.host) {
|
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.");
|
throw new Error("IMAP is not configured. Check IMAP_* env vars.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.email.domains.length) {
|
if (!config.email.domains.length) {
|
||||||
|
debug('Email domains validation failed: no domains configured')
|
||||||
throw new Error("No EMAIL_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;
|
module.exports = config;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
const config = require('./config')
|
const config = require('./config')
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
|
const debug = require('debug')('48hr-email:helper')
|
||||||
|
|
||||||
class Helper {
|
class Helper {
|
||||||
|
|
||||||
|
|
@ -8,9 +9,11 @@ class Helper {
|
||||||
* @returns {Date}
|
* @returns {Date}
|
||||||
*/
|
*/
|
||||||
purgeTimeStamp() {
|
purgeTimeStamp() {
|
||||||
return moment()
|
const cutoff = moment()
|
||||||
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
|
.subtract(config.email.purgeTime.time, config.email.purgeTime.unit)
|
||||||
.toDate()
|
.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 nowMs = now instanceof Date ? now.getTime() : now;
|
||||||
const pastMs = past instanceof Date ? past.getTime() : new Date(past).getTime();
|
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),
|
* Convert time to highest possible unit (minutes → hours → days),
|
||||||
* rounding if necessary and prefixing "~" when rounded.
|
* rounding if necessary and prefixing "~" when rounded.
|
||||||
|
|
@ -76,9 +81,8 @@ class Helper {
|
||||||
}
|
}
|
||||||
|
|
||||||
const footer = `<label title="${Tooltip}">
|
const footer = `<label title="${Tooltip}">
|
||||||
<h4 style="display: inline;"><u><i>${time}</i></u></h4>
|
<h4 style="display: inline;"><u><i>${time}</i></u></h4>
|
||||||
</Label>`
|
</Label>`
|
||||||
|
|
||||||
return footer
|
return footer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +91,6 @@ class Helper {
|
||||||
* @param {Array} array
|
* @param {Array} array
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
shuffleArray(array) {
|
shuffleArray(array) {
|
||||||
for (let i = array.length - 1; i >= 0; i--) {
|
for (let i = array.length - 1; i >= 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
|
@ -101,7 +104,6 @@ class Helper {
|
||||||
* @param {Array} array
|
* @param {Array} array
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
shuffleFirstItem(array) {
|
shuffleFirstItem(array) {
|
||||||
let first = array[Math.floor(Math.random() * array.length)]
|
let first = array[Math.floor(Math.random() * array.length)]
|
||||||
array = array.filter((value) => value != first);
|
array = array.filter((value) => value != first);
|
||||||
|
|
@ -126,17 +128,26 @@ class Helper {
|
||||||
* Get a domain list from config for use
|
* Get a domain list from config for use
|
||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
getDomains() {
|
getDomains() {
|
||||||
|
debug(`Getting domains with displaySort: ${config.http.displaySort}`)
|
||||||
|
let result;
|
||||||
switch (config.http.displaySort) {
|
switch (config.http.displaySort) {
|
||||||
case 0:
|
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:
|
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:
|
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:
|
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 addressparser = require('nodemailer/lib/addressparser')
|
||||||
const pSeries = require('p-series')
|
const pSeries = require('p-series')
|
||||||
const retry = require('async-retry')
|
const retry = require('async-retry')
|
||||||
const debug = require('debug')('48hr-email:imap')
|
const debug = require('debug')('48hr-email:imap-manager')
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const moment = require('moment')
|
const moment = require('moment')
|
||||||
const Mail = require('../domain/mail')
|
const Mail = require('../domain/mail')
|
||||||
|
|
@ -133,7 +133,7 @@ class ImapService extends EventEmitter {
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.connection.openBox('INBOX')
|
await this.connection.openBox('INBOX')
|
||||||
debug('Connected to imap')
|
debug('Connected to imap Server at ' + this.config.imap.host)
|
||||||
}, {
|
}, {
|
||||||
retries: 5
|
retries: 5
|
||||||
}
|
}
|
||||||
|
|
@ -173,8 +173,14 @@ class ImapService extends EventEmitter {
|
||||||
debug('Load skipped: another load already in progress')
|
debug('Load skipped: another load already in progress')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingInProgress = true
|
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 uids = await this._getAllUids()
|
||||||
const newUids = uids.filter(uid => !this.loadedUids.has(uid))
|
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}`)
|
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
|
this.loadingInProgress = false
|
||||||
debug('Load finished')
|
debug('Finished updating mail summary list')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const EventEmitter = require('events')
|
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 mem = require('mem')
|
||||||
const ImapService = require('./imap-service')
|
const ImapService = require('./imap-service')
|
||||||
const Helper = require('./helper')
|
const Helper = require('./helper')
|
||||||
|
|
@ -33,29 +33,36 @@ class MailProcessingService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMailSummaries(address) {
|
getMailSummaries(address) {
|
||||||
|
debug('Getting mail summaries for', address)
|
||||||
return this.mailRepository.getForRecipient(address)
|
return this.mailRepository.getForRecipient(address)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSpecificEmail(adress, uid) {
|
deleteSpecificEmail(adress, uid) {
|
||||||
|
debug('Deleting specific email', adress, uid)
|
||||||
if (this.mailRepository.removeUid(uid, adress) == true) {
|
if (this.mailRepository.removeUid(uid, adress) == true) {
|
||||||
this.imapService.deleteSpecificEmail(uid)
|
this.imapService.deleteSpecificEmail(uid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getOneFullMail(address, uid, raw = false) {
|
getOneFullMail(address, uid, raw = false) {
|
||||||
|
debug('Cache lookup for', address + ':' + uid, raw ? '(raw)' : '(parsed)')
|
||||||
return this.cachedFetchFullMail(address, uid, raw)
|
return this.cachedFetchFullMail(address, uid, raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllMailSummaries() {
|
getAllMailSummaries() {
|
||||||
|
debug('Getting all mail summaries')
|
||||||
return this.mailRepository.getAll()
|
return this.mailRepository.getAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
getCount() {
|
getCount() {
|
||||||
return this.mailRepository.mailCount()
|
const count = this.mailRepository.mailCount()
|
||||||
|
debug('Mail count requested:', count)
|
||||||
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
onInitialLoadDone() {
|
onInitialLoadDone() {
|
||||||
this.initialLoadDone = true
|
this.initialLoadDone = true
|
||||||
|
debug('Initial load completed, total mails:', this.mailRepository.mailCount())
|
||||||
console.log(`Initial load done, got ${this.mailRepository.mailCount()} mails`)
|
console.log(`Initial load done, got ${this.mailRepository.mailCount()} mails`)
|
||||||
console.log(`Fetching and deleting mails every ${this.config.imap.refreshIntervalSeconds} seconds`)
|
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`)
|
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 => {
|
mail.to.forEach(to => {
|
||||||
|
debug('Adding mail to repository for recipient:', to)
|
||||||
this.mailRepository.add(to, mail)
|
this.mailRepository.add(to, mail)
|
||||||
|
debug('Emitting notification for:', to)
|
||||||
return this.clientNotification.emit(to)
|
return this.clientNotification.emit(to)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -81,8 +90,11 @@ class MailProcessingService extends EventEmitter {
|
||||||
|
|
||||||
async _deleteOldMails() {
|
async _deleteOldMails() {
|
||||||
try {
|
try {
|
||||||
|
debug('Starting deletion of old mails')
|
||||||
await this.imapService.deleteOldMails(helper.purgeTimeStamp())
|
await this.imapService.deleteOldMails(helper.purgeTimeStamp())
|
||||||
|
debug('Completed deletion of old mails')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debug('Error deleting old messages:', error.message)
|
||||||
console.log('Cant delete old messages', error)
|
console.log('Cant delete old messages', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
|
||||||
padding: 10px 10px 0px;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #131516;
|
padding: 20px 20px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient( 135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)), #131516;
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
|
backdrop-filter: blur(18px) saturate(120%);
|
||||||
|
-webkit-backdrop-filter: blur(18px) saturate(120%);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
body::-webkit-scrollbar {
|
body::-webkit-scrollbar {
|
||||||
|
|
@ -21,6 +25,11 @@ a {
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #9b4cda;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
|
|
@ -43,10 +52,14 @@ p {
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
|
background-color: #1D2021;
|
||||||
|
color: white;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
height: 60vh;
|
height: 60vh;
|
||||||
border: 1px dotted black;
|
margin: 2rem auto;
|
||||||
margin-left: 10%;
|
display: block;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
|
|
@ -71,11 +84,33 @@ text-muted {
|
||||||
|
|
||||||
.action-links {
|
.action-links {
|
||||||
float: right;
|
float: right;
|
||||||
text-align: end;
|
justify-content: flex-end;
|
||||||
|
/* aligns all links to the right */
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
/* ensures they stay in one line */
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-links a {
|
.action-links a {
|
||||||
display: block;
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(155, 77, 202, 0.2);
|
||||||
|
border: 1px solid rgba(155, 77, 202, 0.35);
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
backdrop-filter: blur(12px) saturate(120%);
|
||||||
|
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
||||||
|
box-shadow: 0 5px 15px rgba(155, 77, 202, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-links a:hover {
|
||||||
|
background: rgba(155, 77, 202, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(155, 77, 202, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -100,6 +135,13 @@ select:hover {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
background: linear-gradient( 135deg, rgba(155, 77, 202, 0.12), rgba(155, 77, 202, 0.04)), rgba(255, 255, 255, 0.04);
|
||||||
|
backdrop-filter: blur(20px) saturate(125%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(125%);
|
||||||
|
border-radius: 22px;
|
||||||
|
border: 1px solid rgba(155, 77, 202, 0.25);
|
||||||
|
padding: 40px 36px;
|
||||||
|
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(155, 77, 202, 0.08) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login h1,
|
#login h1,
|
||||||
|
|
@ -124,7 +166,7 @@ select:hover {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
background-color: #131516;
|
background: linear-gradient( 135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)), #131516;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,16 +198,32 @@ select:hover {
|
||||||
}
|
}
|
||||||
|
|
||||||
#login .buttons {
|
#login .buttons {
|
||||||
margin-top: 1.5rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
gap: 16px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
/* keeps them aligned right like action-links */
|
||||||
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login .buttons>* {
|
#login .buttons>* {
|
||||||
width: 100%;
|
flex: 1 1 auto;
|
||||||
flex: 1;
|
min-width: 120px;
|
||||||
font-size: 1.3rem;
|
border-radius: 20px;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(155, 77, 202, 0.2);
|
||||||
|
border: 1px solid rgba(155, 77, 202, 0.35);
|
||||||
|
backdrop-filter: blur(12px) saturate(120%);
|
||||||
|
-webkit-backdrop-filter: blur(12px) saturate(120%);
|
||||||
|
box-shadow: 0 8px 20px rgba(155, 77, 202, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login .buttons>*:hover {
|
||||||
|
background: rgba(155, 77, 202, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 12px 25px rgba(155, 77, 202, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mail_attachments {
|
.mail_attachments {
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,38 @@ const router = new express.Router()
|
||||||
const config = require('../../../application/config')
|
const config = require('../../../application/config')
|
||||||
const Helper = require('../../../application/helper')
|
const Helper = require('../../../application/helper')
|
||||||
const helper = new(Helper)
|
const helper = new(Helper)
|
||||||
|
const debug = require('debug')('48hr-email:routes')
|
||||||
|
|
||||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||||
|
|
||||||
router.get('/:address/:errorCode', async(req, res) => {
|
router.get('/:address/:errorCode', async(req, res, next) => {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
try {
|
||||||
const count = await mailProcessingService.getCount()
|
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
|
debug(`Rendering error page ${errorCode} with message: ${message}`)
|
||||||
const message = req.query.message || (req.session && req.session.errorMessage) || 'An error occurred'
|
res.status(errorCode)
|
||||||
|
res.render('error', {
|
||||||
res.status(errorCode)
|
title: `${config.http.branding[0]} | ${errorCode}`,
|
||||||
res.render('error', {
|
purgeTime: purgeTime,
|
||||||
title: `${config.http.branding[0]} | ${errorCode}`,
|
address: req.params.address,
|
||||||
purgeTime: purgeTime,
|
count: count,
|
||||||
address: req.params.address,
|
message: message,
|
||||||
count: count,
|
status: errorCode,
|
||||||
message: message,
|
branding: config.http.branding
|
||||||
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 express = require('express')
|
||||||
const router = new express.Router()
|
const router = new express.Router()
|
||||||
const { param } = require('express-validator')
|
const { param } = require('express-validator')
|
||||||
|
const debug = require('debug')('48hr-email:routes')
|
||||||
|
|
||||||
const config = require('../../../application/config')
|
const config = require('../../../application/config')
|
||||||
const Helper = require('../../../application/helper')
|
const Helper = require('../../../application/helper')
|
||||||
|
|
@ -16,17 +17,28 @@ const sanitizeAddress = param('address').customSanitizer(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, _next) => {
|
router.get('^/:address([^@/]+@[^@/]+)', sanitizeAddress, async(req, res, next) => {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
try {
|
||||||
const count = await mailProcessingService.getCount()
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
res.render('inbox', {
|
if (!mailProcessingService) {
|
||||||
title: `${config.http.branding[0]} | ` + req.params.address,
|
throw new Error('Mail processing service not available')
|
||||||
purgeTime: purgeTime,
|
}
|
||||||
address: req.params.address,
|
debug(`Inbox request for ${req.params.address}`)
|
||||||
count: count,
|
const count = await mailProcessingService.getCount()
|
||||||
mailSummaries: mailProcessingService.getMailSummaries(req.params.address),
|
debug(`Rendering inbox with ${count} total mails`)
|
||||||
branding: config.http.branding,
|
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(
|
router.get(
|
||||||
|
|
@ -35,6 +47,7 @@ router.get(
|
||||||
async(req, res, next) => {
|
async(req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
debug(`Viewing email ${req.params.uid} for ${req.params.address}`)
|
||||||
const count = await mailProcessingService.getCount()
|
const count = await mailProcessingService.getCount()
|
||||||
const mail = await mailProcessingService.getOneFullMail(
|
const mail = await mailProcessingService.getOneFullMail(
|
||||||
req.params.address,
|
req.params.address,
|
||||||
|
|
@ -48,6 +61,7 @@ router.get(
|
||||||
|
|
||||||
// Emails are immutable, cache if found
|
// Emails are immutable, cache if found
|
||||||
res.set('Cache-Control', 'private, max-age=600')
|
res.set('Cache-Control', 'private, max-age=600')
|
||||||
|
debug(`Rendering email view for UID ${req.params.uid}`)
|
||||||
res.render('mail', {
|
res.render('mail', {
|
||||||
title: mail.subject + " | " + req.params.address,
|
title: mail.subject + " | " + req.params.address,
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
|
|
@ -58,10 +72,12 @@ router.get(
|
||||||
branding: config.http.branding,
|
branding: config.http.branding,
|
||||||
})
|
})
|
||||||
} else {
|
} 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!'
|
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`)
|
res.redirect(`/error/${req.params.address}/404`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debug(`Error fetching email ${req.params.uid} for ${req.params.address}:`, error.message)
|
||||||
console.error('Error while fetching email', error)
|
console.error('Error while fetching email', error)
|
||||||
next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
|
|
@ -75,12 +91,15 @@ router.get(
|
||||||
async(req, res, next) => {
|
async(req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
debug(`Deleting all emails for ${req.params.address}`)
|
||||||
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
|
const mailSummaries = await mailProcessingService.getMailSummaries(req.params.address)
|
||||||
for (mail in mailSummaries) {
|
for (mail in mailSummaries) {
|
||||||
await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid)
|
await mailProcessingService.deleteSpecificEmail(req.params.address, mailSummaries[mail].uid)
|
||||||
}
|
}
|
||||||
|
debug(`Deleted all emails for ${req.params.address}`)
|
||||||
res.redirect(`/inbox/${req.params.address}`)
|
res.redirect(`/inbox/${req.params.address}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debug(`Error deleting all emails for ${req.params.address}:`, error.message)
|
||||||
console.error('Error while deleting email', error)
|
console.error('Error while deleting email', error)
|
||||||
next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
|
|
@ -95,9 +114,12 @@ router.get(
|
||||||
async(req, res, next) => {
|
async(req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
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)
|
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}`)
|
res.redirect(`/inbox/${req.params.address}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debug(`Error deleting email ${req.params.uid} for ${req.params.address}:`, error.message)
|
||||||
console.error('Error while deleting email', error)
|
console.error('Error while deleting email', error)
|
||||||
next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
|
|
@ -110,11 +132,13 @@ router.get(
|
||||||
async(req, res, next) => {
|
async(req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
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 uid = parseInt(req.params.uid, 10)
|
||||||
const count = await mailProcessingService.getCount()
|
const count = await mailProcessingService.getCount()
|
||||||
|
|
||||||
// Validate UID is a valid integer
|
// Validate UID is a valid integer
|
||||||
if (isNaN(uid) || uid <= 0) {
|
if (isNaN(uid) || uid <= 0) {
|
||||||
|
debug(`Invalid UID provided: ${req.params.uid}`)
|
||||||
req.session.errorMessage = 'Invalid/Malformed UID provided.'
|
req.session.errorMessage = 'Invalid/Malformed UID provided.'
|
||||||
return res.redirect(`/error/${req.params.address}/400`)
|
return res.redirect(`/error/${req.params.address}/400`)
|
||||||
}
|
}
|
||||||
|
|
@ -125,6 +149,7 @@ router.get(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!mail || !mail.attachments) {
|
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!'
|
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`)
|
return res.redirect(`/error/${req.params.address}/404`)
|
||||||
}
|
}
|
||||||
|
|
@ -134,20 +159,24 @@ router.get(
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
try {
|
try {
|
||||||
|
debug(`Serving attachment: ${attachment.filename}`)
|
||||||
res.set('Content-Disposition', `attachment; filename=${attachment.filename}`);
|
res.set('Content-Disposition', `attachment; filename=${attachment.filename}`);
|
||||||
res.set('Content-Type', attachment.contentType);
|
res.set('Content-Type', attachment.contentType);
|
||||||
res.send(attachment.content);
|
res.send(attachment.content);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debug(`Error serving attachment: ${error.message}`)
|
||||||
console.error('Error while fetching attachment', error);
|
console.error('Error while fetching attachment', error);
|
||||||
next(error);
|
next(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} 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!'
|
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`)
|
return res.redirect(`/error/${req.params.address}/404`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debug(`Error fetching attachment: ${error.message}`)
|
||||||
console.error('Error while fetching attachment', error)
|
console.error('Error while fetching attachment', error)
|
||||||
next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
|
|
@ -162,11 +191,13 @@ router.get(
|
||||||
async(req, res, next) => {
|
async(req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
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 uid = parseInt(req.params.uid, 10)
|
||||||
const count = await mailProcessingService.getCount()
|
const count = await mailProcessingService.getCount()
|
||||||
|
|
||||||
// Validate UID is a valid integer
|
// Validate UID is a valid integer
|
||||||
if (isNaN(uid) || uid <= 0) {
|
if (isNaN(uid) || uid <= 0) {
|
||||||
|
debug(`Invalid UID provided for raw view: ${req.params.uid}`)
|
||||||
req.session.errorMessage = 'Invalid/Malformed UID provided.'
|
req.session.errorMessage = 'Invalid/Malformed UID provided.'
|
||||||
return res.redirect(`/error/${req.params.address}/400`)
|
return res.redirect(`/error/${req.params.address}/400`)
|
||||||
}
|
}
|
||||||
|
|
@ -180,15 +211,18 @@ router.get(
|
||||||
mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
mail = mail.replace(/(?:\r\n|\r|\n)/g, '<br>')
|
||||||
// Emails are immutable, cache if found
|
// Emails are immutable, cache if found
|
||||||
res.set('Cache-Control', 'private, max-age=600')
|
res.set('Cache-Control', 'private, max-age=600')
|
||||||
|
debug(`Rendering raw email view for UID ${req.params.uid}`)
|
||||||
res.render('raw', {
|
res.render('raw', {
|
||||||
title: req.params.uid + " | raw | " + req.params.address,
|
title: req.params.uid + " | raw | " + req.params.address,
|
||||||
mail
|
mail
|
||||||
})
|
})
|
||||||
} else {
|
} 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!'
|
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`)
|
res.redirect(`/error/${req.params.address}/404`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
debug(`Error fetching raw email ${req.params.uid}: ${error.message}`)
|
||||||
console.error('Error while fetching raw email', error)
|
console.error('Error while fetching raw email', error)
|
||||||
next(error)
|
next(error)
|
||||||
}
|
}
|
||||||
|
|
@ -206,4 +240,4 @@ router.get(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const router = new express.Router()
|
const router = new express.Router()
|
||||||
const { check, validationResult } = require('express-validator')
|
const { check, validationResult } = require('express-validator')
|
||||||
|
const debug = require('debug')('48hr-email:routes')
|
||||||
|
|
||||||
const randomWord = require('random-word')
|
const randomWord = require('random-word')
|
||||||
const config = require('../../../application/config')
|
const config = require('../../../application/config')
|
||||||
|
|
@ -9,21 +10,36 @@ const helper = new(Helper)
|
||||||
|
|
||||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||||
|
|
||||||
router.get('/', async(req, res, _next) => {
|
router.get('/', async(req, res, next) => {
|
||||||
const count = await req.app.get('mailProcessingService').getCount()
|
try {
|
||||||
res.render('login', {
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
if (!mailProcessingService) {
|
||||||
username: randomWord(),
|
throw new Error('Mail processing service not available')
|
||||||
purgeTime: purgeTime,
|
}
|
||||||
domains: helper.getDomains(),
|
debug('Login page requested')
|
||||||
count: count,
|
const count = await mailProcessingService.getCount()
|
||||||
branding: config.http.branding,
|
debug(`Rendering login page with ${count} total mails`)
|
||||||
example: config.email.examples.account,
|
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) => {
|
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) => {
|
router.get('/logout', (req, res, _next) => {
|
||||||
|
|
@ -40,22 +56,35 @@ router.post(
|
||||||
check('username').isLength({ min: 1 }),
|
check('username').isLength({ min: 1 }),
|
||||||
check('domain').isIn(config.email.domains)
|
check('domain').isIn(config.email.domains)
|
||||||
],
|
],
|
||||||
async(req, res) => {
|
async(req, res, next) => {
|
||||||
const errors = validationResult(req)
|
try {
|
||||||
const count = await req.app.get('mailProcessingService').getCount()
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
if (!errors.isEmpty()) {
|
if (!mailProcessingService) {
|
||||||
return res.render('login', {
|
throw new Error('Mail processing service not available')
|
||||||
userInputError: true,
|
}
|
||||||
title: `${config.http.branding[0]} | Your temporary Inbox`,
|
const errors = validationResult(req)
|
||||||
purgeTime: purgeTime,
|
const count = await mailProcessingService.getCount()
|
||||||
username: randomWord(),
|
if (!errors.isEmpty()) {
|
||||||
domains: helper.getDomains(),
|
debug(`Login validation failed for ${req.body.username}@${req.body.domain}: ${errors.array().map(e => e.msg).join(', ')}`)
|
||||||
count: count,
|
return res.render('login', {
|
||||||
branding: config.http.branding,
|
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>
|
<title>{{ title }}</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
|
<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 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">
|
<meta property="og:image" content="/images/logo.png">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimal-ui">
|
<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 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">
|
<meta property="og:image" content="/images/logo.png">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ const Helper = require('../../application/helper')
|
||||||
const helper = new(Helper)
|
const helper = new(Helper)
|
||||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
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
|
// Init express middleware
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(helmet())
|
app.use(helmet())
|
||||||
|
|
@ -34,7 +41,7 @@ app.use(express.urlencoded({ extended: false }))
|
||||||
|
|
||||||
// Session middleware
|
// Session middleware
|
||||||
app.use(session({
|
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,
|
resave: false,
|
||||||
saveUninitialized: true,
|
saveUninitialized: true,
|
||||||
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours
|
cookie: { maxAge: 1000 * 60 * 60 * 24 } // 24 hours
|
||||||
|
|
@ -82,22 +89,29 @@ app.use((req, res, next) => {
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
app.use(async(err, req, res, _next) => {
|
app.use(async(err, req, res, _next) => {
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
try {
|
||||||
const count = await mailProcessingService.getCount()
|
debug('Error handler triggered:', err.message)
|
||||||
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
const count = await mailProcessingService.getCount()
|
||||||
|
|
||||||
// Set locals, only providing error in development
|
// Set locals, only providing error in development
|
||||||
res.locals.message = err.message
|
res.locals.message = err.message
|
||||||
res.locals.error = req.app.get('env') === 'development' ? err : {}
|
res.locals.error = req.app.get('env') === 'development' ? err : {}
|
||||||
|
|
||||||
// Render the error page
|
// Render the error page
|
||||||
res.status(err.status || 500)
|
res.status(err.status || 500)
|
||||||
res.render('error', {
|
res.render('error', {
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
address: req.params.address,
|
address: req.params && req.params.address,
|
||||||
count: count,
|
count: count,
|
||||||
branding: config.http.branding
|
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)
|
debug('Listening on ' + bind)
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = { app, io, server }
|
module.exports = { app, io, server }
|
||||||
17614
package-lock.json
generated
17614
package-lock.json
generated
File diff suppressed because it is too large
Load diff
160
package.json
160
package.json
|
|
@ -1,83 +1,85 @@
|
||||||
{
|
{
|
||||||
"name": "48hr.email",
|
"name": "48hr.email",
|
||||||
"version": "1.6.1",
|
"version": "1.6.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"description": "48hr.email is your favorite open-source tempmail client. ",
|
||||||
"start": "node --trace-warnings ./app.js",
|
"keywords": [
|
||||||
"test": "xo",
|
"tempmail",
|
||||||
"debug": "node --nolazy --inspect-brk=9229 ./app.js"
|
"48hr.email",
|
||||||
},
|
"disposable-email"
|
||||||
"dependencies": {
|
],
|
||||||
"array.prototype.flatmap": "^1.3.2",
|
"homepage": "https://48hr.email/",
|
||||||
"async-retry": "^1.3.3",
|
"bugs": {
|
||||||
"compression": "^1.7.4",
|
"url": "https://github.com/Crazyco-xyz/48hr.email/issues"
|
||||||
"debug": "^2.6.9",
|
|
||||||
"dotenv": "^17.2.3",
|
|
||||||
"encodings": "^1.0.0",
|
|
||||||
"express": "^4.21.1",
|
|
||||||
"express-validator": "^7.2.0",
|
|
||||||
"helmet": "^3.23.3",
|
|
||||||
"http-errors": "~1.6.2",
|
|
||||||
"imap-simple": "^4.3.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"mailparser": "^3.7.1",
|
|
||||||
"mem": "^4.3.0",
|
|
||||||
"mnemonist": "^0.27.2",
|
|
||||||
"moment": "^2.30.1",
|
|
||||||
"morgan": "~1.9.0",
|
|
||||||
"nodemailer": "^6.9.15",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"xo": {
|
|
||||||
"semicolon": false,
|
|
||||||
"prettier": true,
|
|
||||||
"rules": {
|
|
||||||
"no-unused-vars": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"argsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"overrides": [
|
"repository": {
|
||||||
{
|
"type": "git",
|
||||||
"files": "public/javascripts/*.js",
|
"url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
|
||||||
"esnext": false,
|
},
|
||||||
"env": [
|
"license": "GPL-3.0",
|
||||||
"browser"
|
"author": "ClaraCrazy",
|
||||||
],
|
"type": "commonjs",
|
||||||
"globals": [
|
"main": "app.js",
|
||||||
"io"
|
"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.3",
|
||||||
|
"async-retry": "^1.3.3",
|
||||||
|
"compression": "^1.8.1",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"encodings": "^1.0.0",
|
||||||
|
"express": "^4.22.1",
|
||||||
|
"express-session": "^1.18.2",
|
||||||
|
"express-validator": "^7.3.1",
|
||||||
|
"helmet": "^3.23.3",
|
||||||
|
"http-errors": "~1.6.2",
|
||||||
|
"imap-simple": "^1.6.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mailparser": "^3.9.1",
|
||||||
|
"mem": "^4.3.0",
|
||||||
|
"mnemonist": "^0.27.2",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"morgan": "^1.10.1",
|
||||||
|
"nodemailer": "^7.0.12",
|
||||||
|
"p-series": "^2.1.0",
|
||||||
|
"random-word": "^2.0.0",
|
||||||
|
"sanitize-html": "^2.17.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
|
"socket.io": "^4.8.3",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "22.x"
|
|
||||||
},
|
|
||||||
"description": "48hr.email is your favorite open-source tempmail client. ",
|
|
||||||
"main": "app.js",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/Crazyco-xyz/48hr.email.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tempmail",
|
|
||||||
"48hr.email",
|
|
||||||
"disposable-email"
|
|
||||||
],
|
|
||||||
"author": "ClaraCrazy",
|
|
||||||
"license": "GPL-3.0",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/Crazyco-xyz/48hr.email/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://48hr.email/"
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue