# PERCENT_HACK を定義してから require すると '%' を解釈します. module RFC822_staff # CHAR = ; ( 0-177, 0.-127.) # CTL = ; ( 177, 127.) # SPACE = ; ( 40, 32.) # field = field-name ":" [ field-body ] CRLF # field-name = 1* # field-body = field-body-contents # [CRLF LWSP-char field-body] # field-body-contents = # # (":" = \072) # HEADER_REGEX = /^([\041-\071\073-\176]+):\s*([^\r\n]*)(\r\n|\n|\r)$/ HEADER_REGEX = /^(\w+):\s?(.*)$/ FOLDED_HEADER_REGEX = /^[ \t]+([^\r\n]*)(\r\n|\n|\r)$/ # LWSP-char = SPACE / HTAB ; semantics = SPACE # linear-white-space = 1*([CRLF] LWSP-char) ; semantics = SPACE # ; CRLF => folding LINEAR_WHITE_SPACE = ["\r\n", "\r", "\n", " +", "\t+"] # specials = "(" / ")" / "<" / ">" / "@" ; Must be in quoted- # / "," / ";" / ":" / "\" / <"> ; string, to use # / "." / "[" / "]" ; within a word. if defined?(PERCENT_HACK) && PERCENT_HACK SPECIALS = ['\(', '\)', '<', '>', '@', ',', ';', ':', '\\\\', '"', '\.', '\[', '\]', '%'] else SPECIALS = ['\(', '\)', '<', '>', '@', ',', ';', ':', '\\\\', '"', '\.', '\[', '\]'] end # delimiters = specials / linear-white-space / comment DELIMITERS = SPECIALS + LINEAR_WHITE_SPACE SPECIALS_REGEXP = /(#{SPECIALS.join("|")})/ SPECIALS_REGEXPs = /^(#{SPECIALS.join("|")})$/ DELIMITERS_REGEXP = /(#{DELIMITERS.join("|")})/ DELIMITERS_REGEXPs = /^(#{DELIMITERS.join("|")})$/ LINEAR_WHITE_SPACE_REGEXP = /(#{LINEAR_WHITE_SPACE.join("|")})/ LINEAR_WHITE_SPACE_REGEXPs = /^(#{LINEAR_WHITE_SPACE.join("|")})$/ # unfold 済みの文字列を delimiters で分割した後で, # comment, quoted-string, quoted-pair をひとまとめにした配列を作る. def delimit(structured_string) t = structured_string.split(DELIMITERS_REGEXP) t.delete('') # comment = "(" *(ctext / quoted-pair / comment) ")" # ctext = may be folded # ")", "\" & CR, & including # linear-white-space> # quoted-string = <"> *(qtext/quoted-pair) <">; Regular qtext or # ; quoted chars. # qtext = , ; => may be folded # "\" & CR, and including # linear-white-space> # quoted-pair = "\" CHAR ; may quote any char comment = 0 quoted = 0 escape = 0 delimited = [''] # この '' はダミーデータ fifo = ['']*25 t.each {|x| fifo.shift; fifo.push(x) if comment == 0 && quoted == 0 && escape == 0 delimited << '' end delimited[-1] << x if escape > 0 escape -= 1 elsif x == '\\' escape += 1 if delimited[-2] !~ DELIMITERS_REGEXPs t = delimited.pop delimited[-1] << t end else if comment > 0 comment -= 1 if x == ')' comment += 1 if x == '(' elsif quoted > 0 quoted -= 1 if x == '"' else case x when '(' # comment の始まり. comment += 1 when ')' # ")" が余ってる :-( parse_error("unbalanced `)'", fifo) when '"' # quoted-string の始まり. quoted += 1 when DELIMITERS_REGEXP # その他の delimiters …何もしない else # その他の文字(列) if delimited[-2] !~ DELIMITERS_REGEXPs t = delimited.pop delimited[-1] << t end end end end } if escape > 0 || quoted > 0 # "\" または <"> が余っている :-( if quoted > 0 delimited.pop while delimited[-1] != '"' end if delimited.size > 25 parse_error(delimited[delimited.size - 25, 25]) else parse_error(delimited) end end return delimited end module_function :delimit # unfold 済みの文字列からコメントを取り除く. # コメントを取り除いたあとの文字列を返す. def uncomment(structured_string) t = delimit(structured_string) t.delete_if {|x| /^\(.*\)$/o =~ x } return t.join('') end module_function :uncomment # day = "Mon" / "Tue" / "Wed" / "Thu" # / "Fri" / "Sat" / "Sun" WDAY = {'Mon' => 1, 'Tue' => 2, 'Wed' => 3, 'Thu' => 4, 'Fri' => 5, 'Sat' => 6, 'Sun' => 0} # month = "Jan" / "Feb" / "Mar" / "Apr" # / "May" / "Jun" / "Jul" / "Aug" # / "Sep" / "Oct" / "Nov" / "Dec" MONTH = {'Jan' => 1, 'Feb' => 2, 'Mar' => 3, 'Apr' => 4, 'May' => 5, 'Jun' => 6, 'Jul' => 7, 'Aug' => 8, 'Sep' => 9, 'Oct' => 10, 'Nov' => 11, 'Dec' => 12} # zone = "UT" / "GMT" ; Universal Time # ; North American : UT # / "EST" / "EDT" ; Eastern: - 5/ - 4 # / "CST" / "CDT" ; Central: - 6/ - 5 # / "MST" / "MDT" ; Mountain: - 7/ - 6 # / "PST" / "PDT" ; Pacific: - 8/ - 7 # / 1ALPHA ; Military: Z = UT; # ; A:-1; (J not used) # ; M:-12; N:+1; Y:+12 # / ( ("+" / "-") 4DIGIT ) ; Local differential # ; hours+min. (HHMM) TZ = {'UT' => 0, 'GMT' => 0, 'EST' => -5, 'EDT' => -4, 'CST' => -6, 'CDT' => -5, 'MST' => -7, 'MDT' => -6, 'PST' => -8, 'PDT' => -7, 'JST' => 9, # ホントはダメなんだけどね 'A' => -1, 'B' => -2, 'C' => -3, 'D' => -4, 'E' => -5, 'F' => -6, 'G' => -7, 'H' => -8, 'I' => -9, 'K' => -10, 'L' => -11, 'M' => -12, 'N' => 1, 'O' => 2, 'P' => 3, 'Q' => 4, 'R' => 5, 'S' => 6, 'T' => 7, 'U' => 8, 'V' => 9, 'W' => 10, 'X' => 11, 'Y' => 12, 'Z' => 0} # date-time = [ day "," ] date time ; dd mm yy # ; hh:mm:ss zzz # date = 1*2DIGIT month 2DIGIT ; day month year # ; e.g. 20 Jun 82 # time = hour zone ; ANSI and Military # hour = 2DIGIT ":" 2DIGIT [":" 2DIGIT] # ; 00:00:00 - 23:59:59 DATE_REGEXP = /^\s*((#{WDAY.keys.join('|')}), +)?(\d{1,2}) +(#{MONTH.keys.join('|')}) +(\d{2,4}) +(\d{1,2}):(\d{1,2})(:(\d{1,2}))?( +(#{TZ.keys.join('|')}|[-+]\d{3,4}))?\s*$/ # これ(↑)は zone がない不正な Date: ヘッダに対応したもの. # 厳密にはこうすべき(↓) # DATE_REGEXP = /^\s*((#{WDAY.keys.join('|')}), +)?(\d{1,2}) +(#{MONTH.keys.join('|')}) +(\d{2,4}) +(\d{1,2}):(\d{1,2})(:(\d{1,2}))? +(#{TZ.keys.join('|')}|[-+]\d{3,4})\s*$/ # ctime 形式(Wdy Mon DD HH:MM:SS YYYY) CTIME_REGEXP = /^\s*(#{WDAY.keys.join('|')}) +(#{MONTH.keys.join('|')}) +(\d{1,2}) +(\d{1,2}):(\d{1,2}):(\d{1,2}) +(\d{2,4})\s*$/ # Date: ヘッダ形式の文字列を元にして # Time オブジェクトを生成する. def date_to_time(date_string) case uncomment(date_string) when DATE_REGEXP wday = WDAY[$2.capitalize] if $2 mday = $3.to_i month = MONTH[$4.capitalize] year = $5.to_i hour = $6.to_i min = $7.to_i sec = $9.to_i if $9 sec = 0 unless sec tz = $11 t = Time.gm(year, month, mday, hour, min, sec) when CTIME_REGEXP wday = WDAY[$1.capitalize] month = $2.to_i mday = $3.to_i hour = $4.to_i min = $5.to_i sec = $6.to_i year = $7.to_i tz = nil t = Time.local(year, month, mday, hour, min, sec) else # エラーなんだけど… return nil end d = 0 if tz if /^([-+])(\d{1,2})(\d{2})$/o =~ tz d = "#{$1}1".to_i*(($2.to_i*3600) + $3.to_i*60) elsif TZ.key?(tz) d = TZ[tz]*60*60 else # ホントはエラーなんだけど GMT として扱っちゃう, か. end end return t - d end module_function :date_to_time # 致命的な文法エラー(例外)を起こす. # fifo は delimit 済みの配列で # その配列における start_pos .. end_pos に # エラー位置を意味する '^' を負荷したメッセージを生成する. def parse_error(message, fifo, start_pos = -1, end_pos = -1) pos_str = fifo.join() fifo = fifo.collect {|x| ' '*x.size} for i in start_pos .. end_pos fifo[i] = '^'*fifo[i].size end pos_arrow = fifo.join() raise(message + ":\n\t\t" + pos_str + "\n\t\t" + pos_arrow) end private :parse_error # mailbox 中のトークンの属性値(class Mailbox などで使用する) ATTR_ROUTE = 0x0001 ATTR_LOCAL = 0x0002 ATTR_AT = 0x0004 ATTR_DOMAIN = 0x0008 ATTR_ROUTE_BEGIN = 0x0010 # @ ATTR_ROUTE_END = 0x0020 # : ATTR_ADDR_BEGIN = 0x0040 # < ATTR_ADDR_END = 0x0080 # > ATTR_PHRASE = 0x0100 ATTR_COMMENT = 0x0200 ATTR_LWSP = 0x0400 ATTR_DELIMITER = 0x1000 ATTR_GROUP_BEGIN = 0x2000 # : ATTR_GROUP_END = 0x4000 # ; ATTR_UNKNOWN = 0x0000 ATTR_ADDRESS = ATTR_ROUTE | ATTR_LOCAL | ATTR_AT | ATTR_DOMAIN | ATTR_ROUTE_BEGIN | ATTR_ROUTE_END ATTR_IGNORE_CASE = ATTR_DOMAIN | ATTR_ROUTE #!!! debug COLOR_LOCAL = '' # local-part COLOR_DOMAIN = '' # domain-part COLOR_ROUTE = '' # route COLOR_ROUTE_BE = '' # beginning/end of route COLOR_ADDR_BE = '' # beginning/end of route-addr COLOR_GROUP_BE = '' # beginning/end of group COLOR_PHRASE = '' # phrase COLOR_COMMENT = '' # comment COLOR_ELSE = '' # others COLOR_UNKNOWN = '' # X-( COLOR__END = '' def dump_attr_token(token, prefix = '') print prefix token.each {|x| next unless x.attribute if x.attribute == ATTR_UNKNOWN str = COLOR_LOCAL elsif x.attribute & ATTR_LOCAL > 0 str = COLOR_LOCAL elsif x.attribute & ATTR_ROUTE > 0 str = COLOR_ROUTE elsif x.attribute & ATTR_DOMAIN > 0 str = COLOR_DOMAIN elsif x.attribute & (ATTR_ROUTE_BEGIN | ATTR_ROUTE_END) > 0 str = COLOR_ROUTE_BE elsif x.attribute & (ATTR_AT | ATTR_ADDR_BEGIN | ATTR_ADDR_END) > 0 str = COLOR_ADDR_BE elsif x.attribute & (ATTR_GROUP_BEGIN | ATTR_GROUP_END) > 0 str = COLOR_GROUP_BE elsif x.attribute & ATTR_PHRASE > 0 str = COLOR_PHRASE elsif x.attribute & ATTR_COMMENT > 0 str = COLOR_COMMENT else str = COLOR_ELSE end str += html_escape(x.body) + COLOR__END print str } print "\n" end #!!! debug def html_escape(str) escaped_str = str.dup escaped_str.gsub!(/&/o, '&') escaped_str.gsub!(//o, '>') escaped_str.gsub!(/\"/o, '"') return escaped_str end end # RFC822 形式のテキストを扱うためのクラス # o ヘッダとボディを分離できる. (と思う) # o ボディを取り出すことができる. (と思う) # new がイテレータとして呼ばれた場合には # ボディの中身を一行ずつ取り出すことができる. (と思う) # o 指定したヘッダの値を取り出せる. (と思う) # o ヘッダを正しく unfold できる. (と思う) # o ヘッダを正しく分解できる. (と思う) # o ヘッダから正しく comment を除去できる(→ module RFC822_staff) # o ヘッダを正しく folding できる. (まだやってない) # o アドレスを記述するヘッダから # メールアドレスを取り出せる(→ class Address) # o Date: ヘッダを解釈できる(→ module RFC822_staff) # o Date: ヘッダを元に日付順にソートできる (と思う) # o トレース情報(Received:)を追跡できる (まだやってない) class RFC822 include Comparable include RFC822_staff def initialize(io, delimiter = nil) parse_header(io) if iterator? # イテレータとして呼ばれた場合には # 本文を保存せず 1 行ずつ yield する parse_body(io, delimiter) {|line| yield(line)} else parse_body(io, delimiter) end end # ヘッダを解析する. def parse_header(io) @header = {} # 各ヘッダが格納される @header_string = '' # 生のヘッダ全体が格納される while line = io.gets() @header_string << line next if /^From /o =~ line # UNIX From 行は無視する break if /^(\r\n|\r|\n)$/o =~ line # 空行でヘッダの終り # ヘッダの抽出 + unfolding 処理 # ・物理行で複数行のヘッダを 1 行にまとめる # ・同じヘッダが複数ある場合(Received: ヘッダなど)には # 改行文字(\n)で区切って連結しておく. if HEADER_REGEX =~ line field_name = $1.capitalize if @header.key?(field_name) @header[field_name] << "\n" + $2 else @header[field_name] = $2 end elsif FOLDED_HEADER_REGEX =~ line @header[field_name] << ' ' + $1 else # エラーなんだけど… end end # それぞれヘッダ From:, Reply-To:, To:, Cc: に対する # Address クラスのインスタンスを保持している. @from = get_address('From') @reply_to = get_address('Reply-to') @to = get_address('To') @cc = get_address('Cc') # そのヘッダを持つメールの発信時刻(Time オブジェクト). @date = date_to_time(@header['Date']) if @header.key?('Date') end private :parse_header # ボディを @body に格納するか, またはイテレータとして処理する. def parse_body(io, delimiter) if iterator? # イテレータとして呼ばれた場合. @body = nil while line = io.gets() if delimiter break if delimiter =~ line end yield(line.sub(/(\r\n|\n|\r)$/o, "\n")) end else @body = '' while line = io.gets() if delimiter break if delimiter =~ line end @body << line.sub(/(\r\n|\n|\r)$/o, "\n") end end end private :parse_body # 指定されたヘッダの内容を文字列として返す. def [](field_name) if @header.include?(field_name) return @header[field_name] else return nil end end # 指定されたヘッダが存在しているかどうかを返す. def include?(field_name) return @header.include?(field_name) end alias key? include? # 指定されたヘッダに対する class Address のオブジェクトを作る(うーむ). # たとえば To が複数ある場合には @header['To'] は # "hoge hoge\nfugafuga" のようになっていることに注意. def get_address(field_name) if @header.key?(field_name) return Address.new(@header[field_name].gsub(/\n/, ', ')) else return nil end end # 与えられた文字列から msg-id 形式の文字列を取り出し配列で返す. def get_id(string) msgids = [] id_string = '' part_to_token(string).each {|token| if token == '>' token = '>,' end id_string << token } msgid = Address.new(id_string.sub!(/,$/o, '')) if msgid msgid.addresses.each {|id| msgids.push('<' + id + '>') } else msgids = nil end return msgids end private :get_id # 参照しているメッセージの Message-ID を配列で返す. # 先頭の ID が直前の Message-ID. def parent_id references = [] if @header.key?('References') references = get_id(@header['References']) end if @header.key?('In-reply-to') get_id(@header['In-reply-to']).each {|id| unless references.include?(id) references.push(id) end } end references.reverse! return references end # 指定された field_name の内容について適宜 quote する. def quote(field_name) raise('Not implemented yet') end # 指定された field_name の中の quote(<">, "\") を外す def unquote(field_name) raise('Not implemented yet') end # Received: ヘッダを元にしてメールの配送経路を追跡する. ## class RFC1036 < RFC822 がもしできたら ## Path: ヘッダ用に置き換えるかな? def traceroute # うひ〜 raise('Not implemented yet') end # sort 用 def <=>(o) if o.kind_of?(RFC822) # 相手も RFC822 クラスなら Date: ヘッダ情報同士を突き合わせ me = self.date oth = o.date elsif o.kind_of?(Time) # Time クラスだったら, Date: ヘッダ情報と突き合わせ me = self.date oth = o elsif o.kind_of?(Integer) # 整数値だったら Date: ヘッダを time に直したものとして比較 # (日付を指定しての検索とかに使うのか?) me = self.header.date.to_i oth = o elsif o.kind_of?(Strings) # 文字列なら Subject: ヘッダの中身であるとして比較 # (Subject: ヘッダでの検索? 分割送信されたメールを掻き集めるとか?) me = self.header['Subject'] oth = o else # それ以外はエラーだ end return me <=> oth end def coerce(o) if o.kind_of?(Integer) return o, self.date.to_i elsif o.kind_of?(Strings) return o, self.header['Subject'] else super end end # 参照している RFC822 テキストをベースにして, # 適宜, ヘッダの re-folding や # ボディへのテキストの挿入などを行う. def reformat raise('Not implemented yet') end # これはとりあえずの実装. あとでちゃんと実装しよう. # しかしこれは, ほとんどデバッグ用… (^_^;) def make_reply_header #(my_address) h = '' if @reply_to h << 'To: ' + @reply_to.addresses.join(", \n ") + "\n" elsif @from h << 'To: ' + @from.addresses.join(", \n ") + "\n" else h << "To: (none)\n" end if @header.key?('Message-id') h << 'In-Reply-To: ' if rr = @header['Date'] h << 'Your message of "' + rr + '"' + "\n\t" end h << get_id(@header['Message-id'])[0] + "\n" end if @header.key?('Subject') h << 'Subject: ' + @header['Subject'].gsub(/^(Re:\s*)*/i, 'Re: ') + "\n" end return h end attr :header attr :body attr :date attr :from attr :reply_to attr :to attr :cc # メールアドレスを含む stractured filed を扱うためのクラス # o アドレスを記述するヘッダから # メールアドレス(addr-spec)を取り出せる. (と思う) # o 各メールアドレスに対する phrase を取り出せる. (と思う) # o 各メールアドレスに付属する comment を取り出せる. (と思う) class Address include RFC822_staff # address = mailbox ; one addressee # / group ; named list # group = phrase ":" [#mailbox] ";" # mailbox = addr-spec ; simple address # / phrase route-addr ; name & addr-spec # addr-spec = local-part "@" domain ; global address # route-addr = "<" [route] addr-spec ">" # route = 1#("@" domain) ":" ; path-relative # local-part = word *("." word) ; uninterpreted # ; case-preserved # domain = sub-domain *("." sub-domain) # sub-domain = domain-ref / domain-literal # domain-ref = atom ; symbolic reference # domain-literal = "[" *(dtext / quoted-pair) "]" # dtext = may be folded # "]", "\" & CR, & including # linear-white-space> # phrase = 1*word ; Sequence of words # word = atom / quoted-string # atom = 1* def initialize(address_string) # @mailbox: address_string に含まれる mailbox の配列 @mailboxes = parse(address_string) end def parse(address_string) mailboxes = get_mailboxes(address_string) mailboxes.each_index {|i| if mailboxes[i].size > 1 # mailboxes[i][0]〜[i][-1] は group # (mailboxes[i][0][-1] == ':' && mailboxes[i][-1][0] == ';') mailboxes[i] = Group.new(mailboxes[i]) else # mailboxes[i][0] は mailbox mailboxes[i] = Mailbox.new(mailboxes[i][0]) end } return mailboxes end # 与えられた address 文字列を 1) delimit し, # 2) address 単位に分けた上でさらにそれが # 3) group であればその中を mailbox に分けた # 配列の配列の配列を返す. # 例: "foo, bar , baz: a, b <@bhost:b>, c;" の場合 # [[["foo"]], # [["bar", "<", "bar", ">"]], # [["baz", ":"], # ["a"], # ["b", " ", "<", "@", "bhost", ":", "b", ">"], # ["c"], # [";"]]] def get_mailboxes(address_string) delimited = delimit(address_string) mailboxes = [[[]]] fifo = ['']*25 in_route_addr = false in_group = false delimited.each {|x| mailboxes[-1][-1].push(x) fifo.shift; fifo.push(x) case x when '<' if in_route_addr # special parse_error("unbalanced `<'", fifo) else # route-addr の始まり in_route_addr = true end when '>' if in_route_addr # route-addr の終り in_route_addr = false else # special parse_error("unbalanced `>'", fifo) end when ':' if in_route_addr # route の終り else # group の始まり in_group = true delete_lwsp!(mailboxes[-1][-1]) mailboxes[-1] << [] end when ';' if in_group && !in_route_addr # group の終り in_group = false mailboxes[-1][-1].pop delete_lwsp!(mailboxes[-1][-1]) mailboxes[-1] << [] mailboxes[-1][-1].push(';') else # special parse_error("unbalanced `;'", fifo) end when ',' mailboxes[-1][-1].pop delete_lwsp!(mailboxes[-1][-1]) if in_group # group の中での mailbox 区切り mailboxes[-1] << [] else # mailbox 区切り mailboxes << [[]] in_route_addr = false in_group = false end end } delete_lwsp!(mailboxes[-1][-1]) parse_error("unbalanced `<'", fifo) if in_route_addr parse_error("unbalanced `:'", fifo) if in_group return mailboxes end private :get_mailboxes # その Address オブジェクトが含んでいる # Mailbox オブジェクトの配列を返す def get_mailbox(unique = false) mailboxes = [] @mailboxes.each_index {|i| if @mailboxes[i].kind_of?(Mailbox) mailboxes << @mailboxes[i] elsif @mailboxes[i].kind_of?(Group) @mailboxes[i].each {|x| mailboxes << x } end } if unique mailboxes = mailboxes & mailboxes end return mailboxes end # その Address オブジェクトが含んでいるアドレスの配列を返す def get_address(unique = false, downcase = false) addresses = [] self.get_mailbox(unique).each {|x| addresses << x.get_address(downcase) } addresses.delete('') return addresses end # その Address オブジェクトが含んでいる # 各 Mailbox オブジェクトに対するイテレータ def each(unique = false) self.get_mailbox.each {|x| yield(x) } end #!!! debug def dump @mailboxes.each {|x| x.dump } end # 配列の先頭と末尾から LINEAR_WHITE_SPACE_REGEXPs に # マッチする要素を取り除く. def delete_lwsp!(array) array.shift while LINEAR_WHITE_SPACE_REGEXPs =~ array[0] array.pop while LINEAR_WHITE_SPACE_REGEXPs =~ array[-1] end private :delete_lwsp! TOKEN = Struct.new('Token', :body, :attribute) class Group include RFC822_staff def initialize(tokens) # tokens は Address#get_mailboxes をかました後で得られた, たとえば # ["baz", ":"], ["a"], ["b", " ", "<", "@", "bhost", ":", "b", ">"], # ["c"], [";"] という感じの配列の配列でなければならない. # なお tokens の最小構成は ["一文字", ":"], [";"] である. @token = [] @mailboxes = [] set_attribute(tokens[0]) tokens[1 .. -2].each {|x| @token << TOKEN.new(nil, nil) @mailboxes << Mailbox.new(x) } set_attribute(tokens[-1]) end def set_attribute(token) token.each {|x| case x when ':' @token << TOKEN.new(x, (ATTR_GROUP_BEGIN | ATTR_DELIMITER)) when ';' @token << TOKEN.new(x, (ATTR_GROUP_END | ATTR_DELIMITER)) when /^\(.*\)$/o @token << TOKEN.new(x, (ATTR_COMMENT | ATTR_DELIMITER)) when LINEAR_WHITE_SPACE_REGEXPs @token << TOKEN.new(x, (ATTR_LWSP | ATTR_DELIMITER)) else @token << TOKEN.new(x, (ATTR_PHRASE)) end } end private :set_attribute # そのグループが含んでいる Mailbox オブジェクトの配列を返す def get_mailbox(unique = false) if unique return @mailboxes & @mailboxes else return @mailboxes end end # そのグループが含んでいるアドレスの配列を返す def get_address(unique = false, downcase = false) addresses = [] self.get_mailbox(unique).each {|x| addresses << x.get_address(downcase) } return addresses end # そのグループが含んでいる各 Mailbox オブジェクトに対するイテレータ def each(unique = false) self.get_mailbox(unique).each {|x| yield(x) } end def dump(prefix = '') dump_attr_token(@token, prefix) @mailboxes.each {|x| x.dump(' ') } end end # class Group class Mailbox include Comparable include RFC822_staff def initialize(token) attribute = parse(token) @token = [] token.each_index {|i| @token << TOKEN.new(token[i], attribute[i]) } end attr :token #!!! # 各トークンと属性に対するイテレータ def each(downcase = false) if downcase @token.each {|x| if x.attribute & ATTR_IGNORE_CASE > 0 yield(TOKEN.new(x.body.downcase, x.attribute)) else yield(x) end } else @token.each {|x| yield(x) } end end def get_address(downcase = false) address = '' self.each(downcase) {|x| address << x.body if x.attribute & ATTR_ADDRESS > 0 } return address end def get_primary_address(downcase = false) address = '' appear_local = false self.each(downcase) {|x| if x.attribute & ATTR_ROUTE_BEGIN > 0 if appear_local address.sub!(/%/o, '@') break end end if x.attribute & ATTR_LOCAL > 0 address = '' appear_local = true end address << x.body if x.attribute & ATTR_ADDRESS > 0 } return address end # たどるべきホストを, たどるべき順番に並べた配列を返す. def get_route(downcase = false) route = nil route_host = '' self.each(downcase) {|x| if x.attribute & ATTR_ROUTE_BEGIN > 0 route = [] end next unless route if x.attribute & ATTR_AT > 0 case x.body when '@' route.push(route_host) when '%' route.unshift(route_host) end route_host = '' elsif x.attribute & ATTR_ROUTE_END > 0 route.push(route_host) route_host = '' break elsif x.attribute & ATTR_ROUTE > 0 route_host << x.body end } if route_host.size > 0 # ATTR_ROUTE_END を見つけられなかった. つまり '%' route route.unshift(route_host) end return route end #!!! debug def dump(prefix = '') dump_attr_token(@token, prefix) end # 与えられた mailbox 配列をうけて # 属性(phrase, domain, local-part など)を解析する. def parse(token) type = [] fifo = ['']*25 tmp = [] # フラグ X-< in_domain = false in_route = false in_route_percent = false has_local_part = false prev = '' i = 0 while i < token.size fifo.shift; fifo.push(token[i]) case token[i] when '<' # commit! # それまでの部分はすべて phrase type.concat(tmp.collect {|x| if x != ATTR_LWSP; ATTR_PHRASE; else; x; end}) type << (ATTR_ADDR_BEGIN | ATTR_DELIMITER) in_domain = false in_route = false in_route_percent = false has_local_part = false tmp = [] when '>' # commit! type.concat(tmp.collect {|x| x == ATTR_UNKNOWN ? ATTR_LOCAL : x}) type.concat(ATTR_ADDR_END | ATTR_DELIMITER) tmp = [] break when '@' if prev == '<' tmp << (ATTR_ROUTE_BEGIN | ATTR_DELIMITER) in_route = true elsif in_route tmp << (ATTR_AT | ATTR_DELIMITER) elsif has_local_part parse_error("multiple `@'", fifo) else tmp = tmp.collect {|x| x == ATTR_UNKNOWN ? ATTR_LOCAL : x} # @ の前の未定義文字列は local-part tmp << (ATTR_AT | ATTR_DELIMITER) has_local_part = true end in_domain = true when '%' if in_route_percent tmp << (ATTR_ROUTE_BEGIN | ATTR_DELIMITER) in_route = true in_route_percent = false elsif in_route tmp << (ATTR_AT | ATTR_DELIMITER) else tmp = tmp.collect {|x| x == ATTR_UNKNOWN ? ATTR_LOCAL : x} # @ の前の未定義文字列は local-part tmp << (ATTR_AT | ATTR_DELIMITER) has_local_part = true in_route_percent = true end in_domain = true when '.', '[', ']' if in_domain tmp << (ATTR_DELIMITER | ATTR_DOMAIN) tmp[-1] |= ATTR_ROUTE if in_route else tmp << ATTR_UNKNOWN end when ':' if in_route # commit! type.concat(tmp) type.concat(ATTR_ROUTE_END | ATTR_DELIMITER) in_domain = false in_route = false tmp = [] else tmp << ATTR_UNKNOWN end when /^\(.*\)$/o tmp << (ATTR_COMMENT | ATTR_DELIMITER) when LINEAR_WHITE_SPACE_REGEXPs tmp << (ATTR_LWSP | ATTR_DELIMITER) else if in_domain tmp << ATTR_DOMAIN tmp[-1] |= ATTR_ROUTE if in_route else tmp << ATTR_UNKNOWN end end prev = token[i] if tmp[-1] != ATTR_LWSP i += 1 end type.concat(tmp.collect {|x| x == ATTR_UNKNOWN ? ATTR_LOCAL : x}) # 残った部分は基本的に phrase i += 1 if i < token.size - 1 for x in token[i .. token.size] type.concat(LINEAR_WHITE_SPACE_REGEXPs =~ x ? ATTR_LWSP : ATTR_PHRASE) end end # local-part " " local-part はひとまとまりの local-part, など. # ただし pharese " " pharese などはそのまま. for i in 1 .. type.size - 1 if type[i] & ATTR_LWSP > 0 && type[i - 1] == type[i + 1] && type[i - 1] & (ATTR_PHRASE | ATTR_COMMENT) == 0 && type[i - 1] !~ SPECIALS_REGEXPs type[i] = type[i - 1] end end return type end private :parse def <=>(o) if o.kind_of?(Mailbox) self.get_address(true) <=> o.get_address(true) else self.get_address(true) <=> o.to_s end end end # class Mailbox end # class Address end # class RFC822