internet_message 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ class InternetMessage
2
+ class Address
3
+
4
+ attr_reader :local_part, :domain
5
+
6
+ # @param [String] local_part local part of mail address
7
+ # @param [String] domain domain part of mail address
8
+ def initialize(local_part, domain)
9
+ @local_part, @domain = local_part, domain
10
+ end
11
+
12
+ # @return [String] mail address
13
+ def to_s
14
+ if @local_part =~ /\A[0-9a-zA-Z\!\#\$\%\'\*\+\-\/\=\?\^\_\`\{\|\}\~]+(\.[0-9a-zA-Z\!\#\$\%\'\*\+\-\/\=\?\^\_\`\{\|\}\~]+)*\z/n
15
+ l = @local_part
16
+ else
17
+ l = quote_string(@local_part)
18
+ end
19
+ "#{l}@#{@domain}"
20
+ end
21
+
22
+ # @private
23
+ def quote_string(s)
24
+ '"'+s.gsub(/[\\\"]/){"\\#{$&}"}+'"'
25
+ end
26
+
27
+ # Compare self to other. local_part and domain are case insensitive.
28
+ # @param [Address] other
29
+ # @return [true, false]
30
+ def ==(other)
31
+ other.is_a?(Address) && other.local_part.downcase == self.local_part.downcase && other.domain.downcase == self.domain.downcase
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,54 @@
1
+ class InternetMessage
2
+ # @private
3
+ module ContentAttribute
4
+ def self.parse_attribute(tokens)
5
+ attr = {}
6
+ until tokens.empty?
7
+ break unless tokens.size >= 4 && tokens[0].value == ';' && tokens[2].value == '='
8
+ attr[tokens[1].value.downcase] = tokens[3].value
9
+ tokens.shift 4
10
+ end
11
+
12
+ newattr = {}
13
+ h = Hash.new{|hash,k| hash[k] = []}
14
+ char_lang = {}
15
+ attr.each do |key, value|
16
+ case key
17
+ when /^([^\*]+)(\*0)?\*$/no
18
+ name, ord = $1, $2
19
+ char, lang, v = value.split(/\'/, 3)
20
+ char_lang[name] = [char, lang]
21
+ if v.nil?
22
+ v = lang || char
23
+ end
24
+ v = v.gsub(/%([0-9A-F][0-9A-F])/ni){$1.hex.chr}
25
+ if ord
26
+ h[name] << [0, v]
27
+ else
28
+ newattr[name] = v
29
+ end
30
+ when /^([^\*]+)\*([1-9]\d*)\*$/no
31
+ name, ord = $1, $2.to_i
32
+ v = value.gsub(/%([0-9A-F][0-9A-F])/ni){$1.hex.chr}
33
+ h[name] << [ord, v]
34
+ when /^([^\*]+)\*([0-9]\d*)$/no
35
+ name, ord = $1, $2.to_i
36
+ h[name] << [ord, value]
37
+ else
38
+ newattr[key] = value
39
+ end
40
+ end
41
+ h.each do |k, v|
42
+ newattr[k] = v.sort{|a,b| a[0]<=>b[0]}.map{|a| a[1]}.join
43
+ end
44
+ newattr.keys.each do |k|
45
+ v = newattr[k]
46
+ if char_lang.key? k
47
+ v.force_encoding(char_lang[k][0]) rescue nil
48
+ end
49
+ newattr[k] = v
50
+ end
51
+ return newattr
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ require "#{File.dirname __FILE__}/tokenizer"
2
+ require "#{File.dirname __FILE__}/content_attribute"
3
+
4
+ class InternetMessage
5
+ class ContentDisposition
6
+
7
+ TOKEN_RE = /[0-9a-zA-Z\!\#\$\%\&\'\*\+\-\.\^\_\`\{\|\}\~]+/i
8
+
9
+ # @param [String, Array of Tokenizer] src
10
+ # @return [ContentDisposition]
11
+ def self.parse(src)
12
+ tokens = src.is_a?(String) ? Tokenizer.new(src, :token_re=>TOKEN_RE).tokenize : src.dup
13
+ tokens.delete_if{|t| t.type == :WSP or t.type == :COMMENT}
14
+ unless tokens.size >= 1 && tokens[0].type == :TOKEN
15
+ return nil
16
+ end
17
+ type = tokens[0].value
18
+ tokens.shift
19
+ ContentDisposition.new(type, ContentAttribute.parse_attribute(tokens))
20
+ end
21
+
22
+ attr_reader :type, :attribute
23
+
24
+ # @param [String] type
25
+ # @param [Hash] attribute
26
+ def initialize(type, attribute={})
27
+ @type, @attribute = type.downcase, attribute
28
+ end
29
+
30
+ # Compare self and other.
31
+ # @param [ContentDisposition] other
32
+ # @return [true, false]
33
+ def ==(other)
34
+ other.is_a?(ContentDisposition) && other.type == self.type && other.attribute == self.attribute
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,38 @@
1
+ require "#{File.dirname __FILE__}/tokenizer"
2
+ require "#{File.dirname __FILE__}/content_attribute"
3
+
4
+ class InternetMessage
5
+ class ContentType
6
+
7
+ TOKEN_RE = /[0-9a-zA-Z\!\#\$\%\&\'\*\+\-\.\^\_\`\{\|\}\~]+/i
8
+
9
+ # @param [String, Array of Tokenizer] src
10
+ # @return [ContentType]
11
+ def self.parse(src)
12
+ tokens = src.is_a?(String) ? Tokenizer.new(src, :token_re=>TOKEN_RE).tokenize : src.dup
13
+ tokens.delete_if{|t| t.type == :WSP or t.type == :COMMENT}
14
+ unless tokens.size >= 3 && tokens[0].type == :TOKEN && tokens[1].value == '/' && tokens[2].type == :TOKEN
15
+ return nil
16
+ end
17
+ type, subtype = tokens[0].value, tokens[2].value
18
+ tokens.shift 3
19
+ ContentType.new(type, subtype, ContentAttribute.parse_attribute(tokens))
20
+ end
21
+
22
+ attr_reader :type, :subtype, :attribute
23
+
24
+ # @param [String] type
25
+ # @param [String] subtype
26
+ # @param [Hash] attribute
27
+ def initialize(type, subtype, attribute={})
28
+ @type, @subtype, @attribute = type.downcase, subtype.downcase, attribute
29
+ end
30
+
31
+ # Compare self and other
32
+ # @param [ContentType] other
33
+ # @return [true, false]
34
+ def ==(other)
35
+ other.is_a?(ContentType) && other.type == self.type && other.subtype == self.subtype && other.attribute == self.attribute
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,49 @@
1
+ require "#{File.dirname __FILE__}/tokenizer"
2
+
3
+ class InternetMessage
4
+ class Group
5
+ # @param [String, Array of Tokenizer] src
6
+ # @param [true, false] decode_mime_header Set true to decode MIME header (RFC2047).
7
+ # @return [Group]
8
+ def self.parse(src, decode_mime_header=nil)
9
+ tokens = src.is_a?(String) ? Tokenizer.new(src).tokenize : src.dup
10
+ tokens.delete_if{|t| t.type == :WSP or t.type == :COMMENT}
11
+ i = tokens.index(Token.new(:CHAR, ':'))
12
+ j = tokens.index(Token.new(:CHAR, ';')) || tokens.size
13
+ if i and i < j
14
+ disp_tokens = tokens[0..i-1]
15
+ display_name = i == 0 ? '' : decode_mime_header ? InternetMessage.decode_mime_header_words(disp_tokens) : disp_tokens.join(' ')
16
+ mailbox_list = Mailbox.parse_list(tokens[i+1..j-1], decode_mime_header)
17
+ Group.new(display_name, mailbox_list)
18
+ else
19
+ Group.new('', Mailbox.parse_list(tokens))
20
+ end
21
+ end
22
+
23
+ attr_reader :mailbox_list, :display_name
24
+
25
+ # @param [String] display_name
26
+ # @param [Array of Mailbox] mailbox_list
27
+ def initialize(display_name, mailbox_list)
28
+ @display_name, @mailbox_list = display_name, mailbox_list
29
+ end
30
+
31
+ # @private
32
+ def to_s
33
+ d = @display_name.split(/[ \t]+/).map do |w|
34
+ if w =~ /\A[0-9a-zA-Z\!\#\$\%\'\*\+\-\/\=\?\^\_\`\{\|\}\~]+\z/n
35
+ w
36
+ else
37
+ quote_string w
38
+ end
39
+ end.join(' ')
40
+ "#{d}: "+mailbox_list.join(', ')+';'
41
+ end
42
+
43
+ private
44
+
45
+ def quote_string(s)
46
+ '"'+s.gsub(/[\\\"]/){"\\#{$&}"}+'"'
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,129 @@
1
+ class InternetMessage
2
+ class HeaderField
3
+ attr_reader :name, :orig_value, :raw
4
+
5
+ # @param [String] name field name
6
+ # @param [MmapScanner] value field value
7
+ # @param [MmapScanner] raw field line
8
+ def initialize(name, value, raw)
9
+ @name, @orig_value, @raw = name, value, raw
10
+ end
11
+
12
+ # @return [String] value as String
13
+ def value
14
+ @orig_value.to_s
15
+ end
16
+
17
+ # @param [true, false] decode_mime_header Set true to decode MIME header (RFC2047).
18
+ # @return parsed value
19
+ def parse(decode_mime_header=nil)
20
+ case @name
21
+ when 'date', 'resent-date'
22
+ DateTime.parse(value.gsub(/\r?\n/, '')) rescue nil
23
+ when 'from', 'resent-from'
24
+ self.class.parse_mailboxlist(value, decode_mime_header)
25
+ when 'from', 'sender', 'resent-from', 'resent-sender'
26
+ Mailbox.parse(value, decode_mime_header)
27
+ when 'message-id', 'content-id', 'resent-message-id'
28
+ MessageId.parse(value)
29
+ when 'in-reply-to', 'references'
30
+ MessageId.parse_list(value)
31
+ when 'mime-version', 'content-transfer-encoding'
32
+ tokens = Tokenizer.new(value).tokenize2
33
+ tokens.empty? ? nil : tokens.join
34
+ when 'content-type'
35
+ ContentType.parse(value)
36
+ when 'content-disposition'
37
+ ContentDisposition.parse(value)
38
+ when 'reply-to', 'to', 'cc', 'bcc', 'resent-to', 'resent-cc', 'resent-bcc'
39
+ self.class.parse_addrlist(value, decode_mime_header)
40
+ when 'keywords'
41
+ self.class.parse_keywords(value, decode_mime_header)
42
+ when 'return-path'
43
+ self.class.parse_return_path(value)
44
+ when 'received'
45
+ Received.parse value
46
+ else
47
+ s = value.gsub(/\r?\n/, '')
48
+ decode_mime_header ? InternetMessage.decode_mime_header_str(s) : s
49
+ end
50
+ end
51
+
52
+ # @private
53
+ def self.parse_mailboxlist(str, decode_mime_header=nil)
54
+ ret = []
55
+ tokens = Tokenizer.new(str).tokenize2
56
+ until tokens.empty?
57
+ i = tokens.index(Token.new(:CHAR, ','))
58
+ if i == 0
59
+ tokens.shift
60
+ next
61
+ end
62
+ if i
63
+ ret.push Mailbox.parse(tokens.slice!(0..i-1), decode_mime_header)
64
+ else
65
+ ret.push Mailbox.parse(tokens, decode_mime_header)
66
+ tokens.clear
67
+ end
68
+ end
69
+ ret
70
+ end
71
+
72
+ # @private
73
+ def self.parse_addrlist(str, decode_mime_header=nil)
74
+ ret = []
75
+ tokens = Tokenizer.new(str).tokenize2
76
+ until tokens.empty?
77
+ i = tokens.index(Token.new(:CHAR, ','))
78
+ if i == 0
79
+ tokens.shift
80
+ next
81
+ end
82
+ j = tokens.index(Token.new(:CHAR, ':'))
83
+ if i && j && j < i || !i && j
84
+ i = tokens.index(Token.new(:CHAR, ';')) || -1
85
+ ret.push Group.parse(tokens.slice!(0..i), decode_mime_header)
86
+ elsif i
87
+ ret.push Mailbox.parse(tokens.slice!(0..i-1), decode_mime_header)
88
+ else
89
+ ret.push Mailbox.parse(tokens, decode_mime_header)
90
+ tokens.clear
91
+ end
92
+ end
93
+ ret
94
+ end
95
+
96
+ # @private
97
+ def self.parse_keywords(str, decode_mime_header=nil)
98
+ keys = []
99
+ tokens = Tokenizer.new(str).tokenize2
100
+ while true
101
+ i = tokens.index(Token.new(:CHAR, ','))
102
+ break unless i
103
+ if i > 0
104
+ key = decode_mime_header ? InternetMessage.decode_mime_header_words(tokens[0, i]) : tokens[0, i].join(' ')
105
+ keys.push key
106
+ end
107
+ tokens.shift i+1
108
+ end
109
+ unless tokens.empty?
110
+ key = decode_mime_header ? InternetMessage.decode_mime_header_words(tokens) : tokens.join(' ')
111
+ keys.push key
112
+ end
113
+ keys
114
+ end
115
+
116
+ # @private
117
+ def self.parse_return_path(str)
118
+ tokens = Tokenizer.new(str).tokenize2
119
+ i = tokens.index(Token.new(:CHAR, '<'))
120
+ return unless i
121
+ tokens.shift i+1
122
+ i = tokens.index(Token.new(:CHAR, '>'))
123
+ return unless i
124
+ tokens = tokens[0, i]
125
+ i = tokens.rindex(Token.new(:CHAR, '@'))
126
+ i && Address.new(tokens[0, i].join, tokens[i+1..-1].join)
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,102 @@
1
+ require "#{File.dirname __FILE__}/tokenizer"
2
+ require "#{File.dirname __FILE__}/address"
3
+
4
+ class InternetMessage
5
+ class Mailbox
6
+ # @param [String, Array of Tokenizer] src
7
+ # @param [true, false] decode_mime_header Set true to decode MIME header (RFC2047).
8
+ # @return [Mailbox]
9
+ def self.parse(src, decode_mime_header=nil)
10
+ tokens = src.is_a?(String) ? Tokenizer.new(src).tokenize : src.dup
11
+ tokens.delete_if{|t| t.type == :WSP or t.type == :COMMENT}
12
+ if i = tokens.index(Token.new(:CHAR, '<'))
13
+ display_name = decode_mime_header ? InternetMessage.decode_mime_header_words(tokens[0..i-1]) : tokens[0..i-1].join(' ')
14
+ if j = tokens.index(Token.new(:CHAR, '>'))
15
+ tokens = tokens[i+1..j-1]
16
+ else
17
+ tokens = tokens[i+1..-1]
18
+ end
19
+ end
20
+ i = tokens.rindex(Token.new(:CHAR, '@'))
21
+ return unless i
22
+ local = i == 0 ? '' : tokens[0..i-1].join
23
+ domain = tokens[i+1..-1].join
24
+ Mailbox.new(Address.new(local, domain), display_name)
25
+ end
26
+
27
+ # @param [String, Array of Tokenizer] src
28
+ # @param [true, false] decode_mime_header Set true to decode MIME header (RFC2047).
29
+ # @return [Array of Mailbox]
30
+ def self.parse_list(src, decode_mime_header=nil)
31
+ tokens = src.is_a?(String) ? Tokenizer.new(src).tokenize : src.dup
32
+ ret = []
33
+ until tokens.empty?
34
+ i = tokens.index(Token.new(:CHAR, ','))
35
+ break unless i
36
+ if i > 0
37
+ ret.push self.parse(tokens.slice!(0, i), decode_mime_header)
38
+ end
39
+ tokens.shift
40
+ end
41
+ ret.push self.parse(tokens, decode_mime_header) unless tokens.empty?
42
+ ret
43
+ end
44
+
45
+ attr_reader :address, :display_name
46
+
47
+ # @overload initialize(addr, display_name=nil)
48
+ # @param [Address] addr
49
+ # @param [String] display_name
50
+ # @overload initialize(local_part, domain, display_name=nil)
51
+ # @param [String] local_part
52
+ # @param [String] domain
53
+ # @param [String] display_name
54
+ def initialize(addr, *args)
55
+ if addr.is_a? Address and args.size <= 1
56
+ @address = addr
57
+ @display_name = args.first
58
+ elsif args.size >= 1 and args.size <= 2
59
+ @address = Address.new(addr, args[0])
60
+ @display_name = args[1]
61
+ else
62
+ raise ArgumentError
63
+ end
64
+ end
65
+
66
+ # @return [String] local_part
67
+ def local_part
68
+ @address.local_part
69
+ end
70
+
71
+ # @return [String] domain
72
+ def domain
73
+ @address.domain
74
+ end
75
+
76
+ # @private
77
+ def to_s
78
+ if @display_name
79
+ d = @display_name.split(/[ \t]+/).map do |w|
80
+ if w =~ /\A[0-9a-zA-Z\!\#\$\%\'\*\+\-\/\=\?\^\_\`\{\|\}\~]+\z/n
81
+ w
82
+ else
83
+ quote_string w
84
+ end
85
+ end.join(' ')
86
+ "#{d} <#{@address.to_s}>"
87
+ else
88
+ @address.to_s
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def quote_string(s)
95
+ '"'+s.gsub(/[\\\"]/){"\\#{$&}"}+'"'
96
+ end
97
+
98
+ def ==(other)
99
+ other.is_a?(Mailbox) && other.address == self.address && other.display_name == self.display_name
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,47 @@
1
+ require "#{File.dirname __FILE__}/tokenizer"
2
+
3
+ class InternetMessage
4
+ class MessageId
5
+ # @param [String, Array of Tokenizer] src
6
+ # @return [MessageId]
7
+ def self.parse(str)
8
+ tokens = Tokenizer.new(str).tokenize2
9
+ i = tokens.index(Token.new(:CHAR, '<'))
10
+ return unless i
11
+ tokens.shift i+1
12
+ i = tokens.index(Token.new(:CHAR, '>'))
13
+ return unless i
14
+ self.new tokens[0, i].join
15
+ end
16
+
17
+ # @param [String, Array of Tokenizer] src
18
+ # @return [Array of MessageId]
19
+ def self.parse_list(str)
20
+ tokens = Tokenizer.new(str).tokenize2
21
+ ret = []
22
+ while true
23
+ i = tokens.index(Token.new(:CHAR, '<'))
24
+ break unless i
25
+ tokens.shift i+1
26
+ i = tokens.index(Token.new(:CHAR, '>'))
27
+ break unless i
28
+ ret.push MessageId.new(tokens[0, i].join)
29
+ end
30
+ ret
31
+ end
32
+
33
+ attr_reader :msgid
34
+
35
+ # @param [String] msgid
36
+ def initialize(msgid)
37
+ @msgid = msgid
38
+ end
39
+
40
+ # Compare self to other
41
+ # @param [String] other
42
+ # @return [true, false]
43
+ def ==(other)
44
+ other.is_a?(MessageId) && other.msgid == self.msgid
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,59 @@
1
+ require 'date'
2
+ require "#{File.dirname __FILE__}/tokenizer"
3
+
4
+ class InternetMessage
5
+ class Received
6
+ # @param [String, Array of Tokenizer] src
7
+ # @return [Received]
8
+ def self.parse(src)
9
+ tokens = src.is_a?(String) ? Tokenizer.new(src).tokenize : src.dup
10
+ i = tokens.index(Token.new(:CHAR, ';'))
11
+ return unless i
12
+ date = DateTime.parse(tokens[i+1..-1].join) rescue nil
13
+ tokens = tokens[0, i]
14
+
15
+ list = tokens.inject([[]]){|r, t|
16
+ if t.type == :WSP or t.type == :COMMENT
17
+ r.push []
18
+ else
19
+ r.last.push t
20
+ end
21
+ r
22
+ }.reject(&:empty?)
23
+
24
+ while list.size >= 2
25
+ case list.shift.join.downcase
26
+ when 'from'
27
+ from = list.shift.join
28
+ when 'by'
29
+ by = list.shift.join
30
+ when 'via'
31
+ via = list.shift.join
32
+ when 'with'
33
+ with = list.shift.join
34
+ when 'id'
35
+ id = list.shift.join
36
+ when 'for'
37
+ m = Mailbox.parse(list.shift)
38
+ for_ = m && m.address
39
+ end
40
+ end
41
+ self.new(from, by, via, with, id, for_, date)
42
+ end
43
+
44
+ attr_reader :from, :by, :via, :with, :id, :for, :date
45
+
46
+ # @param [String] from
47
+ # @param [String] by
48
+ # @param [String] via
49
+ # @param [String] with
50
+ # @param [String] id
51
+ # @param [Mailbox] for_
52
+ # @param [DateTime] date
53
+ def initialize(from, by, via, with, id, for_, date)
54
+ @from, @by, @via, @with, @id, @for, @date =
55
+ from, by, via, with, id, for_, date
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,61 @@
1
+ require 'strscan'
2
+
3
+ class InternetMessage
4
+ class Tokenizer
5
+ TOKEN_RE = /[0-9a-zA-Z\!\#\$\%\'\*\+\-\/\=\?\^\_\`\{\|\}\~\.]+/n
6
+
7
+ def initialize(s, opt={})
8
+ @ss = StringScanner.new(s.gsub(/\r?\n/, ''))
9
+ @token_re = opt[:token_re] || TOKEN_RE
10
+ end
11
+
12
+ def tokenize(opt={})
13
+ ret = []
14
+ until @ss.eos?
15
+ case
16
+ when s = @ss.scan(/[ \t]+/)
17
+ ret.push Token.new(:WSP, s) unless opt[:skip_wsp]
18
+ when s = @ss.scan(@token_re)
19
+ ret.push Token.new(:TOKEN, s)
20
+ when s = @ss.scan(/\"(\\.|[^\"])+\"/)
21
+ ret.push Token.new(:QUOTED, s.gsub(/\A\"|\"\z/,'').gsub(/\\(.)/){$1})
22
+ when @ss.check(/\(/)
23
+ comment = scan_comment
24
+ ret.push Token.new(:COMMENT, comment) unless opt[:skip_comment]
25
+ else
26
+ ret.push Token.new(:CHAR, @ss.scan(/./))
27
+ end
28
+ end
29
+ ret
30
+ end
31
+
32
+ def tokenize2
33
+ tokenize(:skip_wsp=>true, :skip_comment=>true)
34
+ end
35
+
36
+ def scan_comment
37
+ ret = []
38
+ @ss.scan(/\(/) or return ret
39
+ until @ss.scan(/\)/) or @ss.eos?
40
+ s = @ss.scan(/(\\.|[^\\\(\)])*/) and ret.push s.gsub(/\\(.)/){$1}
41
+ @ss.check(/\(/) and ret.push scan_comment
42
+ end
43
+ ret
44
+ end
45
+ end
46
+
47
+ class Token
48
+ attr_reader :type, :value
49
+
50
+ def initialize(type, value)
51
+ @type, @value = type, value
52
+ end
53
+
54
+ alias to_s value
55
+
56
+ def ==(other)
57
+ other.is_a?(self.class) && other.type == self.type && other.value == self.value
58
+ end
59
+ end
60
+
61
+ end