tmail/tmail/field.rb
#
# field.rb
#
# Copyright (c) 1998-1999 Minero Aoki <aamine@dp.u-netsurf.ne.jp>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'delegate'
require 'amstd/to_s'
require 'tmail/mailp'
require 'tmail/encode'
module TMail
MSGID = /<[^\@>]+\@[^>\@]+>/o
ZONESTR_TABLE = {
'jst' => 9 * 60,
'eet' => 2 * 60,
'bst' => 1 * 60,
'met' => 1 * 60,
'gmt' => 0,
'utc' => 0,
'ut' => 0,
'nst' => -(3 * 60 + 30),
'ast' => -4 * 60,
'edt' => -4 * 60,
'est' => -5 * 60,
'cdt' => -5 * 60,
'cst' => -6 * 60,
'mdt' => -6 * 60,
'mst' => -7 * 60,
'pdt' => -7 * 60,
'pst' => -8 * 60,
'a' => -1 * 60,
'b' => -2 * 60,
'c' => -3 * 60,
'd' => -4 * 60,
'e' => -5 * 60,
'f' => -6 * 60,
'g' => -7 * 60,
'h' => -8 * 60,
'i' => -9 * 60,
# j not use
'k' => -10 * 60,
'l' => -11 * 60,
'm' => -12 * 60,
'n' => 1 * 60,
'o' => 2 * 60,
'p' => 3 * 60,
'q' => 4 * 60,
'r' => 5 * 60,
's' => 6 * 60,
't' => 7 * 60,
'u' => 8 * 60,
'v' => 9 * 60,
'w' => 10 * 60,
'x' => 11 * 60,
'y' => 12 * 60,
'z' => 0 * 60
}
TSPECIAL = %_()<>@,.;:\\"/[]?=_
CONTROL = "\\\x00-\\\x20\\\x7f-\\\xff"
INSECURE = /[#{Regexp.quote TSPECIAL}#{CONTROL}]/o
class << self
def msgid?( str )
MSGID === str
end
def zonestr2i( str )
if /([\+\-])(\d\d?)(\d\d)/ === str then
minu = $2.to_i * 60 + $3.to_i
$1 == '-' ? -minu : minu
else
unless tmp = ZONESTR_TABLE[ str.downcase ] then
raise ParseError, "wrong timezone format '#{str}'"
end
tmp * 60
end
end
def time2str( time )
ret = time.strftime( "%a, #{time.mday} %b %Y %X " )
# Time#gmtime changes self!!!
tm = Time.at( time.to_i )
# [ruby-list:7928]
offset = tm.to_i - Time.mktime( *tm.gmtime.to_a[0, 6].reverse ).to_i
ret << '%+.2d%.2d' % (offset / 60).divmod( 60 )
ret
end
def quote( str )
if INSECURE === str then
%("#{str}")
else
str
end
end
# internal use
def define_header_to_s( mod, eoptarg, doptarg )
mod.module_eval %^
def encoded( eol = "\r\n", charset = 'j',
ret = '' #{eoptarg ? ',' + eoptarg : ''} )
ret = ''
v = ::TMail::HFencoder.new( ret, eol, charset, limit )
accept v
v.write_in
ret
end
alias to_s encoded
def decoded( eol = "\n", charset = 'e',
ret = '' #{doptarg ? ',' + doptarg : ''} )
ret = ''
v = ::TMail::HFdecoder.new( ret, charset )
accept v
v.write_in
ret
end
alias inspect decoded
^
end
end # class << self
class HeaderField
def initialize( fname, fbody, strict )
@name = fname
fbody.strip!
@body = fbody
@strict = strict
@parsed = false
@parsing = false
@written = false
end
R = (1 << 0) # readable
W = (1 << 1) # writable
NODUP = (1 << 2) # dup?
class << self
alias hf_original_new new
def new( fname, fbody, strict = false )
if self == HeaderField then
tmp = fname.downcase
klass = if /\Ax-/ === tmp
then StringH
else STR2CLASS[tmp] || UnknownH
end
klass.hf_original_new( fname, fbody, strict )
else
hf_original_new( fname, fbody, strict )
end
end
def parse_on_rw
@parse_on_rw = true
end
def parse_on_create
@parse_on_rw = false
module_eval %^
def initialize( fname, fbody, strict )
super
parse
end
^
end
def header_attr( name, type, flags, ali = nil )
name = _name2str( name )
type = _type2str( type )
r = (flags & R != 0)
w = (flags & W != 0)
c = (flags & NODUP != 0)
if r then
parse = if @parse_on_rw then
%-
unless @parsed or @parsing then
parse
end
-
else
''
end
write = unless w then
'@written = true'
else
''
end
retrn = if c then
"@#{name}"
else
"@#{name} and @#{name}.dup"
end
module_eval sprintf( %-
def #{name}
%s
%s
%s
end
-, parse, write, retrn )
end
if w then
parse = if @parse_on_rw then
%-
unless @parsed or @parsing then
parse
@written = true
end
-
else
%-
unless @parsing then
@written = true
end
-
end
module_eval sprintf( %-
def #{name}=( arg )
%s
@#{name} = arg
end
-, parse )
end
if ali then
ali.each do |a|
na = _name2str( a )
module_eval %(alias #{na} #{name})
module_eval %(alias #{na}= #{name}=) if w
end
end
end
end
# abstract parse
def hash
@name.hash
end
def eql?( oth )
self.type === oth and @name == oth.name
end
def ==( oth ) # don't alias
eql? oth
end
def name
@name.dup
end
def body
ret = ''
v = HFdecoder.new( ret )
if @written then
do_accept v
else
v.header_body @body
end
v.write_in
ret
end
def accept( visitor )
visitor.header_name @name
unless @written then
visitor.header_body @body
else
do_accept visitor
end
end
::TMail.define_header_to_s self, 'limit = 72', nil
def do_accept( visitor )
visitor.header_body body
end
end
# string type ---------------------------------------------
class StringH < HeaderField
def initialize( fname, fbody, strict )
super
@decoded = nil
@written = true
end
def body
unless @decoded then
@decoded = HFdecoder.decode( @body )
end
@decoded.dup
end
def body=( str )
@body = nil
@decoded = str
end
def eql?( oth )
super and
body == oth.body
end
def do_accept( visitor )
visitor.text @body || @decoded
end
end
# struct type -----------------------------------------
class StructH < HeaderField
parse_on_rw
header_attr :comments, Array, R | NODUP
private
def init
@comments = []
end
def parse
@parsing = true
init
if @strict then
Mailp.parse( @body, self, nil )
else
begin
Mailp.parse( @body, self, nil )
rescue ParseError, ScanError
;
end
end
@parsing = false
@parsed = true
end
end
# unknown type ----------------------------------------
class UnknownH < StructH
def initialize( fname, fbody, strict )
super
@decoded = nil
@written = true
end
def body
unless @decoded then
@decoded = Bencode.decode( @body )
end
@decoded.dup
end
def body=( str )
@body = nil
@decoded = str
end
def eql?( oth )
super and
body == oth.body
end
private
def parse
end
def do_accept( visitor )
visitor.text @body || @decoded
end
end
# date type -------------------------------------------
class DateH < StructH
parse_on_rw
header_attr :date, Time, R | W | NODUP
def date=( arg )
@date = arg.localtime
@written = true
end
def eql?( oth )
super and
@date == oth.date
end
def do_accept( visitor )
visitor.meta ::TMail.time2str( @date )
end
end
# return path type -------------------------------------
class RetpathH < StructH
parse_on_rw
header_attr :route, Array, R | NODUP, [:routes]
header_attr :addr, String, R | W
def eql?( oth )
super and
self.addr == oth.addr and
self.routes == oth.routes
end
private
def init
super
@route = []
end
def do_accept( visitor )
visitor.meta '<'
unless @route.empty? then
last = @routes[-1]
@route.each do |i|
s = '@' + i
s << ',' unless i.equal? last
visitor.meta
end
visitor.meta ':'
end
visitor.meta @addr
visitor.meta '>'
end
end
# address classes ---------------------------------------------
class Address
def initialize( local, domain )
@local = local
@domain = domain
@phrase = nil
@route = []
end
attr :phrase, true
attr :route
alias routes route
attr_writer :route # internal use only
def address
s = @local.collect {|i| ::TMail.quote i }.join('.')
s << '@' << @domain.collect {|i| ::TMail.quote i }.join('.') if @domain
s
end
def address=( str )
tmp = str.split( '@', 2 )
@local = tmp[0].split('.')
@domain = tmp[1].split('.')
end
alias spec address
alias spec= address=
alias addr address
alias addr= address=
def eql?( other )
self.type === other and
addr == other.addr and
route == other.route and
phrase == other.phrase
end
alias == eql?
def dup
i = nil
n = type.new( @local, @domain )
n.phrase = @phrase.dup if @phrase
n.route = @routes.collect{|i| i.dup } unless @routes.empty?
n
end
def accept( visitor )
spec_p = !@phrase and @route.empty?
if @phrase then
visitor.text @phrase
visitor.spc
end
tmp = spec_p ? '' : '<'
unless @route.empty? then
tmp << @route.collect {|i| '@' + i }.join( ',' ) << ':'
end
tmp << address
tmp << '>' unless spec_p
visitor.meta tmp
end
::TMail.define_header_to_s self, nil, nil
end
class AddressGroup < DelegateClass( Array )
def initialize( name, arg = nil )
@name = name
super arg ? arg.dup : []
end
attr :name, true
def eql?( oth )
super( oth ) and self.name == oth.name
end
alias == eql?
def each_address
bare_each do |mbox|
if AddressGroup === mbox then
mbox.each {|i| yield i }
else
yield mbox
end
end
end
def accept( visitor )
visitor.text @name
visitor.meta ':'
visitor.spc
last = self[-1]
bare_each do |mbox|
mbox.accept visitor
visitor.meta mbox.equal?(last) ? ';' : ','
end
end
::TMail.define_header_to_s self, nil, nil
end
# saddr type -------------------------------------------
class SaddrH < StructH
parse_on_rw
header_attr :addr, Address, R | W
def eql?( oth )
super and
self.addr == oth.addr
end
def do_accept( visitor )
@addr.accept visitor
visitor.comments @comments unless @comments.empty?
end
end
class SmboxH < SaddrH
end
# maddr type -----------------------------------------
class MaddrH < StructH
parse_on_rw
header_attr :addrs, Array, R | NODUP
def eql?( oth )
super and
addrs == oth.addrs
end
private
def init
super
@addrs = []
end
def do_accept( visitor )
first = true
last = @addrs[-1]
@addrs.each do |a|
if first then
first = false
else
visitor.spc
end
a.accept visitor
visitor.meta ',' unless a.equal? last
end
unless @comments.empty? then
visitor.spc
@comments.each do |c|
visitor.meta '('
visitor.text c
visitor.meta ')'
end
end
end
end
class MmboxH < MaddrH
=begin
def do_accept( visitor )
first = true
last = @addrs[-1]
end
=end
end
# ref type -------------------------------------------
class RefH < StructH
parse_on_rw
header_attr :refs, Array, R | NODUP
def eql?( oth )
super and
self.refs == oth.refs
end
def each_msgid
refs.each do |i|
yield i if ::TMail.msgid? i
end
end
def each_phrase
refs.each do |i|
yield i unless ::TMail.msgid? i
end
end
private
def init
super
@refs = []
end
def do_accept( visitor )
first = true
@refs.each do |i|
if first then
first = false
else
visitor.spc
end
if ::TMail.msgid? i then
visitor.meta i
else
visitor.text i
end
end
end
end
# key type -------------------------------------------
class KeyH < StructH
parse_on_rw
header_attr :keys, Array, R | NODUP
def eql?( oth )
super and
self.keys == oth.keys
end
private
def init
super
@keys = []
end
def do_accept( visitor )
save = @keys.pop
@keys.each do |i|
visitor.meta i + ','
end
visitor.meta save
@keys.push save
end
end
# received type ---------------------------------------
class RecvH < StructH
parse_on_rw
header_attr :from, String, R | W
header_attr :by, String, R | W
header_attr :via, String, R | W
header_attr :with, Array, R | NODUP
header_attr :msgid, String, R | W
header_attr :for_, String, R | W, [:ford, :for_domain]
header_attr :date, Time, R | W | NODUP
def eql?( oth )
super and
from == oth.from and
by == oth.by and
via == oth.via and
with == oth.with and
msgid == oth.msgid and
for_ == oth.for_ and
date == oth.date
end
private
def init
super
@with = []
end
Tag = %w( from by via with id for )
def do_accept( visitor )
val = [ @from, @by, @via ] + @with + [ @msgid, @for_ ]
val.compact!
c = [ @from ? 1 : 0, @by ? 1 : 0, @via ? 1 : 0,
@with.size, @msgid ? 1 : 0, @for_ ? 1 : 0 ]
i = 0
c.each_with_index do |count, idx|
label = Tag[idx]
count.times do
visitor.spc unless i == 0
visitor.meta label
visitor.spc
visitor.meta val[i]
i += 1
end
end
if @date then
visitor.meta ';'
visitor.spc
visitor.meta ::TMail.time2str( @date )
end
end
end
# message-id type ----------------------------------------------------
class MsgidH < StructH
parse_on_rw
def initialize( fname, fbody, strict )
super
@msgid = fbody.strip
end
header_attr :msgid, String, R | W
def eql?( oth )
super and
msgid == oth.msgid
end
def do_accept( visitor )
visitor.meta @msgid
end
end
# encrypted type ------------------------------------------------------
class EncH < StructH
parse_on_rw
header_attr :encrypter, String, R | W
header_attr :keyword, String, R | W
def eql?( oth )
super and
self.encrypter == oth.encrypter and
self.keyword == oth.keyword
end
def init
super
@keyword = nil
end
def do_accept( visitor )
if @key then
visitor.meta @encrypter + ','
visitor.spc
visitor.meta @key
else
visitor.meta @encrypter
end
end
end
# version type -----------------------------------------
class VersionH < StructH
parse_on_rw
header_attr :major, :Integer, R | W | NODUP
header_attr :minor, :Integer, R | W | NODUP
def version
sprintf( '%d.%d', major, minor )
end
def eql?( oth )
super and
self.major == oth.major and
self.minor == oth.minor
end
def do_accept( visitor )
visitor.meta "#{@major}.#{@minor}"
end
end
# content type ---------------------------------------
class CTypeH < StructH
parse_on_create
header_attr :main, :String, R | W
header_attr :sub, :String, R | W
header_attr :params, :Hash, R | NODUP
def []( key )
params[key]
end
def eql?( oth )
super and
main == oth.main and
sub == oth.sub and
params == oth.params
end
private
def init
super
@params = {}
end
def do_accept( visitor )
visitor.meta "#{@main}/#{@sub}"
@params.each do |k,v|
visitor.meta ';'
visitor.spc
visitor.meta k
visitor.meta '='
visitor.text v
end
end
end
# encoding type ----------------------------
class CEncodingH < StructH
parse_on_rw
header_attr :encoding, :String, R | W
def eql?( oth )
super and
encoding == oth.encoding
end
def do_accept( visitor )
visitor.meta @encoding
end
end
# disposition type ----------------------------------
class CDispositionH < StructH
parse_on_rw
header_attr :disposition, :String, R | W
header_attr :params, :Hash, R | NODUP
def []( key )
params[key]
end
def eql?( oth )
super and
self.disposition == oth.disposition and
self.params == oth.params
end
private
def init
super
@params = {}
end
def do_accept( visitor )
visitor.meta @disposition
@params.each do |k,v|
visitor.meta ';'
visitor.spc
visitor.meta k
visitor.meta '='
visitor.text v
end
end
end
# ---------------------------------------------------
class HeaderField # backward definition
STR2CLASS = {
'date' => DateH,
'resent-date' => DateH,
'received' => RecvH,
'return-path' => RetpathH,
'sender' => SaddrH,
'resent-sender' => SaddrH,
'to' => MaddrH,
'cc' => MaddrH,
'bcc' => MaddrH,
'from' => MmboxH,
'reply-to' => MaddrH,
'resent-to' => MaddrH,
'resent-cc' => MaddrH,
'resent-bcc' => MaddrH,
'resent-from' => MmboxH,
'resent-reply-to' => MaddrH,
'message-id' => MsgidH,
'resent-message-id' => MsgidH,
'content-id' => MsgidH,
'in-reply-to' => RefH,
'references' => RefH,
'keywords' => KeyH,
'encrypted' => EncH,
'mime-version' => VersionH,
'content-type' => CTypeH,
'content-transfer-encoding' => CEncodingH,
'content-disposition' => CDispositionH,
'subject' => StringH,
'comments' => StringH,
'content-description' => StringH
}
end
end # module TMail