nilclass-maildir 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +113 -0
- data/Rakefile +32 -0
- data/VERSION +1 -0
- data/benchmarks/runner +30 -0
- data/lib/maildir.rb +115 -0
- data/lib/maildir/keywords.rb +111 -0
- data/lib/maildir/message.rb +241 -0
- data/lib/maildir/serializer/base.rb +20 -0
- data/lib/maildir/serializer/json.rb +13 -0
- data/lib/maildir/serializer/mail.rb +13 -0
- data/lib/maildir/serializer/marshal.rb +12 -0
- data/lib/maildir/serializer/yaml.rb +13 -0
- data/lib/maildir/subdirs.rb +70 -0
- data/lib/maildir/unique_name.rb +72 -0
- data/maildir.gemspec +81 -0
- data/test/test_helper.rb +38 -0
- data/test/test_keywords.rb +16 -0
- data/test/test_maildir.rb +73 -0
- data/test/test_message.rb +226 -0
- data/test/test_serializers.rb +37 -0
- data/test/test_subdirs.rb +46 -0
- data/test/test_unique_name.rb +43 -0
- metadata +125 -0
data/.gitignore
ADDED
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
|