mime-0.4.4/0000755000004100000410000000000013126650165012503 5ustar www-datawww-datamime-0.4.4/Rakefile0000644000004100000410000000112013126650165014142 0ustar www-datawww-datarequire 'rdoc/task' require 'rubygems/package_task' require 'rake/testtask' Gem::PackageTask.new(eval File.read('mime.gemspec')) do |pkg| pkg.need_tar_gz = true end RDoc::Task.new do |rdoc| rdoc.main = 'README.rdoc' rdoc.rdoc_dir = 'doc' rdoc.rdoc_files.include('README.rdoc', 'lib/') rdoc.options << "--all" end Rake::TestTask.new do |t| t.warning = true end desc 'Upload RDoc HTML files to server' task :upload_rdoc => :rdoc do %x[ rdir=/var/www/sites/ecentryx.com/public/gems/mime rsvr=hercules cd ./doc && pax -w . | ssh $rsvr "cd $rdir && rm -rf ./*; pax -r" ] end mime-0.4.4/mime.gemspec0000644000004100000410000000137413126650165015004 0ustar www-datawww-dataversion = File.read('./lib/mime.rb')[/VERSION = '(.*)'/, 1] Gem::Specification.new('mime', version) do |s| s.author = 'Clint Pachl' s.email = 'pachl@ecentryx.com' s.homepage = 'https://ecentryx.com/gems/mime' s.license = 'ISC' s.summary = 'Multipurpose Internet Mail Extensions (MIME) Library' s.description = <<-EOF 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. EOF s.files = Dir['README.rdoc', 'Rakefile', 'mime.gemspec', 'lib/**/*.rb', 'test/**/*'] s.test_files = Dir['test/*.rb'] end mime-0.4.4/README.rdoc0000644000004100000410000003403513126650165014316 0ustar www-datawww-data== 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 Also * MIME for RFCs used to implement the library (other RFCs scattered throughout) * MIME::CompositeMedia for a description of composite media types * MIME::DiscreteMedia 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 Inheritance Heirarchy Media* ^ | |--DiscreteMedia* | ^ | | | |--Application | |--Audio | |--Image | |--Text | +--Video | +--CompositeMedia* ^ | |--Message** | ^ | | | |--ExternalBody** | |--Partial** | +--RFC822** | +--Multipart* ^ | |--Alternative |--Digest** |--Encrypted** |--FormData |--Mixed |--Parallel** |--Related |--Report** +--Signed** * Abstract Class ** Not implemented == MIME Message Format ________________ -------------------+ | | | | RFC822 & MIME | | | Headers | | |________________| | ________________ | | | | | MIME Headers | | |~~~~~~~~~~~~~~~~| <-- MIME Entity* |--- RFC822/MIME | Body* | (N) | Message |________________| | ________________ | | | | | MIME Headers | | |~~~~~~~~~~~~~~~~| <-- MIME Entity* | | Body* | (N+1) | |________________| | -------------------+ * Optional Each MIME Entity must be a discrete (MIME::DiscreteMedia) or composite (MIME::CompositeMedia) 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 DiscreteMedia 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 = Text.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 = Mail.new # blank message with current date and message ID headers msg.date -= 3600 # change date to 1 hour ago msg.subject = 'This is important' # add subject msg.headers.set('Priority', 'urgent') # add custom header msg.body = Text.new('hello, world!', 'plain', 'charset' => 'us-ascii') # # The previous line is equivalent to the following snippet: # # msg.body = 'hello, world!' # msg.headers.set('Content-Type', 'text/plain; charset=us-ascii') msg.from = {'boss@example.com' => 'Boss Man'} # mailbox hash msg.bcc = 'boss+home@example.com' # mailbox string msg.cc = %w(secretary@example.com manager@example.com) # mailbox array msg.to = { 'list@example.com' => nil, # no name display 'john@example.com' => 'John Doe', 'jane@example.com' => 'Jane Doe', } msg.to_s # ready to be sent via SMTP === RFC822 email message with image body Embedding a single image within an email message requires a single discrete media image. However, embedding multiple images requires a multipart/mixed composite media type to encapsulate all of the discrete media images. ==== Email body with single image img = Image.new(File.read('screenshot.png'), 'png') img.disposition = 'inline' img.description = 'My screenshot' email = Mail.new(img) ==== Email body with multiple images msg = Multipart::Mixed.new msg.inline Image.new(File.read('screenshot1.png'), 'png') msg.inline Image.new(File.read('screenshot2.png'), 'png') msg.description = 'My screenshots' email = Mail.new(msg) === 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 = Multipart::Mixed.new mixed_msg.add(text) mixed_msg.attach(image) 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 = Text.new(<

Hello, world!

Ruby is cool!

HTML_DATA msg = Multipart::Alternative.new msg.add(text_msg) # add the simplest representations first msg.add(html_msg) msg.to_s # or send in an email: Mail.new(msg) === 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 _src_ of the _img_ tag. image = DiscreteMediaFactory.create('/tmp/ruby.png') image.transfer_encoding = 'binary' # could base64 encode the image instead html_msg = Text.new(< 'utf-8')

A Shiny Red Ruby

