nilclass-maildir 0.4.1

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