|
@@ -0,0 +1,203 @@
|
|
|
+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
|