2 # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and associated documentation files (the
6 # "Software"), to deal in the Software without restriction, including
7 # without limitation the rights to use, copy, modify, merge, publish,
8 # distribute, sublicense, and/or sell copies of the Software, and to
9 # permit persons to whom the Software is furnished to do so, subject to
10 # the following conditions:
12 # The above copyright notice and this permission notice shall be
13 # included in all copies or substantial portions of the Software.
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
24 # with permission of Minero Aoki.
27 require 'tmail/encode'
28 require 'tmail/address'
29 require 'tmail/parser'
30 require 'tmail/config'
36 # Provides methods to handle and manipulate headers in the email
45 def new( name
, body
, conf
= DEFAULT_CONFIG
)
46 klass
= FNAME_TO_CLASS
[name
.downcase
] || UnstructuredHeader
47 klass
.newobj body
, conf
50 # Returns a HeaderField object matching the header you specify in the "name" param.
51 # Requires an initialized TMail::Port to be passed in.
53 # The method searches the header of the Port you pass into it to find a match on
54 # the header line you pass. Once a match is found, it will unwrap the matching line
55 # as needed to return an initialized HeaderField object.
57 # If you want to get the Envelope sender of the email object, pass in "EnvelopeSender",
58 # if you want the From address of the email itself, pass in 'From'.
60 # This is because a mailbox doesn't have the : after the From that designates the
61 # beginning of the envelope sender (which can be different to the from address of
64 # Other fields can be passed as normal, "Reply-To", "Received" etc.
66 # Note: Change of behaviour in 1.2.1 => returns nil if it does not find the specified
67 # header field, otherwise returns an instantiated object of the correct header class
70 # port = TMail::FilePort.new("/test/fixtures/raw_email_simple")
71 # h = TMail::HeaderField.new_from_port(port, "From")
72 # h.addrs.to_s #=> "Mikel Lindsaar <mikel@nowhere.com>"
73 # h = TMail::HeaderField.new_from_port(port, "EvelopeSender")
74 # h.addrs.to_s #=> "mike@anotherplace.com.au"
75 # h = TMail::HeaderField.new_from_port(port, "SomeWeirdHeaderField")
77 def new_from_port( port
, name
, conf
= DEFAULT_CONFIG
)
78 if name
== "EnvelopeSender"
80 re
= Regexp
.new('\A(From) ', 'i')
82 re
= Regexp
.new('\A(' + Regexp
.quote(name
) + '):', 'i')
87 if m
= re
.match(line
) then str
= m
.post_match
.strip
88 elsif str
and /\A[\t ]/ === line
then str
<< ' ' << line
.strip
89 elsif /\A-*\s*\z/ === line
then break
94 new(name
, str
, Config
.to_config(conf
)) if str
97 def internal_new( name
, conf
)
98 FNAME_TO_CLASS
[name
].newobj('', conf
, true)
103 def initialize( body
, conf
, intern
= false )
117 "#<#{self.class} #{@body.inspect}>"
126 return true if @illegal
141 def clear_parse_status
150 v
= Decoder
.new(s
= '')
161 include StrategyInterface
163 def accept( strategy
)
174 class UnstructuredHeader
< HeaderField
192 @body = Decoder
.decode(@body.gsub(/\n|\r\n|\r/, ''))
199 def do_accept( strategy
)
206 class StructuredHeader
< HeaderField
211 [Decoder
.decode(@comments[0])]
226 if not save
and mime_encoded
? @body
228 @body = Decoder
.decode(save
)
235 raise if @config.strict_parse
?
246 obj
= Parser
.parse(self.class::PARSE_TYPE, @body, @comments)
253 class DateTimeHeader
< StructuredHeader
255 PARSE_TYPE
= :DATETIME
281 def do_accept( strategy
)
282 strategy
.meta
time2str(@date)
288 class AddressHeader
< StructuredHeader
290 PARSE_TYPE
= :MADDRESS
311 def do_accept( strategy
)
323 @comments.each
do |c
|
334 class ReturnPathHeader
< AddressHeader
336 PARSE_TYPE
= :RETPATH
343 a
= addr() or return nil
348 a
= addr() or return nil
354 def do_accept( strategy
)
358 unless a
.routes
.empty
?
359 strategy
.meta a
.routes
.map
{|i
| '@' + i
}.join(',')
363 strategy
.meta spec
if spec
370 class SingleAddressHeader
< AddressHeader
378 def do_accept( strategy
)
381 @comments.each
do |c
|
392 class MessageIdHeader
< StructuredHeader
415 @id = @body.slice(MESSAGE_ID
) or
416 raise SyntaxError
, "wrong Message-ID format: #{@body}"
419 def do_accept( strategy
)
426 class ReferencesHeader
< StructuredHeader
434 self.refs
.each
do |i
|
435 yield i
if MESSAGE_ID
=== i
445 self.refs
.each
do |i
|
446 yield i
unless MESSAGE_ID
=== i
452 each_phrase
{|i
| ret
.push i
}
469 while m
= MESSAGE_ID
.match(str
)
470 pre
= m
.pre_match
.strip
471 @refs.push pre
unless pre
.empty
?
477 @refs.push str
unless str
.empty
?
480 def do_accept( strategy
)
495 class ReceivedHeader
< StructuredHeader
497 PARSE_TYPE
= :RECEIVED
567 @from = @by = @via = @with = @id = @_for = nil
573 @from, @by, @via, @with, @id, @_for, @date = *args
577 @with.empty
? and not (@from or @by or @via or @id or @_for or @date)
580 def do_accept( strategy
)
582 list
.push
'from ' + @from if @from
583 list
.push
'by ' + @by if @by
584 list
.push
'via ' + @via if @via
586 list
.push
'with ' + i
588 list
.push
'id ' + @id if @id
589 list
.push
'for <' + @_for + '>' if @_for
593 strategy
.space
unless first
600 strategy
.meta
time2str(@date)
607 class KeywordsHeader
< StructuredHeader
609 PARSE_TYPE
= :KEYWORDS
630 def do_accept( strategy
)
645 class EncryptedHeader
< StructuredHeader
647 PARSE_TYPE
= :ENCRYPTED
654 def encrypter
=( arg
)
677 @encrypter, @keyword = args
681 not (@encrypter or @keyword)
684 def do_accept( strategy
)
686 strategy
.meta
@encrypter + ','
688 strategy
.meta
@keyword
690 strategy
.meta
@encrypter
697 class MimeVersionHeader
< StructuredHeader
699 PARSE_TYPE
= :MIMEVERSION
722 sprintf('%d.%d', major
, minor
)
733 @major, @minor = *args
737 not (@major or @minor)
740 def do_accept( strategy
)
741 strategy
.meta
sprintf('%d.%d', @major, @minor)
747 class ContentTypeHeader
< StructuredHeader
756 def main_type
=( arg
)
773 @sub ? sprintf('%s/%s', @main, @sub) : @main
778 unless @params.blank
?
779 @params.each
do |k
, v
|
780 @params[k
] = unquote(v
)
788 @params and unquote(@params[key
])
793 (@params ||= {})[key
] = val
799 @main = @sub = @params = nil
803 @main, @sub, @params = *args
810 def do_accept( strategy
)
812 strategy
.meta
sprintf('%s/%s', @main, @sub)
816 @params.each
do |k
,v
|
820 strategy
.kv_pair k
, v
828 class ContentTransferEncodingHeader
< StructuredHeader
830 PARSE_TYPE
= :CENCODING
856 def do_accept( strategy
)
857 strategy
.meta
@encoding.capitalize
863 class ContentDispositionHeader
< StructuredHeader
865 PARSE_TYPE
= :CDISPOSITION
872 def disposition
=( str
)
874 @disposition = str
.downcase
879 unless @params.blank
?
880 @params.each
do |k
, v
|
881 @params[k
] = unquote(v
)
889 @params and unquote(@params[key
])
894 (@params ||= {})[key
] = val
900 @disposition = @params = nil
904 @disposition, @params = *args
908 not @disposition and (not @params or @params.empty
?)
911 def do_accept( strategy
)
912 strategy
.meta
@disposition
913 @params.each
do |k
,v
|
916 strategy
.kv_pair k
, unquote(v
)
923 class HeaderField
# redefine
926 'date' => DateTimeHeader
,
927 'resent-date' => DateTimeHeader
,
928 'to' => AddressHeader
,
929 'cc' => AddressHeader
,
930 'bcc' => AddressHeader
,
931 'from' => AddressHeader
,
932 'reply-to' => AddressHeader
,
933 'resent-to' => AddressHeader
,
934 'resent-cc' => AddressHeader
,
935 'resent-bcc' => AddressHeader
,
936 'resent-from' => AddressHeader
,
937 'resent-reply-to' => AddressHeader
,
938 'sender' => SingleAddressHeader
,
939 'resent-sender' => SingleAddressHeader
,
940 'return-path' => ReturnPathHeader
,
941 'message-id' => MessageIdHeader
,
942 'resent-message-id' => MessageIdHeader
,
943 'in-reply-to' => ReferencesHeader
,
944 'received' => ReceivedHeader
,
945 'references' => ReferencesHeader
,
946 'keywords' => KeywordsHeader
,
947 'encrypted' => EncryptedHeader
,
948 'mime-version' => MimeVersionHeader
,
949 'content-type' => ContentTypeHeader
,
950 'content-transfer-encoding' => ContentTransferEncodingHeader
,
951 'content-disposition' => ContentDispositionHeader
,
952 'content-id' => MessageIdHeader
,
953 'subject' => UnstructuredHeader
,
954 'comments' => UnstructuredHeader
,
955 'content-description' => UnstructuredHeader