ソースを参照

Merge pull request #28 from 0wQ/smtp

:sparkles: 支持 SMTP 协议
Mizore 2 年 前
コミット
99b437e55e
4 ファイル変更237 行追加1 行削除
  1. 1 0
      README.md
  2. 10 0
      script/config.lua
  3. 203 0
      script/lib_smtp.lua
  4. 23 1
      script/util_notify.lua

+ 1 - 0
README.md

@@ -13,6 +13,7 @@
     - [x] [邮件 next-smtp-proxy](https://github.com/0wQ/next-smtp-proxy)
     - [x] [Gotify](https://gotify.net)
     - [x] [Inotify](https://github.com/xpnas/Inotify) / [合宙官方的推送服务](https://push.luatos.org)
+    - [x] 邮件 (SMTP协议)
 - [x] 通过短信控制设备
     - [x] 发短信, 格式: `SMS,10010,余额查询`
 - [x] 定时基站定位

+ 10 - 0
script/config.lua

@@ -50,6 +50,16 @@ return {
     NEXT_SMTP_PROXY_TO_EMAIL = "",
     NEXT_SMTP_PROXY_SUBJECT = "来自 Air780E 的通知",
     --
+    -- smtp 通知配置
+    SMTP_HOST = "smtp.qq.com",
+    SMTP_PORT = 25,
+    SMTP_USERNAME = "",
+    SMTP_PASSWORD = "",
+    SMTP_MAIL_FROM = "",
+    SMTP_MAIL_TO = "",
+    SMTP_MAIL_SUBJECT = "来自 Air780E 的通知",
+    SMTP_TLS_ENABLE = false,
+    --
     -- gotify 通知配置, https://gotify.net/
     GOTIFY_API = "",
     GOTIFY_TITLE = "Air780E",

+ 203 - 0
script/lib_smtp.lua

@@ -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

+ 23 - 1
script/util_notify.lua

@@ -1,3 +1,5 @@
+local lib_smtp = require "lib_smtp"
+
 local util_notify = {}
 
 -- 消息队列
@@ -308,6 +310,26 @@ local notify = {
         log.info("util_notify", "POST", config.NEXT_SMTP_PROXY_API)
         return util_http.fetch(nil, "POST", config.NEXT_SMTP_PROXY_API, header, urlencodeTab(body))
     end,
+    ["smtp"] = function(msg)
+        local smtp_config = {
+            host = config.SMTP_HOST,
+            port = config.SMTP_PORT,
+            username = config.SMTP_USERNAME,
+            password = config.SMTP_PASSWORD,
+            mail_from = config.SMTP_MAIL_FROM,
+            mail_to = config.SMTP_MAIL_TO,
+            tls_enable = config.SMTP_TLS_ENABLE
+        }
+        local result = lib_smtp.send(msg, config.SMTP_MAIL_SUBJECT, smtp_config)
+        log.info("util_notify", "SMTP", result.success, result.message, result.is_retry)
+        if result.success then
+            return 200, nil, result.message
+        end
+        if result.is_retry then
+            return 500, nil, result.message
+        end
+        return 400, nil, result.message
+    end,
     -- 发送到 serial
     ["serial"] = function(msg)
         uart.write(1, msg)
@@ -315,7 +337,7 @@ local notify = {
         log.info("util_notify", "消息已转发到串口")
         sys.wait(1000)
         return 200
-    end
+    end,
 }
 
 local function append()