Multipurpose Internet Mail Extensions (MIME) is an Internet Standard
that extends the format of e-mail to support non-text attachments and
multi-part message bodies.
I added some MIME support to Suneido so I could send emails with attachments from our applications.
I based the design on the Python email module as described in Foundations of Python Network Programming, however I did not try to copy it exactly.
Here's an example of generating a simple plain text message:
MimeText("hello world").
To("joe@hotmail.com").
From("sue@mail.com").
Subject("html test").
Date().
Message_ID().
ToString()
would produce:
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
To: joe@hotmail.com
From: sue@mail.com
Date: Fri, 19 Oct 2007 14:41:02 -0600
Subject: html test
Message-ID: <2d91ef3e.5685.4d7c.b664.cdac914677f0@suneido.com>
hello world
And here's an example of generating a message with an attachment:
MimeMultiPart().
To("joe@hotmail.com").
From("sue@mail.com").
Subject("multipart test").
Date().
Message_ID().
AttachFile("image.jpg").
ToString()
The code is in several classes. Here is the base class MimeBase
class
{
New(maintype = 'text', subtype = 'plain')
{
.hdr = Object().Set_default(false)
.extra = Object().Set_default('')
.maintype = maintype
.subtype = subtype
.fields = .fields.Copy()
}
payload: ''
SetPayload(s)
{
.payload = s
return this
}
fields: ('Content-Transfer-Encoding', To, From, Date, Subject,
'Message-ID')
Default(@args)
{
field = args[0].Tr('_', '-')
if .fields.Has?(field)
{
if args.Size() is 1
{
if field is 'Date'
value = Date()
else if field is 'Message-ID'
value = .message_id()
}
else
value = args[1]
.AddHeader(field, value)
}
else
throw "MimeText: method not found: " $ args[0]
return this
}
AddHeader(@args)
{
name = args[0]
value = args[1]
.hdr[name] = Date?(value) ? .date(value) : value
.fields.AddUnique(name)
if args.Size() is 3
{
m = args.Members()[2]
.extra[name] = '; ' $ m $ '=' $ Display(args[m])
}
return this
}
AddExtra(@args)
{
name = args.Members()[1]
value = args[name]
.extra[args[0]] = '; ' $ name $ '=' $ Display(value)
return this
}
date(date)
{
return date.Format("ddd, d MMM yyyy HH:mm:ss") $
' -' $ (Date.GetLocalGMTBias() / 60).Pad(2) $ '00'
}
message_id()
{
return '<' $ UuidString().Tr('-', '.') $ '@suneido.com' $ '>'
}
encode(s) { s }
Base64()
{
.AddHeader('Content-Transfer-Encoding', 'base64')
.encode = Base64.EncodeLines
return this
}
ToString()
{
s = 'Content-Type: ' $ .maintype $ '/' $ .subtype $
.extra['Content-Type'] $ '\r\n' $
'MIME-Version: 1.0\r\n'
for f in .fields
if .hdr.Member?(f)
s $= f $ ': ' $ .hdr[f] $
.extra[f] $ '\r\n'
s $= '\r\n'
s $= (.encode)(.payload)
if s.Substr(-2) isnt '\r\n'
s $= '\r\n'
return s
}
}
This
is mostly straightforward. One "trick" is using the Default method to
handle methods for the standard headers. This avoids having to define
separate methods for each. Since method names don't allow dashes (-),
undescores are converted to dashes. Date and Message-ID are handled
specially to provide default values. Note: These are shortcut
convenience methods - you could use AddHeader.
The AddExtra
method is used to add "extra" information to an existing header field
e.g. to add a charset value to Content-Type. Extra information is also
handled by AddHeader e.g. AddHeader("Content-Disposition", "attachment",
filename: "image.jpg")
Most of the methods return "this" to allow "chaining method calls as in the examples above.
This code requires a new EncodeLines method in Base64
EncodeLines(src, eol = '\r\n', linelen = 70)
{
src = .Encode(src)
for (dst = ""; src isnt ""; src = src.Substr(linelen))
dst $= src.Substr(0, linelen) $ eol
return dst
}
Text messages do not require much more than MimeBase so MimeText is quite small:
MimeBase
{
New(text = "", subtype = "plain", charset = "us-ascii")
{
super('text', subtype)
.AddExtra('Content-Type', charset: charset)
.Content_Transfer_Encoding('7bit')
.SetPayload(text.ChangeEol('\r\n'))
}
}
Multipart (and alternative) messages are also quite simple:
MimeBase
{
New(subtype = 'mixed')
{
super('multipart', subtype)
.parts = Object()
}
Attach(mime)
{
.parts.Add(mime)
return this
}
AttachFile(filename)
{
ext = filename.AfterLast('.')
type = MimeTypes.GetDefault(ext, 'application/octet-stream').Split('/')
if type[0] is 'text'
m = MimeText(GetFile(filename), type[1])
else
{
m = MimeBase(type[0], type[1])
if false is s = GetFile(filename)
throw "MimeMultiPart: AttachFile: can't get: " $ filename
m.SetPayload(s).Base64()
}
m.AddHeader('Content-Disposition', 'attachment',
filename: filename.Basename())
.Attach(m)
}
ToString()
{
boundary = '='.Repeat(20) $ Random(100000) $ Random(100000)
.AddExtra('Content-Type', boundary: boundary)
boundary = '--' $ boundary $ '\r\n'
s = super.ToString() $ boundary
for p in .parts
s $= p.ToString() $ boundary
s = s.Substr(0, -2) $ '--\r\n'
return s
}
}
There are other features that could be added, but this code provides the main functions you need to send MIME messages.