maildir 0.3.0 → 0.4.0
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/.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
|