a ruby gem EOF related_msg = Multipart::Related.new related_msg.add(html_msg) # first entity added becomes the root object related_msg.inline(image) # the root object references the inline image email_msg = Mail.new(related_msg) email_msg.to = 'jane@example.com' email_msg.from = 'john@example.com' 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 = Text.new('Joe Blow') portrait_path = '/tmp/joe_portrait.jpg' portrait_field = Image.new(File.read(portrait_path), 'jpeg') portrait_field.transfer_encoding = 'binary' form_data = Multipart::FormData.new form_data.add(name_field, # field value 'name') # field name, i.e. HTML input type=text form_data.add(portrait_field, # field value 'portrait', # field name, i.e. HTML input type=file portrait_path) # suggest image 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 and automatically set the content type and FormData filename. name_field = Text.new('Joe Blow') portrait_path = '/tmp/joe_portrait.jpg' portrait_field = DiscreteMediaFactory.create(portrait_path) portrait_field.transfer_encoding = 'binary' form_data = Multipart::FormData.new form_data.add(name_field, 'name') form_data.add(portrait_field, 'portrait') 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 = Text.new(flowed_txt, 'plain', 'format' => 'flowed') flowed_msg.to_s # neatly formatted text compatible with small to large screens == More Examples For many more examples, check the test class MIMETest. == Links Homepage :: https://ecentryx.com/gems/mime Ruby Gem :: https://rubygems.org/gems/mime Source Code :: https://bitbucket.org/pachl/mime/src Bug Tracker :: https://bitbucket.org/pachl/mime/issues Validators :: Please check *all* of your messages using the following lint tools and report errors to the bug tracker or directly to pachl@ecentryx.com with "Ruby MIME" in the subject. - http://www.apps.ietf.org/content/message-lint - https://tools.ietf.org/tools/msglint/ == 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). 4. 2014-04-18, v0.4.0 * Major API disruption! * Rename classes: - HeaderContainter => Header - remove "Media" suffix from the 5 DiscreteMedia and 2 CompositeMedia subclasses. See {commit}[https://bitbucket.org/pachl/mime/commits/4876b1390624a1c07682d2a11fd90296a83c002d] for details. - MIME::Message => MIME::Mail * Rename methods: - remove "_entity" suffix from add, inline, and attach in CompositeMedia. - remove "content_" prefix from id, disposition, description, and transfer_encoding in Headers::MIME. * Remove methods: - Header#add * Add methods: - Header#set (replace Header#add) - Header#get - Header#delete * Reverse order of entities in CompositeMedia::Body, which most likely reverses all CompositeMedia #add, #inline, and #attach method calls. See {commit}[https://bitbucket.org/pachl/mime/commits/a51745f346b517929383bdb6e60301c385ffab49] for details. * Other changes * Use From header field domain in the Message-ID header. * Add more randomness when generating header IDs. * Header field names are now case-insensitive to comply with RFCs. * Accept String, Array, and Hash for originator and destination mailboxes. * Add CompositeMedia::Body class for nesting MIME entities. * Improve docs and examples for Content-Disposition (inline/attachment). * FIX: remove trailing CRLF in Mail#to_s and update tests. * Add README links to message lint tools on IETF.org. * Add Send, In-Reply-To, and References RFC 5322 header fields. * Comply with RFC regarding parameter quoting in header field bodies. I.e., do not quote atom/dot-atom parameter values. * Many fixes and improvements in code, tests, documentation, and examples. 5. 2014-04-20, v0.4.1 * Add bug tracker URL. * Link to referenced commit messages. 6. 2014-06-13, v0.4.2 * FIX: remove header field when set to nil. * RFC compliance: set Sender field to first From address if multiple From addresses and the Sender header is not already set. * Improve TextFlowed tests. * Add TODO.mkd. * FIX: Ruby 1.8.7 compatibility issue. 7. 2015-11-05, v0.4.3 * FIX: Add "type" parameter to Multipart/Related messages (RFC conformance). * FIX: Documention bug regarding Multipart/Alternative entity order. 8. 2017-06-03, v0.4.4 * FIX: Deprecated test code. * Update README URLs. == License ({ISC License}[http://opensource.org/licenses/ISC]) Copyright (c) 2017, Clint Pachl 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. mime-0.4.4/lib/0000755000004100000410000000000013126650165013251 5ustar www-datawww-datamime-0.4.4/lib/mime/0000755000004100000410000000000013126650165014200 5ustar www-datawww-datamime-0.4.4/lib/mime/media.rb0000644000004100000410000000107313126650165015605 0ustar www-datawww-datarequire 'mime/headers/mime' module MIME # # Abstract top-level media class. # class Media include Headers::MIME attr_reader :headers, :body def initialize content, content_type, content_params = {} AbstractClassError.no_instantiation(self, Media) @headers = Header.new @body = content self.id = ID.generate_gid self.type = append_field_params(content_type, content_params) end # # Format the Media object as a MIME message. # def to_s "#{headers}\r\n\r\n#{body}" end end end mime-0.4.4/lib/mime/composite_media.rb0000644000004100000410000001663013126650165017674 0ustar www-datawww-datamodule MIME # # Composite media types allow encapsulating, mixing, and hierarchical # structuring of entities of different types within a single message. # Therefore, a CompositeMedia body is composed of one or more CompositeMedia # and/or DiscreteMedia objects. # # CompositeMedia implements Content-Disposition for dictating presentation # style of body entities via #add, #attach, and #inline. For more information # on disposition parameters, such as filename, size, and modification-date, # see https://tools.ietf.org/html/rfc2183. # # This class is abstract. # class CompositeMedia < Media class Body # # Create new composite body. # def initialize boundary @boundary = boundary @body = Array.new end # # Format the CompositeMedia object as a MIME message. # def to_s all_entities = @body.join("\r\n--#{@boundary}\r\n") "--#{@boundary}\r\n#{all_entities}\r\n--#{@boundary}--\r\n" end # # Add +entity+ to the composite body. # def add entity @body.push(entity) end end attr_reader :boundary def initialize content_type AbstractClassError.no_instantiation(self, CompositeMedia) @boundary = "Boundary_#{ID.generate_id}" # delimits body entities super(Body.new(boundary), content_type, 'boundary' => boundary) end # # Add a Media +entity+ to the message. # # The entity will be added to the main body of the message with no # disposition specified. Presentation of the entity will be dictated by # the display user agent. # # === Text and HTML Multipart/Alternative message # # A display user agent may only be capable of displaying plain text. If so, # it will choose to display the Text/Plain entity. However, if it is capable # of displaying HTML, it may choose to display the Text/HTML version. # # msg = MIME::Multipart::Alternative.new # msg.add(MIME::Text.new('plain text')) # msg.add(MIME::Text.new('html text', 'html')) # # The order in which the entities are added is significant. Add the simplest # representations first or in increasing order of complexity. # def add entity raise Error.new('can only add Media objects') unless entity.is_a? Media @body.add(entity) end # # Attach a Media +entity+ to the message. # # The entity will be presented as separate from the main body of the # message. Thus, display of the entity will not be automatic, but contingent # upon some further action of the user. For example, the display user agent # may present an icon representation of the entity, which the user can # select to view or save the entity. # # === Attachment with filename and size parameters: # # f = File.open('file.txt') # file = MIME::Text.new(f.read) # text = MIME::Text.new('See the attached file.') # # msg = MIME::Multipart::Mixed.new # msg.inline(text) # msg.attach(file, 'filename' => f.path, 'size' => f.size) # def attach entity, params = {} entity.set_disposition('attachment', params) add(entity) end # # Inline a Media +entity+ in the message. # # The entity will be embedded within the main body of the message. Thus, # display of the entity will be automatic upon display of the message. # Inline entities should be added in the order in which they occur within # the message. # # === Message with two embedded images: # # msg = MIME::Multipart::Mixed.new # msg.inline(MIME::Image.new(File.read('screenshot1.png'), 'png')) # msg.inline(MIME::Image.new(File.read('screenshot2.png'), 'png')) # msg.description = 'My screenshots' # def inline entity, params = {} entity.set_disposition('inline', params) add(entity) end end # # Message is intended to encapsulate another message. In particular, the # message/rfc822 content type is used to encapsulate RFC 822 # messages. # # TODO Implement # class Message < CompositeMedia end # # The abstract base class for all multipart message subtypes. The entities of # a multipart message are delimited by a unique boundary. # class Multipart < CompositeMedia def initialize media_subtype AbstractClassError.no_instantiation(self, Multipart) super("multipart/#{media_subtype}") end end # # The Alternative subtype indicates that each contained entity is an # alternatively formatted version of the same content. # # In general, when composing multipart/alternative entities, add the body # parts in increasing order of preference; that is, with the preferred format # last. For example, to prioritize for fancy text, add the plainest format # first and the richest format last. Typically, receiving user agents select # the last format they are capable of displaying or interpreting. # class Multipart::Alternative < Multipart # # Returns a Multipart::Alternative object with a content type of # multipart/alternative. # def initialize super('alternative') end end # # The FormData subtype expresses values for HTML form data submissions. # --- # RFCs consulted during implementation: # # * RFC-1867 Form-based File Upload in HTML # * RFC-2388 Returning Values from Forms: multipart/form-data # class Multipart::FormData < Multipart # # Returns a Multipart::FormData object with a content type of # multipart/form-data. # def initialize super('form-data') end # # Add the Media object, +entity+, to the FormData object. +name+ is # typically an HTML input tag variable name. If the input tag is of type # _file_, then +filename+ must be specified to indicate a file upload. # def add entity, name, filename = nil entity.set_disposition('form-data', 'name' => name, 'filename' => filename) super(entity) end end # # The Mixed subtype aggregates contextually independent entities. # class Multipart::Mixed < Multipart # # Returns a Multipart::Mixed object with a content type of # multipart/mixed. # def initialize super('mixed') end end # # The Related subtype aggregates multiple related entities. The message # consists of a root (the first entity) which references subsequent inline # entities. Message entities should be referenced by their Content-ID header. # The syntax of a reference is unspecified and is instead dictated by the # encoding or protocol used in the entity. # --- # RFC consulted during implementation: # # * RFC-2387 The MIME Multipart/Related Content-type # class Multipart::Related < Multipart # # Returns a Multipart::Related object with a content type of # multipart/related. # def initialize super('related') end # # The first entity added becomes the root object. The related message # type is set to the value of the root object media type. #-- # The "type" parameter is required and should equal the root media type. # https://tools.ietf.org/html/rfc2387#section-3.1 # def add entity unless type.include? '; type=' root_type = entity.type.partition(';').first # omit parameters self.type = append_field_params(type, 'type' => root_type) end super end end end mime-0.4.4/lib/mime/discrete_media_factory.rb0000644000004100000410000000437513126650165021226 0ustar www-datawww-datamodule MIME # # Module used only for initializing derived DiscreteMedia objects. # module DiscreteMediaFactory module DispositionParameters attr_accessor :path, :size end class << self include ContentTypes # # Creates a corresponding DiscreteMedia subclass object for the given # +file+ based on +file+'s filename extension. +file+ can be a file path # or File object. # # +content_type+ can be specified in order to override the auto detected # content type. If the +content_type+ cannot be detected, an # UnknownContentError exception will be raised. # # Creates and sets the singleton method +path+ on the created object. The # +path+ method is utilized by other methods in the MIME library, # therefore, eliminating redundant and explicit filename assignments. # # === Comparison Example # # file1 = '/tmp/file1.txt' # file2 = '/tmp/file2.txt' # entity1 = Text.new(File.read(file1)) # entity2 = DiscreteMediaFactory.create(file2) # # mixed_msg = Multipart::Mixed.new # mixed_msg.attach(entity1, 'filename' => file1) # mixed_msg.attach(entity2) # filename automatically added # def create file, content_type = nil if file.is_a? File cntnt = file.read ctype = content_type || file_type(file.path) fname = file.path else cntnt = IO.read(file) ctype = content_type || file_type(file) fname = file end type, subtype = ctype.to_s.split('/') if type.to_s.empty? || subtype.to_s.empty? raise UnknownContentError, "invalid content type: #{ctype}" end media_obj = case type when 'application'; Application.new(cntnt, subtype) when 'audio' ; Audio.new(cntnt, subtype) when 'image' ; Image.new(cntnt, subtype) when 'text' ; Text.new(cntnt, subtype) when 'video' ; Video.new(cntnt, subtype) else raise UnknownContentError, "invalid content type: #{ctype}" end media_obj.extend(DispositionParameters) media_obj.path = fname media_obj end end end end mime-0.4.4/lib/mime/mail.rb0000644000004100000410000000260313126650165015450 0ustar www-datawww-datarequire 'time' require 'mime/headers/internet' module MIME # # Construct RFC 2822 Internet messages. # class Mail include Headers::Internet attr_reader :headers, :body # # Initialize a Mail object with body set to +content+. # def initialize content = nil @headers = Header.new self.body = content self.date = Time.now end # # Format the Mail object as an Internet message. # def to_s self.sender ||= sender_address self.message_id ||= ID.generate_gid(domain) body.mime_version ||= "1.0 (Ruby MIME v#{VERSION})" #-- # In an RFC 2822 message, the header and body sections must be separated # by two line breaks (i.e., 2*CRLF). One line break is deliberately # omitted here so the MIME body supplier can append headers to the # top-level message header section. #++ "#{headers}\r\n#{body}" end def body= content @body = content.is_a?(Media) ? content : Text.new(content.to_s) end private # # Return the first From address as the sender if multiple From addresses. # def sender_address case from when Hash; Hash[*from.first] if from.size > 1 when Array; from.first if from.size > 1 end end def domain headers.get('from').match(/@([[:alnum:].-]+)/)[1] rescue nil end end end mime-0.4.4/lib/mime/parser.rb0000644000004100000410000000036013126650165016020 0ustar www-datawww-datamodule MIME # # Parse MIME messages, constructing Media objects for each MIME entity. # # TODO Implement # class Parser def initialize raise NotImplementedError.new('building a parser is a TODO') end end end mime-0.4.4/lib/mime/content_types.rb0000644000004100000410000000771313126650165017433 0ustar www-datawww-datamodule MIME # # Handles MIME content type detection using file name extension. # # === See Also # # * http://w3schools.com/media/media_mimeref.asp for more content types # * rubygem MIME::Types for extensive content type lookup # module ContentTypes # # Content types are in the form of media-type/sub-type. Each # content type has one or more file extentions associated with it. # # The content types below are listed in alphabetical order and the # extention arrays are in order of preference. # # The following content types were stolen from the NGiNX webserver. NGiNX # is great server, check it out at http://nginx.net. # CONTENT_TYPES = { 'application/atom+xml' => %w(atom), 'application/java-archive' => %w(jar war ear), 'application/mac-binhex40' => %w(hqx), 'application/msword' => %w(doc), 'application/octet-stream' => %w(bin exe dll deb dmg eot iso img msi msp msm), 'application/pdf' => %w(pdf), 'application/postscript' => %w(ps eps ai), 'application/rtf' => %w(rtf), 'application/vnd.ms-excel' => %w(xls), 'application/vnd.ms-powerpoint' => %w(ppt), 'application/vnd.wap.wmlc' => %w(wmlc), 'application/vnd.wap.xhtml+xml' => %w(xhtml), 'application/x-cocoa' => %w(cco), 'application/x-java-archive-diff' => %w(jardiff), 'application/x-java-jnlp-file' => %w(jnlp), 'application/x-javascript' => %w(js), 'application/x-makeself' => %w(run), 'application/x-perl' => %w(pl pm), 'application/x-pilot' => %w(prc pdb), 'application/x-rar-compressed' => %w(rar), 'application/x-redhat-package-manager' => %w(rpm), 'application/x-sea' => %w(sea), 'application/x-shockwave-flash' => %w(swf), 'application/x-stuffit' => %w(sit), 'application/x-tcl' => %w(tcl tk), 'application/x-x509-ca-cert' => %w(crt pem der), 'application/x-xpinstall' => %w(xpi), 'application/zip' => %w(zip), 'audio/midi' => %w(mid midi kar), 'audio/mpeg' => %w(mp3), 'audio/x-realaudio' => %w(ra), 'image/gif' => %w(gif), 'image/jpeg' => %w(jpg jpeg), 'image/png' => %w(png), 'image/tiff' => %w(tif tiff), 'image/vnd.wap.wbmp' => %w(wbmp), 'image/x-icon' => %w(ico), 'image/x-jng' => %w(jng), 'image/x-ms-bmp' => %w(bmp), 'image/svg+xml' => %w(svg), 'text/css' => %w(css), 'text/html' => %w(html htm shtml), 'text/mathml' => %w(mml), 'text/plain' => %w(txt), 'text/x-component' => %w(htc), 'text/xml' => %w(xml rss), 'video/3gpp' => %w(3gpp 3gp), 'video/mpeg' => %w(mpeg mpg), 'video/quicktime' => %w(mov), 'video/x-flv' => %w(flv), 'video/x-mng' => %w(mng), 'video/x-ms-asf' => %w(asx asf), 'video/x-ms-wmv' => %w(wmv), 'video/x-msvideo' => %w(avi) } # # Return the MIME content type of the +file+ path or object. # def file_type file filename = file.respond_to?(:path) ? file.path : file extension = File.extname(filename)[1..-1] # remove leading period type, _ = CONTENT_TYPES.find do |content_type, extentions| extentions.include? extension end type end end end mime-0.4.4/lib/mime/header.rb0000644000004100000410000000176513126650165015766 0ustar www-datawww-datamodule MIME # # Header section for Internet and MIME messages. # class Header def initialize @headers = Hash.new end # # Convert all headers to their string equivalents and join them using the # RFC 2822 CRLF line separator. #-- # TODO fold lines to 78 chars. # word.scan(/(.,?){1,78}/) OR word.split # def to_s @headers.to_a.map {|kv| kv.join(": ")}.join("\r\n") end # # Get header value associated with +name+. # def get name _, value = @headers.find {|k,v| name.downcase == k.downcase } value end # # Set header +name+ to +value+. If a header of the same name exists it will # be overwritten. Header names are _case-insensitive_. # def set name, value delete(name) @headers.store(name, value) unless value.nil? end # # Delete header associated with +name+. # def delete name @headers.delete_if {|k,v| name.downcase == k.downcase } end end end mime-0.4.4/lib/mime/discrete_media.rb0000644000004100000410000000431113126650165017465 0ustar www-datawww-datamodule MIME # # Discrete media must be handled by non-MIME mechanisms; they are opaque to # MIME processors. Therefore, the body of a DiscreteMedia object does not need # further MIME processing. # # This class is abstract. # class DiscreteMedia < Media def initialize(content, content_type, content_params) AbstractClassError.no_instantiation(self, DiscreteMedia) super end end # # Application is intended for discrete data that is to be processed by some # type of application program. The body contains information which must be # processed by an application before it is viewable or usable by a user. # # Application is the catch all class. If your content cannot be identified as # another DiscreteMedia, then it is application media. # class Application < DiscreteMedia def initialize(body, subtype = 'octet-stream', params = {}) super(body, "application/#{subtype}", params) end end # # Audio is intended for discrete audio content. The +subtype+ indicates the # specific audio format, such as *mpeg* or *midi*. # class Audio < DiscreteMedia def initialize(body, subtype = 'basic', params = {}) super(body, "audio/#{subtype}", params) end end # # Image is intented for discrete image content. The +subtype+ indicates the # specific image format, such as *jpeg* or *gif*. # class Image < DiscreteMedia def initialize(body, subtype = 'jpeg', params = {}) super(body, "image/#{subtype}", params) end end # # Text is intended for content which is principally textual in form. The # +subtype+ indicates the specific text type, such as *plain* or *html*. # class Text < DiscreteMedia def initialize(body, subtype = 'plain', params = {}) super(body, "text/#{subtype}", params) end end # # Video is intended for discrete video content. The content +subtype+ # indicates the specific video format. The RFC describes video media as # content that contains a time-varying-picture image, possibly with color and # coordinated sound. # class Video < DiscreteMedia def initialize(body, subtype = 'mpeg', params = {}) super(body, "video/#{subtype}", params) end end end mime-0.4.4/lib/mime/headers/0000755000004100000410000000000013126650165015613 5ustar www-datawww-datamime-0.4.4/lib/mime/headers/mime.rb0000644000004100000410000000653013126650165017073 0ustar www-datawww-datamodule MIME module Headers # # The RFC 2045 MIME message header fields. # module MIME attr_reader( :mime_version, :description, :disposition, :id, :transfer_encoding, :type ) # # Describes the content, which can be useful for non-MIME clients. # def description= description @description = description headers.set('Content-Description', description) end # # Specifies the disposition of the content relative to its enclosing # message. Valid values for +disposition+ are _inline_ and _attachment_. # Parameters can also be specified here; see the RFC for details. # # RFC 2183 Communicating Presentation Information in Internet Messages. # def disposition= disposition @disposition = disposition headers.set('Content-Disposition', disposition) end # # Globally unique ID that identifies a top-level message or message # entity. Content IDs can be used for referencing or caching purposes. # def id= id @id = id headers.set('Content-ID', "<#{id}>") end # # The mechanism used for encoding the top-level message content. # # Common Encoding Mechanisms # * 7bit # * 8bit # * binary # * quoted-printable # * base64 # def transfer_encoding= encoding @transfer_encoding = encoding headers.set('Content-Transfer-Encoding', encoding) end # # Currently only version 1.0 exists. # def mime_version= version @mime_version = version headers.set('MIME-Version', version) end protected # # Specifies the media type and subtype of the content. +type+ will have # the form media-type/subtype. # # Common Content Types # * application/octet-stream # * audio/mpeg # * image/jpeg # * text/plain # * video/mpeg # def type= type @type = type headers.set('Content-Type', type) end # # +type+ is the disposition type of either "inline" or "attachment". # +params+ is a Hash with zero or more of the following keys: # # +filename+ :: name of file # +creation-date+ :: RFC2822 date-time # +modification-date+ :: RFC2822 date-time # +read-date+ :: RFC2822 date-time # +size+ :: file size in octets # # The values for the *-date keys may use Time::rfc2822. # def set_disposition type, params = {} if params['filename'] params['filename'] = File.basename(params['filename']) elsif self.respond_to?(:path) params['filename'] = File.basename(self.path) end self.disposition = append_field_params(type, params) end private # # Append parameters to header field body. # Used for Content-Type and Content-Disposition headers. # def append_field_params body, params = {} params.each do |name, value| next unless value unless value =~ Internet::DOT_ATOM value = '"' + value.gsub('"','\"') + '"' end body << "; #{name}=#{value}" end body end end end end mime-0.4.4/lib/mime/headers/internet.rb0000644000004100000410000001153313126650165017773 0ustar www-datawww-datamodule MIME module Headers # # The RFC 2822 Internet message header fields. # # Mailbox fields #to, #from, #cc, #bcc, and #reply_to may be a single email # address, an array of email addresses, or a hash of _email_ => _name_ # pairs. When using a hash, set _name_ to +nil+ to omit email display name. # The #sender field is a special case and can only contain a single mailbox. # module Internet # Internet message character specifications (RFC 5322) ATOM = /[[:alnum:]!#\$%&'*+\/=?^_`{|}~-]/ DOT_ATOM = /^#{ATOM}+(#{ATOM}|\.)*$/ SPECIALS = /[()<>\[\]:;@\,."]/ attr_reader( # Required Headers :to, :from, :date, # Optional Headers :cc, :bcc, :sender, :reply_to, :message_id, :in_reply_to, :references, :comments, :keywords, :subject ) # # Origination date at which the creator of the message indicated that the # message was complete and ready to enter the mail delivery system. # def date= date @date = date headers.set('Date', date.rfc2822) end # # Person(s) or system(s) responsible for writing the message. # def from= mailbox @from = mailbox headers.set('From', stringify_mailbox(mailbox)) end # # Mailbox of the agent responsible for actual transmission of the message. # Sender field is required if the From field contains multiple mailboxes. # # === Example scenario # If a secretary were to send a message for another person, the mailbox of # the secretary would appear in the Sender field and the mailbox of the # actual author would appear in the From field. # def sender= mailbox if (mailbox.is_a?(Hash) || mailbox.is_a?(Array)) && mailbox.size != 1 raise ArgumentError, '"Sender" must be a single mailbox specification' end @sender = mailbox headers.set('Sender', stringify_mailbox(mailbox)) end # # Mailbox(es) of the primary recipient(s). # def to= mailbox @to = mailbox headers.set('To', stringify_mailbox(mailbox)) end # # Mailbox(es) of others who are to receive the message, though the content # of the message may not be directed at them; "Carbon Copy." # def cc= mailbox @cc = mailbox headers.set('Cc', stringify_mailbox(mailbox)) end # # Mailbox(es) of recipients of the message whose addresses are not to be # revealed to other recipients of the message; "Blind Carbon Copy." # def bcc= mailbox @bcc = mailbox headers.set('Bcc', stringify_mailbox(mailbox)) end # # Mailbox(es) to which the author suggests that replies be sent. # def reply_to= mailbox @reply_to = mailbox headers.set('Reply-To', stringify_mailbox(mailbox)) end # # Globally unique identifier of the message. # # The message +id+ must contain an embedded "@" symbol. An example +id+ # might be some-unique-id@domain.com. # def message_id= id @message_id = id headers.set('Message-ID', "<#{id}>") end # # The +id+ of the message to which this message is a reply. #-- # TODO fully implement and test # def in_reply_to= id @in_reply_to = id headers.set('In-Reply-To', "<#{id}>") end # # The +id+ used to identify a "thread" of conversation. #-- # TODO fully implement and test # def references= id @references = id headers.set('References', "<#{id}>") end # # Additional comments about the message content. # def comments= comments @comments = comments headers.set('Comments', comments) end # # Comma-separated list of important words and phrases that might be useful # for the recipient. # def keywords= keywords @keywords = keywords headers.set('Keywords', keywords) end # # The message topic. # def subject= subject @subject = subject headers.set('Subject', subject) end private def stringify_mailbox mailbox case mailbox when Hash mailbox.map do |email, name| if name if name =~ SPECIALS name.gsub!('"', '\"') %["#{name}" <#{email}>] else %[#{name} <#{email}>] end else email end end.join(', ') when Array mailbox.join(', ') else return mailbox end end end end end mime-0.4.4/lib/mime/content_formats/0000755000004100000410000000000013126650165017405 5ustar www-datawww-datamime-0.4.4/lib/mime/content_formats/text_flowed.rb0000644000004100000410000000740413126650165022263 0ustar www-datawww-data# # == Content Formats # # [Text/Plain Flowed] TextFlowed # [Quoted-Printable] To be implemented ... # [Base64] To be implemented ... # module MIME::ContentFormats # # Minimal implementation of RFC 2646: The Text/Plain Format Parameter # # https://tools.ietf.org/html/rfc2646 # # == Excerpts from RFC # # This memo proposes a new parameter to be used with Text/Plain, and, in the # presence of this parameter, the use of trailing whitespace to indicate # flowed lines. This results in an encoding which appears as normal # Text/Plain in older implementations, since it is in fact normal # Text/Plain. # # Each paragraph is displayed, starting at the left margin (or paragraph # indent), and continuing to the right until a word is encountered which # does not fit in the remaining display width. This word is displayed at the # left margin of the next line. This continues until the paragraph ends (a # CRLF is seen). # # == MIME format parameter to the text/plain media type # # Name: Format # Value: Fixed, Flowed # Example: Content-Type: text/plain; charset=iso-8859-1; format=flowed # #-- # == TODO # - Implement RFC 3676, which obsoletes RFC 2646. # - Usenet signature convention (section 4.3) # - Space-Stuffing (section 4.4) # - Quoting (section 4.5) # - Perhaps this should be subsumed into the MIME project. # module TextFlowed MAX_FLOWED_LINE = 79 # # Encode plain +text+ into flowed format, reducing long lines to +max+ # characters or less using soft line breaks (i.e., SPACE+CRLF). # # According to the RFC, the +max+ flowed line length is 79 characters. Line # lengths of 66 and 72 characters are common. # # The features of RFC 2646, such as line quoting and space-stuffing, # are not implemented. # def self.encode(text, max = MAX_FLOWED_LINE) if max > MAX_FLOWED_LINE raise ArgumentError, "flowed lines must be #{MAX_FLOWED_LINE} characters or less" end out = [] text.split(/\r\n|\n/).each do |paragraph| # tab use is discouraged # http://tools.ietf.org/html/rfc822#section-3.4.2 paragraph.gsub!(/\t/, ' '*4) # trim spaces before hard break # http://tools.ietf.org/html/rfc2646#section-4.1 paragraph.rstrip! if paragraph.length <= max out << paragraph else # flow text line = '' word = '' paragraph.each_char do |char| if char == ' ' # Omit spaces after soft break to prevent stuffing on next line. next if word.empty? && (line.size == 0 || line.size == max) if (line.size + word.size) < max line << word + char else # soft break situation unless line.empty? out << line.dup line.clear end if word.size < max line << word + char else word.scan(/.{1,#{MIME::MAX_LINE_LENGTH}}/) {|s| out << s } end end word.clear else # accumulate non-space characters in buffer word += char end end # flush buffers in an orderly fashion if ! word.empty? if (line.size + word.size) <= max out << line + word else out << line unless line.empty? word.scan(/.{1,#{MIME::MAX_LINE_LENGTH}}/) {|s| out << s } end elsif ! line.empty? out << line end end end out.join("\r\n") end # # Decode flowed plain +text+. # def self.decode(text) raise NotImplementedError end end end mime-0.4.4/lib/mime/error.rb0000644000004100000410000000131413126650165015655 0ustar www-datawww-datamodule MIME # # General purpose MIME errors. # class Error < StandardError ; end # # Undetectable MIME content type error. # class UnknownContentError < Error ; end # # Abstract class object initialization error. Many classes in the MIME # library are abstract and will raise this error. # class AbstractClassError < Error # # A helper method for detecting the intialization of an object in an # abstract class. +myself+ must always be *self* and +klass+ is the # abstract class object. # def self.no_instantiation myself, klass if myself.class == klass raise AbstractClassError.new('cannot construct abstract class') end end end end mime-0.4.4/lib/mime.rb0000644000004100000410000000246513126650165014534 0ustar www-datawww-data# # = Construct Multipurpose Internet Mail Extensions (MIME) messages. # # --- # # RFCs referenced during the implementation of this library: # # * RFC-2822 Internet Message Format (obsoletes 822) # * RFC-2045 MIME Part 1: Format of Internet Message Bodies # * RFC-2046 MIME Part 2: Media Types # * RFC-2047 MIME Part 3: Message Header Extensions for Non-ASCII Text # * RFC-2048 MIME Part 4: Registration Procedures # * RFC-2049 MIME Part 5: Conformance Criteria and Examples # # --- # # See SOAP::MIMEMessage for other implementation ideas. # module MIME VERSION = '0.4.4' # Defined in RFC: https://tools.ietf.org/html/rfc5322#section-2.1.1 MAX_LINE_LENGTH = 998 module ID # # Generate local ID. # def self.generate_id timestamp = (Time.now.to_f * 1E7).to_i rand(1E9).to_s(36) + object_id.to_s(36) + timestamp.to_s(36) end # # Generate global ID for "Message-ID" or "Content-ID" header. # def self.generate_gid domain = nil generate_id + "@" + (domain || "#{generate_id}.local") end end end require 'mime/content_types' require 'mime/content_formats/text_flowed' require 'mime/error' require 'mime/header' require 'mime/media' require 'mime/composite_media' require 'mime/discrete_media' require 'mime/discrete_media_factory' require 'mime/mail' mime-0.4.4/test/0000755000004100000410000000000013126650165013462 5ustar www-datawww-datamime-0.4.4/test/test_text_flowed.rb0000644000004100000410000001165513126650165017402 0ustar www-datawww-datagem 'minitest' # minitest in 1.9 stdlib is crufty require 'minitest/autorun' require 'mime' class TextFlowedTest < Minitest::Test include MIME::ContentFormats def test_empty_string assert_equal '', TextFlowed.encode('') end def test_single_word_will_not_break_regardless_of_line_length txt = "1234567890" assert_equal txt, TextFlowed.encode(txt, 9) # < assert_equal txt, TextFlowed.encode(txt, 10) # = assert_equal txt, TextFlowed.encode(txt, 11) # > end def test_max_line_length_around_first_word txt = "1234567890 A" expected1 = "1234567890\r\nA" expected2 = "1234567890 \r\nA" assert_equal expected1, TextFlowed.encode(txt, 9) # < assert_equal expected1, TextFlowed.encode(txt, 10) # = assert_equal expected2, TextFlowed.encode(txt, 11) # > end def test_words_with_variable_spacing txt = "123 456 789" expected1 = "123 \r\n456 \r\n789" expected2 = "123 456 \r\n789" expected3 = "123 456 \r\n789" expected4 = "123 456 \r\n789" expected5 = txt assert_equal expected1, TextFlowed.encode(txt, 8) assert_equal expected2, TextFlowed.encode(txt, 9) assert_equal expected3, TextFlowed.encode(txt, 10) assert_equal expected4, TextFlowed.encode(txt, 11) assert_equal expected4, TextFlowed.encode(txt, 12) assert_equal expected4, TextFlowed.encode(txt, 13) assert_equal expected5, TextFlowed.encode(txt, 14) assert_equal expected5, TextFlowed.encode(txt, 15) end def test_paragraphs_with_variable_lengths txt = "123 456 789\n\none two three\n\nEND" expected1 = "123 \r\n456 \r\n789\r\n\r\none \r\ntwo \r\nthree\r\n\r\nEND" expected2 = "123 456 \r\n789\r\n\r\none two \r\nthree\r\n\r\nEND" expected3 = "123 456 789\r\n\r\none two \r\nthree\r\n\r\nEND" expected4 = "123 456 789\r\n\r\none two three\r\n\r\nEND" assert_equal expected1, TextFlowed.encode(txt, 4) assert_equal expected2, TextFlowed.encode(txt, 10) assert_equal expected3, TextFlowed.encode(txt, 11) assert_equal expected3, TextFlowed.encode(txt, 12) assert_equal expected4, TextFlowed.encode(txt, 13) assert_equal expected4, TextFlowed.encode(txt, 14) assert_equal expected4, TextFlowed.encode(txt, txt.length) assert_equal expected4, TextFlowed.encode(txt, txt.length+1) end def test_trailing_newlines_removed txt = "123 456 789\r\nabc def ghi" assert_equal txt, TextFlowed.encode(txt+"\n", 11) assert_equal txt, TextFlowed.encode(txt+"\r\n", 11) end def test_tab_expansion txt = "123\t456" expected1 = "123\r\n456" expected2 = "123 \r\n456" expected3 = "123 \r\n456" expected4 = "123 \r\n456" expected5 = "123 \r\n456" expected6 = "123 456" assert_equal expected1, TextFlowed.encode(txt, 2) assert_equal expected1, TextFlowed.encode(txt, 3) assert_equal expected2, TextFlowed.encode(txt, 4) assert_equal expected3, TextFlowed.encode(txt, 5) assert_equal expected4, TextFlowed.encode(txt, 6) assert_equal expected5, TextFlowed.encode(txt, 7) assert_equal expected5, TextFlowed.encode(txt, 8) assert_equal expected5, TextFlowed.encode(txt, 9) assert_equal expected6, TextFlowed.encode(txt, 10) end def test_spacing_after_hard_line_break txt = "123\r\n\r\n abc" exp = "123\r\n\r\nabc" assert_equal exp, TextFlowed.encode(txt, 4) # remove SP if line > max assert_equal txt, TextFlowed.encode(txt, 5) # preserve SP if line == max assert_equal txt, TextFlowed.encode(txt, 6) # preserve SP if line < max end def test_trim_spaces_before_hard_line_breaks txt1 = "123 \r\n\r\n456" txt2 = "123 \r\n\r\n456 " expected = "123\r\n\r\n456" assert_equal expected, TextFlowed.encode(txt1, 3) assert_equal expected, TextFlowed.encode(txt1, 4) assert_equal expected, TextFlowed.encode(txt1, 5) assert_equal expected, TextFlowed.encode(txt2, 3) assert_equal expected, TextFlowed.encode(txt2, 4) assert_equal expected, TextFlowed.encode(txt2, 5) end def test_trim_trailing_spaces txt = "123 456 " expected = "123 \r\n456" assert_equal expected, TextFlowed.encode(txt, 4) end def test_rfc_79_character_max_flowed_line_length assert_equal (max = 79), TextFlowed::MAX_FLOWED_LINE TextFlowed.encode('', max) # no error assert_raises(ArgumentError) { TextFlowed.encode('', max+1) } end def test_rfc_max_smtp_line_length_boundary long_word = 'x' * MIME::MAX_LINE_LENGTH txt1 = long_word txt2 = 'x' + long_word txt3 = 'x ' + long_word txt4 = 'x ' + long_word + ' x' expected1 = long_word expected2 = long_word + "\r\nx" expected3 = "x \r\n" + long_word expected4 = "x \r\n" + long_word + "\r\nx" assert_equal expected1, TextFlowed.encode(txt1) assert_equal expected2, TextFlowed.encode(txt2) assert_equal expected3, TextFlowed.encode(txt3) assert_equal expected4, TextFlowed.encode(txt4) end end mime-0.4.4/test/scaffold/0000755000004100000410000000000013126650165015243 5ustar www-datawww-datamime-0.4.4/test/scaffold/image.msg0000644000004100000410000000063413126650165017040 0ustar www-datawww-dataContent-ID: <10608075780@307765356478579> Content-Type: image/jpeg Content-Transfer-Encoding: binary JFIFHHCreated with GIMPCP7 Content-Type: multipart/related; boundary=Boundary_370; type=text/html --Boundary_370 Content-ID: <37009594064000@17566297955131172> Content-Type: text/html; charset=iso-8859-1 Content-Transfer-Encoding: 7bit

