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 +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
|