mbox 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|