diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..99ca20b --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,12 @@ +module.exports = { + apps: [{ + name: "haraka-email", + script: "haraka", + args: "-c src/email_server --debug --nodejs --no-daemon", + cwd: "home/ploi/2weekmail.fyi", + watch: true, + env: { + NODE_ENV: "development", + } + }] +} \ No newline at end of file diff --git a/scripts/update_admin_password.js b/scripts/update_admin_password.js index 42cdf73..179e976 100644 --- a/scripts/update_admin_password.js +++ b/scripts/update_admin_password.js @@ -5,6 +5,7 @@ const knexConfig = require('../src/config/database'); const knex = Knex(knexConfig.development); Model.knex(knex); const crypto = require('crypto'); +const bcrypt = require('bcrypt'); const email = process.argv[2]; let password = process.argv[3]; @@ -30,7 +31,7 @@ async function main() { password = crypto.randomBytes(16).toString('hex'); } - user.password = password; + user.password = await bcrypt.hash(password, 10); await user.$query().patch(); console.log(`User password updated: ${user.email}`); diff --git a/src/db/migrations/20250131051814_stats_table.js b/src/db/migrations/20250131051814_stats_table.js new file mode 100644 index 0000000..fa1eafc --- /dev/null +++ b/src/db/migrations/20250131051814_stats_table.js @@ -0,0 +1,25 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema.createTable("stats", (table) => { + table.increments('id').primary(); + table.date('date').notNullable(); + table.integer('email_count').notNullable().defaultTo(0); + table.integer('message_count').notNullable().defaultTo(0); + table.timestamp('created_at').defaultTo(knex.fn.now()); + table.timestamp('updated_at').defaultTo(knex.fn.now()); + + table.unique('date'); + table.index('date'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema.dropTable('stats'); +}; diff --git a/src/db/models/DailyStats.js b/src/db/models/DailyStats.js new file mode 100644 index 0000000..59366ca --- /dev/null +++ b/src/db/models/DailyStats.js @@ -0,0 +1,93 @@ +const BaseModel = require('./Base'); +const { mysqlSafeTimestamp } = require('../../utils/functions'); + +class DailyStats extends BaseModel { + static get tableName() { + return 'stats'; + } + + static get jsonSchema() { + return { + type: 'object', + required: ['date', 'email_count', 'message_count'], + properties: { + id: { type: 'integer' }, + date: { type: 'string', format: 'date' }, + email_count: { type: 'integer', minimum: 0 }, + message_count: { type: 'integer', minimum: 0 }, + created_at: { type: 'string', format: 'date-time' }, + updated_at: { type: 'string', format: 'date-time' } + } + }; + } + + static async incrementEmailCount() { + const today = mysqlSafeTimestamp(true); + + return this.query() + .insert({ + date: today, + email_count: 1, + message_count: 0 + }) + .onConflict('date') + .merge({ + email_count: this.knex().raw('stats.email_count + 1'), + updated_at: mysqlSafeTimestamp() + }); + } + + static async incrementMessageCount() { + const today = mysqlSafeTimestamp(true); + + return this.query() + .insert({ + date: today, + email_count: 0, + message_count: 1 + }) + .onConflict('date') + .merge({ + message_count: this.knex().raw('stats.message_count + 1'), + updated_at: mysqlSafeTimestamp() + }); + } + + static async getTotalStats() { + const result = await this.query() + .select( + this.knex().raw('SUM(email_count) as total_emails'), + this.knex().raw('SUM(message_count) as total_messages') + ) + .first(); + + return { + emailsCreated: parseInt(result.total_emails) || 0, + messagesReceived: parseInt(result.total_messages) || 0 + }; + } + + static async getWeeklyStats() { + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(startDate.getDate() - 6); // Last 7 days including today + + const stats = await this.query() + .select('date', 'email_count', 'message_count') + .whereBetween('date', [ + startDate.toISOString().split('T')[0], + endDate.toISOString().split('T')[0] + ]) + .orderBy('date'); + + // Convert to Recharts format + const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return stats.map(stat => ({ + name: days[new Date(stat.date).getDay()], + emails: stat.email_count, + messages: stat.message_count + })); + } +} + +module.exports = DailyStats; \ No newline at end of file diff --git a/src/db/seeds/domains.js b/src/db/seeds/domains.js index dc71c1c..188a759 100644 --- a/src/db/seeds/domains.js +++ b/src/db/seeds/domains.js @@ -9,10 +9,12 @@ exports.seed = async function(knex) { { name: 'icantreadpls.fyi', active: true}, { name: '20is20butimnotgay.top', active: true}, { name: '20is20butimnotgay.fyi', active: true}, + { name: '20is20butimnotgay.com', active: true}, { name: 'bigwhitevanfbi.top', active: true}, { name: 'bigwhitevanfbi.fyi', active: true}, - { name: 'idonthaveabig.wang,wang', active: true}, - { name: '2weekmail.fyi', active: true}, - { name: '2weekmail.test', active: true} + { name: 'bigwhitevanfbi.com', active: true}, + { name: 'idonthaveabig.wang', active: true}, + { name: 'idonthaveabigwang.com', active: true}, + { name: '2weekmail.fyi', active: true} ]); }; \ No newline at end of file diff --git a/src/haraka-plugins/queue/store_message.js b/src/haraka-plugins/queue/store_message.js index 8df2417..1076161 100644 --- a/src/haraka-plugins/queue/store_message.js +++ b/src/haraka-plugins/queue/store_message.js @@ -1,6 +1,8 @@ exports.register = function () { const plugin = this; const MessageService = require("../../services/MessageService"); + const path = require('path'); + const DailyStats = require(path.join(__dirname, '../../../db/models/DailyStats')); plugin.store_message = async function (next, connection) { const transaction = connection.transaction; @@ -31,6 +33,7 @@ exports.register = function () { }; await MessageService.store(messageData); + await DailyStats.incrementMessageCount(); next(); } catch (error) { connection.logerror(plugin, `Failed to store message: ${error.message}`); diff --git a/src/routes/email.js b/src/routes/email.js index 5c9e5c1..48600ba 100644 --- a/src/routes/email.js +++ b/src/routes/email.js @@ -4,34 +4,8 @@ const { authenticateToken } = require('../middleware/auth'); const TempEmail = require('../db/models/TempEmail'); router.use(authenticateToken); const Domain = require('../db/models/Domain'); -const { uniqueNamesGenerator, adjectives, animals, colors, names, languages, starWars, countries } = require('unique-names-generator'); - -function generateUniqueName() { - const dictionaries = [adjectives, animals, colors, names, languages, starWars, countries]; - - const randomIndex1 = Math.floor(Math.random() * dictionaries.length); - let randomIndex2 = Math.floor(Math.random() * dictionaries.length); - - while (randomIndex2 === randomIndex1) { - randomIndex2 = Math.floor(Math.random() * dictionaries.length); - } - - const randomName = uniqueNamesGenerator({ - dictionaries: [dictionaries[randomIndex1], dictionaries[randomIndex2]], - separator: '-', - style: 'lowerCase' - }); - - const formattedName = randomName - .replace(/[^a-zA-Z0-9\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .replace(/^-+|-+$/g, ''); - - const randomNumber = Math.floor(Math.random() * 999999) + 1; - - return `${formattedName}${randomNumber}`; -} +const DailyStats = require('../db/models/DailyStats'); +const { generateUniqueName, getRandomDomain, mysqlSafeTimestamp } = require('../utils/functions'); /** * @swagger @@ -78,6 +52,10 @@ function generateUniqueName() { * type: string * description: Custom name for the temporary email (optional) * example: john-doe + * safeTLD: + * type: boolean + * description: Whether to return .com domains. Default is false. + * example: false * responses: * 200: * description: Temporary email successfully generated @@ -164,34 +142,36 @@ function generateUniqueName() { */ router.post('/generate', async (req, res) => { try { - const randomDomain = await Domain.query() - .where('active', true) - .orderByRaw('RAND()') - .first(); + const safeTLD = req.body.safeTLD || false; + const randomDomain = await getRandomDomain(safeTLD); + + if (randomDomain.status === 'error') { + res.status(500).json({ status: 'error', message: randomDomain.message }); + } const emailName = req.body.name || generateUniqueName(); - const randomDomainName = `${emailName}@${randomDomain.name}`; - - const cutoffDate = new Date(); - cutoffDate.setDate(cutoffDate.getDate() + 14); + const randomDomainName = `${emailName}@${randomDomain.domain.name}`; const tempEmail = await TempEmail.query().insert({ email: randomDomainName, user_id: req.user.id, - expires_at: cutoffDate + expires_at: mysqlSafeTimestamp(14) }); + + await DailyStats.incrementEmailCount(); + - res.json(tempEmail); + res.json({ status: 'success', email: { id: tempEmail.id, address: randomDomainName } }); } catch (error) { console.error('Error creating temp email:', error); - res.status(500).json({ error: error.message }); + res.status(500).json({ status: 'error', message: error.message }); } }); router.get('/list', async (req, res) => { const tempEmails = await TempEmail.query().where('user_id', req.user.id); - res.json(tempEmails); + res.json({ status: 'success', emails: tempEmails }); }); router.delete('/delete/:id', async (req, res) => { diff --git a/src/utils/functions.js b/src/utils/functions.js new file mode 100644 index 0000000..5839ea4 --- /dev/null +++ b/src/utils/functions.js @@ -0,0 +1,74 @@ +const { uniqueNamesGenerator, adjectives, animals, colors, names, languages, starWars, countries } = require('unique-names-generator'); +const Domain = require('../db/models/Domain'); + +/** + * @param {number} days - Number of days to add to the current date + * @returns {string} - MySQL safe timestamp + */ +function mysqlSafeTimestamp(dateOnly = false, days) { + if (dateOnly && days) { + return new Date(new Date().setDate(new Date().getDate() + days)).toISOString().split('T')[0]; + } + + if (days) { + return new Date(new Date().setDate(new Date().getDate() + days)).toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ''); + } + + if (dateOnly) { + return new Date().toISOString().split('T')[0]; + } + + return new Date().toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ''); +} + +function generateUniqueName() { + const dictionaries = [adjectives, animals, colors, names, languages, starWars, countries]; + + const randomIndex1 = Math.floor(Math.random() * dictionaries.length); + let randomIndex2 = Math.floor(Math.random() * dictionaries.length); + + while (randomIndex2 === randomIndex1) { + randomIndex2 = Math.floor(Math.random() * dictionaries.length); + } + + const randomName = uniqueNamesGenerator({ + dictionaries: [dictionaries[randomIndex1], dictionaries[randomIndex2]], + separator: '-', + style: 'lowerCase' + }); + + const formattedName = randomName + .replace(/[^a-zA-Z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, ''); + + const randomNumber = Math.floor(Math.random() * 999999) + 1; + + return `${formattedName}${randomNumber}`; +} + +async function getRandomDomain(safeTLD = false) { + let query = Domain.query().where('active', true); + + if (safeTLD) { + query = query.where('name', 'like', '%.com'); + } + + const domains = await query; + + if (domains.length === 0) { + return { status: 'error', message: 'No matching domains found' }; + } + + const randomDomain = domains[Math.floor(Math.random() * domains.length)]; + + return { status: 'success', domain: { id: randomDomain.id, name: randomDomain.name } }; +} + + +module.exports = { + mysqlSafeTimestamp, + generateUniqueName, + getRandomDomain +} \ No newline at end of file diff --git a/test.js b/test.js index c62c0d5..0ef148e 100644 --- a/test.js +++ b/test.js @@ -1,30 +1,19 @@ -const { uniqueNamesGenerator, adjectives, animals, colors, names, languages, starWars, countries } = require('unique-names-generator'); -function generateUniqueName() { - const dictionaries = [adjectives, animals, colors, names, languages, starWars, countries]; - - const randomIndex1 = Math.floor(Math.random() * dictionaries.length); - let randomIndex2 = Math.floor(Math.random() * dictionaries.length); - - while (randomIndex2 === randomIndex1) { - randomIndex2 = Math.floor(Math.random() * dictionaries.length); - } +const { Model } = require('objection'); +const Knex = require('knex'); +const knexConfig = require('./src/config/database'); +const knex = Knex(knexConfig.development); +Model.knex(knex); +const { getRandomDomain, mysqlSafeTimestamp } = require('./src/utils/functions'); +const DailyStats = require('./src/db/models/DailyStats'); - const randomName = uniqueNamesGenerator({ - dictionaries: [dictionaries[randomIndex1], dictionaries[randomIndex2]], - separator: '-', - style: 'lowerCase' - }); - - const formattedName = randomName - .replace(/[^a-zA-Z0-9\s-]/g, '') // Remove special characters except spaces and hyphens - .replace(/\s+/g, '-') // Replace spaces with hyphens - .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen - .replace(/^-+|-+$/g, ''); // Remove hyphens from start and end - - const randomNumber = Math.floor(Math.random() * 999999) + 1; - - return `${formattedName}${randomNumber}`; +async function main() { + // const domain = await getRandomDomain(true); + // console.log(domain); + // console.log(mysqlSafeTimestamp(true, 14)); + const stats = await DailyStats.getTotalStats(); + console.log(stats); + process.exit(0); } -console.log(generateUniqueName()); \ No newline at end of file +main(); \ No newline at end of file