#!/usr/bin/env node // RateRight Fleet Mailer v3 — Gmail API over HTTPS (bypasses SMTP port blocks) // Usage: node rateright-mailer.js --to "email" --subject "subject" --body "body" [--attach "path"] const https = require('https'); const fs = require('fs'); const path = require('path'); const args = {}; for (let i = 2; i < process.argv.length; i += 2) { const key = process.argv[i].replace('--', ''); args[key] = process.argv[i + 1]; } if (!args.to || !args.subject || (!args.body && !args['body-file'])) { console.error('Usage: node rateright-mailer.js --to "email" --subject "subject" --body "text" [--body-file "path"] [--attach "path"] [--from-name "Name"]'); process.exit(1); } function buildMimeMessage(from, to, subject, bodyText, attachPath) { const boundary = 'boundary_' + Date.now(); let mime = ''; if (attachPath) { mime += `From: ${from}\r\n`; mime += `To: ${to}\r\n`; mime += `Subject: ${subject}\r\n`; mime += `MIME-Version: 1.0\r\n`; mime += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n`; mime += `--${boundary}\r\n`; mime += `Content-Type: text/plain; charset="UTF-8"\r\n\r\n`; mime += bodyText + '\r\n\r\n'; mime += `--${boundary}\r\n`; const fileData = fs.readFileSync(attachPath); const b64 = fileData.toString('base64'); const filename = path.basename(attachPath); mime += `Content-Type: application/pdf; name="${filename}"\r\n`; mime += `Content-Disposition: attachment; filename="${filename}"\r\n`; mime += `Content-Transfer-Encoding: base64\r\n\r\n`; mime += b64 + '\r\n'; mime += `--${boundary}--\r\n`; } else { mime += `From: ${from}\r\n`; mime += `To: ${to}\r\n`; mime += `Subject: ${subject}\r\n`; mime += `MIME-Version: 1.0\r\n`; mime += `Content-Type: text/plain; charset="UTF-8"\r\n\r\n`; mime += bodyText; } return mime; } async function main() { const creds = JSON.parse(fs.readFileSync('/root/.config/rateright-mail/credentials.json', 'utf8')); const fromName = args['from-name'] || creds.sendAsName; const fromAddr = creds.sendAs || creds.user; const from = `"${fromName}" <${fromAddr}>`; let bodyText = args.body; if (args['body-file']) { bodyText = fs.readFileSync(args['body-file'], 'utf8'); } let attachPath = null; if (args.attach) { if (!fs.existsSync(args.attach)) { console.error(`Attachment not found: ${args.attach}`); process.exit(1); } attachPath = args.attach; } const mime = buildMimeMessage(from, args.to, args.subject, bodyText, attachPath); const encodedMessage = Buffer.from(mime).toString('base64url'); const authHeader = 'Basic ' + Buffer.from(creds.user + ':' + creds.pass).toString('base64'); const postData = JSON.stringify({ raw: encodedMessage }); return new Promise((resolve, reject) => { const req = https.request({ hostname: 'gmail.googleapis.com', path: '/gmail/v1/users/me/messages/send', method: 'POST', family: 4, headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData), 'Authorization': authHeader } }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode === 200) { const result = JSON.parse(data); console.log(JSON.stringify({ status: 'sent', messageId: result.id, to: args.to, subject: args.subject, timestamp: new Date().toISOString() })); } else { console.error(JSON.stringify({ status: 'error', error: `HTTP ${res.statusCode}: ${data}`, to: args.to, subject: args.subject, timestamp: new Date().toISOString() })); process.exit(1); } }); }); req.on('error', (err) => { console.error(JSON.stringify({ status: 'error', error: err.message, to: args.to, subject: args.subject, timestamp: new Date().toISOString() })); process.exit(1); }); req.write(postData); req.end(); }); } main();