maildir 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.rdoc +8 -2
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/lib/maildir.rb +42 -23
- data/lib/maildir/keywords.rb +103 -0
- data/lib/maildir/message.rb +99 -36
- data/lib/maildir/subdirs.rb +65 -0
- data/maildir.gemspec +7 -2
- data/test/test_helper.rb +13 -11
- data/test/test_maildir.rb +35 -13
- data/test/test_message.rb +151 -94
- data/test/test_serializers.rb +1 -7
- metadata +14 -2
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -85,13 +85,15 @@ Maildir::Serializer::Base simply reads and writes strings to disk.
|
|
85
85
|
|
86
86
|
The Mail serializer takes a ruby Mail object (http://github.com/mikel/mail) and writes RFC2822 email messages.
|
87
87
|
|
88
|
+
require 'maildir/serializer/mail'
|
88
89
|
Maildir::Message.serializer = Maildir::Serializer::Mail.new
|
89
90
|
mail = Mail.new(...)
|
90
|
-
message = maildir.add(mail) # writes
|
91
|
+
message = maildir.add(mail) # writes an RFC2822 message to disk
|
91
92
|
message.data == mail # => true; data is parsed as a Mail object
|
92
93
|
|
93
|
-
The Marshal, JSON, and YAML serializers work similarly. E.g
|
94
|
+
The Marshal, JSON, and YAML serializers work similarly. E.g.:
|
94
95
|
|
96
|
+
require 'maildir/serializer/json'
|
95
97
|
Maildir::Message.serializer = Maildir::Serializer::JSON.new
|
96
98
|
my_data = {"foo" => nil, "my_array" => [1,2,3]}
|
97
99
|
message = maildir.add(my_data) # writes {"foo":null,"my_array":[1,2,3]}
|
@@ -102,6 +104,10 @@ It's trivial to create a custom serializer. Implement the following two methods:
|
|
102
104
|
load(path)
|
103
105
|
dump(data, path)
|
104
106
|
|
107
|
+
== Credits
|
108
|
+
|
109
|
+
niklas | brueckenschlaeger, http://github.com/nilclass added subdir & courierimapkeywords support
|
110
|
+
|
105
111
|
== Copyright
|
106
112
|
|
107
113
|
Copyright (c) 2010 Aaron Suggs. See LICENSE for details.
|
data/Rakefile
CHANGED
@@ -19,6 +19,7 @@ begin
|
|
19
19
|
gemspec.add_development_dependency "shoulda", ">= 0"
|
20
20
|
gemspec.add_development_dependency "mail", ">= 0"
|
21
21
|
gemspec.add_development_dependency "json", ">= 0"
|
22
|
+
gemspec.add_development_dependency "ktheory-fakefs", ">= 0"
|
22
23
|
end
|
23
24
|
Jeweler::GemcutterTasks.new
|
24
25
|
rescue LoadError
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/lib/maildir.rb
CHANGED
@@ -11,7 +11,9 @@ class Maildir
|
|
11
11
|
# Create a new maildir at +path+. If +create+ is true, will ensure that the
|
12
12
|
# required subdirectories exist.
|
13
13
|
def initialize(path, create = true)
|
14
|
-
@path = File.
|
14
|
+
@path = File.expand_path(path)
|
15
|
+
@path = File.join(@path, '/') # Ensure path has a trailing slash
|
16
|
+
@path_regexp = /^#{Regexp.quote(@path)}/ # For parsing directory listings
|
15
17
|
create_directories if create
|
16
18
|
end
|
17
19
|
|
@@ -25,6 +27,11 @@ class Maildir
|
|
25
27
|
self.path <=> maildir.path
|
26
28
|
end
|
27
29
|
|
30
|
+
# Friendly inspect method
|
31
|
+
def inspect
|
32
|
+
"#<#{self.class} path=#{@path}>"
|
33
|
+
end
|
34
|
+
|
28
35
|
# define methods tmp_path, new_path, & cur_path
|
29
36
|
SUBDIRS.each do |subdir|
|
30
37
|
define_method "#{subdir}_path" do
|
@@ -37,7 +44,8 @@ class Maildir
|
|
37
44
|
# exist.
|
38
45
|
def create_directories
|
39
46
|
SUBDIRS.each do |subdir|
|
40
|
-
|
47
|
+
subdir_path = File.join(path, subdir.to_s)
|
48
|
+
FileUtils.mkdir_p(subdir_path)
|
41
49
|
end
|
42
50
|
end
|
43
51
|
|
@@ -47,26 +55,24 @@ class Maildir
|
|
47
55
|
# E.g.
|
48
56
|
# maildir.list(:new) # => all new messages
|
49
57
|
# maildir.list(:cur, :limit => 10) # => 10 oldest messages in cur
|
50
|
-
def list(
|
51
|
-
|
52
|
-
|
53
|
-
raise ArgumentError, "first arg must be new or cur"
|
58
|
+
def list(dir, options = {})
|
59
|
+
unless SUBDIRS.include? dir.to_sym
|
60
|
+
raise ArgumentError, "dir must be :new, :cur, or :tmp"
|
54
61
|
end
|
55
62
|
|
56
|
-
keys = get_dir_listing(
|
57
|
-
|
58
|
-
# Map keys to message objects
|
59
|
-
messages = keys.map{|key| get(key)}
|
63
|
+
keys = get_dir_listing(dir)
|
60
64
|
|
61
|
-
# Sort the
|
65
|
+
# Sort the keys (chronological order)
|
62
66
|
# TODO: make sorting configurable
|
63
|
-
|
67
|
+
keys.sort!
|
64
68
|
|
65
|
-
# Apply the limit
|
69
|
+
# Apply the limit after sorting
|
66
70
|
if limit = options[:limit]
|
67
|
-
|
71
|
+
keys = keys[0,limit]
|
68
72
|
end
|
69
|
-
|
73
|
+
|
74
|
+
# Map keys to message objects
|
75
|
+
keys.map{|key| get(key)}
|
70
76
|
end
|
71
77
|
|
72
78
|
# Writes data object out as a new message. Returns a Maildir::Message. See
|
@@ -75,19 +81,32 @@ class Maildir
|
|
75
81
|
Maildir::Message.create(self, data)
|
76
82
|
end
|
77
83
|
|
84
|
+
# Returns a message object for key
|
78
85
|
def get(key)
|
79
86
|
Maildir::Message.new(self, key)
|
80
87
|
end
|
81
88
|
|
89
|
+
# Deletes the message for key by calling destroy() on the message.
|
90
|
+
def delete(key)
|
91
|
+
get(key).destroy
|
92
|
+
end
|
93
|
+
|
94
|
+
# Finds messages in the tmp folder that have not been modified since
|
95
|
+
# +time+. +time+ defaults to 36 hours ago.
|
96
|
+
def get_stale_tmp(time = Time.now - 129600)
|
97
|
+
list(:tmp).select do |message|
|
98
|
+
(mtime = message.mtime) && mtime < time
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
82
102
|
protected
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
#
|
88
|
-
|
89
|
-
|
90
|
-
message_path.sub!(@dir_listing_regexp, "")
|
103
|
+
# Returns an array of keys in dir
|
104
|
+
def get_dir_listing(dir)
|
105
|
+
search_path = File.join(self.path, dir.to_s, '*')
|
106
|
+
keys = Dir.glob(search_path)
|
107
|
+
# Remove the maildir's path from the keys
|
108
|
+
keys.each do |key|
|
109
|
+
key.sub!(@path_regexp, "")
|
91
110
|
end
|
92
111
|
end
|
93
112
|
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# implements IMAP Keywords as used by the Courier Mail Server
|
2
|
+
# see http://www.courier-mta.org/imap/README.imapkeywords.html for details
|
3
|
+
|
4
|
+
require 'ftools'
|
5
|
+
require 'maildir'
|
6
|
+
module Maildir::Keywords
|
7
|
+
def self.included(base)
|
8
|
+
Maildir::Message.send(:include, MessageExtension)
|
9
|
+
end
|
10
|
+
|
11
|
+
# process contents of courierimapkeywords/ directory as described in README.imapkeywords
|
12
|
+
def read_keywords
|
13
|
+
messages = (list(:cur) + list(:new)).inject({}) { |m, msg| m[msg.unique_name] = msg ; m }
|
14
|
+
t = Time.now.to_i / 300
|
15
|
+
keyword_dir = File.join(path, 'courierimapkeywords')
|
16
|
+
keywords = []
|
17
|
+
state = :head
|
18
|
+
# process :list
|
19
|
+
File.open(File.join(keyword_dir, ':list')).each_line do |line|
|
20
|
+
line.strip!
|
21
|
+
if state == :head
|
22
|
+
if line.empty?
|
23
|
+
state = :messages
|
24
|
+
next
|
25
|
+
end
|
26
|
+
keywords << line
|
27
|
+
else
|
28
|
+
key, ids = line.split(':')
|
29
|
+
if msg = messages[key]
|
30
|
+
msg.set_keywords(ids.split(/\s/).map {|id| keywords[id.to_i - 1] })
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
# collect keyword files
|
35
|
+
keyword_files = (Dir.entries(keyword_dir) - %w(. .. :list)).inject({}) do |keyword_files, file|
|
36
|
+
if file =~ /^\.(\d+)\.(.*)$/
|
37
|
+
n = $1
|
38
|
+
key = $2
|
39
|
+
else
|
40
|
+
n = t + 1
|
41
|
+
key = file
|
42
|
+
File.move(File.join(keyword_dir, file), File.join(keyword_dir, ".#{n}.#{key}"))
|
43
|
+
end
|
44
|
+
if msg = messages[key]
|
45
|
+
(keyword_files[key] ||= []) << [n, key]
|
46
|
+
else # message doesn't exist
|
47
|
+
fname = File.join(keyword_dir, file)
|
48
|
+
if File.stat(fname).ctime < (Time.now - (15 * 60))
|
49
|
+
File.unlink(fname)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# process keyword files
|
54
|
+
keyword_files.each_pair do |key, files|
|
55
|
+
files.sort! { |a, b| a[0] <=> b[0] }
|
56
|
+
files[0..-2].each { |f| File.unlink(File.join(keyword_dir, ".#{f.join('.')}")) } if n_files.last[0] < t
|
57
|
+
msg = messages[key]
|
58
|
+
current_keywords = File.read(files.last).read.split(/\s+/)
|
59
|
+
msg.set_keywords(current_keywords)
|
60
|
+
if (add = (current_keywords - keywords)).any?
|
61
|
+
keywords += add
|
62
|
+
end
|
63
|
+
end
|
64
|
+
# rebuild :list
|
65
|
+
@keywords = {}
|
66
|
+
tmp_file = File.join(path, 'tmp', ':list')
|
67
|
+
File.open(tmp_file, 'w') { |f|
|
68
|
+
f.write(keywords.join("\n")+"\n\n")
|
69
|
+
messages.each_pair do |key, msg|
|
70
|
+
next unless msg.keywords
|
71
|
+
f.puts([key, msg.keywords.map{|kw| keywords.index(kw) + 1 }.sort.join(' ')].join(':'))
|
72
|
+
@keywords[key] = msg.keywords
|
73
|
+
end
|
74
|
+
}
|
75
|
+
File.move(tmp_file, File.join(keyword_dir, ':list'))
|
76
|
+
end
|
77
|
+
|
78
|
+
def keywords(key)
|
79
|
+
read_keywords unless @keywords
|
80
|
+
@keywords[key] || []
|
81
|
+
end
|
82
|
+
|
83
|
+
module MessageExtension
|
84
|
+
def keywords
|
85
|
+
return @keywords if @keywords
|
86
|
+
@maildir.keywords(unique_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
# sets given keywords on the message.
|
90
|
+
def keywords=(list)
|
91
|
+
tmp_fname = File.join(maildir.path, 'tmp', unique_name)
|
92
|
+
File.open(tmp_fname, 'w') { |f| f.write(list.join("\n")) }
|
93
|
+
File.move(tmp_fname, File.join(maildir.path, 'courierimapkeywords', unique_name))
|
94
|
+
end
|
95
|
+
|
96
|
+
# sets @keywords to the given list
|
97
|
+
def set_keywords(list)
|
98
|
+
@keywords = list
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
Maildir.send(:include, Maildir::Keywords)
|
data/lib/maildir/message.rb
CHANGED
@@ -6,27 +6,33 @@ class Maildir::Message
|
|
6
6
|
|
7
7
|
include Comparable
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
9
|
+
# Create a new message in maildir with data.
|
10
|
+
# The message is first written to the tmp dir, then moved to new. This is
|
11
|
+
# a shortcut for:
|
12
|
+
# message = Maildir::Message.new(maildir)
|
13
|
+
# message.write(data)
|
14
|
+
def self.create(maildir, data)
|
15
|
+
message = self.new(maildir)
|
16
|
+
message.write(data)
|
17
|
+
message
|
18
|
+
end
|
19
|
+
|
20
|
+
# The serializer processes data before it is written to disk and after
|
21
|
+
# reading from disk.
|
22
|
+
# Default serializer
|
23
|
+
@@serializer = Maildir::Serializer::Base.new
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
# Get the serializer
|
26
|
+
def self.serializer
|
27
|
+
@@serializer
|
24
28
|
end
|
25
29
|
|
26
|
-
#
|
27
|
-
|
30
|
+
# Set the serializer
|
31
|
+
def self.serializer=(serializer)
|
32
|
+
@@serializer = serializer
|
33
|
+
end
|
28
34
|
|
29
|
-
attr_reader :dir, :unique_name, :info
|
35
|
+
attr_reader :dir, :unique_name, :info
|
30
36
|
|
31
37
|
# Create a new, unwritten message or instantiate an existing message.
|
32
38
|
# If key is nil, create a new message:
|
@@ -39,6 +45,7 @@ class Maildir::Message
|
|
39
45
|
@maildir = maildir
|
40
46
|
if key.nil?
|
41
47
|
@dir = :tmp
|
48
|
+
@unique_name = Maildir::UniqueName.create
|
42
49
|
else
|
43
50
|
parse_key(key)
|
44
51
|
end
|
@@ -46,10 +53,6 @@ class Maildir::Message
|
|
46
53
|
unless Maildir::SUBDIRS.include? dir
|
47
54
|
raise ArgumentError, "State must be in #{Maildir::SUBDIRS.inspect}"
|
48
55
|
end
|
49
|
-
|
50
|
-
if :tmp == dir
|
51
|
-
@unique_name = Maildir::UniqueName.create
|
52
|
-
end
|
53
56
|
end
|
54
57
|
|
55
58
|
# Compares messages by their paths.
|
@@ -62,9 +65,14 @@ class Maildir::Message
|
|
62
65
|
self.path <=> message.path
|
63
66
|
end
|
64
67
|
|
68
|
+
# Friendly inspect method
|
69
|
+
def inspect
|
70
|
+
"#<#{self.class} key=#{key} maildir=#{@maildir.inspect}>"
|
71
|
+
end
|
72
|
+
|
65
73
|
# Returns the class' serializer
|
66
74
|
def serializer
|
67
|
-
|
75
|
+
@@serializer
|
68
76
|
end
|
69
77
|
|
70
78
|
# Writes data to disk. Can only be called on messages instantiated without
|
@@ -88,32 +96,53 @@ class Maildir::Message
|
|
88
96
|
end
|
89
97
|
|
90
98
|
# Set info on a message.
|
91
|
-
# Returns the message's key
|
99
|
+
# Returns the message's key if successful, false otherwise.
|
92
100
|
def info=(info)
|
93
101
|
raise "Can only set info on cur messages" unless :cur == @dir
|
94
102
|
rename(:cur, info)
|
95
103
|
end
|
96
104
|
|
105
|
+
FLAG_NAMES = {
|
106
|
+
:passed => 'P',
|
107
|
+
:replied => 'R',
|
108
|
+
:seen => 'S',
|
109
|
+
:trashed => 'T',
|
110
|
+
:draft => 'D',
|
111
|
+
:flagged => 'F'
|
112
|
+
}
|
113
|
+
|
114
|
+
FLAG_NAMES.each_pair do |key, value|
|
115
|
+
define_method("#{key}?".to_sym) do
|
116
|
+
flags.include?(value)
|
117
|
+
end
|
118
|
+
define_method("#{key}!".to_sym) do
|
119
|
+
add_flag(value)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
97
123
|
# Returns an array of single letter flags applied to the message
|
98
124
|
def flags
|
99
125
|
@info.sub(INFO,'').split('')
|
100
126
|
end
|
101
127
|
|
102
128
|
# Sets the flags on a message.
|
103
|
-
# Returns the message's key
|
129
|
+
# Returns the message's key if successful, false otherwise.
|
104
130
|
def flags=(*flags)
|
105
131
|
self.info = INFO + sort_flags(flags.flatten.join(''))
|
106
132
|
end
|
107
133
|
|
134
|
+
# Adds a flag to a message.
|
135
|
+
# Returns the message's key if successful, false otherwise.
|
108
136
|
def add_flag(flag)
|
109
137
|
self.flags = (flags << flag.upcase)
|
110
138
|
end
|
111
|
-
|
139
|
+
|
140
|
+
# Removes a flag from a message.
|
141
|
+
# Returns the message's key if successful, false otherwise.
|
112
142
|
def remove_flag(flag)
|
113
143
|
self.flags = flags.delete_if{|f| f == flag.upcase}
|
114
144
|
end
|
115
145
|
|
116
|
-
|
117
146
|
# Returns the filename of the message
|
118
147
|
def filename
|
119
148
|
[unique_name, info].compact.join(COLON)
|
@@ -129,19 +158,55 @@ class Maildir::Message
|
|
129
158
|
File.join(@maildir.path, key)
|
130
159
|
end
|
131
160
|
|
132
|
-
# Returns the message's data from disk
|
161
|
+
# Returns the message's data from disk.
|
162
|
+
# If the path doesn't exist, freeze's the object and raises Errno:ENOENT
|
133
163
|
def data
|
134
|
-
serializer.load(path)
|
164
|
+
guard(true) { serializer.load(path) }
|
165
|
+
end
|
166
|
+
|
167
|
+
# Updates the modification and access time. Returns 1 if successful, false
|
168
|
+
# otherwise.
|
169
|
+
def utime(atime, mtime)
|
170
|
+
guard { File.utime(atime, mtime, path) }
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the message's atime, or false if the file doesn't exist.
|
174
|
+
def atime
|
175
|
+
guard { File.atime(path) }
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns the message's mtime, or false if the file doesn't exist.
|
179
|
+
def mtime
|
180
|
+
guard { File.mtime(path) }
|
135
181
|
end
|
136
182
|
|
137
183
|
# Deletes the message path and freezes the message object
|
138
184
|
def destroy
|
139
|
-
File.delete(path)
|
185
|
+
guard { File.delete(path) }
|
140
186
|
freeze
|
141
187
|
end
|
142
188
|
|
143
189
|
protected
|
144
190
|
|
191
|
+
# Guard access to the file system by rescuing Errno::ENOENT, which happens
|
192
|
+
# if the file is missing. When +blocks+ fails and +reraise+ is false, returns
|
193
|
+
# false, otherwise reraises Errno::ENOENT
|
194
|
+
def guard(reraise = false, &block)
|
195
|
+
begin
|
196
|
+
yield
|
197
|
+
rescue Errno::ENOENT
|
198
|
+
if @old_key
|
199
|
+
# Restore ourselves to the old state
|
200
|
+
parse_key(@old_key)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Don't allow further modifications
|
204
|
+
freeze
|
205
|
+
|
206
|
+
reraise ? raise : false
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
145
210
|
# Sets dir, unique_name, and info based on the key
|
146
211
|
def parse_key(key)
|
147
212
|
@dir, filename = key.split(File::SEPARATOR)
|
@@ -155,24 +220,22 @@ class Maildir::Message
|
|
155
220
|
end
|
156
221
|
|
157
222
|
def old_path
|
158
|
-
File.join(@maildir.path, old_key)
|
223
|
+
File.join(@maildir.path, @old_key)
|
159
224
|
end
|
160
225
|
|
226
|
+
# Renames the message. Returns the new key if successful, false otherwise.
|
161
227
|
def rename(new_dir, new_info=nil)
|
162
|
-
#
|
228
|
+
# Save the old key so we can revert to the old state
|
163
229
|
@old_key = key
|
164
230
|
|
165
231
|
# Set the new state
|
166
232
|
@dir = new_dir
|
167
233
|
@info = new_info if new_info
|
168
234
|
|
169
|
-
|
235
|
+
guard do
|
170
236
|
File.rename(old_path, path) unless old_path == path
|
237
|
+
@old_key = nil # So guard() doesn't reset to a bad state
|
171
238
|
return key
|
172
|
-
rescue Errno::ENOENT
|
173
|
-
# Restore ourselves to the old state
|
174
|
-
parse_key(@old_key)
|
175
|
-
raise
|
176
239
|
end
|
177
240
|
end
|
178
241
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# implements subdirs as used by the Courier Mail Server (courier-mta.org)
|
2
|
+
require 'maildir'
|
3
|
+
module Maildir::Subdirs
|
4
|
+
ROOT_NAME = 'INBOX'
|
5
|
+
DELIM = '.'
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.instance_eval do
|
9
|
+
alias_method :inspect_without_subdirs, :inspect
|
10
|
+
alias_method :inspect, :inspect_with_subdirs
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_subdir(name)
|
15
|
+
raise ArgumentError.new("'name' may not contain delimiter character (#{DELIM})") if name.include?(DELIM)
|
16
|
+
full_name = (root? ? [] : subdir_parts(File.basename(path))).push(name).unshift('').join(DELIM)
|
17
|
+
md = Maildir.new(File.join(path, full_name), true)
|
18
|
+
@subdirs << md if @subdirs
|
19
|
+
md
|
20
|
+
end
|
21
|
+
|
22
|
+
# returns the logical mailbox path
|
23
|
+
def mailbox_path
|
24
|
+
@mailbox_path ||= root? ? ROOT_NAME : subdir_parts(File.basename(path)).unshift(ROOT_NAME).join(DELIM)
|
25
|
+
end
|
26
|
+
|
27
|
+
# returns an array of Maildir objects representing the direct subdirectories of this Maildir
|
28
|
+
def subdirs(only_direct=true)
|
29
|
+
if root?
|
30
|
+
@subdirs ||= (Dir.entries(path) - %w(. ..)).select {|e|
|
31
|
+
e =~ /^\./ && File.directory?(File.join(path, e)) && (only_direct ? subdir_parts(e).size == 1 : true)
|
32
|
+
}.map { |e| Maildir.new(File.join(path, e), false) }
|
33
|
+
else
|
34
|
+
my_parts = subdir_parts(File.basename(path))
|
35
|
+
@subdirs ||= root.subdirs(false).select { |md| subdir_parts(File.basename(md.path))[0..-2] == my_parts }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Friendly inspect method
|
40
|
+
def inspect_with_subdirs
|
41
|
+
"#<#{self.class} path=#{@path} mailbox_path=#{mailbox_path}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
# returns the Maildir representing the root directory
|
45
|
+
def root
|
46
|
+
root? ? self : Maildir.new(File.dirname(path), false)
|
47
|
+
end
|
48
|
+
|
49
|
+
# returns true if the parent directory doesn't look like a maildir
|
50
|
+
def root?
|
51
|
+
! ((Dir.entries(File.dirname(path)) & %w(cur new tmp)).size == 3)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def subdir_parts(path)
|
57
|
+
parts = (path.split(DELIM) - [''])
|
58
|
+
# some clients (e.g. Thunderbird) mess up namespaces so subdirs
|
59
|
+
# end up looking like '.INBOX.Trash' instead of '.Trash'
|
60
|
+
parts.shift if parts.first == ROOT_NAME
|
61
|
+
parts
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
Maildir.send(:include, Maildir::Subdirs)
|
data/maildir.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{maildir}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.4.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Aaron Suggs"]
|
12
|
-
s.date = %q{2010-01-
|
12
|
+
s.date = %q{2010-01-24}
|
13
13
|
s.description = %q{A ruby library for reading and writing arbitrary messages in DJB's maildir format}
|
14
14
|
s.email = %q{aaron@ktheory.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -24,12 +24,14 @@ Gem::Specification.new do |s|
|
|
24
24
|
"VERSION",
|
25
25
|
"benchmarks/runner",
|
26
26
|
"lib/maildir.rb",
|
27
|
+
"lib/maildir/keywords.rb",
|
27
28
|
"lib/maildir/message.rb",
|
28
29
|
"lib/maildir/serializer/base.rb",
|
29
30
|
"lib/maildir/serializer/json.rb",
|
30
31
|
"lib/maildir/serializer/mail.rb",
|
31
32
|
"lib/maildir/serializer/marshal.rb",
|
32
33
|
"lib/maildir/serializer/yaml.rb",
|
34
|
+
"lib/maildir/subdirs.rb",
|
33
35
|
"lib/maildir/unique_name.rb",
|
34
36
|
"maildir.gemspec",
|
35
37
|
"test/test_helper.rb",
|
@@ -59,15 +61,18 @@ Gem::Specification.new do |s|
|
|
59
61
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
60
62
|
s.add_development_dependency(%q<mail>, [">= 0"])
|
61
63
|
s.add_development_dependency(%q<json>, [">= 0"])
|
64
|
+
s.add_development_dependency(%q<ktheory-fakefs>, [">= 0"])
|
62
65
|
else
|
63
66
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
64
67
|
s.add_dependency(%q<mail>, [">= 0"])
|
65
68
|
s.add_dependency(%q<json>, [">= 0"])
|
69
|
+
s.add_dependency(%q<ktheory-fakefs>, [">= 0"])
|
66
70
|
end
|
67
71
|
else
|
68
72
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
69
73
|
s.add_dependency(%q<mail>, [">= 0"])
|
70
74
|
s.add_dependency(%q<json>, [">= 0"])
|
75
|
+
s.add_dependency(%q<ktheory-fakefs>, [">= 0"])
|
71
76
|
end
|
72
77
|
end
|
73
78
|
|
data/test/test_helper.rb
CHANGED
@@ -1,24 +1,26 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'test/unit'
|
3
3
|
require 'shoulda'
|
4
|
-
require 'tmpdir'
|
5
|
-
require 'tempfile'
|
6
4
|
require 'fileutils'
|
7
5
|
require 'maildir'
|
8
6
|
|
7
|
+
# Require all the serializers
|
8
|
+
serializers = File.join(File.dirname(__FILE__), "..","lib","maildir","serializer","*")
|
9
|
+
Dir.glob(serializers).each do |serializer|
|
10
|
+
require serializer
|
11
|
+
end
|
12
|
+
|
13
|
+
# Require 'ktheory-fakefs' until issues 28, 29, and 30 are resolved in
|
14
|
+
# defunkt/fakefs. See http://github.com/defunkt/fakefs/issues
|
15
|
+
gem "ktheory-fakefs"
|
16
|
+
require 'fakefs'
|
17
|
+
|
9
18
|
# Create a reusable maildir that's cleaned up when the tests are done
|
10
19
|
def temp_maildir
|
11
|
-
|
12
|
-
|
13
|
-
dir_path = Dir.mktmpdir("maildir_test")
|
14
|
-
at_exit do
|
15
|
-
puts "Cleaning up temp maildir"
|
16
|
-
FileUtils.rm_r(dir_path)
|
17
|
-
end
|
18
|
-
$maildir = Maildir.new(dir_path)
|
20
|
+
Maildir.new("/tmp/maildir_test")
|
19
21
|
end
|
20
22
|
|
21
23
|
# Useful for testing that strings defined & not empty
|
22
24
|
def assert_not_empty(obj, msg='')
|
23
25
|
assert !obj.nil? && !obj.empty?, msg
|
24
|
-
end
|
26
|
+
end
|
data/test/test_maildir.rb
CHANGED
@@ -2,6 +2,10 @@ require 'test_helper'
|
|
2
2
|
class TestMaildir < Test::Unit::TestCase
|
3
3
|
|
4
4
|
context "A maildir" do
|
5
|
+
setup do
|
6
|
+
FakeFS::FileSystem.clear
|
7
|
+
end
|
8
|
+
|
5
9
|
should "have a path" do
|
6
10
|
assert_not_empty temp_maildir.path
|
7
11
|
end
|
@@ -13,31 +17,38 @@ class TestMaildir < Test::Unit::TestCase
|
|
13
17
|
end
|
14
18
|
end
|
15
19
|
|
20
|
+
should "expand paths" do
|
21
|
+
maildir = Maildir.new("~/test_maildir/")
|
22
|
+
expanded_path = File.expand_path("~/test_maildir")
|
23
|
+
expanded_path = File.join(expanded_path, "/")
|
24
|
+
assert_equal expanded_path, maildir.path
|
25
|
+
end
|
26
|
+
|
16
27
|
should "not create directories if specified" do
|
17
|
-
|
18
|
-
maildir = Maildir.new(tmp_dir, false)
|
28
|
+
maildir = Maildir.new("/maildir_without_subdirs", false)
|
19
29
|
%w(tmp new cur).each do |subdir|
|
20
30
|
subdir_path = maildir.send("#{subdir}_path")
|
21
31
|
assert !File.directory?(subdir_path), "Subdir #{subdir} exists"
|
22
32
|
end
|
23
|
-
FileUtils.rm_r(tmp_dir)
|
24
33
|
end
|
25
|
-
|
34
|
+
|
26
35
|
should "be identical to maildirs with the same path" do
|
27
36
|
new_maildir = Maildir.new(temp_maildir.path)
|
28
37
|
assert_equal temp_maildir.path, new_maildir.path
|
29
38
|
assert_equal temp_maildir, new_maildir
|
30
39
|
end
|
31
|
-
|
32
|
-
context "with a message" do
|
33
|
-
setup do
|
34
|
-
@message = temp_maildir.add("")
|
35
|
-
end
|
36
40
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
+
should "list a new message" do
|
42
|
+
@message = temp_maildir.add("")
|
43
|
+
messages = temp_maildir.list(:new)
|
44
|
+
assert_equal messages, [@message]
|
45
|
+
end
|
46
|
+
|
47
|
+
should "list a cur message" do
|
48
|
+
@message = temp_maildir.add("")
|
49
|
+
@message.process
|
50
|
+
messages = temp_maildir.list(:cur)
|
51
|
+
assert_equal messages, [@message]
|
41
52
|
end
|
42
53
|
end
|
43
54
|
|
@@ -48,4 +59,15 @@ class TestMaildir < Test::Unit::TestCase
|
|
48
59
|
end
|
49
60
|
end
|
50
61
|
|
62
|
+
context "Maildirs with stale messages in tmp" do
|
63
|
+
should "be found" do
|
64
|
+
stale_path = File.join(temp_maildir.path, "tmp", "stale_message")
|
65
|
+
File.open(stale_path, "w"){|f| f.write("")}
|
66
|
+
stale_time = Time.now - 30*24*60*60 # 1 month ago
|
67
|
+
File.utime(stale_time, stale_time, stale_path)
|
68
|
+
|
69
|
+
stale_tmp = [temp_maildir.get("tmp/stale_message")]
|
70
|
+
assert_equal stale_tmp, temp_maildir.get_stale_tmp
|
71
|
+
end
|
72
|
+
end
|
51
73
|
end
|
data/test/test_message.rb
CHANGED
@@ -3,13 +3,10 @@ class TestMessage < Test::Unit::TestCase
|
|
3
3
|
|
4
4
|
context "An new, unwritten message" do
|
5
5
|
setup do
|
6
|
+
FakeFS::FileSystem.clear
|
6
7
|
@message = Maildir::Message.new(temp_maildir)
|
7
8
|
end
|
8
9
|
|
9
|
-
should "be instantiated" do
|
10
|
-
assert @message
|
11
|
-
end
|
12
|
-
|
13
10
|
should "be in :tmp" do
|
14
11
|
assert_equal :tmp, @message.dir
|
15
12
|
assert_match(/tmp/, @message.path)
|
@@ -32,138 +29,198 @@ class TestMessage < Test::Unit::TestCase
|
|
32
29
|
@message.info= "2,FRS"
|
33
30
|
end
|
34
31
|
end
|
32
|
+
end
|
35
33
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
34
|
+
context "A written message" do
|
35
|
+
setup do
|
36
|
+
FakeFS::FileSystem.clear
|
37
|
+
@message = Maildir::Message.new(temp_maildir)
|
38
|
+
@data = "foo"
|
39
|
+
@message.write(@data)
|
40
|
+
end
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
42
|
+
should "not be writable" do
|
43
|
+
assert_raise RuntimeError do
|
44
|
+
@message.write("nope!")
|
46
45
|
end
|
46
|
+
end
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
48
|
+
should "have no info" do
|
49
|
+
assert_nil @message.info
|
50
|
+
end
|
51
51
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
52
|
+
should "not be able to set info" do
|
53
|
+
assert_raises RuntimeError do
|
54
|
+
@message.info= "2,FRS"
|
56
55
|
end
|
56
|
+
end
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
should "be in new" do
|
59
|
+
assert_equal :new, @message.dir
|
60
|
+
assert_match(/new/, @message.path)
|
61
|
+
end
|
62
62
|
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
should "have a file" do
|
64
|
+
assert File.exists?(@message.path)
|
65
|
+
end
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
end
|
67
|
+
should "have the correct data" do
|
68
|
+
assert_equal @data, @message.data
|
70
69
|
end
|
71
70
|
end
|
72
71
|
|
73
|
-
context "A
|
72
|
+
context "A processed message" do
|
74
73
|
setup do
|
75
|
-
|
74
|
+
FakeFS::FileSystem.clear
|
75
|
+
@data = "foo"
|
76
76
|
@message = Maildir::Message.create(temp_maildir, @data)
|
77
|
+
@message.process
|
77
78
|
end
|
78
79
|
|
79
|
-
should "
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
context "when processed" do
|
84
|
-
setup do
|
85
|
-
@message.process
|
80
|
+
should "not be writable" do
|
81
|
+
assert_raise RuntimeError do
|
82
|
+
@message.write("nope!")
|
86
83
|
end
|
84
|
+
end
|
87
85
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
end
|
92
|
-
end
|
86
|
+
should "be in cur" do
|
87
|
+
assert_equal :cur, @message.dir
|
88
|
+
end
|
93
89
|
|
94
|
-
|
95
|
-
|
96
|
-
|
90
|
+
should "have info" do
|
91
|
+
assert_equal Maildir::Message::INFO, @message.info
|
92
|
+
end
|
97
93
|
|
98
|
-
|
99
|
-
|
100
|
-
|
94
|
+
should "set info" do
|
95
|
+
info = "2,FRS"
|
96
|
+
@message.info = "2,FRS"
|
97
|
+
assert_equal @message.info, info
|
98
|
+
assert_match /#{info}$/, @message.path
|
99
|
+
end
|
101
100
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
assert_equal @message.info, info
|
106
|
-
assert_match /#{info}$/, @message.path
|
107
|
-
end
|
101
|
+
should "add and remove flags" do
|
102
|
+
@message.add_flag('S')
|
103
|
+
assert_equal ['S'], @message.flags
|
108
104
|
|
109
|
-
|
110
|
-
|
111
|
-
|
105
|
+
# Test lowercase
|
106
|
+
@message.add_flag('r')
|
107
|
+
assert_equal ['R', 'S'], @message.flags
|
112
108
|
|
113
|
-
|
114
|
-
|
115
|
-
assert_equal ['R', 'S'], @message.flags
|
109
|
+
@message.remove_flag('S')
|
110
|
+
assert_equal ['R'], @message.flags
|
116
111
|
|
117
|
-
|
118
|
-
|
112
|
+
# Test lowercase
|
113
|
+
@message.remove_flag('r')
|
114
|
+
assert_equal [], @message.flags
|
115
|
+
end
|
119
116
|
|
120
|
-
|
121
|
-
|
122
|
-
|
117
|
+
flag_tests = {
|
118
|
+
"FRS" => ['F', 'R', 'S'],
|
119
|
+
"Sr" => ['R', 'S'], # test capitalization & sorting
|
120
|
+
'' => []
|
121
|
+
}
|
122
|
+
flag_tests.each do |arg, results|
|
123
|
+
should "set flags: #{arg}" do
|
124
|
+
@message.flags = arg
|
125
|
+
assert_equal results, @message.flags
|
126
|
+
path_suffix = "#{Maildir::Message::INFO}#{results.join('')}"
|
127
|
+
assert_match /#{path_suffix}$/, @message.path
|
123
128
|
end
|
129
|
+
end
|
130
|
+
end
|
124
131
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
132
|
+
context "A destroyed message" do
|
133
|
+
setup do
|
134
|
+
FakeFS::FileSystem.clear
|
135
|
+
@message = Maildir::Message.create(temp_maildir, "foo")
|
136
|
+
@message.destroy
|
137
|
+
end
|
138
|
+
should "be frozen" do
|
139
|
+
assert @message.frozen?, "Message is not frozen"
|
140
|
+
end
|
141
|
+
should "have a nonexistant path" do
|
142
|
+
assert !File.exists?(@message.path), "Message path exists"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context "A message with a bad path" do
|
147
|
+
setup do
|
148
|
+
FakeFS::FileSystem.clear
|
149
|
+
@message = temp_maildir.add("")
|
150
|
+
File.delete(@message.path)
|
151
|
+
end
|
152
|
+
|
153
|
+
should "raise error for data" do
|
154
|
+
assert_raise Errno::ENOENT do
|
155
|
+
@message.data
|
146
156
|
end
|
157
|
+
assert @message.frozen?
|
158
|
+
end
|
159
|
+
|
160
|
+
should "not be processed" do
|
161
|
+
old_key = @message.key
|
162
|
+
assert_equal false, @message.process
|
163
|
+
assert @message.frozen?
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
should "reset to the old key after attempt to process" do
|
168
|
+
old_key = @message.key
|
169
|
+
@message.process
|
170
|
+
assert_equal old_key, @message.key
|
147
171
|
end
|
148
172
|
end
|
149
173
|
|
150
|
-
context "
|
174
|
+
context "Different messages" do
|
151
175
|
setup do
|
152
|
-
|
176
|
+
FakeFS::FileSystem.clear
|
153
177
|
end
|
154
|
-
|
178
|
+
|
155
179
|
should "differ" do
|
156
|
-
@
|
180
|
+
@message1 = temp_maildir.add("")
|
181
|
+
@message2 = temp_maildir.add("")
|
157
182
|
assert_equal -1, @message1 <=> @message2
|
158
183
|
assert_equal 1, @message2 <=> @message1
|
159
184
|
assert_not_equal @message1, @message2
|
160
|
-
|
161
185
|
end
|
162
|
-
|
186
|
+
end
|
187
|
+
|
188
|
+
context "Identical messages" do
|
189
|
+
setup do
|
190
|
+
FakeFS::FileSystem.clear
|
191
|
+
end
|
192
|
+
|
163
193
|
should "be identical" do
|
194
|
+
@message1 = temp_maildir.add("")
|
164
195
|
another_message1 = temp_maildir.get(@message1.key)
|
165
196
|
assert_equal @message1, another_message1
|
166
197
|
end
|
167
198
|
end
|
168
199
|
|
200
|
+
context "Message#utime" do
|
201
|
+
setup do
|
202
|
+
FakeFS::FileSystem.clear
|
203
|
+
end
|
204
|
+
|
205
|
+
should "update the messages mtime" do
|
206
|
+
@message = temp_maildir.add("")
|
207
|
+
time = Time.now - 60
|
208
|
+
|
209
|
+
@message.utime(time, time)
|
210
|
+
|
211
|
+
# Time should be within 1 second of each other
|
212
|
+
assert_in_delta time, @message.mtime, 1
|
213
|
+
end
|
214
|
+
|
215
|
+
# atime not currently supported in FakeFS
|
216
|
+
# should "update the messages atime" do
|
217
|
+
# @message = temp_maildir.add("")
|
218
|
+
# time = Time.now - 60
|
219
|
+
#
|
220
|
+
# @message.utime(time, time)
|
221
|
+
#
|
222
|
+
# # Time should be within 1 second of each other
|
223
|
+
# assert_in_delta time, @message.atime, 1
|
224
|
+
# end
|
225
|
+
end
|
169
226
|
end
|
data/test/test_serializers.rb
CHANGED
@@ -1,11 +1,4 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
|
3
|
-
# Require all the serializers
|
4
|
-
path = File.join(File.dirname(__FILE__), "..","lib","maildir","serializer","*")
|
5
|
-
Dir.glob(path).each do |file|
|
6
|
-
require file
|
7
|
-
end
|
8
|
-
|
9
2
|
class TestSerializers < Test::Unit::TestCase
|
10
3
|
|
11
4
|
serializers = [
|
@@ -18,6 +11,7 @@ class TestSerializers < Test::Unit::TestCase
|
|
18
11
|
serializers.each do |klass, dumper|
|
19
12
|
context "A message serialized with #{klass}" do
|
20
13
|
setup do
|
14
|
+
FakeFS::FileSystem.clear
|
21
15
|
@data = case klass.new
|
22
16
|
when Maildir::Serializer::Mail
|
23
17
|
Mail.new
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: maildir
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Suggs
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-01-
|
12
|
+
date: 2010-01-24 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -42,6 +42,16 @@ dependencies:
|
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: "0"
|
44
44
|
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: ktheory-fakefs
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
45
55
|
description: A ruby library for reading and writing arbitrary messages in DJB's maildir format
|
46
56
|
email: aaron@ktheory.com
|
47
57
|
executables: []
|
@@ -59,12 +69,14 @@ files:
|
|
59
69
|
- VERSION
|
60
70
|
- benchmarks/runner
|
61
71
|
- lib/maildir.rb
|
72
|
+
- lib/maildir/keywords.rb
|
62
73
|
- lib/maildir/message.rb
|
63
74
|
- lib/maildir/serializer/base.rb
|
64
75
|
- lib/maildir/serializer/json.rb
|
65
76
|
- lib/maildir/serializer/mail.rb
|
66
77
|
- lib/maildir/serializer/marshal.rb
|
67
78
|
- lib/maildir/serializer/yaml.rb
|
79
|
+
- lib/maildir/subdirs.rb
|
68
80
|
- lib/maildir/unique_name.rb
|
69
81
|
- maildir.gemspec
|
70
82
|
- test/test_helper.rb
|