You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:运行与测试

  1. 启动中转服务:
    node smtp-proxy.js
    
  2. 在Vue项目中通过路由或直接挂载EmailSender组件,无需修改现有代码;
  3. 填写SMTP信息(如Gmail需开启授权码,QQ邮箱需开启POP3/SMTP并生成授权码),点击发送即可测试。

注意事项

  • 该方案仅适用于学习或内部场景,生产环境禁止前端暴露SMTP账号密码,建议将中转服务改为后端接口并添加鉴权;
  • 部分SMTP服务器要求SSL/TLS加密,若需支持可修改net.connect为tls.connect;
  • WebSocket帧处理为简化实现,仅支持文本帧,大邮件场景需扩展帧解析逻辑。

内容的提问来源于stack exchange,提问作者Venkata Suresh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 05:27:03