lib_smtp.lua 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. local lib_smtp = {}
  2. lib_smtp.socket_debug_enable = false
  3. lib_smtp.packet_size = 512
  4. lib_smtp.timeout = 1000 * 30
  5. --- 日志格式化函数
  6. -- @param content string, 日志内容
  7. -- @return string, 处理后的日志内容
  8. local function logFormat(content)
  9. -- 隐藏 AUTH 用户信息
  10. content = content:gsub("AUTH PLAIN (.-)\r\n", "AUTH PLAIN ***\r\n")
  11. -- 替换换行符
  12. content = content:gsub("\r", "\\r"):gsub("\n", "\\n")
  13. -- 截取
  14. content = content:sub(1, 200) .. (#content > 200 and " ..." or "")
  15. return content
  16. end
  17. --- 转义句号函数
  18. -- @param content string, 需要转义的内容
  19. -- @return string, 转义后的内容
  20. local function escapeDot(content)
  21. return content:gsub(
  22. "(.-\r\n)",
  23. function(line)
  24. if line:sub(1, 1) == "." then
  25. line = "." .. line
  26. end
  27. return line
  28. end
  29. )
  30. end
  31. --- 接收到数据时的处理函数
  32. -- @param netc userdata, socket.create 返回的 netc
  33. -- @param rxbuf userdata, 接收到的数据
  34. -- @param socket_id string, socket id
  35. -- @param current_command string, 当前要发送的命令
  36. local function recvHandler(netc, rxbuf, socket_id, current_command)
  37. local rx_str = rxbuf:toStr(0, rxbuf:used())
  38. log.info("lib_smtp", socket_id, "<-", logFormat(rx_str))
  39. -- 如果返回非 2xx 或 3xx 状态码, 则断开连接
  40. if not rx_str:match("^[23]%d%d") then
  41. log.error("lib_smtp", socket_id, "服务器返回错误状态码, 断开连接, 请检查日志")
  42. sys.publish(socket_id .. "_disconnect", {success = false, message = "服务器返回错误状态码", is_retry = false})
  43. return
  44. end
  45. if current_command == nil then
  46. log.info("lib_smtp", socket_id, "全部发送完成")
  47. sys.publish(socket_id .. "_disconnect", {success = true, message = "发送成功", is_retry = false})
  48. return
  49. end
  50. -- 分包发送
  51. local index = 1
  52. sys.taskInit(
  53. function()
  54. while index <= #current_command do
  55. local packet = current_command:sub(index, index + lib_smtp.packet_size - 1)
  56. socket.tx(netc, packet)
  57. log.info("lib_smtp", socket_id, "->", logFormat(packet))
  58. index = index + lib_smtp.packet_size
  59. sys.wait(100)
  60. end
  61. end
  62. )
  63. end
  64. local function validateParameters(smtp_config)
  65. -- 配置参数验证规则
  66. local validation_rules = {
  67. {field = "host", type = "string", required = true},
  68. {field = "port", type = "number", required = true},
  69. {field = "username", type = "string", required = true},
  70. {field = "password", type = "string", required = true},
  71. {field = "mail_from", type = "string", required = true},
  72. {field = "mail_to", type = "string", required = true},
  73. {field = "tls_enable", type = "boolean", required = false}
  74. }
  75. local result = true
  76. for _, rule in ipairs(validation_rules) do
  77. local value = smtp_config[rule.field]
  78. if rule.type == "string" and (value == nil or value == "") then
  79. log.error("lib_smtp", string.format("`smtp_config.%s` 应为非空字符串", rule.field))
  80. result = false
  81. elseif rule.required and type(value) ~= rule.type then
  82. log.error("lib_smtp", string.format("`smtp_config.%s` 应为 %s 类型", rule.field, rule.type))
  83. result = false
  84. end
  85. end
  86. return result
  87. end
  88. --- 发送邮件
  89. -- @param body string 邮件正文
  90. -- @param subject string 邮件主题
  91. -- @param smtp_config table 配置参数
  92. -- - smtp_config.host string SMTP 服务器地址
  93. -- - smtp_config.username string SMTP 账号用户名
  94. -- - smtp_config.password string SMTP 账号密码
  95. -- - smtp_config.mail_from string 发件人邮箱地址
  96. -- - smtp_config.mail_to string 收件人邮箱地址
  97. -- - smtp_config.port number SMTP 服务器端口号
  98. -- - smtp_config.tls_enable boolean 是否启用 TLS(可选,默认为 false)
  99. -- @return result table 发送结果
  100. -- - result.success boolean 是否发送成功
  101. -- - result.message string 发送结果描述
  102. -- - result.is_retry boolean 是否需要重试
  103. function lib_smtp.send(body, subject, smtp_config)
  104. -- 参数验证
  105. if type(smtp_config) ~= "table" then
  106. log.error("lib_smtp", "`smtp_config` 应为 table 类型")
  107. return {success = false, message = "参数错误", is_retry = false}
  108. end
  109. local valid = validateParameters(smtp_config)
  110. if not valid then
  111. return {success = false, message = "参数错误", is_retry = false}
  112. end
  113. subject = type(subject) == "string" and subject or ""
  114. body = type(body) == "string" and escapeDot(body) or ""
  115. lib_smtp.send_count = (lib_smtp.send_count or 0) + 1
  116. local socket_id = "socket_" .. lib_smtp.send_count
  117. local rxbuf = zbuff.create(256)
  118. local commands = {
  119. "HELO " .. smtp_config.host .. "\r\n",
  120. "AUTH PLAIN " .. string.toBase64("\0" .. smtp_config.username .. "\0" .. smtp_config.password) .. "\r\n",
  121. "MAIL FROM: <" .. smtp_config.mail_from .. ">\r\n",
  122. "RCPT TO: <" .. smtp_config.mail_to .. ">\r\n",
  123. "DATA\r\n",
  124. table.concat(
  125. {
  126. "From: " .. smtp_config.mail_from,
  127. "To: " .. smtp_config.mail_to,
  128. "Subject: " .. subject,
  129. "Content-Type: text/plain; charset=UTF-8",
  130. "",
  131. body,
  132. ".",
  133. ""
  134. },
  135. "\r\n"
  136. )
  137. }
  138. local current_command_index = 1
  139. local function getNextCommand()
  140. local command = commands[current_command_index]
  141. current_command_index = current_command_index + 1
  142. return command
  143. end
  144. -- socket 回调
  145. local function netCB(netc, event, param)
  146. if param ~= 0 then
  147. sys.publish(socket_id .. "_disconnect", {success = false, message = "param~=0", is_retry = true})
  148. return
  149. end
  150. if event == socket.LINK then
  151. log.info("lib_smtp", socket_id, "LINK")
  152. elseif event == socket.ON_LINE then
  153. log.info("lib_smtp", socket_id, "ON_LINE")
  154. elseif event == socket.EVENT then
  155. socket.rx(netc, rxbuf)
  156. socket.wait(netc)
  157. if rxbuf:used() > 0 then
  158. recvHandler(netc, rxbuf, socket_id, getNextCommand())
  159. end
  160. rxbuf:del()
  161. elseif event == socket.TX_OK then
  162. socket.wait(netc)
  163. elseif event == socket.CLOSE then
  164. log.info("lib_smtp", socket_id, "CLOSED")
  165. sys.publish(socket_id .. "_disconnect", {success = false, message = "服务器断开连接", is_retry = true})
  166. end
  167. end
  168. -- 初始化 socket
  169. local netc = socket.create(nil, netCB)
  170. socket.debug(netc, lib_smtp.socket_debug_enable)
  171. socket.config(netc, nil, nil, smtp_config.tls_enable)
  172. -- 连接 smtp 服务器
  173. local is_connect_success = socket.connect(netc, smtp_config.host, smtp_config.port)
  174. if not is_connect_success then
  175. socket.close(netc)
  176. return {success = false, message = "未知错误", is_retry = true}
  177. end
  178. -- 等待发送结果
  179. local is_send_success, send_result = sys.waitUntil(socket_id .. "_disconnect", lib_smtp.timeout)
  180. socket.close(netc)
  181. if is_send_success then
  182. return send_result
  183. else
  184. log.error("lib_smtp", socket_id, "发送超时")
  185. return {success = false, message = "发送超时", is_retry = true}
  186. end
  187. end
  188. return lib_smtp