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