1
2
3
4local type, setmetatable, next = type, setmetatable, next
5local find, lower, format = string.find, string.lower, string.format
6local osdate, osgetenv = os.date, os.getenv
7local random = math.random
8
9local socket = socket or require("socket")
10local headers = socket.headers or require("socket.headers")
11local ltn12 = ltn12 or require("ltn12")
12local tp = socket.tp or require("socket.tp")
13local mime = mime or require("mime")
14
15local mimeb64 = mime.b64
16local mimestuff = mime.stuff
17
18local skipsocket = socket.skip
19local trysocket = socket.try
20local newtrysocket = socket.newtry
21local protectsocket = socket.protect
22
23local normalizeheaders = headers.normalize
24local lowerheaders = headers.lower
25
26local createcoroutine = coroutine.create
27local resumecoroutine = coroutine.resume
28local yieldcoroutine = coroutine.resume
29
30local smtp = {
31 TIMEOUT = 60,
32 SERVER = "localhost",
33 PORT = 25,
34 DOMAIN = osgetenv("SERVER_NAME") or "localhost",
35 ZONE = "-0000",
36}
37
38socket.smtp = smtp
39
40local methods = { }
41local mt = { __index = methods }
42
43function methods.greet(self, domain)
44 local try = self.try
45 local tp = self.tp
46 try(tp:check("2.."))
47 try(tp:command("EHLO", domain or _M.DOMAIN))
48 return skipsocket(1, try(tp:check("2..")))
49end
50
51function methods.mail(self, from)
52 local try = self.try
53 local tp = self.tp
54 try(tp:command("MAIL", "FROM:" .. from))
55 return try(tp:check("2.."))
56end
57
58function methods.rcpt(self, to)
59 local try = self.try
60 local tp = self.tp
61 try(tp:command("RCPT", "TO:" .. to))
62 return try(tp:check("2.."))
63end
64
65function methods.data(self, src, step)
66 local try = self.try
67 local tp = self.tp
68 try(tp:command("DATA"))
69 try(tp:check("3.."))
70 try(tp:source(src, step))
71 try(tp:send("\r\n.\r\n"))
72 return try(tp:check("2.."))
73end
74
75function methods.quit(self)
76 local try = self.try
77 local tp = self.tp
78 try(tp:command("QUIT"))
79 return try(tp:check("2.."))
80end
81
82function methods.close(self)
83 return self.tp:close()
84end
85
86function methods.login(self, user, password)
87 local try = self.try
88 local tp = self.tp
89 try(tp:command("AUTH", "LOGIN"))
90 try(tp:check("3.."))
91 try(tp:send(mimeb64(user) .. "\r\n"))
92 try(tp:check("3.."))
93 try(tp:send(mimeb64(password) .. "\r\n"))
94 return try(tp:check("2.."))
95end
96
97function methods.plain(self, user, password)
98 local try = self.try
99 local tp = self.tp
100 local auth = "PLAIN " .. mimeb64("\0" .. user .. "\0" .. password)
101 try(tp:command("AUTH", auth))
102 return try(tp:check("2.."))
103end
104
105function methods.auth(self, user, password, ext)
106 if not user or not password then
107 return 1
108 end
109 local try = self.try
110 if find(ext, "AUTH[^\n]+LOGIN") then
111 return self:login(user,password)
112 elseif find(ext, "AUTH[^\n]+PLAIN") then
113 return self:plain(user,password)
114 else
115 try(nil, "authentication not supported")
116 end
117end
118
119function methods.send(self, mail)
120 self:mail(mail.from)
121 local receipt = mail.rcpt
122 if type(receipt) == "table" then
123 for i=1,#receipt do
124 self:rcpt(receipt[i])
125 end
126 elseif receipt then
127 self:rcpt(receipt)
128 end
129 self:data(ltn12.source.chain(mail.source, mimestuff()), mail.step)
130end
131
132local function opensmtp(self, server, port, create)
133 if not server or server == "" then
134 server = smtp.SERVER
135 end
136 if not port or port == "" then
137 port = smtp.PORT
138 end
139 local s = {
140 tp = trysocket(tp.connect(server, port, smtp.TIMEOUT, create)),
141 try = newtrysocket(function()
142 s:close()
143 end),
144 }
145 setmetatable(s, mt)
146 return s
147end
148
149smtp.open = opensmtp
150
151local nofboundaries = 0
152
153local function newboundary()
154 nofboundaries = nofboundaries + 1
155 return format('%s%05d==%05u', osdate('%d%m%Y%H%M%S'), random(0,99999), nofboundaries)
156end
157
158local send_message
159
160local function send_headers(headers)
161 yieldcoroutine(normalizeheaders(headers))
162end
163
164local function send_multipart(message)
165 local boundary = newboundary()
166 local headers = lowerheaders(message.headers)
167 local body = message.body
168 local preamble = body.preamble
169 local epilogue = body.epilogue
170 local content = headers['content-type'] or 'multipart/mixed'
171 headers['content-type'] = content .. '; boundary="' .. boundary .. '"'
172 send_headers(headers)
173 if preamble then
174 yieldcoroutine(preamble)
175 yieldcoroutine("\r\n")
176 end
177 for i=1,#body do
178 yieldcoroutine("\r\n--" .. boundary .. "\r\n")
179 send_message(body[i])
180 end
181 yieldcoroutine("\r\n--" .. boundary .. "--\r\n\r\n")
182 if epilogue then
183 yieldcoroutine(epilogue)
184 yieldcoroutine("\r\n")
185 end
186end
187
188local default_content_type = 'text/plain; charset="UTF-8"'
189
190local function send_source(message)
191 local headers = lowerheaders(message.headers)
192 if not headers['content-type'] then
193 headers['content-type'] = default_content_type
194 end
195 send_headers(headers)
196 local getchunk = message.body
197 while true do
198 local chunk, err = getchunk()
199 if err then
200 yieldcoroutine(nil, err)
201 elseif chunk then
202 yieldcoroutine(chunk)
203 else
204 break
205 end
206 end
207end
208
209local function send_string(message)
210 local headers = lowerheaders(message.headers)
211 if not headers['content-type'] then
212 headers['content-type'] = default_content_type
213 end
214 send_headers(headers)
215 yieldcoroutine(message.body)
216end
217
218function send_message(message)
219 local body = message.body
220 if type(body) == "table" then
221 send_multipart(message)
222 elseif type(body) == "function" then
223 send_source(message)
224 else
225 send_string(message)
226 end
227end
228
229local function adjust_headers(message)
230 local headers = lowerheaders(message.headers)
231 if not headers["date"] then
232 headers["date"] = osdate("!%a, %d %b %Y %H:%M:%S ") .. (message.zone or smtp.ZONE)
233 end
234 if not headers["x-mailer"] then
235 headers["x-mailer"] = socket._VERSION
236 end
237 headers["mime-version"] = "1.0"
238 return headers
239end
240
241function smtp.message(message)
242 message.headers = adjust_headers(message)
243 local action = createcoroutine(function()
244 send_message(message)
245 end)
246 return function()
247 local ret, a, b = resumecoroutine(action)
248 if ret then
249 return a, b
250 else
251 return nil, a
252 end
253 end
254end
255
256smtp.send = protectsocket(function(mail)
257 local snd = opensmtp(smtp,mail.server, mail.port, mail.create)
258 local ext = snd:greet(mail.domain)
259 snd:auth(mail.user, mail.password, ext)
260 snd:send(mail)
261 snd:quit()
262 return snd:close()
263end)
264
265package.loaded["socket.smtp"] = smtp
266
267return smtp
268 |