Multipurpose Internet Mail Extensions (MIME)

A library for building RFC compliant Multipurpose Internet Mail Extensions (MIME) messages. It can be used to construct standardized MIME messages for use in client/server communications, such as Internet mail or HTTP multipart/form-data transactions.

See

  • MIME for RFCs used to implement the library (other RFCs scattered throughout)

  • MIME::CompositeMediaType for a description of composite media types

  • MIME::DiscreteMediaType for a description of discrete media types

  • MIME::DiscreteMediaFactory for easy programming of discrete media types

  • MIME::ContentFormats for ways to encode/decode discrete media types

Media Type Inheritance Heirarchy

MediaType*
    ^
    |
    |--DiscreteMediaType*
    |      ^
    |      |
    |      |--ApplicationMedia
    |      |--AudioMedia
    |      |--ImageMedia
    |      |--TextMedia
    |      +--VideoMedia
    |
    +--CompositeMediaType*
           ^
           |
           |--MessageMedia**
           |      ^
           |      |
           |      |--ExternalBody**
           |      |--Partial**
           |      +--RFC822**
           |
           +--MultipartMedia*
                  ^
                  |
                  |--Alternative
                  |--Digest**
                  |--Encrypted**
                  |--FormData
                  |--Mixed
                  |--Parallel**
                  |--Related
                  |--Report**
                  +--Signed**

 * Abstract Class
** Not implemented

MIME Message Structure

 ________________  -------------------+
|                |                    |
| RFC822 & MIME  |                    |
| Message Headers|                    |
|________________|                    |
 ________________                     |
|                |                    |
|  MIME Headers  |                    |
|~~~~~~~~~~~~~~~~|  <-- MIME Entity   |
|      Body      |        (N)         |
|   (optional)   |                    |--- RFC822 Message
|________________|                    |
 ________________                     |
|                |                    |
|  MIME Headers  |                    |
|~~~~~~~~~~~~~~~~|  <-- MIME Entity   |
|      Body      |        (N+1)       |
|   (optional)   |                    |
|________________|                    |
                   -------------------+

Each MIME Entity must be a discrete (MIME::DiscreteMediaType) or composite (MIME::CompositeMediaType) media type. Because MIME is recursive, composite entity bodies may contain other composite or discrete entities and so on. However, discrete entities are non-recursive and contain only non-MIME bodies.

Examples

First things first!

require 'mime'
include MIME   # allow ommision of "MIME::" namespace in examples below

Instantiate a DiscreteMediaType object

Discrete media objects, such as text or video, can be created directly using a specific discrete media class or indirectly via the factory. If the media is file backed, like the example below, the factory will open and read the data file and determine the MIME type for you.

file = '/tmp/data.xml'

text_media = TextMedia.new(File.read(file), 'xml')}       # media class
text_media = DiscreteMediaFactory.create(file)            # media factory

Discrete media objects can then be embedded in MIME messages as we will see in the next example.

Simple text/plain RFC822 email message

Create a well-formed email message with multiple recipients. The string representation of the message (i.e. to_s) can then be sent directly via an SMTP client.

msg = Message.new # blank message with current date and message ID headers
msg.date = (Time.now - 3600).rfc2822    # change date
msg.subject = 'This is important'       # add subject
msg.headers.add('Priority', 'urgent')   # add custom header

msg.body = TextMedia.new('hello, world!', 'plain', 'charset' => 'us-ascii')
#
# The following two snippets are equivalent to the previous line.
#
#   msg.body = "\r\nhello, world!"
#   msg.header.add('Content-Type', 'text/plain; charset=us-ascii')
#
#   --OR-- (notice the header must come first, followed by two CRLFs)
#
#   msg.body = "Content-Type: text/plain; charset=us-ascii\r\n\r\nhello, world!"

msg.to = {
  '[email protected]' => nil,           # no name display
  '[email protected]' => 'James Smith',
  '[email protected]' => 'Clint Pachl',
}
msg.from = {
  '[email protected]'  => 'Boss Man'
}

msg.to_s  # ready to be sent via SMTP

Plain text multipart/mixed message with a file attachment

The multipart/mixed content type can be used to aggregate multiple unrelated entities, such as text and an image.

text  = DiscreteMediaFactory.create('/tmp/data.txt')
image = DiscreteMediaFactory.create('/tmp/ruby.png')

mixed_msg = MultipartMedia::Mixed.new
mixed_msg.attach_entity(image)
mixed_msg.add_entity(text)
mixed_msg.to_s

Plain text and HTML multipart/alternative MIME message

The multipart/alternative content type allows for multiple, alternatively formatted versions of the same content, such as plain text and HTML. Clients are then responsible for choosing the most suitable version for display.

text_msg = TextMedia.new(<<TEXT_DATA, 'plain')
**Hello, world!**

Ruby is cool!
TEXT_DATA

