=begin rdoc = Mail class =end #-- # Copyright (c) 1998-2003 Minero Aoki # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # Note: Originally licensed under LGPL v2+. Using MIT license for Rails # with permission of Minero Aoki. #++ require 'tmail/interface' require 'tmail/encode' require 'tmail/header' require 'tmail/port' require 'tmail/config' require 'tmail/utils' require 'tmail/attachments' require 'tmail/quoting' require 'socket' module TMail # == Mail Class # # Accessing a TMail object done via the TMail::Mail class. As email can be fairly complex # creatures, you will find a large amount of accessor and setter methods in this class! # # Most of the below methods handle the header, in fact, what TMail does best is handle the # header of the email object. There are only a few methods that deal directly with the body # of the email, such as base64_encode and base64_decode. # # === Using TMail inside your code # # The usual way is to install the gem (see the {README}[link:/README] on how to do this) and # then put at the top of your class: # # require 'tmail' # # You can then create a new TMail object in your code with: # # @email = TMail::Mail.new # # Or if you have an email as a string, you can initialize a new TMail::Mail object and get it # to parse that string for you like so: # # @email = TMail::Mail.parse(email_text) # # You can also read a single email off the disk, for example: # # @email = TMail::Mail.load('filename.txt') # # Also, you can read a mailbox (usual unix mbox format) and end up with an array of TMail # objects by doing something like this: # # # Note, we pass true as the last variable to open the mailbox read only # mailbox = TMail::UNIXMbox.new("mailbox", nil, true) # @emails = [] # mailbox.each_port { |m| @emails << TMail::Mail.new(m) } # class Mail class << self # Opens an email that has been saved out as a file by itself. # # This function will read a file non-destructively and then parse # the contents and return a TMail::Mail object. # # Does not handle multiple email mailboxes (like a unix mbox) for that # use the TMail::UNIXMbox class. # # Example: # mail = TMail::Mail.load('filename') # def load( fname ) new(FilePort.new(fname)) end alias load_from load alias loadfrom load # Parses an email from the supplied string and returns a TMail::Mail # object. # # Example: # require 'rubygems'; require 'tmail' # email_string =< # bodyport=nil> # mail.body # #=> "Hello there Mikel!\n\n" def parse( str ) new(StringPort.new(str)) end end def initialize( port = nil, conf = DEFAULT_CONFIG ) #:nodoc: @port = port || StringPort.new @config = Config.to_config(conf) @header = {} @body_port = nil @body_parsed = false @epilogue = '' @parts = [] @port.ropen {|f| parse_header f parse_body f unless @port.reproducible? } end # Provides access to the port this email is using to hold it's data # # Example: # mail = TMail::Mail.parse(email_string) # mail.port # #=> # attr_reader :port def inspect "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>" end # # to_s interfaces # public include StrategyInterface def write_back( eol = "\n", charset = 'e' ) parse_body @port.wopen {|stream| encoded eol, charset, stream } end def accept( strategy ) with_multipart_encoding(strategy) { ordered_each do |name, field| next if field.empty? strategy.header_name canonical(name) field.accept strategy strategy.puts end strategy.puts body_port().ropen {|r| strategy.write r.read } } end private def canonical( name ) name.split(/-/).map {|s| s.capitalize }.join('-') end def with_multipart_encoding( strategy ) if parts().empty? # DO NOT USE @parts yield else bound = ::TMail.new_boundary if @header.key? 'content-type' @header['content-type'].params['boundary'] = bound else store 'Content-Type', % end yield parts().each do |tm| strategy.puts strategy.puts '--' + bound tm.accept strategy end strategy.puts strategy.puts '--' + bound + '--' strategy.write epilogue() end end ### ### header ### public ALLOW_MULTIPLE = { 'received' => true, 'resent-date' => true, 'resent-from' => true, 'resent-sender' => true, 'resent-to' => true, 'resent-cc' => true, 'resent-bcc' => true, 'resent-message-id' => true, 'comments' => true, 'keywords' => true } USE_ARRAY = ALLOW_MULTIPLE def header @header.dup end # Returns a TMail::AddressHeader object of the field you are querying. # Examples: # @mail['from'] #=> # # @mail['to'] #=> # # # You can get the string value of this by passing "to_s" to the query: # Example: # @mail['to'].to_s #=> "mikel@test.com.au" def []( key ) @header[key.downcase] end def sub_header(key, param) (hdr = self[key]) ? hdr[param] : nil end alias fetch [] # Allows you to set or delete TMail header objects at will. # Examples: # @mail = TMail::Mail.new # @mail['to'].to_s # => 'mikel@test.com.au' # @mail['to'] = 'mikel@elsewhere.org' # @mail['to'].to_s # => 'mikel@elsewhere.org' # @mail.encoded # => "To: mikel@elsewhere.org\r\n\r\n" # @mail['to'] = nil # @mail['to'].to_s # => nil # @mail.encoded # => "\r\n" # # Note: setting mail[] = nil actually deletes the header field in question from the object, # it does not just set the value of the hash to nil def []=( key, val ) dkey = key.downcase if val.nil? @header.delete dkey return nil end case val when String header = new_hf(key, val) when HeaderField ; when Array ALLOW_MULTIPLE.include? dkey or raise ArgumentError, "#{key}: Header must not be multiple" @header[dkey] = val return val else header = new_hf(key, val.to_s) end if ALLOW_MULTIPLE.include? dkey (@header[dkey] ||= []).push header else @header[dkey] = header end val end alias store []= # Allows you to loop through each header in the TMail::Mail object in a block # Example: # @mail['to'] = 'mikel@elsewhere.org' # @mail['from'] = 'me@me.com' # @mail.each_header { |k,v| puts "#{k} = #{v}" } # # => from = me@me.com # # => to = mikel@elsewhere.org def each_header @header.each do |key, val| [val].flatten.each {|v| yield key, v } end end alias each_pair each_header def each_header_name( &block ) @header.each_key(&block) end alias each_key each_header_name def each_field( &block ) @header.values.flatten.each(&block) end alias each_value each_field FIELD_ORDER = %w( return-path received resent-date resent-from resent-sender resent-to resent-cc resent-bcc resent-message-id date from sender reply-to to cc bcc message-id in-reply-to references subject comments keywords mime-version content-type content-transfer-encoding content-disposition content-description ) def ordered_each list = @header.keys FIELD_ORDER.each do |name| if list.delete(name) [@header[name]].flatten.each {|v| yield name, v } end end list.each do |name| [@header[name]].flatten.each {|v| yield name, v } end end def clear @header.clear end def delete( key ) @header.delete key.downcase end def delete_if @header.delete_if do |key,val| if Array === val val.delete_if {|v| yield key, v } val.empty? else yield key, val end end end def keys @header.keys end def key?( key ) @header.key? key.downcase end def values_at( *args ) args.map {|k| @header[k.downcase] }.flatten end alias indexes values_at alias indices values_at private def parse_header( f ) name = field = nil unixfrom = nil while line = f.gets case line when /\A[ \t]/ # continue from prev line raise SyntaxError, 'mail is began by space' unless field field << ' ' << line.strip when /\A([^\: \t]+):\s*/ # new header line add_hf name, field if field name = $1 field = $' #.strip when /\A\-*\s*\z/ # end of header add_hf name, field if field name = field = nil break when /\AFrom (\S+)/ unixfrom = $1 when /^charset=.*/ else raise SyntaxError, "wrong mail header: '#{line.inspect}'" end end add_hf name, field if name if unixfrom add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path'] end end def add_hf( name, field ) key = name.downcase field = new_hf(name, field) if ALLOW_MULTIPLE.include? key (@header[key] ||= []).push field else @header[key] = field end end def new_hf( name, field ) HeaderField.new(name, field, @config) end ### ### body ### public def body_port parse_body @body_port end def each( &block ) body_port().ropen {|f| f.each(&block) } end def quoted_body body_port.ropen {|f| return f.read } end def quoted_body= str body_port.wopen { |f| f.write str } str end def body=( str ) # Sets the body of the email to a new (encoded) string. # # We also reparses the email if the body is ever reassigned, this is a performance hit, however when # you assign the body, you usually want to be able to make sure that you can access the attachments etc. # # Usage: # # mail.body = "Hello, this is\nthe body text" # # => "Hello, this is\nthe body" # mail.body # # => "Hello, this is\nthe body" @body_parsed = false parse_body(StringInput.new(str)) parse_body @body_port.wopen {|f| f.write str } str end alias preamble quoted_body alias preamble= quoted_body= def epilogue parse_body @epilogue.dup end def epilogue=( str ) parse_body @epilogue = str str end def parts parse_body @parts end def each_part( &block ) parts().each(&block) end # Returns true if the content type of this part of the email is # a disposition attachment def disposition_is_attachment? (self['content-disposition'] && self['content-disposition'].disposition == "attachment") end # Returns true if this part's content main type is text, else returns false. # By main type is meant "text/plain" is text. "text/html" is text def content_type_is_text? self.header['content-type'] && (self.header['content-type'].main_type != "text") end private def parse_body( f = nil ) return if @body_parsed if f parse_body_0 f else @port.ropen {|f| skip_header f parse_body_0 f } end @body_parsed = true end def skip_header( f ) while line = f.gets return if /\A[\r\n]*\z/ === line end end def parse_body_0( f ) if multipart? read_multipart f else @body_port = @config.new_body_port(self) @body_port.wopen {|w| w.write f.read } end end def read_multipart( src ) bound = @header['content-type'].params['boundary'] is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/ lastbound = "--#{bound}--" ports = [ @config.new_preamble_port(self) ] begin f = ports.last.wopen while line = src.gets if is_sep === line f.close break if line.strip == lastbound ports.push @config.new_part_port(self) f = ports.last.wopen else f << line end end @epilogue = (src.read || '') ensure f.close if f and not f.closed? end @body_port = ports.shift @parts = ports.map {|p| self.class.new(p, @config) } end end # class Mail end # module TMail