HTML multipart/related message

txt before pix cool ruby

txt after pix

--Boundary_370 Content-ID: <37009594072000@985944565636421> Content-Type: image/png Content-Transfer-Encoding: binary Content-Disposition: inline; filename=ruby.png PNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB` --Boundary_370-- mime-0.4.4/test/scaffold/multipart_alternative_related.msg0000644000004100000410000000342113126650165024072 0ustar www-datawww-dataContent-ID: <122647225647800@3229904291767799> Content-Type: multipart/alternative; boundary=Boundary_122647225647800 --Boundary_122647225647800 Content-ID: <122647225660800@4654191570158269> Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit *HTML multipart/alternative message* txt before pix txt after pix --Boundary_122647225647800 Content-ID: <12264@10804189346898185> Content-Type: multipart/related; boundary=Boundary_12264; type=text/html --Boundary_12264 Content-ID: <122647225664200@1404976108524404> Content-Type: text/html; charset=iso-8859-1 Content-Transfer-Encoding: 7bit

HTML multipart/alternative message

txt before pix

cool ruby

txt after pix

--Boundary_12264 Content-ID: <122647225670000@23027439684804796> Content-Type: image/png Content-Transfer-Encoding: binary Content-Disposition: inline; filename=ruby.png PNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB` --Boundary_12264-- --Boundary_122647225647800-- mime-0.4.4/test/scaffold/multipart_form_data_file_and_text.msg0000644000004100000410000000342313126650165024677 0ustar www-datawww-dataContent-ID: <148155451579600@9223840117511017> Content-Type: multipart/form-data; boundary=Boundary_148155451579600 --Boundary_148155451579600 Content-ID: <148155451593000@7221322612240949> Content-Type: image/jpeg Content-Transfer-Encoding: 8bit Content-Disposition: form-data; name=image_1; filename=image.jpg JFIFHHCreated with GIMPCP7 Content-Type: image/png Content-Transfer-Encoding: 8bit Content-Disposition: form-data; name=image_2; filename=ruby.png PNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB` --Boundary_148155451579600 Content-ID: <148155451583400@8153228253460096> Content-Type: text/plain; charset=us-ascii Content-Disposition: form-data; name=description This is plain text description of images. --Boundary_148155451579600-- mime-0.4.4/test/scaffold/text.msg0000644000004100000410000000013413126650165016735 0ustar www-datawww-dataContent-ID: <10218519780@0838656876757202> Content-Type: text/plain a plain text messagemime-0.4.4/test/scaffold/data.htm0000644000004100000410000000012013126650165016657 0ustar www-datawww-data

Sample HTML File

this is the body

mime-0.4.4/test/scaffold/multipart_alternative.msg0000644000004100000410000000073313126650165022375 0ustar www-datawww-dataContent-ID: <154873488180000@3045551269054866> Content-Type: multipart/alternative; boundary=Boundary_154873488180000 --Boundary_154873488180000 Content-ID: <154873488189000@6392244491224042> Content-Type: text/enhanced; charset=us-ascii *Header* message --Boundary_154873488180000 Content-ID: <154873488185800@3424043482513621> Content-Type: text/html; charset=iso-8859-1

Header

message

--Boundary_154873488180000-- mime-0.4.4/test/scaffold/image.jpg0000644000004100000410000000046213126650165017031 0ustar www-datawww-dataJFIFHHCreated with GIMPCP7 Content-Type: multipart/mixed; boundary=Boundary_60624328439400 --Boundary_60624328439400 Content-ID: <60623968567200@4963457629142666> Content-Type: text/plain Content-Disposition: inline Plain Text --Boundary_60624328439400 Content-ID: <60623968574800@4234455563935139> Content-Type: image/jpeg Content-Disposition: attachment; filename=image.jpg JFIFHHCreated with GIMPCP7 Content-Type: video/mpeg Content-Transfer-Encoding: binary 0110000101110101011001000110100101101111mime-0.4.4/test/scaffold/main.css0000644000004100000410000000000013126650165016667 0ustar www-datawww-datamime-0.4.4/test/scaffold/data.xml0000644000004100000410000000072713126650165016704 0ustar www-datawww-data mime-0.4.4/test/scaffold/multipart_mixed_inline_and_attachment2.msg0000644000004100000410000000330413126650165025634 0ustar www-datawww-dataContent-ID: <173318868631200@242395347026774> Content-Type: multipart/mixed; boundary=Boundary_173318868631200 --Boundary_173318868631200 Content-ID: <173318868635600@4928216760231209> Content-Type: text/html Content-Disposition: inline; filename=data.htm

Sample HTML File

this is the body

--Boundary_173318868631200 Content-ID: <173318868652800@9854801548240651> Content-Type: image/png Content-Disposition: attachment; filename=ruby.png PNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB` --Boundary_173318868631200 Content-ID: <173318868669000@9722896565476336> Content-Type: image/jpeg Content-Disposition: attachment; filename=image.jpg JFIFHHCreated with GIMPCP7 Content-ID: Content-Type: multipart/alternative; boundary=Boundary_cjdpeu2zvf08ry83tlhv7rm0dc MIME-Version: 1.0 (Ruby MIME v0.3.0) --Boundary_cjdpeu2zvf08ry83tlhv7rm0dc Content-ID: Content-Type: multipart/mixed; boundary=Boundary_2fl9fd2zvf08ry83tlhv7rlzw6 --Boundary_2fl9fd2zvf08ry83tlhv7rlzw6 Content-ID: Content-Type: text/plain; charset=us-ascii Check the attachment for a cool ruby picture. --Boundary_2fl9fd2zvf08ry83tlhv7rlzw6 Content-ID: <3ldxpt2zvf08ry83tlhv7rlxiy@ggvey82zvf08ry83tlhv7rly4k.local> Content-Type: image/png Content-Transfer-Encoding: binary Content-Disposition: attachment; filename=ruby.png PNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB` --Boundary_2fl9fd2zvf08ry83tlhv7rlzw6-- --Boundary_cjdpeu2zvf08ry83tlhv7rm0dc Content-ID: <5nq5fs2zvf08ry83tlhv7rlz7a@c2sl3e2zvf08ry83tlhv7rlz9e.local> Content-Type: multipart/related; boundary=Boundary_4uhvh4; type=text/html --Boundary_4uhvh4 Content-ID: Content-Type: text/html; charset=iso-8859-1 cool ruby --Boundary_4uhvh4 Content-ID: <3ldxpt2zvf08ry83tlhv7rlxiy@ggvey82zvf08ry83tlhv7rly4k.local> Content-Type: image/png Content-Transfer-Encoding: binary Content-Disposition: attachment; filename=ruby.png PNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB` --Boundary_4uhvh4-- --Boundary_cjdpeu2zvf08ry83tlhv7rm0dc-- mime-0.4.4/test/scaffold/application.msg0000644000004100000410000000024013126650165020252 0ustar www-datawww-dataContent-ID: <10330079680@971971651113797> Content-Type: application/octet-stream Content-Transfer-Encoding: binary 0110000101110101011001000110100101101111mime-0.4.4/test/scaffold/multipart_form_data_mixed.msg0000644000004100000410000000344113126650165023200 0ustar www-datawww-dataContent-ID: <10529354400@247952591512483> Content-Type: multipart/form-data; boundary=Boundary_10529354400 --Boundary_10529354400 Content-ID: <10529364100@545540840196366> Content-Type: text/plain Content-Disposition: form-data; name=field1 Joe Blow --Boundary_10529354400 Content-ID: <10529357100@472735948963124> Content-Type: multipart/mixed; boundary=Boundary_10529357100 Content-Disposition: form-data; name=pics --Boundary_10529357100 Content-ID: <10529358800@653109487522697> Content-Type: image/jpeg Content-Disposition: attachment; filename=image.jpg JFIFHHCreated with GIMPCP7 Content-Type: image/png Content-Disposition: attachment; filename=ruby.png PNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB` --Boundary_10529357100-- --Boundary_10529354400-- mime-0.4.4/test/scaffold/multipart_form_data_text.msg0000644000004100000410000000216713126650165023062 0ustar www-datawww-dataContent-ID: <88135231713600@7894413004791365> Content-Type: multipart/form-data; boundary=Boundary_88135231713600 --Boundary_88135231713600 Content-ID: <88135231717000@5720607746781624> Content-Type: text/xml Content-Disposition: form-data; name=xml --Boundary_88135231713600 Content-ID: <88135231721400@4415016053712104> Content-Type: text/html Content-Disposition: form-data; name=htm

Sample HTML File

this is the body

--Boundary_88135231713600 Content-ID: <88135231725400@11623307734175536> Content-Type: text/plain Content-Disposition: form-data; name=txt text body --Boundary_88135231713600-- mime-0.4.4/test/scaffold/audio.msg0000644000004100000410000000022013126650165017046 0ustar www-datawww-dataContent-ID: <9789078880@11546613041526> Content-Type: audio/midi Content-Transfer-Encoding: binary 0110000101110101011001000110100101101111mime-0.4.4/test/scaffold/rfc822_discrete.msg0000644000004100000410000000056713126650165020653 0ustar www-datawww-dataDate: Tue, 17 Dec 2013 04:54:56 -0700 To: John , paul@example.com, Mary Cc: Head Honcho From: jane@example.com Subject: This is an important email Message-ID: <776432154@10297424291820> Content-ID: <102975158103800@7998663631497803> Content-Type: text/plain MIME-Version: 1.0 (Ruby MIME v0.0.0) Hello, world!mime-0.4.4/test/scaffold/ruby.png0000644000004100000410000000133413126650165016733 0ustar www-datawww-dataPNG  IHDRabKGD pHYs  tIME  ,4 :{iIDAT8ˍKHTqsgqt* ez@PJ-ZdEDA"lQ- EFA=  lDf )3S3̽Uouq7a(-S66'$*_XU+'ƫ'Bz*\135} _^!-E}?z{D=<"dllZ#/MO)f SPgD.k1+]akm4>{K? ] T)"2 @jq%,*K\r iD'[5Ȗ R̐cI~#]':=IЇiހ,9( 9$q(O'_Xo:19M`axI"5[7#ɂ9zbʫX{vߺz)\>oߧg:_,T۟Vomkys tHHIENDB`mime-0.4.4/test/test_mime.rb-try0000644000004100000410000005154613126650165016624 0ustar www-datawww-data# THIS IS STILL HERE BECAUSE A LOT OF WORK WENT INTO MAKING TESTS AGNOSTIC OF MIME HEADER ORDER. # HOWEVER, THINGS GOT MESSY SO REVERTED BACK TO COMPARING AGAINST KNOWN GOOD MESSAGES. # THIS IS HERE JUST IN CASE. IN THE FUTURE, REMOVE IT IF THERE IS NO NEED FOR IT. gem 'minitest' # minitest in 1.9 stdlib is crufty require 'minitest/autorun' require 'mime' # may be able to remove in 2.0 Encoding.default_external = 'ASCII-8BIT' class MIMETest < Minitest::Test CRLF = "\r\n" HDR_BDY_SEP = CRLF * 2 MESSAGE_ID = /^Message-ID: <\d+@\d+>\r\n/ CONTENT_ID = /^Content-ID: <\d+\.\d+>\r\n/ CDISPOSITION = /^Content-Disposition: .*\r\n/ CTYPE = /^Content-Type: .*\r\n/ DATE = /^Date: ..., \d{1,2} ... \d{4} \d\d:\d\d:\d\d -\d{4}\r\n/ VERSION = /^MIME-Version: 1.0 \(Ruby MIME v\d\.\d\)\r\n/ #BOUNDARY = /^--Boundary_\d+\.\d+\r\n/ BOUNDARY = /^--Boundary_\d+\.\d+(--)?\r\n/ BOUNDARY_LAST = /^--Boundary_\d+\.\d+--\r\n/ CID = /cid:\d+\.\d+/ CTYPE_TEXT_PLAIN = /^Content-Type: text\/plain; charset=us-ascii\r\n/ CTYPE_TEXT_HTML = /^Content-Type: text\/html\r\n/ CTYPE_TEXT_XML = /^Content-Type: text\/xml\r\n/ CTYPE_IMAGE_JPEG = /^Content-Type: image\/jpeg\r\n/ CTYPE_IMAGE_PNG = /^Content-Type: image\/png\r\n/ CTYPE_VIDEO_MPEG = /^Content-Type: video\/mpeg\r\n/ CTYPE_AUDIO_MIDI = /^Content-Type: audio\/midi\r\n/ CTYPE_APPLICATION = /^Content-Type: application\/octet-stream\r\n/ CTYPE_MPART_FORM = /^Content-Type: multipart\/form-data; boundary=Boundary_\d+\.\d+\r\n/ CTYPE_MPART_ALT = /^Content-Type: multipart\/alternative; boundary=Boundary_\d+\.\d+\r\n/ CTYPE_MPART_MIXED = /^Content-Type: multipart\/mixed; boundary=Boundary_\d+\.\d+\r\n/ XFER_ENC_BINARY = /^Content-Transfer-Encoding: binary\r\n/ XFER_ENC_8BIT = /^Content-Transfer-Encoding: 8bit\r\n/ BINARY_DATA = '0110000101110101011001000110100101101111' def test_make_top_level_rfc2822_message rfc2822_msg = MIME::Message.new rfc2822_msg.body = "\r\nmessage body" msg = rfc2822_msg.to_s assert_match MESSAGE_ID, msg assert_match DATE, msg assert_match VERSION, msg assert_equal_num_headers 3, msg assert_equal_body "message body", msg end # TODO remove audio.msg def test_make_audio_message audio_media = MIME::AudioMedia.new(BINARY_DATA, 'audio/midi') audio_media.content_transfer_encoding = 'binary' msg = MIME::Message.new(audio_media).to_s [CONTENT_ID, XFER_ENC_BINARY, CTYPE_AUDIO_MIDI].each do |header| assert_match header, msg end assert_equal_num_headers 6, msg assert_equal_body BINARY_DATA, msg end # TODO remove expected_mime_msg = IO.read(sd('/application.msg')) def test_make_application_message application_media = MIME::ApplicationMedia.new(BINARY_DATA) application_media.content_transfer_encoding = 'binary' msg = MIME::Message.new(application_media).to_s [CONTENT_ID, XFER_ENC_BINARY, CTYPE_APPLICATION].each do |header| assert_match header, msg end assert_equal_num_headers 6, msg assert_equal_body BINARY_DATA, msg end # TODO remove image.msg def test_make_image_message image = IO.read(sd('/image.jpg')) image_media = MIME::ImageMedia.new(image) image_media.content_type = 'image/jpeg' image_media.content_transfer_encoding = 'binary' msg = MIME::Message.new(image_media).to_s [CONTENT_ID, XFER_ENC_BINARY, CTYPE_IMAGE_JPEG].each do |header| assert_match header, msg end assert_equal_num_headers 6, msg assert_equal_body image, msg end # TODO remove text.msg def test_make_text_message body = 'a plain text message' msg = MIME::Message.new(MIME::TextMedia.new(body)).to_s [CTYPE_TEXT_PLAIN].each do |header| assert_match header, msg end end # TODO remove video.msg def test_make_video_message video_media = MIME::VideoMedia.new(BINARY_DATA) video_media.content_type = 'video/mpeg' video_media.content_transfer_encoding = 'binary' msg = MIME::Message.new(video_media).to_s [CONTENT_ID, XFER_ENC_BINARY, CTYPE_VIDEO_MPEG].each do |header| assert_match header, msg end assert_equal_num_headers 6, msg assert_equal_body BINARY_DATA, msg end def test_no_instantiation_of_abstract_classes e = MIME::AbstractClassError assert_raises(e) {MIME::MediaType.new(nil, nil)} assert_raises(e) {MIME::DiscreteMediaType.new(nil)} assert_raises(e) {MIME::CompositeMediaType.new(nil)} assert_raises(e) {MIME::MultipartMedia.new(nil)} end # def test_boundaries # # CREATE a multipart message; take simplified version of next form data # # test # e = msg.scan(BOUNDARY).each # first_boundary = e.next # assert_equal first_boundary, e.next # assert_equal first_boundary, e.next # last_boundary = e.next # refute_equal first_boundary, last_boundary # assert_match /^--Boundary_\d+\.\d+\r\n/, first_boundary # assert_match /^--Boundary_\d+\.\d+--\r\n/, last_boundary # end # # def test_unique_content_ids_in_multipart_message # # content_ids = msg.scan(/^Content-ID: <(\d+.\d+)>/) # assert_equal 4, content_ids.flatten.uniq.size # all IDs must be unique # end # TODO rm multipart_form_data_text.msg def test_multipart_form_data_with_text_entities txt_data = 'text body' htm_data = IO.read(sd('data.htm')) xml_data = IO.read(sd('data.xml')) txt = MIME::TextMedia.new('text body') htm = MIME::TextMedia.new(htm_data, 'text/html') xml = MIME::TextMedia.new(xml_data, 'text/xml') form_data = MIME::MultipartMedia::FormData.new form_data.add_entity txt, 'txt' form_data.add_entity htm, 'htm' form_data.add_entity xml, 'xml' msg = form_data.to_s parts = msg.split(BOUNDARY) assert_equal 5, parts.size, 'main header, 3 entities, last boundary' assert_equal '--', parts.pop, 'remnant of last boundary marker' # main header assert_match CONTENT_ID, parts[0] assert_match CTYPE_MPART_FORM, parts[0] assert_equal_num_headers 2, parts[0] assert_equal_body '', parts[0] # xml entity assert_match CONTENT_ID, parts[1] assert_match CTYPE_TEXT_XML, parts[1] assert_match /^Content-Disposition: form-data; name="xml"\r\n/, parts[1] assert_equal_num_headers 3, parts[1] assert_equal_body xml_data, parts[1] # html entity assert_match CONTENT_ID, parts[2] assert_match CTYPE_TEXT_HTML, parts[2] assert_match /^Content-Disposition: form-data; name="htm"\r\n/, parts[2] assert_equal_num_headers 3, parts[2] assert_equal_body htm_data, parts[2] # text entity assert_match CONTENT_ID, parts[3] assert_match CTYPE_TEXT_PLAIN, parts[3] assert_match /^Content-Disposition: form-data; name="txt"\r\n/, parts[3] assert_equal_num_headers 3, parts[3] assert_equal_body txt_data, parts[3] end # TODO test that only basename of the file is included in header # test if no filename is used, then probably no filename param # see next test for ideas def test_content_disposition_filename pass end # rm multipart_form_data_file.msg # rm multipart_form_data_file_and_text.msg def test_multipart_form_data_with_text_and_file_entities img1_filename = 'image.jpg' img2_filename = 'ruby.png' img1_data = IO.read(sd(img1_filename)) img2_data = IO.read(sd(img2_filename)) img1 = MIME::ImageMedia.new(img1_data, 'image/jpeg') img2 = MIME::ImageMedia.new(img2_data, 'image/png') img1.content_transfer_encoding = '8bit' img2.content_transfer_encoding = '8bit' desc_data = 'This is plain text description of images.' desc = MIME::TextMedia.new(desc_data) form_data = MIME::MultipartMedia::FormData.new form_data.add_entity desc, 'description' form_data.add_entity img2, 'image_2', img2_filename form_data.add_entity img1, 'image_1', img1_filename msg = form_data.to_s parts = msg.split(BOUNDARY) assert_equal 5, parts.size, 'main header, 3 entities, last boundary' assert_equal '--', parts.pop, 'remnant of last boundary marker' # main header assert_match CONTENT_ID, parts[0] assert_match CTYPE_MPART_FORM, parts[0] assert_equal_num_headers 2, parts[0] assert_equal_body '', parts[0] # first image entity assert_match CONTENT_ID, parts[1] assert_match CTYPE_IMAGE_JPEG, parts[1] assert_match XFER_ENC_8BIT, parts[1] assert_match /^Content-Disposition: form-data; name="image_1"; filename="#{img1_filename}"\r\n/, parts[1] assert_equal_num_headers 4, parts[1] assert_equal_body img1_data, parts[1] # second image entity assert_match CONTENT_ID, parts[2] assert_match CTYPE_IMAGE_PNG, parts[2] assert_match XFER_ENC_8BIT, parts[2] assert_match /^Content-Disposition: form-data; name="image_2"; filename="#{img2_filename}"\r\n/, parts[2] assert_equal_num_headers 4, parts[2] assert_equal_body img2_data, parts[2] # plain text entity assert_match CONTENT_ID, parts[3] assert_match CTYPE_TEXT_PLAIN, parts[3] assert_match /^Content-Disposition: form-data; name="description"\r\n/, parts[3] assert_equal_num_headers 3, parts[3] assert_equal_body desc_data, parts[3] end # rm plain_text_email.msg def test_construction_of_plain_text_email_message subject = 'This is an important email' body = 'This is the all important email body.' email_msg = MIME::Message.new email_msg.to = { 'john@example.com' => 'John', 'paul@example.com' => nil, 'mary@example.com' => 'Mary' } email_msg.cc = {'Head Honcho' => 'boss@example.com'} email_msg.from = {'jane@example.com' => nil} email_msg.subject = subject email_msg.body = MIME::TextMedia.new(body) msg = email_msg.to_s assert_equal_num_headers 9, msg assert_match MESSAGE_ID, msg assert_match CONTENT_ID, msg assert_match CTYPE_TEXT_PLAIN, msg assert_match DATE, msg assert_match VERSION, msg assert_match /^Subject: #{subject}\r\n/, msg assert_match /^From: jane@example.com\r\n/, msg assert_match /^To: .+, .+, .+\r\n/, msg # 3 addresses assert_match /^To: .*John .*\r\n/, msg assert_match /^To: .*Mary .*\r\n/, msg assert_match /^To: .*paul@example.com.*\r\n/, msg assert_match /^Cc: boss@example.com \r\n/, msg assert_equal_body body, msg end def test_content_type_detection (o = Object.new).extend(MIME::ContentTypes) # test using file path, relative and absolute assert_equal 'application/pdf', o.file_type('book.pdf') assert_equal 'video/quicktime', o.file_type('mini.mov') assert_equal 'application/octet-stream', o.file_type('dsk.iso') assert_equal 'audio/mpeg', o.file_type('/tmp/song.mp3') assert_equal 'text/css', o.file_type('/tmp/main.css') assert_equal nil, o.file_type('unknown.yyy') # test using file object img_type = open(sd('ruby.png')) {|f| o.file_type(f)} assert_equal 'image/png', img_type refute_equal 'image/jpeg', img_type end def test_object_instantiation_using_discrete_media_factory app_file = sd('book.pdf') audio_file = sd('song.mp3') text_file = sd('data.xml') video_file = sd('mini.mov') image_file = sd('image.jpg') unknown_file = sd('unknown.yyy') dmf = MIME::DiscreteMediaFactory # test using file path assert_kind_of MIME::ApplicationMedia, dmf.create(app_file) assert_kind_of MIME::AudioMedia, dmf.create(audio_file) assert_kind_of MIME::TextMedia, dmf.create(text_file) assert_kind_of MIME::VideoMedia, dmf.create(video_file) assert_kind_of MIME::ImageMedia, dmf.create(image_file) # test using file object open(image_file) do |image_file_obj| assert_kind_of MIME::ImageMedia, dmf.create(image_file_obj) end open(text_file) do |text_file_obj| assert_kind_of MIME::TextMedia, dmf.create(text_file_obj) end # raise for unknown file path and File object assert_raises(MIME::UnknownContentError) {dmf.create(unknown_file)} open(unknown_file) do |unknown_file_obj| assert_raises(MIME::UnknownContentError) {dmf.create(unknown_file_obj)} end end def test_discrete_media_factory_creates_path_singleton_method pdf_file_path = sd('book.pdf') media_obj = MIME::DiscreteMediaFactory.create(pdf_file_path) assert_equal pdf_file_path, media_obj.path open(pdf_file_path) do |pdf_file_obj| media_obj = MIME::DiscreteMediaFactory.create(pdf_file_obj) assert_equal pdf_file_path, media_obj.path end end # rm multipart_alternative.msg def test_multipart_alternative_message_construction txt_data = "*Header*\nmessage" htm_data = "

