Netrequire Server

Catalogue > mime.lua

Sends MIME-encoded email attachements using the SMTP protocol. A wrapper around net.smtp.send.

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
109:
110:
111:
112:
113:
114:
115:
116:
117:
118:
119:
120:
121:
122:
123:
124:
125:
126:
127:
128:
129:
130:
131:
132:
133:
134:
135:
136:
137:
138:
139:
140:
141:
142:
143:
144:
145:
146:
147:
148:
149:
150:
151:
152:
153:
154:
155:
156:
157:
158:
159:
160:
161:
162:
163:
164:
165:
166:
167:
168:
169:
170:
171:
172:
173:
174:
175:
176:
177:
178:
179:
180:
181:
182:
183:
184:
185:
186:
187:
188:
189:
190:
191:
192:
193:
194:
195:
196:
197:
198:
199:
200:
201:
202:
203:
204:
205:
206:
207:
208:
209:
210:
211:
212:
213:
214:
215:
216:
217:
218:
219:
220:
221:
222:
223:
224:
225:
226:
227:
228:
229:
230:
231:
232:
233:
234:
235:
236:
237:
238:
239:
240:
241:
242:
243:
244:
245:
246:
247:
248:
249:
250:
251:
252:
253:
254:
255:
256:
257:
258:
259:
260:
261:
262:
263:
264:
265:
266:
267:
268:
269:
270:
271:
272:
273:
274:
275:
276:
277:
278:
279:
280:
281:
282:
283:
284:
285:
286:
287:
288:
289:
290:
291:
292:
293:
294:
295:
296:
297:
298:
299:
300:
-- $Revision: 1.7 $
-- $Date: 2013-06-17 17:49:30 $

--
-- The mime module
-- Copyright (c) 2011-2012 iNTERFACEWARE Inc. ALL RIGHTS RESERVED
-- iNTERFACEWARE permits you to use, modify, and distribute this file in
-- accordance with the terms of the iNTERFACEWARE license agreement
-- accompanying the software in which it is used.
--
 
-- Basic SMTP/MIME module for sending MIME formatted attachments via
-- SMTP.
--
-- An attempt is made to format the MIME parts with the correct headers,
-- and pathnames that represent non-plain-text data are Base64 encoded
-- when constructing the part for that attachment.
--
-- SMTP/MIME is a large and complicated standard; only part of those
-- standards are supported here. The assumption is that most mailers
-- and mail transfer agents will do their best to handle inconsistencies.
--
-- Example usage:
--
-- local Results = mime.send{
--    server='smtp://mysmtp.com:25', username='john', password='password',
--    from='john@smith.com', to={'john@smith.com', 'jane@smith.com'},
--    header={['Subject']='Test Subject'}, body='Test Email Body', use_ssl='try',
--    attachments={'/home/jsmith/pictures/test.jpeg'},
-- }

mime = {}

local mimehelp = {
   Title="mime.send";
   Usage="mime.send{server=<value> [, username=<value>] [, ...]}",
   Desc=[[Sends an email using the SMTP protocol. A wrapper around net.smtp.send.
   Accepts the same parameters as net.smtp.send, with an additional "attachments"
   parameter:
   ]];
   ["Returns"] = {
      {Desc="nothing."},
   };
   ParameterTable= true,
   Parameters= {
      {attachments= {Desc='A table of absolute filenames to be attached to the email.'}},
   };
   Examples={
      [[local Results = mime.send{
      server='smtp://mysmtp.com:25', username='john', password='password',
      from='john@smith.com', to={'john@smith.com', 'jane@smith.com'},
      header={['Subject']='Test Subject'}, body='Test Email Body', use_ssl='try',
      attachments={'/home/jsmith/pictures/test.jpeg'},
   }]],
   };
   SeeAlso={
      {
         Title="net.smtp - sending mail",
         Link="http://wiki.interfaceware.com/1039.html#send"
      },
      {
         Title="Tips and tricks from John Verne",
         Link="http://wiki.interfaceware.com/1342.html"
      }
   }
}

-- Common file extensions and the corresponding
-- MIME sub-type we will probably encounter.
-- Add more as necessary.
local MIMEtypes = {
  ['pdf']  = 'application/pdf',
  ['jpeg'] = 'image/jpeg',
  ['jpg']  = 'image/jpeg',
  ['gif']  = 'image/gif',
  ['png']  = 'image/png',
  ['zip']  = 'application/zip',
  ['gzip'] = 'application/gzip',
  ['tiff'] = 'image/tiff',
  ['html'] = 'text/html',
  ['htm']  = 'text/html',
  ['mpeg'] = 'video/mpeg',
  ['mp4']  = 'video/mp4',
  ['txt']  = 'text/plain',
  ['exe']  = 'application/plain',
  ['js']   = 'application/javascript',
}

-- Most mailers support UTF-8
local defaultCharset = 'utf8'

--
-- Local helper functions
--

-- Given a filespec, open it up and see if it is a
-- "binary" file or not. This is a best guess.
-- Tweak the pattern to suit.
local function isBinary(filename)
  local input = assert(io.open(filename, "rb"))

  local isbin = false
  local chunk_size = 2^12 -- 4k bytes

  repeat
    local chunk = input.read(input, chunk_size)
    if not chunk then break end

    if (string.find(chunk, "[^\f\n\r\t\032-\128]")) then
      isbin = true
      break
    end
  until false
  input:close()

  return isbin
