nilclass-maildir 0.4.1

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 ADDED
@@ -0,0 +1,3 @@
1
+ pkg/*
2
+ benchmarks/scratch.rb
3
+ .DS_Store
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Aaron Suggs
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,113 @@
1
+ = Maildir
2
+
3
+ A ruby library for reading and writing messages in the maildir format.
4
+
5
+ == What's so great about the maildir format
6
+
7
+ See http://cr.yp.to/proto/maildir.html and http://en.wikipedia.org/wiki/Maildir
8
+
9
+ As Daniel J. Berstein puts it: "Two words: no locks." The maildir format allows multiple processes to read and write arbitrary messages without file locks.
10
+
11
+ New messages are initially written to a "tmp" directory with an automatically-generated unique filename. After the message is written, it's moved to the "new" directory where other processes may read it.
12
+
13
+ While the maildir format was created for email, it works well for arbitrary data. This library can read & write email messages or arbitrary data. See Pluggable serializers for more.
14
+
15
+ == Install
16
+
17
+ sudo gem install maildir
18
+
19
+ == Usage
20
+
21
+ Create a maildir in /home/aaron/mail
22
+
23
+ maildir = Maildir.new("/home/aaron/mail") # creates tmp, new, and cur dirs
24
+ # call Maildir.new("/home/aaron/mail", false) to skip directory creation.
25
+
26
+ Add a new message. This creates a new file with the contents "Hello World!"; returns the path fragment to the file. Messages are written to the tmp dir then moved to new.
27
+
28
+ message = maildir.add("Hello World!")
29
+
30
+ List new messages
31
+
32
+ maildir.list(:new) # => [message]
33
+
34
+ Move the message from "new" to "cur" to indicate that some process has retrieved the message.
35
+
36
+ message.process
37
+
38
+ Indeed, the message is in cur, not new.
39
+
40
+ maildir.list(:new) # => []
41
+ maildir.list(:cur) # => [message]
42
+
43
+ Add some flags to the message to indicate state. See "What can I put in info" at http://cr.yp.to/proto/maildir.html for flag conventions.
44
+
45
+ message.add_flag("S") # Mark the message as "seen"
46
+ message.add_flag("F") # Mark the message as "flagged"
47
+ message.remove_flag("F") # unflag the message
48
+ message.add_flag("T") # Mark the message as "trashed"
49
+
50
+ Get a key to uniquely identify the message
51
+
52
+ key = message.key
53
+
54
+ Load the contents of the message
55
+
56
+ data = message.data
57
+
58
+ Find the message based using the key
59
+
60
+ message_copy = maildir.get(key)
61
+ message == message_copy # => true
62
+
63
+ Delete the message from disk
64
+
65
+ message.destroy # => returns the frozen message
66
+ maildir.list(:cur) # => []
67
+
68
+ == Pluggable serializers
69
+
70
+ By default, message data are written and read from disk as a string. It's often desirable to process the string into a useful object. Maildir supports configurable serializers to convert message data into a useful object.
71
+
72
+ The following serializers are included:
73
+
74
+ * Maildir::Serializer::Base (the default)
75
+ * Maildir::Serializer::Mail
76
+ * Maildir::Serializer::Marshal
77
+ * Maildir::Serializer::JSON
78
+ * Maildir::Serializer::YAML
79
+
80
+ Maildir::Serializer::Base simply reads and writes strings to disk.
81
+
82
+ Maildir::Message.serializer # => Maildir::Serializer::Base.new (by default)
83
+ message = maildir.add("Hello World!") # writes "Hello World!" to disk
84
+ message.data # => "Hello World!"
85
+
86
+ The Mail serializer takes a ruby Mail object (http://github.com/mikel/mail) and writes RFC2822 email messages.
87
+
88
+ require 'maildir/serializer/mail'
89
+ Maildir::Message.serializer = Maildir::Serializer::Mail.new
90
+ mail = Mail.new(...)
91
+ message = maildir.add(mail) # writes an RFC2822 message to disk
92
+ message.data == mail # => true; data is parsed as a Mail object
93
+
94
+ The Marshal, JSON, and YAML serializers work similarly. E.g.:
95
+
96
+ require 'maildir/serializer/json'
97
+ Maildir::Message.serializer = Maildir::Serializer::JSON.new
98
+ my_data = {"foo" => nil, "my_array" => [1,2,3]}
99
+ message = maildir.add(my_data) # writes {"foo":null,"my_array":[1,2,3]}
100
+ message.data == my_data # => true
101
+
102
+ It's trivial to create a custom serializer. Implement the following two methods:
103
+
104
+ load(path)
105
+ dump(data, path)
106
+
107
+ == Credits
108
+
109
+ niklas | brueckenschlaeger, http://github.com/nilclass added subdir & courierimapkeywords support
110
+
111
+ == Copyright
112
+
113
+ Copyright (c) 2010 Aaron Suggs. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ require 'rake/testtask'
2
+ Rake::TestTask.new do |t|
3
+ t.libs << "test"
4
+ t.test_files = FileList['test/test*.rb']
5
+ t.verbose = true
6
+ end
7
+
8
+ task :default => :test
9
+
10
+ begin
11
+ require 'jeweler'
12
+ Jeweler::Tasks.new do |gemspec|
13
+ gemspec.name = "nilclass-maildir"
14
+ gemspec.summary = "Read & write messages in the maildir format"
15
+ gemspec.description = "A ruby library for reading and writing arbitrary messages in DJB's maildir format"
16
+ gemspec.email = "aaron@ktheory.com"
17
+ gemspec.homepage = "http://github.com/ktheory/maildir"
18
+ gemspec.authors = ["Aaron Suggs", "Niklas E. Cathor"]
19
+ gemspec.add_development_dependency "shoulda", ">= 0"
20
+ gemspec.add_development_dependency "mail", ">= 0"
21
+ gemspec.add_development_dependency "json", ">= 0"
22
+ gemspec.add_development_dependency "ktheory-fakefs", ">= 0"
23
+ end
24
+ Jeweler::GemcutterTasks.new
25
+ rescue LoadError
26
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
27
+ end
28
+
29
+ desc "Run benchmarks"
30
+ task :bench do
31
+ load File.join(File.dirname(__FILE__), "benchmarks", "runner")
32
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.1
data/benchmarks/runner ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'maildir'
4
+ require 'benchmark'
5
+
6
+ maildir_path = ENV['MAILDIR'] || "./tmp"
7
+ maildir = Maildir.new(maildir_path)
8
+
9
+ n = 300
10
+ message = "Write #{n} messages:"
11
+ tms = Benchmark.bmbm(message.size) do |x|
12
+ x.report(message) { n.times { maildir.add("") } }
13
+ end
14
+
15
+ puts "#{n/tms.first.real} messages per second"
16
+
17
+
18
+ message = "List new:"
19
+ tms = Benchmark.bm(message.size) do |x|
20
+ x.report(message) { n.times { maildir.list_keys(:new)} }
21
+ end
22
+
23
+ # require 'ruby-prof'
24
+ # result = RubyProf.profile do
25
+ # n.times { maildir.list_keys(:new) }
26
+ # end
27
+ #
28
+ # # Print a graph profile to text
29
+ # printer = RubyProf::GraphPrinter.new(result)
30
+ # printer.print(STDOUT, 0)
data/lib/maildir.rb ADDED
@@ -0,0 +1,115 @@
1
+ require 'fileutils' # For create_directories
2
+ class Maildir
3
+
4
+ SUBDIRS = [:tmp, :new, :cur].freeze
5
+ READABLE_DIRS = SUBDIRS.reject{|s| :tmp == s}.freeze
6
+
7
+ include Comparable
8
+
9
+ attr_reader :path
10
+
11
+ # Create a new maildir at +path+. If +create+ is true, will ensure that the
12
+ # required subdirectories exist.
13
+ def initialize(path, create = true)
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
17
+ create_directories if create
18
+ end
19
+
20
+ # Compare maildirs by their paths.
21
+ # If maildir is a different class, return nil.
22
+ # Otherwise, return 1, 0, or -1.
23
+ def <=>(maildir)
24
+ # Return nil if comparing different classes
25
+ return nil unless self.class === maildir
26
+
27
+ self.path <=> maildir.path
28
+ end
29
+
30
+ # Friendly inspect method
31
+ def inspect
32
+ "#<#{self.class} path=#{@path}>"
33
+ end
34
+
35
+ # define methods tmp_path, new_path, & cur_path
36
+ SUBDIRS.each do |subdir|
37
+ define_method "#{subdir}_path" do
38
+ File.join(path, subdir.to_s)
39
+ end
40
+ end
41
+
42
+ # Ensure subdirectories exist. This can safely be called multiple times, but
43
+ # must hit the disk. Avoid calling this if you're certain the directories
44
+ # exist.
45
+ def create_directories
46
+ SUBDIRS.each do |subdir|
47
+ subdir_path = File.join(path, subdir.to_s)
48
+ FileUtils.mkdir_p(subdir_path)
49
+ end
50
+ end
51
+
52
+ # Returns an arry of messages from :new or :cur directory, sorted by key.
53
+ # If options[:limit] is specified, returns only so many keys.
54
+ #
55
+ # E.g.
56
+ # maildir.list(:new) # => all new messages
57
+ # maildir.list(:cur, :limit => 10) # => 10 oldest messages in cur
58
+ def list(dir, options = {})
59
+ unless SUBDIRS.include? dir.to_sym
60
+ raise ArgumentError, "dir must be :new, :cur, or :tmp"
61
+ end
62
+
63
+ keys = get_dir_listing(dir)
64
+
65
+ # Sort the keys (chronological order)
66
+ # TODO: make sorting configurable
67
+ keys.sort!
68
+
69
+ # Apply the limit after sorting
70
+ if limit = options[:limit]
71
+ keys = keys[0,limit]
72
+ end
73
+
74
+ # Map keys to message objects
75
+ keys.map{|key| get(key)}
76
+ end
77
+
78
+ # Writes data object out as a new message. Returns a Maildir::Message. See
79
+ # Maildir::Message.create for more.
80
+ def add(data)
81
+ Maildir::Message.create(self, data)
82
+ end
83
+
84
+ # Returns a message object for key
85
+ def get(key)
86
+ Maildir::Message.new(self, key)
87
+ end
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
+
102
+ protected
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, "")
110
+ end
111
+ end
112
+ end
113
+ require 'maildir/unique_name'
114
+ require 'maildir/serializer/base'
115
+ require 'maildir/message'
@@ -0,0 +1,111 @@
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
+ def keyword_dir
12
+ @keyword_dir ||= File.join(path, 'courierimapkeywords')
13
+ Dir.mkdir(@keyword_dir) unless File.directory?(@keyword_dir)
14
+ return @keyword_dir
15
+ end
16
+
17
+ # process contents of courierimapkeywords/ directory as described in README.imapkeywords
18
+ def read_keywords
19
+ messages = (list(:cur) + list(:new)).inject({}) { |m, msg| m[msg.unique_name] = msg ; m }
20
+ t = Time.now.to_i / 300
21
+ keywords = []
22
+ state = :head
23
+ # process :list
24
+ list_file = File.join(keyword_dir, ':list')
25
+ File.open(list_file).each_line do |line|
26
+ line.strip!
27
+ if state == :head
28
+ if line.empty?
29
+ state = :messages
30
+ next
31
+ end
32
+ keywords << line
33
+ else
34
+ key, ids = line.split(':')
35
+ if msg = messages[key]
36
+ msg.set_keywords(ids.split(/\s/).map {|id| keywords[id.to_i - 1] })
37
+ end
38
+ end
39
+ end if File.exist?(list_file)
40
+ # collect keyword files
41
+ keyword_files = (Dir.entries(keyword_dir) - %w(. .. :list)).inject({}) do |keyword_files, file|
42
+ if file =~ /^\.(\d+)\.(.*)$/
43
+ n = $1
44
+ key = $2
45
+ else
46
+ n = t + 1
47
+ key = file
48
+ File.move(File.join(keyword_dir, file), File.join(keyword_dir, ".#{n}.#{key}"))
49
+ end
50
+ if msg = messages[key]
51
+ (keyword_files[key] ||= []) << [n, key]
52
+ else # message doesn't exist
53
+ fname = File.join(keyword_dir, file)
54
+ if File.stat(fname).ctime < (Time.now - (15 * 60))
55
+ File.unlink(fname)
56
+ end
57
+ end
58
+ next(keyword_files)
59
+ end
60
+ # process keyword files
61
+ keyword_files.each_pair do |key, files|
62
+ files.sort! { |a, b| a[0] <=> b[0] }
63
+ files[0..-2].each { |f| File.unlink(File.join(keyword_dir, ".#{f.join('.')}")) } if files.last[0] < t
64
+ msg = messages[key]
65
+ file = (File.exist?(File.join(keyword_dir, files.last[1])) ? files.last[1] : ".#{files.last.join('.')}")
66
+ current_keywords = File.read(File.join(keyword_dir, file)).split(/\s+/)
67
+ msg.set_keywords(current_keywords)
68
+ if (add = (current_keywords - keywords)).any?
69
+ keywords += add
70
+ end
71
+ end
72
+ # rebuild :list
73
+ @keywords = {}
74
+ tmp_file = File.join(path, 'tmp', ':list')
75
+ File.open(tmp_file, 'w') { |f|
76
+ f.write(keywords.join("\n")+"\n\n")
77
+ messages.each_pair do |key, msg|
78
+ next unless msg.keywords
79
+ f.puts([key, msg.keywords.map{|kw| keywords.index(kw) + 1 }.sort.join(' ')].join(':'))
80
+ @keywords[key] = msg.keywords
81
+ end
82
+ }
83
+ File.move(tmp_file, list_file)
84
+ end
85
+
86
+ def keywords(key)
87
+ read_keywords unless @keywords
88
+ @keywords[key] || []
89
+ end
90
+
91
+ module MessageExtension
92
+ def keywords
93
+ return @keywords if @keywords
94
+ @maildir.keywords(unique_name)
95
+ end
96
+
97
+ # sets given keywords on the message.
98
+ def keywords=(list)
99
+ tmp_fname = File.join(@maildir.path, 'tmp', unique_name)
100
+ File.open(tmp_fname, 'w') { |f| f.write(list.join("\n")) }
101
+ File.move(tmp_fname, File.join(@maildir.keyword_dir, unique_name))
102
+ end
103
+
104
+ # sets @keywords to the given list
105
+ def set_keywords(list)
106
+ @keywords = list
107
+ end
108
+ end
109
+ end
110
+
111
+ Maildir.send(:include, Maildir::Keywords)
@@ -0,0 +1,241 @@
1
+ class Maildir::Message
2
+ # COLON seperates the unique name from the info
3
+ COLON = ':'
4
+ # The default info, to which flags are appended
5
+ INFO = "2,"
6
+
7
+ include Comparable
8
+
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
24
+
25
+ # Get the serializer
26
+ def self.serializer
27
+ @@serializer
28
+ end
29
+
30
+ # Set the serializer
31
+ def self.serializer=(serializer)
32
+ @@serializer = serializer
33
+ end
34
+
35
+ attr_reader :dir, :unique_name, :info
36
+
37
+ # Create a new, unwritten message or instantiate an existing message.
38
+ # If key is nil, create a new message:
39
+ # Message.new(maildir) # => a new, unwritten message
40
+ #
41
+ # If +key+ is not nil, instantiate a message object for the message at
42
+ # +key+.
43
+ # Message.new(maildir, key) # => an existing message
44
+ def initialize(maildir, key=nil)
45
+ @maildir = maildir
46
+ if key.nil?
47
+ @dir = :tmp
48
+ @unique_name = Maildir::UniqueName.create
49
+ else
50
+ parse_key(key)
51
+ end
52
+
53
+ unless Maildir::SUBDIRS.include? dir
54
+ raise ArgumentError, "State must be in #{Maildir::SUBDIRS.inspect}"
55
+ end
56
+ end
57
+
58
+ # Compares messages by their paths.
59
+ # If message is a different class, return nil.
60
+ # Otherwise, return 1, 0, or -1.
61
+ def <=>(message)
62
+ # Return nil if comparing different classes
63
+ return nil unless self.class === message
64
+
65
+ self.path <=> message.path
66
+ end
67
+
68
+ # Friendly inspect method
69
+ def inspect
70
+ "#<#{self.class} key=#{key} maildir=#{@maildir.inspect}>"
71
+ end
72
+
73
+ # Returns the class' serializer
74
+ def serializer
75
+ @@serializer
76
+ end
77
+
78
+ # Writes data to disk. Can only be called on messages instantiated without
79
+ # a key (which haven't been written to disk). After successfully writing
80
+ # to disk, rename the message to the new dir
81
+ #
82
+ # Returns the message's key
83
+ def write(data)
84
+ raise "Can only write to messages in tmp" unless :tmp == @dir
85
+
86
+ # Write out contents to tmp
87
+ serializer.dump(data, path)
88
+
89
+ rename(:new)
90
+ end
91
+
92
+ # Move a message from new to cur, add info.
93
+ # Returns the message's key
94
+ def process
95
+ rename(:cur, INFO)
96
+ end
97
+
98
+ # Set info on a message.
99
+ # Returns the message's key if successful, false otherwise.
100
+ def info=(info)
101
+ raise "Can only set info on cur messages" unless :cur == @dir
102
+ rename(:cur, info)
103
+ end
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
+
123
+ # Returns an array of single letter flags applied to the message
124
+ def flags
125
+ @info.sub(INFO,'').split('')
126
+ end
127
+
128
+ # Sets the flags on a message.
129
+ # Returns the message's key if successful, false otherwise.
130
+ def flags=(*flags)
131
+ self.info = INFO + sort_flags(flags.flatten.join(''))
132
+ end
133
+
134
+ # Adds a flag to a message.
135
+ # Returns the message's key if successful, false otherwise.
136
+ def add_flag(flag)
137
+ self.flags = (flags << flag.upcase)
138
+ end
139
+
140
+ # Removes a flag from a message.
141
+ # Returns the message's key if successful, false otherwise.
142
+ def remove_flag(flag)
143
+ self.flags = flags.delete_if{|f| f == flag.upcase}
144
+ end
145
+
146
+ # Returns the filename of the message
147
+ def filename
148
+ [unique_name, info].compact.join(COLON)
149
+ end
150
+
151
+ # Returns the key to identify the message
152
+ def key
153
+ File.join(dir.to_s, filename)
154
+ end
155
+
156
+ # Returns the full path to the message
157
+ def path
158
+ File.join(@maildir.path, key)
159
+ end
160
+
161
+ # Returns the message's data from disk.
162
+ # If the path doesn't exist, freeze's the object and raises Errno:ENOENT
163
+ def data
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) }
181
+ end
182
+
183
+ # Deletes the message path and freezes the message object
184
+ def destroy
185
+ guard { File.delete(path) }
186
+ freeze
187
+ end
188
+
189
+ protected
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
+
210
+ # Sets dir, unique_name, and info based on the key
211
+ def parse_key(key)
212
+ @dir, filename = key.split(File::SEPARATOR)
213
+ @dir = @dir.to_sym
214
+ @unique_name, @info = filename.split(COLON)
215
+ end
216
+
217
+ # Ensure the flags are uppercase and sorted
218
+ def sort_flags(flags)
219
+ flags.split('').map{|f| f.upcase}.sort!.uniq.join('')
220
+ end
221
+
222
+ def old_path
223
+ File.join(@maildir.path, @old_key)
224
+ end
225
+
226
+ # Renames the message. Returns the new key if successful, false otherwise.
227
+ def rename(new_dir, new_info=nil)
228
+ # Save the old key so we can revert to the old state
229
+ @old_key = key
230
+
231
+ # Set the new state
232
+ @dir = new_dir
233
+ @info = new_info if new_info
234
+
235
+ guard do
236
+ File.rename(old_path, path) unless old_path == path
237
+ @old_key = nil # So guard() doesn't reset to a bad state
238
+ return key
239
+ end
240
+ end
241
+ end