Header

message

" txt_msg = MIME::TextMedia.new(txt_data) htm_msg = MIME::TextMedia.new(htm_data) txt_msg.content_type = (txt_type = 'text/enhanced; charset=us-ascii') htm_msg.content_type = (htm_type = 'text/html; charset=iso-8859-1') alt_msg = MIME::MultipartMedia::Alternative.new alt_msg.add_entity htm_msg alt_msg.add_entity txt_msg msg = alt_msg.to_s parts = msg.split(BOUNDARY) assert_equal 4, parts.size, 'main header, 2 entities, last boundary' assert_equal '--', parts.pop, 'remnant of last boundary marker' # main header assert_match CONTENT_ID, parts[0] assert_match CTYPE_MPART_ALT, parts[0] assert_equal_num_headers 2, parts[0] assert_equal_body '', parts[0] # text entity assert_match CONTENT_ID, parts[1] assert_match /^Content-Type: #{txt_type}\r\n/, parts[1] assert_equal_num_headers 2, parts[1] assert_equal_body txt_data, parts[1] # html entity assert_match CONTENT_ID, parts[2] assert_match /^Content-Type: #{htm_type}\r\n/, parts[2] assert_equal_num_headers 2, parts[2] assert_equal_body htm_data, parts[2] end # rm multipart_mixed_inline_and_attachment.msg def test_multipart_mixed_with_inline_and_attachment mixed_msg = MIME::MultipartMedia::Mixed.new img_filename = 'image.jpg' img_data = '' open(sd(img_filename)) do |img_file| img_data = img_file.read img_msg = MIME::ImageMedia.new(img_data, 'image/jpeg') mixed_msg.attach_entity(img_msg, 'filename' => img_file.path) end txt_data = 'This is plain text.' mixed_msg.inline_entity(MIME::TextMedia.new(txt_data)) msg = mixed_msg.to_s parts = msg.split(BOUNDARY) assert_equal 4, parts.size, 'main header, 2 entities, last boundary' assert_equal '--', parts.pop, 'remnant of last boundary marker' # main header assert_match CONTENT_ID, parts[0] assert_match CTYPE_MPART_MIXED, parts[0] assert_equal_num_headers 2, parts[0] assert_equal_body '', parts[0] # text entity assert_match CONTENT_ID, parts[1] assert_match CTYPE_TEXT_PLAIN, parts[1] assert_match /^Content-Disposition: inline\r\n/, parts[1] assert_equal_num_headers 3, parts[1] assert_equal_body txt_data, parts[1] # image entity assert_match CONTENT_ID, parts[2] assert_match CTYPE_IMAGE_JPEG, parts[2] assert_match /^Content-Disposition: attachment; filename="#{img_filename}"\r\n/, parts[2] assert_equal_num_headers 3, parts[2] assert_equal_body img_data, parts[2] end # rm multipart_mixed_inline_and_attachment2.msg def test_multipart_mixed_message_construction_using_media_factory img1 = sd(img1_filename = 'image.jpg') img2 = sd(img2_filename = 'ruby.png') txt = sd(txt_filename = 'data.htm') bot_img = MIME::DiscreteMediaFactory.create(img1) top_img = MIME::DiscreteMediaFactory.create(img2) top_txt = MIME::DiscreteMediaFactory.create(txt) mixed_msg = MIME::MultipartMedia::Mixed.new mixed_msg.attach_entity(bot_img) mixed_msg.attach_entity(top_img) mixed_msg.inline_entity(top_txt) msg = mixed_msg.to_s parts = msg.split(BOUNDARY) assert_equal 5, parts.size, 'main header, 2 entities, last boundary' assert_equal '--', parts.pop, 'remnant of last boundary marker' # main header assert_match CONTENT_ID, parts[0] assert_match CTYPE_MPART_MIXED, parts[0] assert_equal_num_headers 2, parts[0] assert_equal_body '', parts[0] # html entity assert_match CONTENT_ID, parts[1] assert_match CTYPE_TEXT_HTML, parts[1] assert_match /^Content-Disposition: inline; filename="#{txt_filename}"\r\n/, parts[1] assert_equal_num_headers 3, parts[1] assert_equal_body top_txt.send(:body), parts[1] # png image entity assert_match CONTENT_ID, parts[2] assert_match CTYPE_IMAGE_PNG, parts[2] assert_match /^Content-Disposition: attachment; filename="#{img2_filename}"\r\n/, parts[2] assert_equal_num_headers 3, parts[2] assert_equal_body top_img.send(:body), parts[2] # jpg image entity assert_match CONTENT_ID, parts[3] assert_match CTYPE_IMAGE_JPEG, parts[3] assert_match /^Content-Disposition: attachment; filename="#{img1_filename}"\r\n/, parts[3] assert_equal_num_headers 3, parts[3] assert_equal_body bot_img.send(:body), parts[3] end def test_multipart_form_data_with_mixed_entity txt = MIME::TextMedia.new('Joe Blow') img1 = MIME::DiscreteMediaFactory.create(sd('image.jpg')) img2 = MIME::DiscreteMediaFactory.create(sd('ruby.png')) mixed_msg = MIME::MultipartMedia::Mixed.new mixed_msg.attach_entity(img2) mixed_msg.attach_entity(img1) form = MIME::MultipartMedia::FormData.new form.add_entity(mixed_msg, 'pics') form.add_entity(txt, 'field1') # similar to example 6 in RFC1867 expected = IO.read(sd('multipart_form_data_mixed.msg')) assert_equal_mime_message expected, form.to_s #IO.write('/tmp/mime.out', msg) end # def test_multipart_related_html_message_with_embedded_image # img = DiscreteMediaFactory.create(sd('/ruby.png')) # img.content_transfer_encoding = 'binary' # # html_msg = TextMedia.new(<<-html, 'text/html; charset=iso-8859-1') # # #

