#!/usr/bin/env node /** * SMTP Email CLI * Send email via SMTP protocol. Works with Gmail, Outlook, 163.com, and any standard SMTP server. * Supports attachments, HTML content, and multiple recipients. */ const nodemailer = require('nodemailer'); const path = require('path'); const os = require('os'); const fs = require('fs'); require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); function validateReadPath(inputPath) { let realPath; try { realPath = fs.realpathSync(inputPath); } catch { realPath = path.resolve(inputPath); } const allowedDirsStr = process.env.ALLOWED_READ_DIRS; if (!allowedDirsStr) { throw new Error('ALLOWED_READ_DIRS not set in .env. File read operations are disabled.'); } const allowedDirs = allowedDirsStr.split(',').map(d => path.resolve(d.trim().replace(/^~/, os.homedir())) ); const allowed = allowedDirs.some(dir => realPath === dir || realPath.startsWith(dir + path.sep) ); if (!allowed) { throw new Error(`Access denied: '${inputPath}' is outside allowed read directories`); } return realPath; } // Parse command-line arguments function parseArgs() { const args = process.argv.slice(2); const command = args[0]; const options = {}; const positional = []; for (let i = 1; i < args.length; i++) { const arg = args[i]; if (arg.startsWith('--')) { const key = arg.slice(2); const value = args[i + 1]; options[key] = value || true; if (value && !value.startsWith('--')) i++; } else { positional.push(arg); } } return { command, options, positional }; } // Create SMTP transporter function createTransporter() { const config = { host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT) || 587, secure: process.env.SMTP_SECURE === 'true', // true for 465, false for other ports auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, tls: { rejectUnauthorized: process.env.SMTP_REJECT_UNAUTHORIZED !== 'false', }, }; if (!config.host || !config.auth.user || !config.auth.pass) { throw new Error('Missing SMTP configuration. Please set SMTP_HOST, SMTP_USER, and SMTP_PASS in .env'); } return nodemailer.createTransport(config); } // Send email async function sendEmail(options) { const transporter = createTransporter(); // Verify connection try { await transporter.verify(); console.error('SMTP server is ready to send'); } catch (err) { throw new Error(`SMTP connection failed: ${err.message}`); } const mailOptions = { from: options.from || process.env.SMTP_FROM || process.env.SMTP_USER, to: options.to, cc: options.cc || undefined, bcc: options.bcc || undefined, subject: options.subject || '(no subject)', text: options.text || undefined, html: options.html || undefined, attachments: options.attachments || [], }; // If neither text nor html provided, use default text if (!mailOptions.text && !mailOptions.html) { mailOptions.text = options.body || ''; } const info = await transporter.sendMail(mailOptions); return { success: true, messageId: info.messageId, response: info.response, to: mailOptions.to, }; } // Read file content for attachments function readAttachment(filePath) { validateReadPath(filePath); if (!fs.existsSync(filePath)) { throw new Error(`Attachment file not found: ${filePath}`); } return { filename: path.basename(filePath), path: path.resolve(filePath), }; } // Send email with file content async function sendEmailWithContent(options) { // Handle attachments if (options.attach) { const attachFiles = options.attach.split(',').map(f => f.trim()); options.attachments = attachFiles.map(f => readAttachment(f)); } return await sendEmail(options); } // Test SMTP connection async function testConnection() { const transporter = createTransporter(); try { await transporter.verify(); const info = await transporter.sendMail({ from: process.env.SMTP_FROM || process.env.SMTP_USER, to: process.env.SMTP_USER, // Send to self subject: 'SMTP Connection Test', text: 'This is a test email from the IMAP/SMTP email skill.', html: '

This is a test email from the IMAP/SMTP email skill.

', }); return { success: true, message: 'SMTP connection successful', messageId: info.messageId, }; } catch (err) { throw new Error(`SMTP test failed: ${err.message}`); } } // Main CLI handler async function main() { const { command, options, positional } = parseArgs(); try { let result; switch (command) { case 'send': if (!options.to) { throw new Error('Missing required option: --to '); } if (!options.subject && !options['subject-file']) { throw new Error('Missing required option: --subject or --subject-file '); } // Read subject from file if specified if (options['subject-file']) { validateReadPath(options['subject-file']); options.subject = fs.readFileSync(options['subject-file'], 'utf8').trim(); } // Read body from file if specified if (options['body-file']) { validateReadPath(options['body-file']); const content = fs.readFileSync(options['body-file'], 'utf8'); if (options['body-file'].endsWith('.html') || options.html) { options.html = content; } else { options.text = content; } } else if (options['html-file']) { validateReadPath(options['html-file']); options.html = fs.readFileSync(options['html-file'], 'utf8'); } else if (options.body) { options.text = options.body; } result = await sendEmailWithContent(options); break; case 'test': result = await testConnection(); break; default: console.error('Unknown command:', command); console.error('Available commands: send, test'); console.error('\nUsage:'); console.error(' send --to --subject [--body ] [--html] [--cc ] [--bcc ] [--attach ]'); console.error(' send --to --subject --body-file [--html-file ] [--attach ]'); console.error(' test Test SMTP connection'); process.exit(1); } console.log(JSON.stringify(result, null, 2)); } catch (err) { console.error('Error:', err.message); process.exit(1); } } main();