目前基于 “合宙Air780” 芯片的短信转发方案已经有不少,但笔者觉得目前这些方案对于没有IT人员的客户不算友好,落地过程中有很多技术 “卡”点,比如:到阿里云创建一台主机,部署服务等,对接接口等。于是笔者有了个想法:
设计一套极简的部署方案,门槛低,但好用,稳定。
我们先看下目前的转发解决方案是怎么样的。如下图所示,「转发板」把收到的「验证码短信」发送到「中转服务」,「中转服务」是个web服务,提供http接口,用于保存、获取短信。影刀应用运行起来后,点击网页中的“发送手机验证码”按钮,等待几秒,从「中转服务」获取「验证码短信」。

方案的可用性和稳定性很好,就是「中转服务」部署成本高。那么有没有现成的互联网服务能起到「中转服务」的作用?有的,就是邮件服务器。邮件服务是每个公司必备的服务,比较正式的沟通都要通过邮件的方式。
Air780的操作系统叫做LuatOS,查看LuatOS官方文档中关于库的部分发现,并没有发送邮件相关的库。百度搜索第三方库有 LuaSocket 可以发送邮件,但LuaSocket库在Lua程序中一般用 LuaRocks 软件包管理工具进行安装,LuaRocks 可以理解为和Python中的pip类似的工具,但在嵌入式开发环境中,并没有LuaRocks ...,那么尝试把LuaSocket库的源码直接拷贝过来,和源码一起烧录进开发板吧。找到LuaSocket的工程,在github,具体地址是:https://github.com/lunarmodules/luasocket/ ,不看不知道,一看吓一跳,此库很庞大,有很多lua源码文件和很多c源码文件,拷贝到 短信转发源码的工程中,点击编译提示目标文件太大,烧录不了。
有现成的轮子用,肯定不自己造,但这次怕是绕不过去啦。写代码吧。
利用sokcet的封装出smtp协议的接口,源码如下 lib_smtp.lua,使用时调用里面的 lib_smtp.send(body, subject, smtp_config),传入 邮件内容,邮件主题和邮件配置。
-- lib_smtp.lua
local lib_smtp = {}
lib_smtp.socket_debug_enable = false
lib_smtp.packet_size = 512
lib_smtp.timeout = 1000 * 30
--- 日志格式化函数
-- @param content string, 日志内容
-- @return string, 处理后的日志内容
local function logFormat(content)
-- 隐藏 AUTH 用户信息
content = content:gsub("AUTH PLAIN (.-)\r\n", "AUTH PLAIN ***\r\n")
-- 替换换行符
content = content:gsub("\r", "\\r"):gsub("\n", "\\n")
-- 截取
content = content:sub(1, 200) .. (#content > 200 and " ..." or "")
return content
end
--- 转义句号函数
-- @param content string, 需要转义的内容
-- @return string, 转义后的内容
local function escapeDot(content)
return content:gsub("(.-\r\n)", function(line)
if line:sub(1, 1) == "." then line = "." .. line end
return line
end)
end
--- 接收到数据时的处理函数
-- @param netc userdata, socket.create 返回的 netc
-- @param rxbuf userdata, 接收到的数据
-- @param socket_id string, socket id
-- @param current_command string, 当前要发送的命令
local function recvHandler(netc, rxbuf, socket_id, current_command)
local rx_str = rxbuf:toStr(0, rxbuf:used())
log.info("lib_smtp", socket_id, "<-", logFormat(rx_str))
-- 如果返回非 2xx 或 3xx 状态码, 则断开连接
if not rx_str:match("^[23]%d%d") then
log.error("lib_smtp", socket_id, "服务器返回错误状态码, 断开连接, 请检查日志")
sys.publish(socket_id .. "_disconnect", { success = false, message = "服务器返回错误状态码", is_retry = false })
return
end
if current_command == nil then
log.info("lib_smtp", socket_id, "全部发送完成")
sys.publish(socket_id .. "_disconnect", { success = true, message = "发送成功", is_retry = false })
return
end
-- 分包发送
local index = 1
sys.taskInit(function()
while index <= #current_command do
local packet = current_command:sub(index, index + lib_smtp.packet_size - 1)
socket.tx(netc, packet)
log.info("lib_smtp", socket_id, "->", logFormat(packet))
index = index + lib_smtp.packet_size
sys.wait(100)
end
end)
end
local function validateParameters(smtp_config)
-- 配置参数验证规则
local validation_rules = {
{ field = "host", type = "string", required = true },
{ field = "port", type = "number", required = true },
{ field = "username", type = "string", required = true },
{ field = "password", type = "string", required = true },
{ field = "mail_from", type = "string", required = true },
{ field = "mail_to", type = "string", required = true },
{ field = "tls_enable", type = "boolean", required = false },
}
local result = true
for _, rule in ipairs(validation_rules) do
local value = smtp_config[rule.field]
if rule.type == "string" and (value == nil or value == "") then
log.error("lib_smtp", string.format("`smtp_config.%s` 应为非空字符串", rule.field))
result = false
elseif rule.required and type(value) ~= rule.type then
log.error("lib_smtp", string.format("`smtp_config.%s` 应为 %s 类型", rule.field, rule.type))
result = false
end
end
return result
end
--- 发送邮件
-- @param body string 邮件正文
-- @param subject string 邮件主题
-- @param smtp_config table 配置参数
-- - smtp_config.host string SMTP 服务器地址
-- - smtp_config.username string SMTP 账号用户名
-- - smtp_config.password string SMTP 账号密码
-- - smtp_config.mail_from string 发件人邮箱地址
-- - smtp_config.mail_to string 收件人邮箱地址
-- - smtp_config.port number SMTP 服务器端口号
-- - smtp_config.tls_enable boolean 是否启用 TLS(可选,默认为 false)
-- @return result table 发送结果
-- - result.success boolean 是否发送成功
-- - result.message string 发送结果描述
-- - result.is_retry boolean 是否需要重试
function lib_smtp.send(body, subject, smtp_config)
-- 参数验证
if type(smtp_config) ~= "table" then
log.error("lib_smtp", "`smtp_config` 应为 table 类型")
return { success = false, message = "参数错误", is_retry = false }
end
local valid = validateParameters(smtp_config)
if not valid then return { success = false, message = "参数错误", is_retry = false } end
subject = type(subject) == "string" and subject or ""
body = type(body) == "string" and escapeDot(body) or ""
lib_smtp.send_count = (lib_smtp.send_count or 0) + 1
local socket_id = "socket_" .. lib_smtp.send_count
local rxbuf = zbuff.create(256)
local commands = {
"HELO " .. smtp_config.host .. "\r\n",
"AUTH PLAIN " .. string.toBase64("\0" .. smtp_config.username .. "\0" .. smtp_config.password) .. "\r\n",
"MAIL FROM: <" .. smtp_config.mail_from .. ">\r\n",
"RCPT TO: <" .. smtp_config.mail_to .. ">\r\n",
"DATA\r\n",
table.concat({
"From: " .. smtp_config.mail_from,
"To: " .. smtp_config.mail_to,
"Subject: " .. subject,
"Content-Type: text/plain; charset=UTF-8",
"",
body,
".",
"",
}, "\r\n"),
}
local current_command_index = 1
local function getNextCommand()
local command = commands[current_command_index]
current_command_index = current_command_index + 1
return command
end
-- socket 回调
local function netCB(netc, event, param)
if param ~= 0 then
sys.publish(socket_id .. "_disconnect", { success = false, message = "param~=0", is_retry = true })
return
end
if event == socket.LINK then
log.info("lib_smtp", socket_id, "LINK")
elseif event == socket.ON_LINE then
log.info("lib_smtp", socket_id, "ON_LINE")
elseif event == socket.EVENT then
socket.rx(netc, rxbuf)
socket.wait(netc)
if rxbuf:used() > 0 then recvHandler(netc, rxbuf, socket_id, getNextCommand()) end
rxbuf:del()
elseif event == socket.TX_OK then
socket.wait(netc)
elseif event == socket.CLOSE then
log.info("lib_smtp", socket_id, "CLOSED")
sys.publish(socket_id .. "_disconnect", { success = false, message = "服务器断开连接", is_retry = true })
end
end
-- 初始化 socket
local netc = socket.create(nil, netCB)
socket.debug(netc, lib_smtp.socket_debug_enable)
socket.config(netc, nil, nil, smtp_config.tls_enable)
-- 连接 smtp 服务器
local is_connect_success = socket.connect(netc, smtp_config.host, smtp_config.port)
if not is_connect_success then
socket.close(netc)
return { success = false, message = "未知错误", is_retry = true }
end
-- 等待发送结果
local is_send_success, send_result = sys.waitUntil(socket_id .. "_disconnect", lib_smtp.timeout)
socket.close(netc)
if is_send_success then
return send_result
else
log.error("lib_smtp", socket_id, "发送超时")
return { success = false, message = "发送超时", is_retry = true }
end
end
return lib_smtp
步骤一:
源码已经整合到可耐的方案中,大家要转发到邮箱的话,只需要在配置文件中设置成转发到邮箱。具体操作:

2. 找到 “smtp 通知配置”,修改配置

SMTP_HOST smtp服务器地址
SMTP_PORT smtp服务器端口
SMTP_USERNAME 登录用户名
SMTP_PASSWORD 登录密码(某些邮箱写授权码)
SMTP_MAIL_FROM 发件邮箱(需要写全, 比如:zhangsan@163.com)
SMTP_MAIL_TO 收件邮箱(需要写全,比如: lisi@outlook.com)
SMTP_MAIL_SUBJECT 邮件主题
步骤二:
用Luatools 把程序烧录进「转发板」,具体步骤看官方文档 烧录教程 - LuatOS 文档,不在这里赘述。
步骤三:
影刀程序读取邮件,取出验证码。
经过测试,从发出短信验证码,到转发到邮箱一般需要5s,最长不超过10s,所以程序读取邮件前等待10s即可。