end

-- Read the passed in filespec into a local variable.
local function readFile(filename)
  local f = assert(io.open(filename, "rb"))
  -- We could read this in chunks, but at the end of the day
  -- we are still streaming it into a local anyway.
  local data = f:read("*a")
  f:close()

  return data
end
    
-- Based on extension return an appropriate MIME sub-type
-- for the filename passed in.
-- Return 'application/unknown' if we can't figure it out.
local function getContentType(extension)
  local MIMEtype = 'application/unknown'

  for ext, subtype in pairs(MIMEtypes) do
    if ext == extension then
      MIMEtype = subtype
    end
  end

  return MIMEtype
end

-- Base64 encode the content passed in. Break the encoded data
-- into reasonable lengths per RFC2821 and friends.
local function ASCIIarmor(content)
  local armored = ''
  local encoded = filter.base64.enc(content)
  
  -- SMTP RFCs suggests that 990 or 1000 is valid for most MTAs and
  -- MUAs. For debugging set this to 72 or some other human-readable
  -- break-point.
  local maxl = 990 - 2  -- Less 2 for the trailing CRLF pair
  local len = encoded:len()
  local start = 1
  local lineend = start + maxl
  while lineend <= len do
    local line = encoded:sub(start, lineend)
    armored = string.format("%s\r\n%s", armored, line)

    -- We got it all; leave now.
    if lineend == len then break end

    -- Move the counters forward
    start = lineend + 1
    lineend = start + maxl

    -- Make sure we pick up the last fragment
    if lineend > len then lineend = len end
  end

  if armored == '' then
    return encoded
  else
    return armored
  end
end

-- Similar to net.smtp.send with a single additional required parameter
-- of an array of local absolute filenames to add to the message
-- body as attachments.
--
-- An attempt is made to add the attachment parts with the right
-- MIME-related headers.
function mime.send(args)
  local server = args.server
  local to = args.to
  local from = args.from
  local header = args.header
  local body = args.body
  local attachments = args.attachments
  local username = args.username
  local password = args.password
  local timeout = args.timeout
  local use_ssl = args.use_ssl
  local live = args.live
  local debug = args.debug
  
  -- Blanket non-optional parameter enforcement.
  if server == nil or to == nil or from == nil
      or header == nil or body == nil
         or attachments == nil then
      error("Missing required parameter.", 2)
  end

  -- Create a unique ID to use for multi-part boundaries.
  local boundaryID = util.guid(128)
  if debug then
    -- Debug hook
    boundaryID = 'xyzzy_0123456789_xyzzy'
  end
  local partBoundary = '--' .. boundaryID
  local endBoundary = '--' .. boundaryID .. '--'

  -- Append our headers, set up the multi-part message.
  header['MIME-Version'] = '1.0'
  header['Content-Type'] = 'multipart/mixed; boundary=' .. boundaryID

  -- Preload the body part.
  local msgBody =
    string.format(
      '%s\r\nContent-Type: text/plain; charset="%s"\r\n\r\n%s',
        partBoundary, defaultCharset, body)

  -- Iterate over each attachment filespec, building up the 
  -- SMTP body chunks as we go.
  for _, filespec in ipairs(attachments) do
    local path, filename, extension =
          string.match(filespec, "(.-)([^\\/]-%.?([^%.\\/]*))$")

    -- Get the (best guess) content-type and file contents.
    -- Cook the contents into Base64 if necessary.
    local contentType = getContentType(extension)
    local isBinary = isBinary(filespec)
    local content = readFile(filespec)
    if isBinary then
      content = ASCIIarmor(content)
    end
    
    -- Existing BodyCRLF
    -- Part-BoundaryCRLF
    -- Content-Type:...CRLF
    -- Content-Disposition:...CRLF
    -- [Content-Transfer-Encoding:...CRLF]
    -- contentCRLF
    local msgContentType =
      string.format('Content-Type: %s; charset="%s"; name="%s"',
        contentType, isBinary and 'B' or defaultCharset, filename)
    local msgContentDisposition =
      string.format('Content-Disposition: inline; filename="%s"',
        filename)
    -- We could use "quoted-printable" to make sure we handle
    -- occasional non-7-bit text data, but then we'd have to break
    -- the passed-in data into max 76 char lines. We don't really
    -- want to munge the original data that much. Defaulting to 
    -- 7bit should work in most cases, and supporting quoted-printable
    -- makes things pretty complicated (and increases the message
    -- size even more.)
    local msgContentTransferEncoding = isBinary and
      'Content-Transfer-Encoding: base64\r\n' or ''

    -- Concatenate the current chunk onto the entire body.
    msgBody =
      string.format('%s\r\n\r\n%s\r\n%s\r\n%s\r\n%s%s\r\n', 
        msgBody, partBoundary, msgContentType, msgContentDisposition,
        msgContentTransferEncoding, content)
  end

  -- End the message body
  msgBody = string.format('%s\r\n%s', msgBody, endBoundary)

  -- Send the message via net.smtp.send()
  net.smtp.send{
    server = server,
    to = to,
    from = from,
    header = header,
    body = msgBody,
    username = username,
    password = password,
    timeout = timeout,
    use_ssl = use_ssl,
    live = live,
    debug = debug
  }

  -- Debug hook
  if debug then
    return msgBody, header
  end

end

-- Hook up the help, if present.
if help then
  help.set{input_function=mime.send, help_data=mimehelp}
end

return mime