mbox 0.0.4
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.
- data/bin/mbox-daemon +91 -0
- data/bin/mbox-do +38 -0
- data/lib/mbox.rb +20 -0
- data/lib/mbox/mail.rb +126 -0
- data/lib/mbox/mail/content.rb +78 -0
- data/lib/mbox/mail/file.rb +55 -0
- data/lib/mbox/mail/headers.rb +146 -0
- data/lib/mbox/mail/headers/content_type.rb +56 -0
- data/lib/mbox/mail/headers/status.rb +46 -0
- data/lib/mbox/mail/metadata.rb +36 -0
- data/lib/mbox/mbox.rb +140 -0
- metadata +57 -0
data/bin/mbox-daemon
ADDED
@@ -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
|
+
}
|
data/bin/mbox-do
ADDED
@@ -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
|
data/lib/mbox.rb
ADDED
@@ -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'
|
data/lib/mbox/mail.rb
ADDED
@@ -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
|
data/lib/mbox/mbox.rb
ADDED
@@ -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: []
|