mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
338 lines
No EOL
12 KiB
JavaScript
338 lines
No EOL
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/* eslint unicorn/no-process-exit: 0 */
|
|
|
|
// Check .env file permissions before loading config
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const envPath = path.resolve('.env')
|
|
if (fs.existsSync(envPath)) {
|
|
const mode = fs.statSync(envPath).mode
|
|
const perms = (mode & parseInt('777', 8)).toString(8)
|
|
const groupReadable = parseInt(perms[1], 10) >= 4
|
|
const otherReadable = parseInt(perms[2], 10) >= 4
|
|
if (groupReadable || otherReadable) {
|
|
console.error(`\nSECURITY ERROR: .env file has insecure permissions (${perms})`)
|
|
console.error(`Run: chmod 600 ${envPath}\n`)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
const config = require('./application/config')
|
|
const debug = require('debug')('48hr-email:app')
|
|
const Helper = require('./application/helper')
|
|
const helper = new(Helper)
|
|
const { app, io, server } = require('./infrastructure/web/web')
|
|
const ClientNotification = require('./infrastructure/web/client-notification')
|
|
const ImapService = require('./application/imap-service')
|
|
const MockMailService = require('./application/mocks/mock-mail-service')
|
|
const MailProcessingService = require('./application/mail-processing-service')
|
|
const SmtpService = require('./application/smtp-service')
|
|
const AuthService = require('./application/auth-service')
|
|
const MockAuthService = require('./application/mocks/mock-auth-service')
|
|
const MailRepository = require('./domain/mail-repository')
|
|
const InboxLock = require('./domain/inbox-lock')
|
|
const MockInboxLock = require('./application/mocks/mock-inbox-lock')
|
|
const VerificationStore = require('./domain/verification-store')
|
|
const UserRepository = require('./domain/user-repository')
|
|
const MockUserRepository = require('./application/mocks/mock-user-repository')
|
|
const StatisticsStore = require('./domain/statistics-store')
|
|
const ApiTokenRepository = require('./domain/api-token-repository')
|
|
|
|
const clientNotification = new ClientNotification()
|
|
debug('Client notification service initialized')
|
|
clientNotification.use(io)
|
|
|
|
// Initialize SMTP service only if not in UX debug mode
|
|
const smtpService = config.uxDebugMode ? null : new SmtpService(config)
|
|
if (smtpService) {
|
|
debug('SMTP service initialized')
|
|
} else {
|
|
debug('SMTP service disabled (UX debug mode)')
|
|
}
|
|
app.set('smtpService', smtpService)
|
|
|
|
const verificationStore = new VerificationStore()
|
|
debug('Verification store initialized')
|
|
app.set('verificationStore', verificationStore)
|
|
|
|
// Set config in app for route access
|
|
app.set('config', config)
|
|
|
|
// Initialize user repository and auth service (if enabled)
|
|
let inboxLock = null
|
|
let statisticsStore = null
|
|
let apiTokenRepository = null
|
|
|
|
if (config.user.authEnabled && !config.uxDebugMode) {
|
|
// Migrate legacy database files for backwards compatibility
|
|
Helper.migrateDatabase(config.user.databasePath)
|
|
|
|
const userRepository = new UserRepository(config.user.databasePath)
|
|
debug('User repository initialized')
|
|
app.set('userRepository', userRepository)
|
|
|
|
// Initialize API token repository with same database connection
|
|
apiTokenRepository = new ApiTokenRepository(userRepository.db)
|
|
debug('API token repository initialized')
|
|
app.set('apiTokenRepository', apiTokenRepository)
|
|
|
|
// Initialize statistics store with database connection
|
|
statisticsStore = new StatisticsStore(userRepository.db)
|
|
debug('Statistics store initialized with database persistence')
|
|
app.set('statisticsStore', statisticsStore)
|
|
|
|
const authService = new AuthService(userRepository, config)
|
|
debug('Auth service initialized')
|
|
app.set('authService', authService)
|
|
|
|
// Initialize inbox locking with user repository
|
|
inboxLock = new InboxLock(userRepository)
|
|
app.set('inboxLock', inboxLock)
|
|
debug('Inbox lock service initialized (user-based)')
|
|
|
|
// Check for inactive locked inboxes (users who haven't logged in for 7 days)
|
|
setInterval(() => {
|
|
const inactive = inboxLock.getInactive(config.user.lockReleaseHours)
|
|
if (inactive.length > 0) {
|
|
debug(`Auto-releasing ${inactive.length} locked inbox(es) due to user inactivity (${config.user.lockReleaseHours} hours without login)`)
|
|
inactive.forEach(lock => {
|
|
try {
|
|
inboxLock.release(lock.userId, lock.address)
|
|
debug(`Released lock on ${lock.address} for inactive user ${lock.userId}`)
|
|
} catch (error) {
|
|
debug(`Failed to release lock on ${lock.address}: ${error.message}`)
|
|
}
|
|
})
|
|
}
|
|
}, config.imap.refreshIntervalSeconds * 1000)
|
|
} else {
|
|
// No auth enabled OR UX debug mode - initialize statistics store without persistence
|
|
statisticsStore = new StatisticsStore()
|
|
if (config.uxDebugMode) {
|
|
debug('Statistics store initialized (UX debug mode - clean slate)')
|
|
|
|
// In UX debug mode, create mock auth system
|
|
const mockUserRepository = new MockUserRepository(config)
|
|
debug('Mock user repository initialized')
|
|
app.set('userRepository', mockUserRepository)
|
|
|
|
const mockAuthService = new MockAuthService()
|
|
debug('Mock auth service initialized')
|
|
app.set('authService', mockAuthService)
|
|
|
|
inboxLock = new MockInboxLock(mockUserRepository)
|
|
app.set('inboxLock', inboxLock)
|
|
debug('Mock inbox lock service initialized')
|
|
|
|
debug('Mock authentication system enabled for UX debug mode')
|
|
} else {
|
|
debug('Statistics store initialized (in-memory only, no database)')
|
|
app.set('userRepository', null)
|
|
app.set('apiTokenRepository', null)
|
|
app.set('authService', null)
|
|
app.set('inboxLock', null)
|
|
debug('User authentication system disabled')
|
|
}
|
|
app.set('statisticsStore', statisticsStore)
|
|
|
|
if (!config.uxDebugMode) {
|
|
debug('User authentication system disabled')
|
|
}
|
|
}
|
|
|
|
// Initialize IMAP or Mock service based on debug mode
|
|
const imapService = config.uxDebugMode ?
|
|
new MockMailService(config) :
|
|
new ImapService(config, inboxLock)
|
|
|
|
if (config.uxDebugMode) {
|
|
debug('Mock Mail Service initialized (UX Debug Mode)')
|
|
} else {
|
|
debug('IMAP service initialized')
|
|
}
|
|
app.set('imapService', imapService)
|
|
|
|
const mailProcessingService = new MailProcessingService(
|
|
new MailRepository(),
|
|
imapService,
|
|
clientNotification,
|
|
config,
|
|
smtpService,
|
|
verificationStore,
|
|
statisticsStore
|
|
)
|
|
debug('Mail processing service initialized')
|
|
|
|
// Initialize statistics with current count
|
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, async() => {
|
|
// In UX debug mode, populate mock emails first
|
|
if (config.uxDebugMode) {
|
|
// Load mock emails into repository
|
|
const mockEmails = imapService.getMockEmails()
|
|
mockEmails.forEach(({ mail }) => {
|
|
mailProcessingService.onNewMail(mail)
|
|
})
|
|
debug(`UX Debug Mode: Loaded ${mockEmails.length} mock emails`)
|
|
}
|
|
|
|
// Then initialize statistics with the correct count
|
|
const count = mailProcessingService.getCount()
|
|
statisticsStore.initialize(count)
|
|
|
|
if (config.uxDebugMode) {
|
|
statisticsStore.updateLargestUid(2) // 2 mock emails
|
|
debug(`UX Debug Mode: Statistics initialized with ${count} emails, largest UID: 2`)
|
|
} else {
|
|
// Get and set the largest UID for all-time total
|
|
const largestUid = await helper.getLargestUid(imapService)
|
|
statisticsStore.updateLargestUid(largestUid)
|
|
debug(`Statistics initialized with ${count} emails, largest UID: ${largestUid}`)
|
|
}
|
|
})
|
|
|
|
// Set up timer sync broadcasting after IMAP is ready
|
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => {
|
|
clientNotification.startTimerSync(imapService)
|
|
})
|
|
|
|
// Display startup banner when everything is ready
|
|
let imapReady = false
|
|
let serverReady = false
|
|
|
|
function displayStartupBanner() {
|
|
if (!imapReady || !serverReady) return
|
|
|
|
const mailCount = mailProcessingService.getCount()
|
|
const domains = config.email.domains.join(', ')
|
|
const purgeTime = `${config.email.purgeTime.time} ${config.email.purgeTime.unit}`
|
|
const refreshInterval = config.uxDebugMode ? 'N/A' : `${config.imap.refreshIntervalSeconds}s`
|
|
|
|
// Determine mode based on environment
|
|
let mode = 'PRODUCTION'
|
|
if (config.uxDebugMode) {
|
|
mode = 'UX DEBUG'
|
|
} else if (process.env.DEBUG) {
|
|
mode = 'DEBUG'
|
|
}
|
|
|
|
console.log('\n' + '═'.repeat(70))
|
|
console.log(` 48hr.email - ${mode} MODE`)
|
|
console.log('═'.repeat(70))
|
|
console.log(` Server: http://localhost:${config.http.port}`)
|
|
console.log(` Domains: ${domains}`)
|
|
console.log(` Emails loaded: ${mailCount}`)
|
|
console.log(` Purge after: ${purgeTime}`)
|
|
console.log(` IMAP refresh: ${refreshInterval}`)
|
|
|
|
if (!config.uxDebugMode && config.email.examples.account && config.email.examples.uids) {
|
|
console.log(` Example inbox: ${config.email.examples.account}`)
|
|
console.log(` Example UIDs: ${config.email.examples.uids.join(', ')}`)
|
|
}
|
|
|
|
if (config.uxDebugMode) {
|
|
console.log(` Authentication: Mock (any username/password works)`)
|
|
const mockUserRepo = app.get('userRepository')
|
|
if (mockUserRepo) {
|
|
console.log(` Demo forward: ${mockUserRepo.mockForwardEmail}`)
|
|
console.log(` Demo locked: ${mockUserRepo.mockLockedInbox}`)
|
|
}
|
|
} else if (config.user.authEnabled) {
|
|
console.log(` Authentication: Enabled`)
|
|
}
|
|
|
|
if (config.http.features.statistics) {
|
|
console.log(` Statistics: Enabled`)
|
|
}
|
|
|
|
console.log('═'.repeat(70))
|
|
console.log(` Ready! Press Ctrl+C to stop\n`)
|
|
}
|
|
|
|
imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => {
|
|
imapReady = true
|
|
displayStartupBanner()
|
|
})
|
|
|
|
server.on('ready', () => {
|
|
serverReady = true
|
|
displayStartupBanner()
|
|
})
|
|
|
|
// Track IMAP initialization state
|
|
let isImapReady = false
|
|
app.set('isImapReady', false)
|
|
|
|
// 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()
|
|
isImapReady = true
|
|
app.set('isImapReady', true)
|
|
})
|
|
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)
|
|
if (!config.uxDebugMode) {
|
|
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)
|
|
if (!config.uxDebugMode) {
|
|
process.exit(1)
|
|
}
|
|
})
|
|
|
|
app.set('mailProcessingService', mailProcessingService)
|
|
app.set('config', config)
|
|
|
|
app.locals.imapService = imapService
|
|
app.locals.mailProcessingService = mailProcessingService
|
|
|
|
if (config.uxDebugMode) {
|
|
debug('Starting Mock Mail Service (UX Debug Mode)')
|
|
} else {
|
|
debug('Starting IMAP connection and message loading')
|
|
}
|
|
|
|
imapService.connectAndLoadMessages().catch(error => {
|
|
debug('Failed to connect:', error.message)
|
|
console.error('Fatal error from mail service', error)
|
|
if (!config.uxDebugMode) {
|
|
process.exit(1)
|
|
}
|
|
})
|
|
|
|
server.on('error', error => {
|
|
if (error.syscall !== 'listen') {
|
|
console.error('Fatal web server error', error)
|
|
return
|
|
}
|
|
|
|
// Handle specific listen errors with friendly messages
|
|
switch (error.code) {
|
|
case 'EACCES':
|
|
console.error(
|
|
'Port ' + config.http.port + ' requires elevated privileges'
|
|
)
|
|
process.exit(1)
|
|
case 'EADDRINUSE':
|
|
console.error('Port ' + config.http.port + ' is already in use')
|
|
process.exit(1)
|
|
default:
|
|
console.error('Fatal web server error', error)
|
|
process.exit(1)
|
|
}
|
|
}) |