Vue.js无需API及依赖,基于SMTP协议发送邮件方案问询
Vue.js 无依赖SMTP邮件发送实现方案
前提说明
浏览器安全策略禁止直接发起TCP连接到SMTP服务器(如25/587/465端口),因此我们采用WebSocket中转方案:用原生Node.js写一个无依赖的WebSocket服务,负责转发前端的邮件请求到SMTP服务器;Vue前端通过原生WebSocket API与中转服务通信,全程无外部依赖或第三方API。
步骤1:编写无依赖Node.js WebSocket中转服务
创建smtp-proxy.js文件,使用Node.js原生http、net模块实现WebSocket握手与SMTP转发,无任何第三方依赖:
const http = require('http'); const net = require('net'); const server = http.createServer((req, res) => { res.writeHead(404); res.end('Not Found'); }); // 处理WebSocket握手 server.on('upgrade', (req, socket) => { if (req.headers['upgrade'] !== 'websocket') { socket.end('HTTP/1.1 400 Bad Request'); return; } // 生成握手响应 const acceptKey = req.headers['sec-websocket-key']; const hash = require('crypto').createHash('sha1') .update(acceptKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11') .digest('base64'); const responseHeaders = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${hash}` ]; socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); // 处理WebSocket文本帧(简化实现) let buffer = Buffer.alloc(0); socket.on('data', (data) => { buffer = Buffer.concat([buffer, data]); while (buffer.length >= 2) { const fin = (buffer[0] & 0x80) !== 0; const opcode = buffer[0] & 0x0F; const mask = (buffer[1] & 0x80) !== 0; let payloadLength = buffer[1] & 0x7F; let offset = 2; if (payloadLength === 126) { payloadLength = buffer.readUInt16BE(offset); offset += 2; } else if (payloadLength === 127) { payloadLength = buffer.readUInt64BE(offset); offset += 8; } if (mask) offset += 4; // 跳过掩码(简化处理) if (buffer.length < offset + payloadLength) break; const payload = buffer.slice(offset, offset + payloadLength).toString(); buffer = buffer.slice(offset + payloadLength); if (opcode === 1) { // 文本帧 handleMailRequest(payload, socket); } } }); // 发送WebSocket文本帧到前端 function sendWebSocketMessage(socket, message) { const payload = Buffer.from(message); const frame = Buffer.alloc(2 + payload.length); frame[0] = 0x81; // FIN + TEXT frame[1] = payload.length; payload.copy(frame, 2); socket.write(frame); } // 处理邮件请求并转发到SMTP服务器 function handleMailRequest(payload, socket) { try { const { smtpHost, smtpPort, smtpUser, smtpPass, from, to, subject, body } = JSON.parse(payload); // 连接SMTP服务器 const smtpSocket = net.connect(smtpPort, smtpHost, () => { smtpSocket.write(`EHLO localhost\r\n`); }); let smtpBuffer = ''; smtpSocket.on('data', (data) => { smtpBuffer += data.toString(); // 按SMTP响应流程执行命令 if (smtpBuffer.includes('250-AUTH')) { smtpSocket.write(`AUTH LOGIN\r\n`); } else if (smtpBuffer.includes('334 VXNlcm5hbWU6')) { smtpSocket.write(`${Buffer.from(smtpUser).toString('base64')}\r\n`); } else if (smtpBuffer.includes('334 UGFzc3dvcmQ6')) { smtpSocket.write(`${Buffer.from(smtpPass).toString('base64')}\r\n`); } else if (smtpBuffer.includes('235 2.7.0 Authentication successful')) { smtpSocket.write(`MAIL FROM:<${from}>\r\n`); } else if (smtpBuffer.includes('250 2.1.0 Ok')) { smtpSocket.write(`RCPT TO:<${to}>\r\n`); } else if (smtpBuffer.includes('250 2.1.5 Ok')) { smtpSocket.write(`DATA\r\n`); } else if (smtpBuffer.includes('354 End data with <CR><LF>.<CR><LF>')) { const emailContent = `Subject: ${subject}\r\n\r\n${body}\r\n.`; smtpSocket.write(`${emailContent}\r\n`); } else if (smtpBuffer.includes('250 2.0.0 Ok: queued')) { sendWebSocketMessage(socket, JSON.stringify({ success: true, message: '邮件发送成功' })); smtpSocket.end(`QUIT\r\n`); } else if (smtpBuffer.startsWith('5')) { sendWebSocketMessage(socket, JSON.stringify({ success: false, message: smtpBuffer.trim() })); smtpSocket.end(); } }); smtpSocket.on('error', (err) => { sendWebSocketMessage(socket, JSON.stringify({ success: false, message: `SMTP连接错误: ${err.message}` })); }); smtpSocket.on('end', () => { socket.end(); }); } catch (err) { sendWebSocketMessage(socket, JSON.stringify({ success: false, message: `请求解析错误: ${err.message}` })); } } }); const PORT = 8080; server.listen(PORT, () => { console.log(`SMTP中转服务运行在 ws://localhost:${PORT}`); });
步骤2:编写独立Vue邮件发送组件
在Vue项目中创建EmailSender.vue组件,使用原生WebSocket API,不引入任何外部依赖,可独立挂载不影响现有代码:
<template> <div class="email-sender"> <h3>邮件发送器</h3> <div class="form-group"> <label>SMTP服务器:</label> <input v-model="form.smtpHost" type="text" placeholder="例如: smtp.gmail.com"> </div> <div class="form-group"> <label>SMTP端口:</label> <input v-model.number="form.smtpPort" type="number" placeholder="例如: 587"> </div> <div class="form-group"> <label>邮箱账号:</label> <input v-model="form.smtpUser" type="email"> </div> <div class="form-group"> <label>邮箱密码/授权码:</label> <input v-model="form.smtpPass" type="password"> </div> <div class="form-group"> <label>发件人:</label> <input v-model="form.from" type="email"> </div> <div class="form-group"> <label>收件人:</label> <input v-model="form.to" type="email"> </div> <div class="form-group"> <label>主题:</label> <input v-model="form.subject" type="text"> </div> <div class="form-group"> <label>内容:</label> <textarea v-model="form.body" rows="5"></textarea> </div> <button @click="sendEmail" :disabled="isSending"> {{ isSending ? '发送中...' : '发送邮件' }} </button> <div v-if="resultMessage" :class="`result ${resultSuccess ? 'success' : 'error'}`"> {{ resultMessage }} </div> </div> </template> <script> export default { name: 'EmailSender', data() { return { form: { smtpHost: '', smtpPort: 587, smtpUser: '', smtpPass: '', from: '', to: '', subject: '', body: '' }, isSending: false, resultMessage: '', resultSuccess: false, ws: null }; }, methods: { sendEmail() { // 表单必填项验证 const requiredFields = ['smtpHost', 'smtpPort', 'smtpUser', 'smtpPass', 'from', 'to', 'subject', 'body']; const emptyField = requiredFields.find(field => !this.form[field]); if (emptyField) { this.showResult(false, `请填写${this.getFieldName(emptyField)}`); return; } this.isSending = true; this.resultMessage = ''; // 连接WebSocket中转服务 this.ws = new WebSocket('ws://localhost:8080'); this.ws.onopen = () => { this.ws.send(JSON.stringify(this.form)); }; this.ws.onmessage = (event) => { const response = JSON.parse(event.data); this.showResult(response.success, response.message); this.ws.close(); }; this.ws.onerror = (err) => { this.showResult(false, `连接中转服务失败: ${err.message}`); this.ws.close(); }; this.ws.onclose = () => { this.isSending = false; }; }, showResult(success, message) { this.resultSuccess = success; this.resultMessage = message; setTimeout(() => { this.resultMessage = ''; }, 3000); }, getFieldName(field) { const names = { smtpHost: 'SMTP服务器', smtpPort: 'SMTP端口', smtpUser: '邮箱账号', smtpPass: '邮箱密码/授权码', from: '发件人', to: '收件人', subject: '主题', body: '内容' }; return names[field] || field; } }, beforeUnmount() { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.close(); } } }; </script> <style scoped> .email-sender { max-width: 500px; margin: 20px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; } input, textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } button { padding: 10px 20px; background-color: #2385bb; color: white; border: none; border-radius: 4px; cursor: pointer; } button:disabled { background-color: #ccc; cursor: not-allowed; } .result { margin-top: 15px; padding: 10px; border-radius: 4px; } .success { background-color: #d4edda; color: #155724; } .error { background-color: #f8d7da; color: #721c24; } </style>
步骤3:运行与测试
- 启动中转服务:
node smtp-proxy.js - 在Vue项目中通过路由或直接挂载
EmailSender组件,无需修改现有代码; - 填写SMTP信息(如Gmail需开启授权码,QQ邮箱需开启POP3/SMTP并生成授权码),点击发送即可测试。
注意事项
- 该方案仅适用于学习或内部场景,生产环境禁止前端暴露SMTP账号密码,建议将中转服务改为后端接口并添加鉴权;
- 部分SMTP服务器要求SSL/TLS加密,若需支持可修改
net.connect为tls.connect; - WebSocket帧处理为简化实现,仅支持文本帧,大邮件场景需扩展帧解析逻辑。
内容的提问来源于stack exchange,提问作者Venkata Suresh
相关产品推荐
相关产品推荐

