From 3b92e317d0f4dc16d66a38419e2d6e625350cd7f Mon Sep 17 00:00:00 2001 From: kromskii2 Date: Thu, 10 Sep 2026 15:15:19 +0700 Subject: [PATCH] Add email address generator: 3 styles, 14 domains, inline menu, /email command, tests --- index.js | 105 ++++++++++++++++++++++++++++++++++++++++++++++- lib/generator.js | 36 ++++++++++++++++ package.json | 2 +- tests/email.mjs | 17 ++++++++ 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/email.mjs diff --git a/index.js b/index.js index 8ec4ea7..f286c9c 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ import 'dotenv/config'; import fs from 'node:fs'; import { Telegraf, Markup } from 'telegraf'; -import { generatePassword, strength } from './lib/generator.js'; +import { generatePassword, strength, generateEmail, EMAIL_DOMAINS } from './lib/generator.js'; const boot = (m) => fs.appendFileSync( @@ -28,6 +28,9 @@ const defaults = () => ({ symbols: true, excludeAmbiguous: false, pronounceable: false, + emailStyle: 0, + emailDomain: 0, + emailNumber: true, }); const users = new Map(); const history = new Map(); // userId -> [passwords] @@ -49,6 +52,7 @@ const mainMenu = () => Markup.inlineKeyboard([ [Markup.button.callback('🔑 Сгенерировать пароль', 'gen')], [Markup.button.callback('🎰 Пачка из 5', 'gen5')], + [Markup.button.callback('📧 Почтовый адрес', 'email')], [Markup.button.callback('⚙️ Настройки', 'settings')], [Markup.button.callback('📋 История', 'history')], [Markup.button.callback('ℹ️ О боте', 'about')], @@ -107,6 +111,39 @@ const passwordText = (pwd, s) => { ].join('\n'); }; +const EMAIL_STYLES = ['random (psxaiq)', 'pronounceable (sevemafi)', 'wordnum (nova.turbo86)']; +const EMAIL_STYLE_KEYS = ['random', 'pronounceable', 'wordnum']; + +const emailKb = (s) => + Markup.inlineKeyboard([ + [Markup.button.callback('🔁 Ещё раз', 'email'), Markup.button.callback('🎰 Пачка из 5', 'email5')], + [ + Markup.button.callback('◀️', 'edom:-1'), + Markup.button.callback('🌐 ' + EMAIL_DOMAINS[s.emailDomain], 'noop'), + Markup.button.callback('▶️', 'edom:+1'), + ], + [Markup.button.callback('🎨 Стиль: ' + EMAIL_STYLES[s.emailStyle], 'estyle')], + [ + Markup.button.callback((s.emailNumber ? '✅' : '❎') + ' Цифра в адресе', 'enum'), + Markup.button.callback('⬅️ Меню', 'main'), + ], + ]); + +const emailText = (mail, s) => + '📧 Почтовый адрес:\n\n' + + BT2(mail) + + '\n\n🎨 Стиль: ' + EMAIL_STYLES[s.emailStyle] + '\n' + + '🌐 Домен: ' + EMAIL_DOMAINS[s.emailDomain] + '\n\n' + + '🛡 crypto.randomInt — нажми на адрес, чтобы скопировать.'; +const BT2 = (x) => "`" + x + "`" + '\n\n'; + +const makeEmail = (s) => + generateEmail({ + style: EMAIL_STYLE_KEYS[s.emailStyle] || 'random', + domain: EMAIL_DOMAINS[s.emailDomain], + withNumber: s.emailNumber, + length: Math.max(6, Math.min(s.length, 20)), + }); /* ---------- Shared actions ---------- */ const sendPassword = async (ctx, s) => { const pwd = generatePassword(s); @@ -211,6 +248,63 @@ bot.action('gen5', async (ctx) => { .catch(() => {}); }); +bot.action('email', (ctx) => { + const s = state(ctx.from.id); + const mail = makeEmail(s); + addHistory(ctx.from.id, '📧 ' + mail); + return ctx + .replyWithMarkdown(emailText(mail, s), emailKb(s)) + .then(() => ctx.answerCbQuery()) + .catch(() => {}); +}); + +bot.action('email5', (ctx) => { + const s = state(ctx.from.id); + const rows = []; + for (let i = 0; i < 5; i++) { + const mail = makeEmail(s); + addHistory(ctx.from.id, '📧 ' + mail); + rows.push(i + 1 + '. ' + BT + mail + BT); + } + return ctx + .replyWithMarkdown( + '🎰 Пачка из 5 адресов (' + EMAIL_DOMAINS[s.emailDomain] + ', ' + EMAIL_STYLES[s.emailStyle] + '):\n\n' + rows.join('\n'), + Markup.inlineKeyboard([ + [Markup.button.callback('🔁 Ещё пачку', 'email5')], + [Markup.button.callback('⬅️ Назад', 'email'), Markup.button.callback('🏠 Меню', 'main')], + ]) + ) + .then(() => ctx.answerCbQuery()) + .catch(() => {}); +}); + +bot.action(/^edom:(-?\d+)$/, (ctx) => { + const delta = Number(ctx.match[1]); + const s = state(ctx.from.id); + s.emailDomain = (s.emailDomain + delta + EMAIL_DOMAINS.length) % EMAIL_DOMAINS.length; + return ctx + .editMessageText('📧 Настройки адреса — жми «Ещё раз»:', emailKb(s)) + .then(() => ctx.answerCbQuery(EMAIL_DOMAINS[s.emailDomain])) + .catch(() => {}); +}); + +bot.action('estyle', (ctx) => { + const s = state(ctx.from.id); + s.emailStyle = (s.emailStyle + 1) % EMAIL_STYLES.length; + return ctx + .editMessageText('📧 Настройки адреса — жми «Ещё раз»:', emailKb(s)) + .then(() => ctx.answerCbQuery(EMAIL_STYLES[s.emailStyle])) + .catch(() => {}); +}); + +bot.action('enum', (ctx) => { + const s = state(ctx.from.id); + s.emailNumber = !s.emailNumber; + return ctx + .editMessageText('📧 Настройки адреса — жми «Ещё раз»:', emailKb(s)) + .then(() => ctx.answerCbQuery()) + .catch(() => {}); +}); bot.action('history', (ctx) => { const h = history.get(ctx.from.id) || []; const text = h.length @@ -242,6 +336,7 @@ bot.action('about', (ctx) => '📏 Длина 4–128 символов.\n' + '🔤 Наборы: A-Z, a-z, 0-9, символы; опции «без похожих» и «произносимый».\n' + '📊 Оценка стойкости в битах энтропии.\n' + + '📧 Почтовые адреса: 3 стиля, 14 доменов (gmail, mail.ru, yandex, proton…).\n' + '🔒 Пароли не отправляются на серверы и не сохраняются на диске.', backKb() ) @@ -265,6 +360,13 @@ bot.command('gen', (ctx) => { return sendPassword(ctx, s).catch(() => {}); }); +bot.command('email', (ctx) => { + const s = state(ctx.from.id); + const mail = makeEmail(s); + addHistory(ctx.from.id, '📧 ' + mail); + return ctx.replyWithMarkdown(emailText(mail, s), emailKb(s)).catch(() => {}); +}); + bot.on('text', (ctx) => ctx.reply('Используй /start 🙂', mainMenu())); bot.catch((err) => { @@ -277,6 +379,7 @@ bot.catch((err) => { const COMMANDS = [ { command: 'start', description: 'Главное меню' }, { command: 'gen', description: 'Сгенерировать пароль (/gen 24)' }, + { command: 'email', description: 'Сгенерировать почтовый адрес' }, { command: 'help', description: 'Справка' }, ]; diff --git a/lib/generator.js b/lib/generator.js index 46fab19..a6d64f1 100644 --- a/lib/generator.js +++ b/lib/generator.js @@ -100,3 +100,39 @@ export function strength(password) { return { bits, level, bar }; } + +/* ---------- Email (почтовые адреса) ---------- */ +export const EMAIL_DOMAINS = [ + 'gmail.com','googlemail.com','mail.ru','yandex.ru','ya.ru','outlook.com', + 'hotmail.com','yahoo.com','icloud.com','proton.me','protonmail.com','bk.ru','inbox.ru','list.ru', +]; + +const EMAIL_WORDS = [ + 'swift','dark','cyber','frost','neon','lunar','solar','storm','raven','wolf', + 'fox','pixel','turbo','quantum','echo','vortex','nova','pulse','drift','orbit', + 'lynx','onyx','zephyr','flux','ember','cobalt','delta','omega','prime','crux', +]; + +const LOCAL_RANDOM = 'abcdefghijkmnpqrstuvwxyz'; + +/** + * Стили local-части: random | pronounceable | wordnum + * @param {object} opts { style, domain, withNumber, length } + */ +export function generateEmail(opts = {}) { + const { style = 'random', domain = 'gmail.com', withNumber = true, length = 8 } = opts; + const n = Math.min(Math.max(Number(length) || 8, 4), 24); + + let local; + if (style === 'pronounceable') { + local = pronounceablePassword(n).toLowerCase(); + } else if (style === 'wordnum') { + local = pick(EMAIL_WORDS) + '.' + pick(EMAIL_WORDS) + String(crypto.randomInt(10, 100)); + } else { + local = Array.from({ length: n }, () => pick(LOCAL_RANDOM)).join(''); + } + + if (withNumber && !new RegExp('[0-9]').test(local)) local += String(crypto.randomInt(10, 1000)); + const dom = EMAIL_DOMAINS.includes(domain) ? domain : EMAIL_DOMAINS[0]; + return local.toLowerCase() + '@' + dom; +} diff --git a/package.json b/package.json index 2cd3a9b..1721963 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "start": "node index.js", "dev": "node --watch index.js", - "test": "node tests/smoke.mjs", + "test": "node tests/smoke.mjs && node tests/email.mjs", "docker": "docker build -t tg_bot_gen ." }, "keywords": [ diff --git a/tests/email.mjs b/tests/email.mjs new file mode 100644 index 0000000..e16d016 --- /dev/null +++ b/tests/email.mjs @@ -0,0 +1,17 @@ +import { generateEmail, EMAIL_DOMAINS } from '../lib/generator.js'; +import assert from 'node:assert/strict'; + + +for (const style of ['random', 'pronounceable', 'wordnum']) { + for (const domain of EMAIL_DOMAINS) { + for (let i = 0; i < 200; i++) { + const e = generateEmail({ style, domain, withNumber: i % 2 === 0 }); + assert.match(e, /^[a-z0-9][a-z0-9.]*@[a-z.]+$/, 'format for ' + style + '/' + domain + ' -> ' + e); + assert.ok(e.endsWith('@' + domain), 'domain kept: ' + e); + const local = e.split('@')[0]; + assert.ok(local.length >= 5, 'local length ok: ' + e); + if (i % 2 === 0) assert.match(local, /\d/, 'has number: ' + e); + } + } +} +console.log('EMAIL SMOKE OK — ' + EMAIL_DOMAINS.length * 3 * 200 + ' адресов валидны');