mbox 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,91 @@
1
+ #! /usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'json'
4
+ require 'eventmachine'
5
+ require 'mbox'
6
+
7
+ options = {}
8
+
9
+ OptionParser.new do |o|
10
+ options[:host] = 'localhost'
11
+ options[:port] = 9001
12
+ options[:every] = (ENV['MBOX_DAEMON_EVERY'] || 120).to_f
13
+ options[:mail] = {
14
+ directory: ENV['MBOX_DAEMON_DIR'] || "#{ENV['HOME']}/mail",
15
+ boxes: (ENV['MBOX_DAEMON_BOXES'] || 'inbox').split(/\s*[;,]\s*/)
16
+ }
17
+
18
+ o.on '-h', '--host HOST', 'the host to bind to' do |value|
19
+ options[:host] = value
20
+ end
21
+
22
+ o.on '-p', '--port PORT', 'the port to bind to' do |value|
23
+ options[:port] = value.to_i
24
+ end
25
+
26
+ o.on '-e', '--every SECONDS', 'the seconds to check the email every' do |value|
27
+ options[:every] = value.to_f
28
+ end
29
+
30
+ o.on '-m', '--mail-directory PATH', 'the path where the mailboxes are at' do |value|
31
+ options[:mail][:directory] = File.realpath(File.expand_path(value))
32
+ end
33
+
34
+ o.on '-b', '--mail-boxes BOX...', Array, 'the mailboxes to check' do |value|
35
+ options[:mail][:boxes].push(*value)
36
+ end
37
+ end.parse!
38
+
39
+ %w[INT KILL].each {|sig|
40
+ trap sig do
41
+ EM.stop_event_loop
42
+ end
43
+ }
44
+
45
+ class Connection < EventMachine::Protocols::LineAndTextProtocol
46
+ @@unread = {}
47
+
48
+ attr_accessor :boxes
49
+
50
+ def receive_line (line)
51
+ whole, target, command, rest = line.match(/^(.*?)\s+(.*?)(?:\s+(.+))?$/).to_a
52
+
53
+ boxes = target == '*' ? @boxes : @boxes.select { |box| target.include? box.name }
54
+
55
+ if command == 'list'
56
+ command = rest
57
+
58
+ if command == 'unread'
59
+ send_response boxes.select { |box|
60
+ if @@unread[box] && @@unread[box].last_check < [File.ctime(box.path), File.mtime(box.path)].max
61
+ @@unread[box] = Struct.new(:status, :last_check).new(box.has_unread?, Time.new)
62
+ else
63
+ @@unread[box] ||= Struct.new(:status, :last_check).new(box.has_unread?, Time.new)
64
+ end
65
+
66
+ @@unread[box].status
67
+ }.map(&:name)
68
+ end
69
+ end
70
+ rescue Exception => e
71
+ send_data %{{"error":#{e.to_s.inspect}}}
72
+ end
73
+
74
+ def send_response (data)
75
+ send_data "#{data.to_json}\n"
76
+ end
77
+ end
78
+
79
+ EM.run {
80
+ boxes = options[:mail][:boxes].map {|name|
81
+ Mbox.open("#{options[:mail][:directory]}/#{name}")
82
+ }
83
+
84
+ EM.start_server options[:host], options[:port], Connection do |c|
85
+ c.boxes = boxes
86
+ end
87
+
88
+ EM.add_periodic_timer options[:every] do
89
+ `fetchmail`
90
+ end
91
+ }
@@ -0,0 +1,38 @@
1
+ #! /usr/bin/env ruby
2
+ require 'socket'
3
+ require 'json'
4
+ require 'optparse'
5
+
6
+ options = {}
7
+
8
+ OptionParser.new do |o|
9
+ options[:host] = 'localhost'
10
+ options[:port] = 9001
11
+ options[:target] = '*'
12
+
13
+ o.on '-h', '--host HOST', 'the host to connect to' do |value|
14
+ options[:host] = value
15
+ end
16
+
17
+ o.on '-p', '--port PORT', 'the port to connect to' do |value|
18
+ options[:port] = value.to_i
19
+ end
20
+
21
+ o.on '-t', '--target TARGET', 'the mailboxes to query' do |value|
22
+ options[:target] = value
23
+ end
24
+ end.parse!
25
+
26
+ socket = TCPSocket.new(options[:host], options[:port]) rescue abort('could not connect')
27
+ command = ARGV.shift
28
+
29
+ if command == 'list'
30
+ command = ARGV.shift
31
+
32
+ if command == 'unread'
33
+ socket.puts("#{options[:target]} list unread")
34
+ puts JSON.parse(socket.gets).join("\n")
35
+ end
36
+ end
37
+
38
+ socket.close
@@ -0,0 +1,20 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ require 'mbox/mbox'
@@ -0,0 +1,126 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ require 'mbox/mail/metadata'
21
+ require 'mbox/mail/headers'
22
+ require 'mbox/mail/content'
23
+
24
+ class Mbox
25
+
26
+ class Mail
27
+ def self.parse (input, options = {})
28
+ metadata = Mbox::Mail::Metadata.new
29
+ headers = Mbox::Mail::Headers.new
30
+ content = Mbox::Mail::Content.new(headers)
31
+
32
+ inside = {
33
+ metadata: true,
34
+ headers: false,
35
+ content: false
36
+ }
37
+
38
+ last = {
39
+ line: '',
40
+ stuff: ''
41
+ }
42
+
43
+ next until input.eof? || (line = input.readline).match(options[:separator])
44
+
45
+ return if !line || line.empty?
46
+
47
+ metadata.parse_from line
48
+
49
+ until input.eof? || ((line = input.readline).match(options[:separator]) && last[:line].empty?)
50
+ if inside[:metadata]
51
+ if line.match(/^>+/)
52
+ metadata.parse_from line
53
+ else
54
+ inside[:metadata] = false
55
+ inside[:headers] = true
56
+
57
+ last[:line] = line.chomp
58
+
59
+ next
60
+ end
61
+ elsif inside[:headers]
62
+ if line.strip.empty?
63
+ inside[:headers] = false
64
+ inside[:content] = true
65
+
66
+ headers.parse(last[:stuff])
67
+
68
+ last[:line] = line.chomp
69
+ last[:stuff] = ''
70
+
71
+ next
72
+ end
73
+
74
+ last[:stuff] << line
75
+ elsif inside[:content]
76
+ if options[:headers_only]
77
+ last[:line] = line.chomp
78
+
79
+ next
80
+ end
81
+
82
+ last[:stuff] << line
83
+ end
84
+
85
+ last[:line] = line.chomp
86
+ end
87
+
88
+ unless last[:stuff].empty?
89
+ content.parse(last[:stuff])
90
+ end
91
+
92
+ if !input.eof? && line
93
+ input.seek(-line.length, IO::SEEK_CUR)
94
+ end
95
+
96
+ Mail.new(metadata, headers, content)
97
+ end
98
+
99
+ attr_reader :metadata, :headers, :content
100
+
101
+ def initialize (metadata, headers, content)
102
+ @metadata = metadata
103
+ @headers = headers
104
+ @content = content
105
+ end
106
+
107
+ def save_to (path)
108
+ File.open(path, 'w') {|f|
109
+ f.write to_s
110
+ }
111
+ end
112
+
113
+ def unread?
114
+ !headers[:status].read? rescue true
115
+ end
116
+
117
+ def to_s
118
+ "#{headers}\n#{content}"
119
+ end
120
+
121
+ def inspect
122
+ "#<Mail:#{headers['From']}>"
123
+ end
124
+ end
125
+
126
+ end
@@ -0,0 +1,78 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ require 'mbox/mail/file'
21
+
22
+ class Mbox; class Mail
23
+
24
+ class Content < Array
25
+ attr_reader :headers, :attachments
26
+
27
+ def initialize (headers, content = [], attachments = [])
28
+ @headers = headers
29
+ @attachments = attachments
30
+
31
+ push *content
32
+ end
33
+
34
+ def parse (text, headers = {})
35
+ headers = @headers.merge(headers)
36
+ type = headers[:content_type]
37
+
38
+ if matches = type.mime.match(%r{multipart/(\w+)})
39
+ text.sub(/^.*?--#{type.boundary}\n/m, '').sub(/--#{type.boundary}--$/m, '').split("--#{type.boundary}\n").each {|part|
40
+ stream = StringIO.new(part)
41
+
42
+ headers = ''
43
+ until stream.eof? || (line = stream.readline).chomp.empty?
44
+ headers << line
45
+ end
46
+ headers = Headers.parse(headers)
47
+
48
+ content = !stream.eof? ? stream.readline : ''
49
+ until stream.eof? || line = stream.readline
50
+ content << line
51
+ end
52
+ content.chomp!
53
+
54
+ file = File.new(headers, content)
55
+
56
+ if (headers[:content_disposition] || '').match(/^attachment/)
57
+ attachments << file
58
+ else
59
+ self << file
60
+ end
61
+ }
62
+ else
63
+ stream = StringIO.new(text)
64
+
65
+ content = (!stream.eof?) ? stream.readline : ''
66
+ until stream.eof? || line = stream.readline
67
+ content << line
68
+ end
69
+ content.chomp!
70
+
71
+ self << File.new(Headers.new, content)
72
+ end
73
+
74
+ self
75
+ end
76
+ end
77
+
78
+ end; end
@@ -0,0 +1,55 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ require 'base64'
21
+
22
+ class Mbox; class Mail
23
+
24
+ class File
25
+ attr_reader :name, :headers, :content
26
+
27
+ def initialize (headers, content)
28
+ if headers[:content_type]
29
+ content.force_encoding headers[:content_type].charset
30
+ end
31
+
32
+ if headers[:content_transfer_encoding] == 'base64'
33
+ content = Base64.decode64(content)
34
+ end
35
+
36
+ if matches = headers[:content_disposition].match(/filename="(.*?)"/) rescue nil
37
+ @name = matches[1]
38
+ end
39
+
40
+ @headers = headers
41
+ @content = content
42
+ end
43
+
44
+ def to_s
45
+ @content
46
+ end
47
+
48
+ alias to_str to_s
49
+
50
+ def inspect
51
+ "#<File:#{name}>"
52
+ end
53
+ end
54
+
55
+ end; end
@@ -0,0 +1,146 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ require 'stringio'
21
+
22
+ require 'mbox/mail/headers/status'
23
+ require 'mbox/mail/headers/content_type'
24
+
25
+ class Mbox; class Mail
26
+
27
+ class Headers
28
+ def self.name_to_symbol (name)
29
+ return name if name.is_a? Symbol
30
+
31
+ name = name.to_s.downcase.gsub('-', '_').to_sym
32
+
33
+ if name.empty?
34
+ raise ArgumentError, 'cannot pass empty name'
35
+ end
36
+
37
+ name
38
+ end
39
+
40
+ def self.symbol_to_name (name)
41
+ name.to_s.downcase.gsub('_', '-').gsub(/(\A|-)(.)/) {|match|
42
+ match.upcase
43
+ }
44
+ end
45
+
46
+ def self.parse (input)
47
+ new.parse(input)
48
+ end
49
+
50
+ include Enumerable
51
+
52
+ def initialize (start = {})
53
+ @data = {}
54
+
55
+ merge! start
56
+ end
57
+
58
+ def each (&block)
59
+ @data.each(&block)
60
+ end
61
+
62
+ def [] (name)
63
+ @data[Headers.name_to_symbol(name)]
64
+ end
65
+
66
+ def []= (name, value)
67
+ name = Headers.name_to_symbol(name)
68
+
69
+ value = case name
70
+ when :status then Status.parse(value)
71
+ when :content_type then ContentType.parse(value)
72
+ else value
73
+ end
74
+
75
+ if tmp = @data[name] && !tmp.is_a?(Array)
76
+ @data[name] = [tmp]
77
+ end
78
+
79
+ if @data[name].is_a?(Array)
80
+ @data[name] << value
81
+ else
82
+ @data[name] = value
83
+ end
84
+ end
85
+
86
+ def delete (name)
87
+ @data.delete(Headers.name_to_symbol(name))
88
+ end
89
+
90
+ def merge! (other)
91
+ other.each {|name, value|
92
+ self[name] = value
93
+ }
94
+
95
+ self
96
+ end
97
+
98
+ def merge (other)
99
+ clone.merge!(other)
100
+ end
101
+
102
+ def parse (input)
103
+ input = if input.respond_to? :to_io
104
+ input.to_io
105
+ elsif input.is_a? String
106
+ StringIO.new(input)
107
+ else
108
+ raise ArgumentError, 'I do not know what to do.'
109
+ end
110
+
111
+ last = nil
112
+
113
+ until input.eof? || (line = input.readline).chomp.empty?
114
+ if !line.match(/^\s/)
115
+ next unless matches = line.match(/^([^:]+):\s*(.+)$/)
116
+
117
+ whole, name, value = matches.to_a
118
+
119
+ self[name] = value
120
+ last = name
121
+ elsif self[last]
122
+ if self[last].is_a?(String)
123
+ self[last] << " #{line}"
124
+ elsif self[last].is_a?(Array)
125
+ self[last].last << " #{line}"
126
+ end
127
+ end
128
+ end
129
+
130
+ self
131
+ end
132
+
133
+ def to_s
134
+ result = ''
135
+
136
+ each {|name, values|
137
+ [values].flatten.each {|value|
138
+ result << "#{Headers.symbol_to_name(name)}: #{value}\n"
139
+ }
140
+ }
141
+
142
+ result
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,56 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ class Mbox; class Mail; class Headers
21
+
22
+ class ContentType
23
+ def self.parse (text)
24
+ return text if text.is_a?(ContentType)
25
+
26
+ return ContentType.new unless text && text.is_a?(String)
27
+
28
+ stuff = text.gsub(/\n\r/, '').split(/\s*;\s*/)
29
+ type = stuff.shift
30
+
31
+ ContentType.new(Hash[stuff.map {|stuff|
32
+ stuff = stuff.strip.split(/=/)
33
+ stuff[0] = stuff[0].to_sym
34
+
35
+ if stuff[1][0] == '"' && stuff[1][stuff[1].length-1] == '"'
36
+ stuff[1] = stuff[1][1, stuff[1].length-2]
37
+ end
38
+
39
+ stuff
40
+ }].merge(mime: type))
41
+ end
42
+
43
+ attr_accessor :mime, :charset, :boundary
44
+
45
+ def initialize (data = {})
46
+ @mime = data[:mime] || 'text/plain'
47
+ @charset = data[:charset]
48
+ @boundary = data[:boundary]
49
+ end
50
+
51
+ def to_s
52
+ "#{mime}#{"; charset=#{charset}" if charset}#{"; boundary=#{boundary}" if boundary}"
53
+ end
54
+ end
55
+
56
+ end; end; end
@@ -0,0 +1,46 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ class Mbox; class Mail; class Headers
21
+
22
+ class Status
23
+ def self.parse (text)
24
+ return text if text.is_a?(self)
25
+
26
+ return Status.new unless text && text.is_a?(String)
27
+
28
+ Status.new(text.include?('R'), text.include?('O'))
29
+ end
30
+
31
+ def initialize (read = false, old = false)
32
+ @read = read
33
+ @old = old
34
+ end
35
+
36
+ def read?; @read; end
37
+ def old?; @old; end
38
+
39
+ def unread?; !read?; end
40
+
41
+ def to_s
42
+ "#{'R' if read?}#{'O' if old?}"
43
+ end
44
+ end
45
+
46
+ end; end; end
@@ -0,0 +1,36 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ class Mbox; class Mail
21
+
22
+ class Metadata
23
+ attr_reader :from
24
+
25
+ def initialize
26
+ @from = []
27
+ end
28
+
29
+ def parse_from (line)
30
+ line.match /^>*From ([^\s]+) (.{24})/ do |m|
31
+ @from << Struct.new(:name, :date).new(m[1], m[2])
32
+ end
33
+ end
34
+ end
35
+
36
+ end; end
@@ -0,0 +1,140 @@
1
+ #--
2
+ # Copyleft meh. [http://meh.doesntexist.org | meh@paranoici.org]
3
+ #
4
+ # This file is part of ruby-mbox.
5
+ #
6
+ # ruby-mbox is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU Affero General Public License as published
8
+ # by the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # ruby-mbox is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Affero General Public License
17
+ # along with ruby-mbox. If not, see <http://www.gnu.org/licenses/>.
18
+ #++
19
+
20
+ require 'stringio'
21
+
22
+ require 'mbox/mail'
23
+
24
+ class Mbox
25
+ def self.open (path, options = {})
26
+ input = File.open(File.expand_path(path), 'r+:ASCII-8BIT')
27
+
28
+ Mbox.new(input, options).tap {|mbox|
29
+ mbox.path = path
30
+ mbox.name = File.basename(path)
31
+
32
+ if block_given?
33
+ yield mbox
34
+
35
+ mbox.close
36
+ else
37
+ ObjectSpace.define_finalizer mbox, finalizer(input)
38
+ end
39
+ }
40
+ end
41
+
42
+ def self.finalizer (io)
43
+ proc { io.close }
44
+ end
45
+
46
+ include Enumerable
47
+
48
+ attr_reader :options
49
+ attr_accessor :name, :path
50
+
51
+ def initialize (what, options = {})
52
+ @input = if what.respond_to? :to_io
53
+ what.to_io
54
+ elsif what.is_a? String
55
+ StringIO.new(what)
56
+ else
57
+ raise ArgumentError, 'I do not know what to do.'
58
+ end
59
+
60
+ @options = { separator: /^From [^\s]+ .{24}/ }.merge(options)
61
+ end
62
+
63
+ def close
64
+ @input.close
65
+ end
66
+
67
+ def each (opts = {})
68
+ @input.seek 0
69
+
70
+ while mail = Mail.parse(@input, options.merge(opts))
71
+ yield mail
72
+ end
73
+ end
74
+
75
+ def [] (index, opts = {})
76
+ seek index
77
+
78
+ if @input.eof?
79
+ raise IndexError, "#{index} is out of range"
80
+ end
81
+
82
+ Mail.parse(@input, options.merge(opts))
83
+ end
84
+
85
+ def seek (to, whence = IO::SEEK_SET)
86
+ if whence == IO::SEEK_SET
87
+ @input.seek 0
88
+ end
89
+
90
+ last = ''
91
+ index = -1
92
+
93
+ while line = @input.readline rescue nil
94
+ if line.match(options[:separator]) && last.chomp.empty?
95
+ index += 1
96
+
97
+ if index >= to
98
+ @input.seek(-line.length, IO::SEEK_CUR)
99
+
100
+ break
101
+ end
102
+ end
103
+
104
+ last = line
105
+ end
106
+
107
+ self
108
+ end
109
+
110
+ def length
111
+ @input.seek(0)
112
+
113
+ last = ''
114
+ length = 0
115
+
116
+ while line = @input.readline rescue nil
117
+ if line.match(options[:separator]) && last.chomp.empty?
118
+ length += 1
119
+ end
120
+
121
+ last = line
122
+ end
123
+
124
+ length
125
+ end
126
+
127
+ alias size length
128
+
129
+ def has_unread?
130
+ each headers_only: true do |mail|
131
+ return true if mail.unread?
132
+ end
133
+
134
+ false
135
+ end
136
+
137
+ def inspect
138
+ "#<Mbox:#{name} length=#{length}>"
139
+ end
140
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mbox
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - meh.
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-06 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A simple library to read mbox files.
15
+ email: meh@paranoici.org
16
+ executables:
17
+ - mbox-do
18
+ - mbox-daemon
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/mbox.rb
23
+ - lib/mbox/mbox.rb
24
+ - lib/mbox/mail.rb
25
+ - lib/mbox/mail/headers.rb
26
+ - lib/mbox/mail/metadata.rb
27
+ - lib/mbox/mail/file.rb
28
+ - lib/mbox/mail/content.rb
29
+ - lib/mbox/mail/headers/status.rb
30
+ - lib/mbox/mail/headers/content_type.rb
31
+ - bin/mbox-do
32
+ - bin/mbox-daemon
33
+ homepage: http://github.com/meh/ruby-mbox
34
+ licenses: []
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 1.8.23
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: A simple library to read mbox files.
57
+ test_files: []