HTML multipart/related message

#

txt before pix

# cool ruby #

txt after pix

# # # html # html_msg.content_transfer_encoding = '7bit' # # related_msg = MultipartMedia::Related.new # related_msg.inline_entity(img) # related_msg.add_entity(html_msg) # # expected = IO.read(sd('/multipart_related.msg')) # # assert_equal_mime_message expected, related_msg.to_s # end # # def test_multipart_alternative_with_related_html_entity # img = DiscreteMediaFactory.create(sd('/ruby.png')) # img.content_transfer_encoding = 'binary' # # html_msg = TextMedia.new(<<-html, 'text/html; charset=iso-8859-1') # # #

HTML multipart/alternative message

#

txt before pix

# cool ruby #

txt after pix

# # # html # html_msg.content_transfer_encoding = '7bit' # # text_msg = TextMedia.new(<<-text) # *HTML multipart/alternative message* # txt before pix # # txt after pix # text # text_msg.content_transfer_encoding = '7bit' # # related_msg = MultipartMedia::Related.new # related_msg.inline_entity(img) # related_msg.add_entity(html_msg) # # alt_msg = MultipartMedia::Alternative.new # alt_msg.add_entity(related_msg) # alt_msg.add_entity(text_msg) # # expected = IO.read(sd('/multipart_alternative_related.msg')) # # assert_equal_mime_message expected, alt_msg.to_s # end private # # Test the equality of the normalized +expected+ and +actual+ MIME messages. # def assert_equal_mime_message expected, actual assert_equal normalize_message(expected), normalize_message(actual) end # # Make messages comparable by removing *-ID header values, library version # comment in MIME-Version header, Date header value, multipart/related # content IDs, and boundaries. # def normalize_message message # these are very delicate REs that are inter-dependent, be careful match_id_headers = /-ID: <[^>]+>\r\n/ match_boundaries = /Boundary_\d+\.\d+/ match_lib_version = / \(Ruby MIME v\d\.\d\)/ match_date_header = /^Date: .*\d{4}\r\n/ match_related_cid = /cid:\d+\.\d+/ message. gsub(match_related_cid, "cid"). gsub(match_id_headers, "-ID:\r\n"). gsub(match_boundaries, "Boundary_"). sub(match_date_header, "Date:\r\n"). sub(match_lib_version, " (Ruby MIME v0.0)") end def assert_equal_num_headers num, msg assert_equal num, msg.split(HDR_BDY_SEP).first.split(CRLF).count end def assert_equal_body expected_body, msg # FIXME the next line should be a fix headers, actual_body = msg.split(HDR_BDY_SEP) assert_equal expected_body, actual_body.to_s.chomp #assert_equal body, msg.split("\r\n").last end # # Return the absolute path of +file+ under the test/scaffold directory. # def sd file File.join(File.dirname(__FILE__), 'scaffold', file) end end mime-0.4.4/test/test_mime.rb0000644000004100000410000005043313126650165016002 0ustar www-datawww-datarequire 'minitest/autorun' require 'mime' Encoding.default_external = 'ASCII-8BIT' class MIMETest < Minitest::Test CRLF = "\r\n" BINARY_DATA = '0110000101110101011001000110100101101111' ID_SPEC = '\w+@[\w.-]+' DATE_SPEC = '..., \d{1,2} ... \d{4} \d\d:\d\d:\d\d -\d{4}' VERSION_SPEC = '\(Ruby MIME v\d\.\d\.\d\)' BOUNDARY_SPEC = 'Boundary_\w+' ### RFC 822 MESSAGE CONSTRUCTION ### def test_rfc822_message msg = MIME::Mail.new('message body') headers, body = msg.to_s.split(CRLF*2) assert_match(/^Message-ID: <#{ID_SPEC}>/, headers) assert_match(/^Date: #{DATE_SPEC}/, headers) assert_match(/^MIME-Version: 1.0 #{VERSION_SPEC}/, headers) assert_match(/^Content-ID: <#{ID_SPEC}>/, headers) assert_match(/^Content-Type: text\/plain/, headers) assert_equal 5, headers.split(CRLF).count assert_equal 'message body', body end def test_rfc822_message_with_empty_body assert_match(/#{VERSION_SPEC}#{CRLF*2}$/, MIME::Mail.new.to_s) end def test_rfc822_message_with_discrete_media_body email = MIME::Mail.new email.to = { 'john@example.com' => 'John', 'paul@example.com' => nil, 'mary@example.com' => 'Mary' } email.cc = {'boss@example.com' => 'Head Honcho'} email.from = 'jane@example.com' email.subject = 'This is an important email' email.body = MIME::Text.new('Hello, world!') assert_equal_mime_msg 'rfc822_discrete', email end def test_rfc822_message_with_composite_media_body img = MIME::DiscreteMediaFactory.create(sd('ruby.png')) img.transfer_encoding = 'binary' htm_msg = MIME::Text.new(< 'iso-8859-1') cool ruby EOF related_htm_msg = MIME::Multipart::Related.new related_htm_msg.add(htm_msg) related_htm_msg.inline(img) txt_msg = MIME::Text.new(< 'us-ascii') Check the attachment for a cool ruby picture. EOF mixed_txt_msg = MIME::Multipart::Mixed.new mixed_txt_msg.add(txt_msg) mixed_txt_msg.attach(img) alt_msg = MIME::Multipart::Alternative.new alt_msg.add(mixed_txt_msg) alt_msg.add(related_htm_msg) email = MIME::Mail.new(alt_msg) email.from = 'john@example.com' email.to = 'jane@example.com' email.subject = 'Cool ruby pic' assert_equal_mime_msg 'rfc822_composite', email end ### DISCRETE MESSAGE CONSTRUCTION (SINGLE ENTITY) ### def test_audio_message audio_media = MIME::Audio.new(BINARY_DATA, 'midi') audio_media.transfer_encoding = 'binary' assert_equal_mime_msg 'audio', audio_media end def test_application_message app_media = MIME::Application.new(BINARY_DATA) app_media.transfer_encoding = 'binary' assert_equal_mime_msg 'application', app_media end def test_image_message image = IO.read(sd('image.jpg')) image_media = MIME::Image.new(image) image_media.transfer_encoding = 'binary' assert_equal_mime_msg 'image', image_media end def test_text_message text_media = MIME::Text.new('a plain text message') assert_equal_mime_msg 'text', text_media end def test_video_message video_media = MIME::Video.new(BINARY_DATA) video_media.transfer_encoding = 'binary' assert_equal_mime_msg 'video', video_media end ### COMPOSITE MESSAGE CONSTRUCTION (MULTIPLE ENTITIES) ### def test_multipart_form_data_with_text_entities txt_data = 'text body' htm_data = IO.read(sd('data.htm')) xml_data = IO.read(sd('data.xml')) txt = MIME::Text.new(txt_data) htm = MIME::Text.new(htm_data, 'html') xml = MIME::Text.new(xml_data, 'xml') form = MIME::Multipart::FormData.new form.add xml, 'xml' form.add htm, 'htm' form.add txt, 'txt' assert_equal_mime_msg 'multipart_form_data_text', form end def test_multipart_form_data_with_text_and_file_entities img1_filename = 'image.jpg' img2_filename = 'ruby.png' img1_data = IO.read(sd(img1_filename)) img2_data = IO.read(sd(img2_filename)) img1 = MIME::Image.new(img1_data, 'jpeg') img2 = MIME::Image.new(img2_data, 'png') img1.transfer_encoding = '8bit' img2.transfer_encoding = '8bit' desc_data = 'This is plain text description of images.' desc = MIME::Text.new(desc_data, 'plain', 'charset' => 'us-ascii') form = MIME::Multipart::FormData.new form.add img1, 'image_1', img1_filename form.add img2, 'image_2', img2_filename form.add desc, 'description' assert_equal_mime_msg 'multipart_form_data_file_and_text', form end # Similar to example 6 in RFC1867. def test_multipart_form_data_with_mixed_entity txt = MIME::Text.new('Joe Blow') img1 = MIME::DiscreteMediaFactory.create(sd('image.jpg')) img2 = MIME::DiscreteMediaFactory.create(sd('ruby.png')) mixed_msg = MIME::Multipart::Mixed.new mixed_msg.attach(img1) mixed_msg.attach(img2) form = MIME::Multipart::FormData.new form.add(txt, 'field1') form.add(mixed_msg, 'pics') assert_equal_mime_msg 'multipart_form_data_mixed', form end def test_multipart_alternative_message txt_data = "*Header*\nmessage" htm_data = "

