tmail/tmail/tmail.rb
#
# tmail.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 'nkf'
require 'amstd/to_s'
require 'amstd/bug'
require 'amstd/must'
require 'tmail/field'
require 'tmail/port'
require 'tmail/loader'
require 'tmail/encode'
class MailError < StandardError ; end
module TMail
Version = '0.8.0'
class Mail
class << self
def loadfrom( fname )
new FilePort.new( fname )
end
def boundary
'mimepart_' + random_tag
end
def msgid
if host = ENV['HOSTNAME'] then
"<#{random_tag}@#{host}>"
else
"<#{random_tag}@tmail.on.ruby>"
end
end
private
def random_tag
ret = ('%x' % Time.now.strftime('%Y%m%d%H%M%S').to_i) + '_'
1.upto(8){ srand ; ret << ('%x' % rand(255)) }
ret
end
end
def initialize( port, strict = false )
@port = port
@strict = strict
@header = {}
@body_port = nil
@epilogue = ''
@parts = []
parse_header
end
attr :port
### body
def body_port
parse_body
@body_port
end
def each
body_port.ropen do |f|
f.each {|line| yield line }
end
end
def body
parse_body
ret = nil
@body_port.ropen {|is| ret = is.readall }
ret
end
def body=( str )
parse_body
@body_port.wopen {|os| os.write str }
true
end
alias preamble body
alias preamble= body=
def epilogue
parse_body
@epilogue.dup
end
def epilogue=( str )
parse_body
@epilogue = str
str
end
def parts
parse_body
@parts
end
### all
class << self
end
def encoded( eol = "\r\n", charset = 'j', ret = '', sep = '' )
visitor = HFencoder.new( ret, eol, charset )
accept visitor, eol, ret, sep
ret
end
alias to_s encoded
def decoded( eol = "\n", charset = 'e', ret = '', sep = '' )
visitor = HFdecoder.new( ret, charset )
accept visitor, eol, ret, sep
ret
end
alias inspect decoded
def accept( visitor, eol = "\n", ret = '', sep = '' )
multipart = ! @parts.empty?
if multipart then
bound = Mail.boundary
h = self['content-type']
if h then
h.params['boundary'] = bound
else
store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
end
end
each_header do |name, header|
header.accept visitor
visitor.write_in
ret << eol
end
ret << sep << eol
body_port.ropen do |is|
is.each do |line|
line.sub!( /[\r\n]+\z/, '' )
ret << line << eol
end
end
if multipart then
boundary = "#{eol}--#{bound}#{eol}"
parts.each do |tm|
ret << boundary
tm.accept( visitor, eol, ret, sep )
end
ret << "#{eol}--#{bound}--#{eol}"
s = epilogue
ret << s
case s[-1] when ?\n then; when ?\r then; else
ret << eol
end
end
end
def write_back # tmp
@port.wopen {|os| encoded os }
true
end
### header
USE_ARRAY = [ 'received' ]
def header
@header.dup
end
def fetch( key, initbody = nil, &block )
dkey = key.downcase
ret = @header[ dkey ]
unless ret then
if iterator? then
initbody = yield
end
if initbody then
ret = mkhf( *ret.split(':', 2) )
store dkey, ret
end
end
ret
end
alias [] fetch
def store( key, val )
dkey = key.downcase
if val.nil? then
@header.delete dkey
return
end
case val
when String
val = mkhf( key, val )
when HeaderField
;
when Array
unless USE_ARRAY.include? dkey then
raise ArgumentError, "#{key}: Header must not be multiple"
end
@header[dkey] = val
return
else
val.must HeaderField, String, Array
end
if USE_ARRAY.include? dkey then
if arr = @header[dkey] then
arr.push val
else
@header[dkey] = [val]
end
else
@header[dkey] = val
end
end
alias []= store
def each_header
@header.each do |k,v|
if Array === v then
v.each{|i| yield k, i }
else
yield k, v
end
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.each_key do |key|
v = fetch( key )
if Array === v then
v.each( &block )
else
yield v
end
end
end
alias each_value each_field
def direct_fetch( key ) # undoc
@header[key.downcase]
end
def direct_store( key, val ) # undoc
@header[key.downcase] = val
end
def clear
@header.clear
end
def delete( key )
@header.delete key.downcase
end
def delete_if
@header.delete_if do |key,v|
v = fetch( key )
if Array === v then
v.delete_if{|f| yield key, f }
v.empty?
else
yield key, v
end
end
end
def keys
@header.keys
end
def has_key?( key )
@header.has_key? key.downcase
end
alias include? has_key?
alias key? has_key?
def values
ret = []
each_field {|v| ret.push v }
ret
end
def has_value?( val )
return false unless HeaderField === val
v = fetch( val.name )
if Array === v then v.include? val
else v ? (val == v) : false
end
end
alias value? has_value?
def indexes( *args )
ret = []
temp = nil
args.each do |k|
case temp = fetch(k)
when Array
ret.concat temp
else
ret.push temp
end
end
return ret
end
alias indices indexes
private
def mkhf( fname, fbody )
fname.strip!
HeaderField.new( fname, fbody, @strict )
end
def parse_header
fname = fbody = nil
errlog = []
src = @stream = @port.ropen
while line = src.gets do # no each !
case line
when /\A[ \t]/ # continue from prev line
unless fbody then
errlog.push 'mail is began by space or tab'
next
end
# line.strip!
# fbody << ' ' << line
fbody << line
when /\A([^\: \t]+):\s*/ # new header line
add_hf fname, fbody if fbody
fname = $1
fbody = $'
# fbody.strip!
when /\A\-*\s*\z/ # end of header
add_hf fname, fbody if fbody
break
else
errlog.push "wrong mail header: '#{line.inspect}'"
end
end
unless errlog.empty? then
raise ParseError, "\n" + errlog.join("\n")
end
src.stop
end
def add_hf( fname, fbody )
key = fname.downcase
hf = mkhf( fname, fbody )
if USE_ARRAY.include? key then
if tmp = @header[key] then
tmp.push hf
else
@header[key] = [hf]
end
else
@header[key] = hf
end
end
def parse_body
if @stream then
parse_body_in @stream
@stream = nil
end
end
def parse_body_in( stream )
begin
stream.restart
if multipart? then
read_multipart stream
else
@body_port = tmp_port
@body_port.wopen do |f|
stream.copy_to f
end
end
ensure
stream.close unless stream.closed?
end
end
def read_multipart( src )
bound = self['content-type'].params['boundary']
is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
lastbound = "--#{bound}--"
f = nil
n = 1
begin
ports = []
ports.push tmp_port
f = ports[-1].wopen
while line = src.gets do # no each !
if is_sep === line then
f.close
line.strip!
if line == lastbound then
break
else
ports.push tmp_port n
n += 1
f = ports[-1].wopen
end
else
f << line
end
end
@epilogue = src.readall
ensure
f.close if f and not f.closed?
end
@body_port = ports.shift
ports.filter {|p| Mail.new( p ) }
@parts = ports
end
def tmp_port( n = 0 )
StringPort.new
end
######
public
class << self
def tmail_attr( mname, hname, part )
module_eval %-
def #{mname.id2name}( default = nil )
header = self['#{hname}']
if header then
header.#{part}
else
default
end
end
-
end
end
tmail_attr :date, 'date', 'date'
tmail_attr :to_addrs, 'to', 'addrs'
tmail_attr :cc_addrs, 'cc', 'addrs'
tmail_attr :bcc_addrs, 'bcc', 'addrs'
def to( dflt = '' )
to_addrs[0] or dflt
end
def to=( addr )
store 'To', addr
end
def destinations
ret = []
%w( to cc bcc ).each do |hname|
if hed = self[ hname ] then
ret.concat hed.addrs
end
end
ret
end
tmail_attr :from_addrs, 'from', 'addrs'
def from( dflt = '' )
tmp = from_addrs[0]
tmp ? tmp.addr : dflt
end
def from_phrase( dflt = '' )
tmp = from_addrs[0]
tmp ? tmp.phrase : dflt
end
def from=( addr )
store 'From', addr
end
tmail_attr :subject, 'subject', 'body'
def subject=( str )
store 'Subject', str
end
tmail_attr :msgid, 'message-id', 'msgid'
def msgid=( str )
store 'Message-ID', str
end
tmail_attr :main_type, 'content-type', 'main'
tmail_attr :sub_type, 'content-type', 'sub'
def content_type=( str )
header = self['content-type']
if header then
header.main, header.sub = str.split('/', 2)
else
store 'Content-Type', str
end
end
def set_content_type( main, sub, param = nil )
if hed = self['content-type'] then
hed.main = main
hed.sub = sub
else
store 'Content-Type', main + '/' + sub
if param then
self['content-type'].params.replace param
end
end
end
tmail_attr :charset, 'content-type', "params['charset']"
def charset=( str )
if hed = self[ 'content-type' ] then
hed.params.store 'charset', str
else
store "text/plain ; charset=#{str}"
end
end
tmail_attr :encoding, 'content-transfer-encoding', 'encoding'
def encoding=( str )
store 'Content-Transfer-Encoding', str
end
def each_dest( &block )
destinations.each do |i|
if Address === i then
yield i
else
i.each &block
end
end
end
def multipart?
main_type('').downcase == 'multipart'
end
end # class TMail::Mail
end # module TMail