mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
Adds UX debug mode while mocking the imap server and other critical parts of the service simply to test UI elements for faster development
330 lines
No EOL
12 KiB
JavaScript
330 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 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
|
|
|
|
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 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('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)
|
|
}
|
|
}) |