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()<pre> The code is in several classes. Here is the base class MimeBase <pre class="prettyprint"> 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.