html_msg = TextMedia.new(<<HTML_DATA, 'html')
<html>
<body>
  <h1>Hello, world!</h1>
  <p>Ruby is cool!</p>
</body>
</html>
HTML_DATA

msg = MultipartMedia::Alternative.new
msg.add_entity(html_msg)  # most complex representations must be added first
msg.add_entity(text_msg)
msg.to_s

HTML multipart/related MIME email with embedded image

Sometimes it is desirable to send a document that is made up of many separate parts. For example, an HTML page with embedded images. The multipart/related content type aggregates all the parts and creates the means for the root entity to reference the other entities.

Notice the img tag src.

image = DiscreteMediaFactory.create('/tmp/ruby.png')
image.content_transfer_encoding = 'binary'

html_msg = TextMedia.new(<<EOF, 'html', 'charset' => 'iso-8859-1')
<html>
<body>
  <h1>Ruby Image</h1>
  <p>
    Check out this cool pic.
    <img alt="ruby is cool" src="cid:#{image.content_id}">
  </p>
  <p>Wasn't it cool?</p>
</body>
</html>
EOF

html_msg.content_transfer_encoding = '7bit'

related_msg = MultipartMedia::Related.new
related_msg.inline_entity(image)
related_msg.add_entity(html_msg)

email_msg = Message.new(related_msg)
email_msg.to = {'[email protected]' => 'Joe Schmo'}
email_msg.from = {'[email protected]' => 'John Doe'}
email_msg.subject = 'Ruby is cool, checkout the picture'
email_msg.to_s  # ready to send HTML email with image

HTML form with file upload using multipart/form-data encoding

This example builds a representation of an HTML form that can be POSTed to an HTTP server. It contains a single text input and a file input.

name_field = TextMedia.new('Joe Blow')

portrait_filename = '/tmp/joe_portrait.jpg'

portrait_field = open(portrait_filename) do |f|
  ImageMedia.new(f.read, 'jpeg')        # explicit content type
end
portrait_field.content_transfer_encoding = 'binary'

form_data = MultipartMedia::FormData.new
form_data.add_entity(name_field,        # TextMedia object
                     'name')            # field name, i.e. HTML input type=text
form_data.add_entity(portrait_field,    # ImageMedia object
                     'portrait',        # field name, i.e. HTML input type=file
                     portrait_filename) # suggest filename to server
form_data.to_s  # ready to POST via HTTP

HTML form with file upload via DiscreteMediaFactory

The outcome of this example is identical to the previous one. The only semantic difference is that the DiscreteMediaFactory module is used to instantiate the image object.

name_field = TextMedia.new('Joe Blow')

img = '/tmp/joe_portrait.jpg'
portrait_field = DiscreteMediaFactory.create(img)   # automatic content type
portrait_field.content_transfer_encoding = 'binary'

form_data = MultipartMedia::FormData.new
form_data.add_entity(name_field, 'name')
form_data.add_entity(portrait_field, 'portrait')    # automatic file name
form_data.to_s

Avoid “embarrassing line wraps” using flowed format for text/plain

Text/Plain is usually displayed as preformatted text, often in a fixed font. That is, the characters start at the left margin of the display window, and advance to the right until a CRLF sequence is seen, at which point a new line is started, again at the left margin. When a line length exceeds the display window, some clients will wrap the line, while others invoke a horizontal scroll bar. The result: embarrassing line wraps.

Flowed format allows the sender to express to the receiver which lines can be considered a logical paragraph, and thus flowed (wrapped and joined) as appropriate.

long_paragraph =
  "This is a continuous fixed-line-length paragraph that is longer than " +
  "80 characters and will be soft line wrapped after the word '80'.\n\n"

flowed_txt = ContentFormats::TextFlowed.encode(long_paragraph * 2)
flowed_msg = TextMedia.new(flowed_txt, 'plain', 'format' => 'flowed')
flowed_msg.to_s # neatly formatted text compatible with large to small screens

More Examples

For many more examples, check the test class MIMETest.

Ruby Gem

rubygems.org/gems/mime

Source Code

bitbucket.org/pachl/mime/src

Documentation

ecentryx.com/gems/mime

History

  1. 2008-11-05, v0.1

    • First public release.

  2. 2013-12-18, v0.2.0

    • Update for Ruby 1.9.3.

    • Update Rakefile test, package, and rdoc tasks.

    • Change test suite from Test::Unit to Minitest.

    • Cleanup existing and add new tests cases.

    • Clarify code comments and README examples.

    • Fix content type detection.

  3. 2014-02-28, v0.3.0

    • Simplify API of DiscreteMediaType subclasses.

    • Disallow Content-Type changes after instantiating DiscreteMediaType.

    • Add flowed format support for text/plain (RFC 2646).

License

(ISC License)

Copyright © 2014, Clint Pachl <[email protected]>

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.