smtp.lua 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. -----------------------------------------------------------------------------
  2. -- SMTP client support for the Lua language.
  3. -- LuaSocket toolkit.
  4. -- Author: Diego Nehab
  5. -----------------------------------------------------------------------------
  6. -----------------------------------------------------------------------------
  7. -- Declare module and import dependencies
  8. -----------------------------------------------------------------------------
  9. local base = _G
  10. local coroutine = require("coroutine")
  11. local string = require("string")
  12. local math = require("math")
  13. local os = require("os")
  14. local socket = require("socket")
  15. local tp = require("socket.tp")
  16. local ltn12 = require("ltn12")
  17. local headers = require("socket.headers")
  18. local mime = require("mime")
  19. socket.smtp = {}
  20. local _M = socket.smtp
  21. -----------------------------------------------------------------------------
  22. -- Program constants
  23. -----------------------------------------------------------------------------
  24. -- timeout for connection
  25. _M.TIMEOUT = 60
  26. -- default server used to send e-mails
  27. _M.SERVER = "localhost"
  28. -- default port
  29. _M.PORT = 25
  30. -- domain used in HELO command and default sendmail
  31. -- If we are under a CGI, try to get from environment
  32. _M.DOMAIN = os.getenv("SERVER_NAME") or "localhost"
  33. -- default time zone (means we don't know)
  34. _M.ZONE = "-0000"
  35. ---------------------------------------------------------------------------
  36. -- Low level SMTP API
  37. -----------------------------------------------------------------------------
  38. local metat = { __index = {} }
  39. function metat.__index:greet(domain)
  40. self.try(self.tp:check("2.."))
  41. self.try(self.tp:command("EHLO", domain or _M.DOMAIN))
  42. return socket.skip(1, self.try(self.tp:check("2..")))
  43. end
  44. function metat.__index:mail(from)
  45. self.try(self.tp:command("MAIL", "FROM:" .. from))
  46. return self.try(self.tp:check("2.."))
  47. end
  48. function metat.__index:rcpt(to)
  49. self.try(self.tp:command("RCPT", "TO:" .. to))
  50. return self.try(self.tp:check("2.."))
  51. end
  52. function metat.__index:data(src, step)
  53. self.try(self.tp:command("DATA"))
  54. self.try(self.tp:check("3.."))
  55. self.try(self.tp:source(src, step))
  56. self.try(self.tp:send("\r\n.\r\n"))
  57. return self.try(self.tp:check("2.."))
  58. end
  59. function metat.__index:quit()
  60. self.try(self.tp:command("QUIT"))
  61. return self.try(self.tp:check("2.."))
  62. end
  63. function metat.__index:close()
  64. return self.tp:close()
  65. end
  66. function metat.__index:login(user, password)
  67. self.try(self.tp:command("AUTH", "LOGIN"))
  68. self.try(self.tp:check("3.."))
  69. self.try(self.tp:send(mime.b64(user) .. "\r\n"))
  70. self.try(self.tp:check("3.."))
  71. self.try(self.tp:send(mime.b64(password) .. "\r\n"))
  72. return self.try(self.tp:check("2.."))
  73. end
  74. function metat.__index:plain(user, password)
  75. local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password)
  76. self.try(self.tp:command("AUTH", auth))
  77. return self.try(self.tp:check("2.."))
  78. end
  79. function metat.__index:auth(user, password, ext)
  80. if not user or not password then return 1 end
  81. if string.find(ext, "AUTH[^\n]+LOGIN") then
  82. return self:login(user, password)
  83. elseif string.find(ext, "AUTH[^\n]+PLAIN") then
  84. return self:plain(user, password)
  85. else
  86. self.try(nil, "authentication not supported")
  87. end
  88. end
  89. -- send message or throw an exception
  90. function metat.__index:send(mailt)
  91. self:mail(mailt.from)
  92. if base.type(mailt.rcpt) == "table" then
  93. for i,v in base.ipairs(mailt.rcpt) do
  94. self:rcpt(v)
  95. end
  96. else
  97. self:rcpt(mailt.rcpt)
  98. end
  99. self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step)
  100. end
  101. function _M.open(server, port, create)
  102. local tp = socket.try(tp.connect(server or _M.SERVER, port or _M.PORT,
  103. _M.TIMEOUT, create))
  104. local s = base.setmetatable({tp = tp}, metat)
  105. -- make sure tp is closed if we get an exception
  106. s.try = socket.newtry(function()
  107. s:close()
  108. end)
  109. return s
  110. end
  111. -- convert headers to lowercase
  112. local function lower_headers(headers)
  113. local lower = {}
  114. for i,v in base.pairs(headers or lower) do
  115. lower[string.lower(i)] = v
  116. end
  117. return lower
  118. end
  119. ---------------------------------------------------------------------------
  120. -- Multipart message source
  121. -----------------------------------------------------------------------------
  122. -- returns a hopefully unique mime boundary
  123. local seqno = 0
  124. local function newboundary()
  125. seqno = seqno + 1
  126. return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'),
  127. math.random(0, 99999), seqno)
  128. end
  129. -- send_message forward declaration
  130. local send_message
  131. -- yield the headers all at once, it's faster
  132. local function send_headers(tosend)
  133. local canonic = headers.canonic
  134. local h = "\r\n"
  135. for f,v in base.pairs(tosend) do
  136. h = (canonic[f] or f) .. ': ' .. v .. "\r\n" .. h
  137. end
  138. coroutine.yield(h)
  139. end
  140. -- yield multipart message body from a multipart message table
  141. local function send_multipart(mesgt)
  142. -- make sure we have our boundary and send headers
  143. local bd = newboundary()
  144. local headers = lower_headers(mesgt.headers or {})
  145. headers['content-type'] = headers['content-type'] or 'multipart/mixed'
  146. headers['content-type'] = headers['content-type'] ..
  147. '; boundary="' .. bd .. '"'
  148. send_headers(headers)
  149. -- send preamble
  150. if mesgt.body.preamble then
  151. coroutine.yield(mesgt.body.preamble)
  152. coroutine.yield("\r\n")
  153. end
  154. -- send each part separated by a boundary
  155. for i, m in base.ipairs(mesgt.body) do
  156. coroutine.yield("\r\n--" .. bd .. "\r\n")
  157. send_message(m)
  158. end
  159. -- send last boundary
  160. coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n")
  161. -- send epilogue
  162. if mesgt.body.epilogue then
  163. coroutine.yield(mesgt.body.epilogue)
  164. coroutine.yield("\r\n")
  165. end
  166. end
  167. -- yield message body from a source
  168. local function send_source(mesgt)
  169. -- make sure we have a content-type
  170. local headers = lower_headers(mesgt.headers or {})
  171. headers['content-type'] = headers['content-type'] or
  172. 'text/plain; charset="iso-8859-1"'
  173. send_headers(headers)
  174. -- send body from source
  175. while true do
  176. local chunk, err = mesgt.body()
  177. if err then coroutine.yield(nil, err)
  178. elseif chunk then coroutine.yield(chunk)
  179. else break end
  180. end
  181. end
  182. -- yield message body from a string
  183. local function send_string(mesgt)
  184. -- make sure we have a content-type
  185. local headers = lower_headers(mesgt.headers or {})
  186. headers['content-type'] = headers['content-type'] or
  187. 'text/plain; charset="iso-8859-1"'
  188. send_headers(headers)
  189. -- send body from string
  190. coroutine.yield(mesgt.body)
  191. end
  192. -- message source
  193. function send_message(mesgt)
  194. if base.type(mesgt.body) == "table" then send_multipart(mesgt)
  195. elseif base.type(mesgt.body) == "function" then send_source(mesgt)
  196. else send_string(mesgt) end
  197. end
  198. -- set defaul headers
  199. local function adjust_headers(mesgt)
  200. local lower = lower_headers(mesgt.headers)
  201. lower["date"] = lower["date"] or
  202. os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or _M.ZONE)
  203. lower["x-mailer"] = lower["x-mailer"] or socket._VERSION
  204. -- this can't be overriden
  205. lower["mime-version"] = "1.0"
  206. return lower
  207. end
  208. function _M.message(mesgt)
  209. mesgt.headers = adjust_headers(mesgt)
  210. -- create and return message source
  211. local co = coroutine.create(function() send_message(mesgt) end)
  212. return function()
  213. local ret, a, b = coroutine.resume(co)
  214. if ret then return a, b
  215. else return nil, a end
  216. end
  217. end
  218. ---------------------------------------------------------------------------
  219. -- High level SMTP API
  220. -----------------------------------------------------------------------------
  221. _M.send = socket.protect(function(mailt)
  222. local s = _M.open(mailt.server, mailt.port, mailt.create)
  223. local ext = s:greet(mailt.domain)
  224. s:auth(mailt.user, mailt.password, ext)
  225. s:send(mailt)
  226. s:quit()
  227. return s:close()
  228. end)
  229. return _M