Header

message

" txt_msg = MIME::Text.new(txt_data, 'enhanced', 'charset' => 'us-ascii') htm_msg = MIME::Text.new(htm_data, 'html', 'charset' => 'iso-8859-1') msg = MIME::Multipart::Alternative.new msg.add txt_msg msg.add htm_msg assert_equal_mime_msg 'multipart_alternative', msg end def test_multipart_alternative_with_related_html_entity img = MIME::DiscreteMediaFactory.create(sd('ruby.png')) img.transfer_encoding = 'binary' html_msg = MIME::Text.new(< 'iso-8859-1')

HTML multipart/alternative message

txt before pix

cool ruby

txt after pix

EOF html_msg.transfer_encoding = '7bit' text_msg = MIME::Text.new(< 'us-ascii') *HTML multipart/alternative message* txt before pix txt after pix EOF text_msg.transfer_encoding = '7bit' related_msg = MIME::Multipart::Related.new related_msg.add(html_msg) related_msg.inline(img) msg = MIME::Multipart::Alternative.new msg.add(text_msg) msg.add(related_msg) assert_equal_mime_msg 'multipart_alternative_related', msg end def test_multipart_mixed_with_inline_and_attachment msg = MIME::Multipart::Mixed.new msg.inline(MIME::Text.new('Plain Text')) open(sd('image.jpg')) do |img_file| img_data = img_file.read img_msg = MIME::Image.new(img_data, 'jpeg') msg.attach(img_msg, 'filename' => img_file.path) end assert_equal_mime_msg 'multipart_mixed_inline_and_attachment', msg end def test_multipart_mixed_message_using_media_factory bot_img = MIME::DiscreteMediaFactory.create(sd('image.jpg')) top_img = MIME::DiscreteMediaFactory.create(sd('ruby.png')) top_txt = MIME::DiscreteMediaFactory.create(sd('data.htm')) msg = MIME::Multipart::Mixed.new msg.inline(top_txt) msg.attach(top_img) msg.attach(bot_img) assert_equal_mime_msg 'multipart_mixed_inline_and_attachment2', msg end def test_multipart_related_html_message_with_embedded_image img = MIME::DiscreteMediaFactory.create(sd('/ruby.png')) img.transfer_encoding = 'binary' html_msg = MIME::Text.new(<

HTML multipart/related message

txt before pix cool ruby

txt after pix

EOF html_msg.transfer_encoding = '7bit' msg = MIME::Multipart::Related.new msg.add(html_msg) msg.inline(img) assert_equal_mime_msg 'multipart_related', msg end ### GENERAL RFC ADHERENCE ### def test_boundary_format form = MIME::Multipart::FormData.new %w(one two three four).each do |ent| form.add(MIME::Text.new(ent), ent) end boundary = form.to_s.scan(/--Boundary_.*\r\n/).each first_boundary = boundary.next assert_equal first_boundary, boundary.next assert_equal first_boundary, boundary.next assert_equal first_boundary, boundary.next refute_equal first_boundary, (last_boundary = boundary.next) assert_match(/^--#{BOUNDARY_SPEC}\r\n/, first_boundary) assert_match(/^--#{BOUNDARY_SPEC}--\r\n/, last_boundary) end def test_message_id_auto_generation msg = MIME::Mail.new assert_nil(msg.message_id) msg.to_s assert_match(/^#{ID_SPEC}$/, msg.message_id) end def test_message_id_manual_assignment msg_id = 'id_1234@example.com' msg = MIME::Mail.new assert_nil(msg.message_id) msg.message_id = msg_id assert_equal("#{msg_id}", msg.message_id) msg.to_s # should not affect message ID assert_equal("#{msg_id}", msg.message_id) end def test_sender_field_auto_generation msg = MIME::Mail.new msg.from = addresses = %w(john@example.com jane@example.com) assert_nil msg.sender msg.to_s # add sender header when multiple from addresses assert_equal addresses.first, msg.sender end def test_sender_field_manual_assignment msg = MIME::Mail.new msg.from = %w(john@example.com jane@example.com) assert_nil msg.sender msg.sender = sender = 'jack@example.com' assert_equal sender, msg.sender msg.to_s # should not affect sender header assert_equal sender, msg.sender end # Related type parameter: https://tools.ietf.org/html/rfc2387#section-3.1 def test_related_message_inherits_type_from_root msg = MIME::Multipart::Related.new refute_includes(msg.type, 'type') msg.add(MIME::Text.new('body', 'plain')) # Root object (first entity) assert_includes(msg.type, 'type=text/plain') # sets the related msg type. msg.add(MIME::Text.new('body', 'html')) # Additional entities have refute_includes(msg.type, 'type=text/html') # no effect on msg type. assert_includes(msg.type, 'type=text/plain') end def test_related_message_inherits_parameterized_type_from_root msg = MIME::Multipart::Related.new refute_includes(msg.type, 'type') root = MIME::Text.new('a message', 'plain', 'charset' => 'us-ascii') assert_equal(root.type, 'text/plain; charset=us-ascii') msg.add(root) assert_includes(msg.type, 'type=text/plain') # Inherits root mime type, refute_includes(msg.type, 'charset=us-ascii') # but params are omitted. end def test_rfc2822_date_format msg = MIME::Mail.new assert_kind_of(Time, msg.date) assert_match(/^Date: #{msg.date.rfc2822}\r\n/, msg.to_s) hour_ago = Time.now() - 3600 msg.date = hour_ago assert_match(/^Date: #{hour_ago.rfc2822}\r\n/, msg.to_s) msg.date += 3600 refute_match(/^Date: #{hour_ago.rfc2822}\r\n/, msg.to_s) end def test_unique_content_ids_in_multipart_message form = MIME::Multipart::FormData.new %w(one two three four).each do |ent| form.add(MIME::Text.new(ent), ent) end # 5 IDs: main header ID + 4 entity IDs content_ids = form.to_s.scan(/^Content-ID: <(#{ID_SPEC})>/) assert_equal 5, content_ids.flatten.uniq.count # IDs must be unique end def test_content_disposition_filenames filename1 = 'book.pdf' filename2 = 'mini.mov' filename3 = 'song.mp3' filename4 = 'none.txt' filename5 = 'none.htm' file1 = MIME::DiscreteMediaFactory.create(sd filename1) file2 = MIME::DiscreteMediaFactory.create(sd filename2) file3 = MIME::DiscreteMediaFactory.create(sd filename3) file4 = MIME::Text.new('txt') file5 = MIME::Text.new('htm') file6 = MIME::Text.new('xml') form = MIME::Multipart::FormData.new # file backed objects form.add file1, 'file_1' # none form.add file2, 'file_2', filename2 # relative form.add file3, 'file_3', "/tmp/#{filename3}" # absolute # non-file backed objects form.add file4, 'file_4', "/tmp/#{filename4}" # absolute form.add file5, 'file_5', filename5 # relative form.add file6, 'file_6' # none msg = form.to_s hdr = 'Content-Disposition: form-data;' # only the file basename should be assigned to filename, never a path assert_match(/^#{hdr} name=file_1; filename=#{filename1}\r\n/, msg) assert_match(/^#{hdr} name=file_2; filename=#{filename2}\r\n/, msg) assert_match(/^#{hdr} name=file_3; filename=#{filename3}\r\n/, msg) assert_match(/^#{hdr} name=file_4; filename=#{filename4}\r\n/, msg) assert_match(/^#{hdr} name=file_5; filename=#{filename5}\r\n/, msg) assert_match(/^#{hdr} name=file_6\r\n/, msg) end # According to RFC 2822, "multiple occurrences of any of the fields" is # "obsolete field syntax" and "interpretation of multiple occurrences of # fields is unspecified." def test_case_insenstive_header_names headers = MIME::Header.new headers.set 'from', 'user1' assert_equal 'from: user1', headers.to_s headers.set 'FROM', 'user1' assert_equal 'FROM: user1', headers.to_s headers.set 'From', 'user2' assert_equal 'From: user2', headers.to_s headers.delete 'fROM' assert_equal '', headers.to_s end def test_header_field_removal_via_set_nil headers = MIME::Header.new headers.set 'a', 'b' assert_equal 'b', headers.get('a') headers.set 'a', nil assert_nil headers.get('a') end def test_header_field_removal_via_delete headers = MIME::Header.new headers.set 'a', 'b' assert_equal 'b', headers.get('a') headers.delete 'a' assert_nil headers.get('a') end def test_mailbox_types e1 = 'john@example.com' e2 = 'jane@example.com' to = "#{e1}, #{e2}" mb_string = to mb_array = [e1, e2] mb_hash = {e1 => nil, e2 => nil} msg = MIME::Mail.new msg.to = mb_string; assert_equal to, msg.headers.get('to') msg.to = mb_array; assert_equal to, msg.headers.get('to') msg.to = mb_hash; assert_equal to, msg.headers.get('to') end def test_mailbox_display_names non_alnum = "!$&*-=^`|~#%+/?_{}'" email = 'john@example.com' msg = MIME::Mail.new mailboxes = [ {email => nil}, # no display name {email => %[A #{non_alnum} Z]}, # non-special chars {email => %[John]}, # one atom {email => %[John Doe]}, # two atoms {email => %[John "Dead Man" Doe]}, # special: double quote {email => %[John D. Doe]}, # special: period {email => %[John Doe, DECEASED]} # special: comma ] expected = [ email, %[A #{non_alnum} Z <#{email}>], %[John <#{email}>], %[John Doe <#{email}>], %["John \\\"Dead Man\\\" Doe" <#{email}>], %["John D. Doe" <#{email}>], %["John Doe, DECEASED" <#{email}>] ] expected.each_with_index do |exp, i| msg.to = mailboxes[i] assert_equal exp, msg.headers.get('to') end end def test_sender_field_specification e1 = 'john@example.com' e2 = 'jane@example.com' msg = MIME::Mail.new # sender must contain a single mailbox assert_raises(ArgumentError) {msg.sender = [e1, e2]} assert_raises(ArgumentError) {msg.sender = {e1=>nil, e2=>nil}} msg.sender = [e1]; assert_equal e1, msg.headers.get('sender') msg.sender = {e1=>nil}; assert_equal e1, msg.headers.get('sender') end # Quoted Strings: https://tools.ietf.org/html/rfc5322#section-3.2.4 def test_content_disposition_parameter_quoting txt = MIME::Text.new('') value_test_cases = { '123' => '123', 'abc' => 'abc', 'a.c' => 'a.c', '{b}' => '{b}', '.bc' => '".bc"', # quote leading period '[b]' => '"[b]"', # quote string 'a c' => '"a c"', # unless DOT_ATOM 'a"c' => '"a\"c"' # escape double quote } value_test_cases.each do |value, expected| txt.__send__(:set_disposition, 'type', 'param' => value) assert_equal "type; param=#{expected}", txt.disposition end end ### LIBRARY OPERATION ### def test_no_instantiation_of_abstract_classes e = MIME::AbstractClassError assert_raises(e) {MIME::Media.new(nil, nil, nil)} assert_raises(e) {MIME::DiscreteMedia.new(nil, nil, nil)} assert_raises(e) {MIME::CompositeMedia.new(nil)} assert_raises(e) {MIME::Multipart.new(nil)} end def test_content_type_detection (o = Object.new).extend(MIME::ContentTypes) # test file extension; file path is irrelevant here assert_equal 'application/pdf', o.file_type('a.pdf') assert_equal 'video/quicktime', o.file_type('b.mov') assert_equal 'application/octet-stream', o.file_type('c.iso') assert_equal 'audio/mpeg', o.file_type('/d/e.mp3') assert_equal 'text/css', o.file_type('/f/g/h.css') assert_nil o.file_type('i.nil') # test using file object img_type = open(sd('ruby.png')) {|f| o.file_type(f)} assert_equal 'image/png', img_type refute_equal 'image/jpeg', img_type end def test_object_instantiation_using_discrete_media_factory app_file = sd('book.pdf') audio_file = sd('song.mp3') text_file = sd('data.xml') video_file = sd('mini.mov') image_file = sd('image.jpg') unknown_file = sd('unknown.yyy') dmf = MIME::DiscreteMediaFactory # test using file path assert_kind_of MIME::Application, dmf.create(app_file) assert_kind_of MIME::Audio, dmf.create(audio_file) assert_kind_of MIME::Text, dmf.create(text_file) assert_kind_of MIME::Video, dmf.create(video_file) assert_kind_of MIME::Image, dmf.create(image_file) # test using file object open(image_file) do |image_file_obj| assert_kind_of MIME::Image, dmf.create(image_file_obj) end open(text_file) do |text_file_obj| assert_kind_of MIME::Text, dmf.create(text_file_obj) end # raise for unknown file path and File object assert_raises(MIME::UnknownContentError) {dmf.create(unknown_file)} open(unknown_file) do |unknown_file_obj| assert_raises(MIME::UnknownContentError) {dmf.create(unknown_file_obj)} end end def test_discrete_media_factory_with_specified_invalid_conent_type invalid_ctype1 = 'application-x/pdf' invalid_ctype2 = 'application' invalid_ctype3 = '' valid_ctype = 'application/pdf' pdf = sd('book.pdf') assert_raises(MIME::UnknownContentError) { MIME::DiscreteMediaFactory.create(pdf, invalid_ctype1) } assert_raises(MIME::UnknownContentError) { MIME::DiscreteMediaFactory.create(pdf, invalid_ctype2) } assert_raises(MIME::UnknownContentError) { MIME::DiscreteMediaFactory.create(pdf, invalid_ctype3) } assert MIME::DiscreteMediaFactory.create(pdf, valid_ctype) end def test_discrete_media_factory_creates_path_singleton_method pdf_file_path = sd('book.pdf') media_obj = MIME::DiscreteMediaFactory.create(pdf_file_path) assert_equal pdf_file_path, media_obj.path open(pdf_file_path) do |pdf_file_obj| media_obj = MIME::DiscreteMediaFactory.create(pdf_file_obj) assert_equal pdf_file_path, media_obj.path end end def test_content_id txt = MIME::Text.new('body') # CID generated on initialize cid1 = txt.id.dup assert_match(/^#{ID_SPEC}$/, cid1) assert_includes(txt.to_s, cid1) # to_s should not change CID assert_equal(cid1, txt.id) txt.id = cid2 = 'id@example.com' refute_equal(cid1, txt.id) assert_equal(cid2, txt.id) assert_includes(txt.to_s, cid2) end private # # Test equality of the +expected+ and +actual+ MIME messages. # def assert_equal_mime_msg expected, actual m1 = generalize_msg(IO.read(sd(expected+'.msg'))) m2 = generalize_msg(actual.to_s) assert_equal m1, m2 end # # Remove unique identifiers to make +message+ structurally comparable. # def generalize_msg message id = /-ID: <#{ID_SPEC}>#{CRLF}/ cid = /cid:#{ID_SPEC}/ date = /^Date: #{DATE_SPEC}#{CRLF}/ version = /#{VERSION_SPEC}#{CRLF}/ boundary = /#{BOUNDARY_SPEC}/ message. gsub(id, "-ID: #{CRLF}"). gsub(cid, "cid:ID-REMOVED"). sub(date, "Date: DATE-REMOVED#{CRLF}"). sub(version, "(Ruby MIME v0.0.0)#{CRLF}"). gsub(boundary, "Boundary_ID-REMOVED") end # # Return the path of the scaffold +file+. # def sd file File.join(File.dirname(__FILE__), 'scaffold', file) end end