7 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
9 # Permission is hereby granted, free of charge, to any person obtaining
10 # a copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to
14 # permit persons to whom the Software is furnished to do so, subject to
15 # the following conditions:
17 # The above copyright notice and this permission notice shall be
18 # included in all copies or substantial portions of the Software.
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
29 # with permission of Minero Aoki.
34 require 'tmail/interface'
35 require 'tmail/encode'
36 require 'tmail/header'
38 require 'tmail/config'
40 require 'tmail/attachments'
41 require 'tmail/quoting'
48 # Accessing a TMail object done via the TMail::Mail class. As email can be fairly complex
49 # creatures, you will find a large amount of accessor and setter methods in this class!
51 # Most of the below methods handle the header, in fact, what TMail does best is handle the
52 # header of the email object. There are only a few methods that deal directly with the body
53 # of the email, such as base64_encode and base64_decode.
55 # === Using TMail inside your code
57 # The usual way is to install the gem (see the {README}[link:/README] on how to do this) and
58 # then put at the top of your class:
62 # You can then create a new TMail object in your code with:
64 # @email = TMail::Mail.new
66 # Or if you have an email as a string, you can initialize a new TMail::Mail object and get it
67 # to parse that string for you like so:
69 # @email = TMail::Mail.parse(email_text)
71 # You can also read a single email off the disk, for example:
73 # @email = TMail::Mail.load('filename.txt')
75 # Also, you can read a mailbox (usual unix mbox format) and end up with an array of TMail
76 # objects by doing something like this:
78 # # Note, we pass true as the last variable to open the mailbox read only
79 # mailbox = TMail::UNIXMbox.new("mailbox", nil, true)
81 # mailbox.each_port { |m| @emails << TMail::Mail.new(m) }
87 # Opens an email that has been saved out as a file by itself.
89 # This function will read a file non-destructively and then parse
90 # the contents and return a TMail::Mail object.
92 # Does not handle multiple email mailboxes (like a unix mbox) for that
93 # use the TMail::UNIXMbox class.
96 # mail = TMail::Mail.load('filename')
99 new(FilePort
.new(fname
))
105 # Parses an email from the supplied string and returns a TMail::Mail
109 # require 'rubygems'; require 'tmail'
110 # email_string =<<HEREDOC
111 # To: mikel@lindsaar.net
113 # Subject: This is a short Email
118 # mail = TMail::Mail.parse(email_string)
119 # #=> #<TMail::Mail port=#<TMail::StringPort:id=0xa30ac0> bodyport=nil>
121 # #=> "Hello there Mikel!\n\n"
123 new(StringPort
.new(str
))
128 def initialize( port
= nil, conf
= DEFAULT_CONFIG
) #:nodoc:
129 @port = port
|| StringPort
.new
130 @config = Config
.to_config(conf
)
140 parse_body f
unless @port.reproducible
?
144 # Provides access to the port this email is using to hold it's data
147 # mail = TMail::Mail.parse(email_string)
149 # #=> #<TMail::StringPort:id=0xa2c952>
153 "\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
162 include StrategyInterface
164 def write_back( eol
= "\n", charset
= 'e' )
166 @port.wopen
{|stream
| encoded eol
, charset
, stream
}
169 def accept( strategy
)
170 with_multipart_encoding(strategy
) {
171 ordered_each
do |name
, field
|
173 strategy
.header_name
canonical(name
)
174 field
.accept strategy
178 body_port().ropen
{|r
|
179 strategy
.write r
.read
186 def canonical( name
)
187 name
.split(/-/).map
{|s
| s
.capitalize
}.join('-')
190 def with_multipart_encoding( strategy
)
191 if parts().empty
? # DO NOT USE @parts
195 bound
= ::TMail.new_boundary
196 if @header.key
? 'content-type'
197 @header['content-type'].params
['boundary'] = bound
199 store
'Content-Type', %<multipart
/mixed
; boundary
="#{bound}">
206 strategy
.puts
'--' + bound
210 strategy
.puts
'--' + bound
+ '--'
211 strategy
.write
epilogue()
223 'resent-date' => true,
224 'resent-from' => true,
225 'resent-sender' => true,
228 'resent-bcc' => true,
229 'resent-message-id' => true,
233 USE_ARRAY
= ALLOW_MULTIPLE
239 # Returns a TMail::AddressHeader object of the field you are querying.
241 # @mail['from'] #=> #<TMail::AddressHeader "mikel@test.com.au">
242 # @mail['to'] #=> #<TMail::AddressHeader "mikel@test.com.au">
244 # You can get the string value of this by passing "to_s" to the query:
246 # @mail['to'].to_s #=> "mikel@test.com.au"
248 @header[key
.downcase
]
251 def sub_header(key
, param
)
252 (hdr
= self[key
]) ? hdr
[param
] : nil
257 # Allows you to set or delete TMail header objects at will.
259 # @mail = TMail::Mail.new
260 # @mail['to'].to_s # => 'mikel@test.com.au'
261 # @mail['to'] = 'mikel@elsewhere.org'
262 # @mail['to'].to_s # => 'mikel@elsewhere.org'
263 # @mail.encoded # => "To: mikel@elsewhere.org\r\n\r\n"
265 # @mail['to'].to_s # => nil
266 # @mail.encoded # => "\r\n"
268 # Note: setting mail[] = nil actually deletes the header field in question from the object,
269 # it does not just set the value of the hash to nil
280 header
= new_hf(key
, val
)
284 ALLOW_MULTIPLE
.include? dkey
or
285 raise ArgumentError
, "#{key}: Header must not be multiple"
289 header
= new_hf(key
, val
.to_s
)
291 if ALLOW_MULTIPLE
.include? dkey
292 (@header[dkey
] ||= []).push header
294 @header[dkey
] = header
302 # Allows you to loop through each header in the TMail::Mail object in a block
304 # @mail['to'] = 'mikel@elsewhere.org'
305 # @mail['from'] = 'me@me.com'
306 # @mail.each_header { |k,v| puts "#{k} = #{v}" }
307 # # => from = me@me.com
308 # # => to = mikel@elsewhere.org
310 @header.each
do |key
, val
|
311 [val
].flatten
.each
{|v
| yield key
, v
}
315 alias each_pair each_header
317 def each_header_name( &block
)
318 @header.each_key(&block
)
321 alias each_key each_header_name
323 def each_field( &block
)
324 @header.values
.flatten
.each(&block
)
327 alias each_value each_field
331 resent-date resent-from resent-sender resent-to
332 resent-cc resent-bcc resent-message-id
333 date from sender reply-to to cc bcc
334 message-id in-reply-to references
335 subject comments keywords
336 mime-version content-type content-transfer-encoding
337 content-disposition content-description
342 FIELD_ORDER
.each
do |name
|
344 [@header[name
]].flatten
.each
{|v
| yield name
, v
}
348 [@header[name
]].flatten
.each
{|v
| yield name
, v
}
357 @header.delete key
.downcase
361 @header.delete_if
do |key
,val
|
363 val
.delete_if
{|v
| yield key
, v
}
376 @header.key
? key
.downcase
379 def values_at( *args
)
380 args
.map
{|k
| @header[k
.downcase
] }.flatten
383 alias indexes values_at
384 alias indices values_at
388 def parse_header( f
)
394 when /\A[ \t]/ # continue from prev line
395 raise SyntaxError
, 'mail is began by space' unless field
396 field
<< ' ' << line
.strip
398 when /\A([^\: \t]+):\s*/ # new header line
399 add_hf name
, field
if field
403 when /\A\-*\s*\z/ # end of header
404 add_hf name, field if field
414 raise SyntaxError, "wrong mail header: '#{line.inspect}'"
417 add_hf name
, field
if name
420 add_hf
'Return-Path', "<#{unixfrom}>" unless @header['return-path']
424 def add_hf( name
, field
)
426 field
= new_hf(name
, field
)
428 if ALLOW_MULTIPLE
.include? key
429 (@header[key
] ||= []).push field
435 def new_hf( name
, field
)
436 HeaderField
.new(name
, field
, @config)
451 body_port().ropen
{|f
| f
.each(&block
) }
455 body_port
.ropen
{|f
| return f
.read
}
459 body_port
.wopen
{ |f
| f
.write str
}
464 # Sets the body of the email to a new (encoded) string.
466 # We also reparses the email if the body is ever reassigned, this is a performance hit, however when
467 # you assign the body, you usually want to be able to make sure that you can access the attachments etc.
471 # mail.body = "Hello, this is\nthe body text"
472 # # => "Hello, this is\nthe body"
474 # # => "Hello, this is\nthe body"
476 parse_body(StringInput
.new(str
))
478 @body_port.wopen
{|f
| f
.write str
}
482 alias preamble quoted_body
483 alias preamble
= quoted_body
=
501 def each_part( &block
)
505 # Returns true if the content type of this part of the email is
506 # a disposition attachment
507 def disposition_is_attachment
?
508 (self['content-disposition'] && self['content-disposition'].disposition
== "attachment")
511 # Returns true if this part's content main type is text, else returns false.
512 # By main type is meant "text/plain" is text. "text/html" is text
513 def content_type_is_text
?
514 self.header
['content-type'] && (self.header
['content-type'].main_type
!= "text")
519 def parse_body( f
= nil )
520 return if @body_parsed
534 return if /\A[\r\n]*\z/ === line
538 def parse_body_0( f
)
542 @body_port = @config.new_body_port(self)
543 @body_port.wopen
{|w
|
549 def read_multipart( src
)
550 bound
= @header['content-type'].params
['boundary']
551 is_sep
= /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
552 lastbound
= "--#{bound}--"
554 ports
= [ @config.new_preamble_port(self) ]
557 while line
= src
.gets
560 break if line
.strip
== lastbound
561 ports
.push
@config.new_part_port(self)
567 @epilogue = (src
.read
|| '')
569 f
.close
if f
and not f
.closed
?
572 @body_port = ports
.shift
573 @parts = ports
.map
{|p
| self.class.new(p
, @config) }