maildir 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  pkg/*
2
2
  benchmarks/scratch.rb
3
+ .DS_Store
data/README.rdoc CHANGED
@@ -85,13 +85,15 @@ Maildir::Serializer::Base simply reads and writes strings to disk.
85
85
 
86
86
  The Mail serializer takes a ruby Mail object (http://github.com/mikel/mail) and writes RFC2822 email messages.
87
87
 
88
+ require 'maildir/serializer/mail'
88
89
  Maildir::Message.serializer = Maildir::Serializer::Mail.new
89
90
  mail = Mail.new(...)
90
- message = maildir.add(mail) # writes and RFC2822 message to disk
91
+ message = maildir.add(mail) # writes an RFC2822 message to disk
91
92
  message.data == mail # => true; data is parsed as a Mail object
92
93
 
93
- The Marshal, JSON, and YAML serializers work similarly. E.g.
94
+ The Marshal, JSON, and YAML serializers work similarly. E.g.:
94
95
 
96
+ require 'maildir/serializer/json'
95
97
  Maildir::Message.serializer = Maildir::Serializer::JSON.new
96
98
  my_data = {"foo" => nil, "my_array" => [1,2,3]}
97
99
  message = maildir.add(my_data) # writes {"foo":null,"my_array":[1,2,3]}
@@ -102,6 +104,10 @@ It's trivial to create a custom serializer. Implement the following two methods:
102
104
  load(path)
103
105
  dump(data, path)
104
106
 
107
+ == Credits
108
+
109
+ niklas | brueckenschlaeger, http://github.com/nilclass added subdir & courierimapkeywords support
110
+
105
111
  == Copyright
106
112
 
107
113
  Copyright (c) 2010 Aaron Suggs. See LICENSE for details.
data/Rakefile CHANGED
@@ -19,6 +19,7 @@ begin
19
19
  gemspec.add_development_dependency "shoulda", ">= 0"
20
20
  gemspec.add_development_dependency "mail", ">= 0"
21
21
  gemspec.add_development_dependency "json", ">= 0"
22
+ gemspec.add_development_dependency "ktheory-fakefs", ">= 0"
22
23
  end
23
24
  Jeweler::GemcutterTasks.new
24
25
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.0
1
+ 0.4.0
data/lib/maildir.rb CHANGED
@@ -11,7 +11,9 @@ class Maildir
11
11
  # Create a new maildir at +path+. If +create+ is true, will ensure that the
12
12
  # required subdirectories exist.
13
13
  def initialize(path, create = true)
14
- @path = File.join(path, '/') # Ensure path has a trailing slash
14
+ @path = File.expand_path(path)
15
+ @path = File.join(@path, '/') # Ensure path has a trailing slash
16
+ @path_regexp = /^#{Regexp.quote(@path)}/ # For parsing directory listings
15
17
  create_directories if create
16
18
  end
17
19
 
@@ -25,6 +27,11 @@ class Maildir
25
27
  self.path <=> maildir.path
26
28
  end
27
29
 
30
+ # Friendly inspect method
31
+ def inspect
32
+ "#<#{self.class} path=#{@path}>"
33
+ end
34
+
28
35
  # define methods tmp_path, new_path, & cur_path
29
36
  SUBDIRS.each do |subdir|
30
37
  define_method "#{subdir}_path" do
@@ -37,7 +44,8 @@ class Maildir
37
44
  # exist.
38
45
  def create_directories
39
46
  SUBDIRS.each do |subdir|
40
- FileUtils.mkdir_p(self.send("#{subdir}_path"))
47
+ subdir_path = File.join(path, subdir.to_s)
48
+ FileUtils.mkdir_p(subdir_path)
41
49
  end
42
50
  end
43
51
 
@@ -47,26 +55,24 @@ class Maildir
47
55
  # E.g.
48
56
  # maildir.list(:new) # => all new messages
49
57
  # maildir.list(:cur, :limit => 10) # => 10 oldest messages in cur
50
- def list(new_or_cur, options = {})
51
- new_or_cur = new_or_cur.to_sym
52
- unless [:new, :cur].include? new_or_cur
53
- raise ArgumentError, "first arg must be new or cur"
58
+ def list(dir, options = {})
59
+ unless SUBDIRS.include? dir.to_sym
60
+ raise ArgumentError, "dir must be :new, :cur, or :tmp"
54
61
  end
55
62
 
56
- keys = get_dir_listing(new_or_cur)
57
-
58
- # Map keys to message objects
59
- messages = keys.map{|key| get(key)}
63
+ keys = get_dir_listing(dir)
60
64
 
61
- # Sort the messages (effectively chronological order)
65
+ # Sort the keys (chronological order)
62
66
  # TODO: make sorting configurable
63
- messages.sort!
67
+ keys.sort!
64
68
 
65
- # Apply the limit
69
+ # Apply the limit after sorting
66
70
  if limit = options[:limit]
67
- messages = messages[0,limit]
71
+ keys = keys[0,limit]
68
72
  end
69
- messages
73
+
74
+ # Map keys to message objects
75
+ keys.map{|key| get(key)}
70
76
  end
71
77
 
72
78
  # Writes data object out as a new message. Returns a Maildir::Message. See
@@ -75,19 +81,32 @@ class Maildir
75
81
  Maildir::Message.create(self, data)
76
82
  end
77
83
 
84
+ # Returns a message object for key
78
85
  def get(key)
79
86
  Maildir::Message.new(self, key)
80
87
  end
81
88
 
89
+ # Deletes the message for key by calling destroy() on the message.
90
+ def delete(key)
91
+ get(key).destroy
92
+ end
93
+
94
+ # Finds messages in the tmp folder that have not been modified since
95
+ # +time+. +time+ defaults to 36 hours ago.
96
+ def get_stale_tmp(time = Time.now - 129600)
97
+ list(:tmp).select do |message|
98
+ (mtime = message.mtime) && mtime < time
99
+ end
100
+ end
101
+
82
102
  protected
83
- def get_dir_listing(new_or_cur)
84
- search_path = File.join(self.path, new_or_cur.to_s, '*')
85
- results = Dir.glob(search_path)
86
-
87
- # Remove the maildir's path from the beginning of the message path
88
- @dir_listing_regexp ||= /^#{Regexp.quote(self.path)}/
89
- results.each do |message_path|
90
- message_path.sub!(@dir_listing_regexp, "")
103
+ # Returns an array of keys in dir
104
+ def get_dir_listing(dir)
105
+ search_path = File.join(self.path, dir.to_s, '*')
106
+ keys = Dir.glob(search_path)
107
+ # Remove the maildir's path from the keys
108
+ keys.each do |key|
109
+ key.sub!(@path_regexp, "")
91
110
  end
92
111
  end
93
112
  end
@@ -0,0 +1,103 @@
1
+ # implements IMAP Keywords as used by the Courier Mail Server
2
+ # see http://www.courier-mta.org/imap/README.imapkeywords.html for details
3
+
4
+ require 'ftools'
5
+ require 'maildir'
6
+ module Maildir::Keywords
7
+ def self.included(base)
8
+ Maildir::Message.send(:include, MessageExtension)
9
+ end
10
+
11
+ # process contents of courierimapkeywords/ directory as described in README.imapkeywords
12
+ def read_keywords
13
+ messages = (list(:cur) + list(:new)).inject({}) { |m, msg| m[msg.unique_name] = msg ; m }
14
+ t = Time.now.to_i / 300
15
+ keyword_dir = File.join(path, 'courierimapkeywords')
16
+ keywords = []
17
+ state = :head
18
+ # process :list
19
+ File.open(File.join(keyword_dir, ':list')).each_line do |line|
20
+ line.strip!
21
+ if state == :head
22
+ if line.empty?
23
+ state = :messages
24
+ next
25
+ end
26
+ keywords << line
27
+ else
28
+ key, ids = line.split(':')
29
+ if msg = messages[key]
30
+ msg.set_keywords(ids.split(/\s/).map {|id| keywords[id.to_i - 1] })
31
+ end
32
+ end
33
+ end
34
+ # collect keyword files
35
+ keyword_files = (Dir.entries(keyword_dir) - %w(. .. :list)).inject({}) do |keyword_files, file|
36
+ if file =~ /^\.(\d+)\.(.*)$/
37
+ n = $1
38
+ key = $2
39
+ else
40
+ n = t + 1
41
+ key = file
42
+ File.move(File.join(keyword_dir, file), File.join(keyword_dir, ".#{n}.#{key}"))
43
+ end
44
+ if msg = messages[key]
45
+ (keyword_files[key] ||= []) << [n, key]
46
+ else # message doesn't exist
47
+ fname = File.join(keyword_dir, file)
48
+ if File.stat(fname).ctime < (Time.now - (15 * 60))
49
+ File.unlink(fname)
50
+ end
51
+ end
52
+ end
53
+ # process keyword files
54
+ keyword_files.each_pair do |key, files|
55
+ files.sort! { |a, b| a[0] <=> b[0] }
56
+ files[0..-2].each { |f| File.unlink(File.join(keyword_dir, ".#{f.join('.')}")) } if n_files.last[0] < t
57
+ msg = messages[key]
58
+ current_keywords = File.read(files.last).read.split(/\s+/)
59
+ msg.set_keywords(current_keywords)
60
+ if (add = (current_keywords - keywords)).any?
61
+ keywords += add
62
+ end
63
+ end
64
+ # rebuild :list
65
+ @keywords = {}
66
+ tmp_file = File.join(path, 'tmp', ':list')
67
+ File.open(tmp_file, 'w') { |f|
68
+ f.write(keywords.join("\n")+"\n\n")
69
+ messages.each_pair do |key, msg|
70
+ next unless msg.keywords
71
+ f.puts([key, msg.keywords.map{|kw| keywords.index(kw) + 1 }.sort.join(' ')].join(':'))
72
+ @keywords[key] = msg.keywords
73
+ end
74
+ }
75
+ File.move(tmp_file, File.join(keyword_dir, ':list'))
76
+ end
77
+
78
+ def keywords(key)
79
+ read_keywords unless @keywords
80
+ @keywords[key] || []
81
+ end
82
+
83
+ module MessageExtension
84
+ def keywords
85
+ return @keywords if @keywords
86
+ @maildir.keywords(unique_name)
87
+ end
88
+
89
+ # sets given keywords on the message.
90
+ def keywords=(list)
91
+ tmp_fname = File.join(maildir.path, 'tmp', unique_name)
92
+ File.open(tmp_fname, 'w') { |f| f.write(list.join("\n")) }
93
+ File.move(tmp_fname, File.join(maildir.path, 'courierimapkeywords', unique_name))
94
+ end
95
+
96
+ # sets @keywords to the given list
97
+ def set_keywords(list)
98
+ @keywords = list
99
+ end
100
+ end
101
+ end
102
+
103
+ Maildir.send(:include, Maildir::Keywords)
@@ -6,27 +6,33 @@ class Maildir::Message
6
6
 
7
7
  include Comparable
8
8
 
9
- class << self
10
- # Create a new message in maildir with data.
11
- # The message is first written to the tmp dir, then moved to new. This is
12
- # a shortcut for:
13
- # message = Maildir::Message.new(maildir)
14
- # message.write(data)
15
- def create(maildir, data)
16
- message = self.new(maildir)
17
- message.write(data)
18
- message
19
- end
9
+ # Create a new message in maildir with data.
10
+ # The message is first written to the tmp dir, then moved to new. This is
11
+ # a shortcut for:
12
+ # message = Maildir::Message.new(maildir)
13
+ # message.write(data)
14
+ def self.create(maildir, data)
15
+ message = self.new(maildir)
16
+ message.write(data)
17
+ message
18
+ end
19
+
20
+ # The serializer processes data before it is written to disk and after
21
+ # reading from disk.
22
+ # Default serializer
23
+ @@serializer = Maildir::Serializer::Base.new
20
24
 
21
- # The serializer processes data before it is written to disk and after
22
- # reading from disk.
23
- attr_accessor :serializer
25
+ # Get the serializer
26
+ def self.serializer
27
+ @@serializer
24
28
  end
25
29
 
26
- # Default serializer
27
- @serializer = Maildir::Serializer::Base.new
30
+ # Set the serializer
31
+ def self.serializer=(serializer)
32
+ @@serializer = serializer
33
+ end
28
34
 
29
- attr_reader :dir, :unique_name, :info, :old_key
35
+ attr_reader :dir, :unique_name, :info
30
36
 
31
37
  # Create a new, unwritten message or instantiate an existing message.
32
38
  # If key is nil, create a new message:
@@ -39,6 +45,7 @@ class Maildir::Message
39
45
  @maildir = maildir
40
46
  if key.nil?
41
47
  @dir = :tmp
48
+ @unique_name = Maildir::UniqueName.create
42
49
  else
43
50
  parse_key(key)
44
51
  end
@@ -46,10 +53,6 @@ class Maildir::Message
46
53
  unless Maildir::SUBDIRS.include? dir
47
54
  raise ArgumentError, "State must be in #{Maildir::SUBDIRS.inspect}"
48
55
  end
49
-
50
- if :tmp == dir
51
- @unique_name = Maildir::UniqueName.create
52
- end
53
56
  end
54
57
 
55
58
  # Compares messages by their paths.
@@ -62,9 +65,14 @@ class Maildir::Message
62
65
  self.path <=> message.path
63
66
  end
64
67
 
68
+ # Friendly inspect method
69
+ def inspect
70
+ "#<#{self.class} key=#{key} maildir=#{@maildir.inspect}>"
71
+ end
72
+
65
73
  # Returns the class' serializer
66
74
  def serializer
67
- self.class.serializer
75
+ @@serializer
68
76
  end
69
77
 
70
78
  # Writes data to disk. Can only be called on messages instantiated without
@@ -88,32 +96,53 @@ class Maildir::Message
88
96
  end
89
97
 
90
98
  # Set info on a message.
91
- # Returns the message's key
99
+ # Returns the message's key if successful, false otherwise.
92
100
  def info=(info)
93
101
  raise "Can only set info on cur messages" unless :cur == @dir
94
102
  rename(:cur, info)
95
103
  end
96
104
 
105
+ FLAG_NAMES = {
106
+ :passed => 'P',
107
+ :replied => 'R',
108
+ :seen => 'S',
109
+ :trashed => 'T',
110
+ :draft => 'D',
111
+ :flagged => 'F'
112
+ }
113
+
114
+ FLAG_NAMES.each_pair do |key, value|
115
+ define_method("#{key}?".to_sym) do
116
+ flags.include?(value)
117
+ end
118
+ define_method("#{key}!".to_sym) do
119
+ add_flag(value)
120
+ end
121
+ end
122
+
97
123
  # Returns an array of single letter flags applied to the message
98
124
  def flags
99
125
  @info.sub(INFO,'').split('')
100
126
  end
101
127
 
102
128
  # Sets the flags on a message.
103
- # Returns the message's key
129
+ # Returns the message's key if successful, false otherwise.
104
130
  def flags=(*flags)
105
131
  self.info = INFO + sort_flags(flags.flatten.join(''))
106
132
  end
107
133
 
134
+ # Adds a flag to a message.
135
+ # Returns the message's key if successful, false otherwise.
108
136
  def add_flag(flag)
109
137
  self.flags = (flags << flag.upcase)
110
138
  end
111
-
139
+
140
+ # Removes a flag from a message.
141
+ # Returns the message's key if successful, false otherwise.
112
142
  def remove_flag(flag)
113
143
  self.flags = flags.delete_if{|f| f == flag.upcase}
114
144
  end
115
145
 
116
-
117
146
  # Returns the filename of the message
118
147
  def filename
119
148
  [unique_name, info].compact.join(COLON)
@@ -129,19 +158,55 @@ class Maildir::Message
129
158
  File.join(@maildir.path, key)
130
159
  end
131
160
 
132
- # Returns the message's data from disk
161
+ # Returns the message's data from disk.
162
+ # If the path doesn't exist, freeze's the object and raises Errno:ENOENT
133
163
  def data
134
- serializer.load(path)
164
+ guard(true) { serializer.load(path) }
165
+ end
166
+
167
+ # Updates the modification and access time. Returns 1 if successful, false
168
+ # otherwise.
169
+ def utime(atime, mtime)
170
+ guard { File.utime(atime, mtime, path) }
171
+ end
172
+
173
+ # Returns the message's atime, or false if the file doesn't exist.
174
+ def atime
175
+ guard { File.atime(path) }
176
+ end
177
+
178
+ # Returns the message's mtime, or false if the file doesn't exist.
179
+ def mtime
180
+ guard { File.mtime(path) }
135
181
  end
136
182
 
137
183
  # Deletes the message path and freezes the message object
138
184
  def destroy
139
- File.delete(path)
185
+ guard { File.delete(path) }
140
186
  freeze
141
187
  end
142
188
 
143
189
  protected
144
190
 
191
+ # Guard access to the file system by rescuing Errno::ENOENT, which happens
192
+ # if the file is missing. When +blocks+ fails and +reraise+ is false, returns
193
+ # false, otherwise reraises Errno::ENOENT
194
+ def guard(reraise = false, &block)
195
+ begin
196
+ yield
197
+ rescue Errno::ENOENT
198
+ if @old_key
199
+ # Restore ourselves to the old state
200
+ parse_key(@old_key)
201
+ end
202
+
203
+ # Don't allow further modifications
204
+ freeze
205
+
206
+ reraise ? raise : false
207
+ end
208
+ end
209
+
145
210
  # Sets dir, unique_name, and info based on the key
146
211
  def parse_key(key)
147
212
  @dir, filename = key.split(File::SEPARATOR)
@@ -155,24 +220,22 @@ class Maildir::Message
155
220
  end
156
221
 
157
222
  def old_path
158
- File.join(@maildir.path, old_key)
223
+ File.join(@maildir.path, @old_key)
159
224
  end
160
225
 
226
+ # Renames the message. Returns the new key if successful, false otherwise.
161
227
  def rename(new_dir, new_info=nil)
162
- # Safe the old key so we can revert to the old state
228
+ # Save the old key so we can revert to the old state
163
229
  @old_key = key
164
230
 
165
231
  # Set the new state
166
232
  @dir = new_dir
167
233
  @info = new_info if new_info
168
234
 
169
- begin
235
+ guard do
170
236
  File.rename(old_path, path) unless old_path == path
237
+ @old_key = nil # So guard() doesn't reset to a bad state
171
238
  return key
172
- rescue Errno::ENOENT
173
- # Restore ourselves to the old state
174
- parse_key(@old_key)
175
- raise
176
239
  end
177
240
  end
178
241
  end
@@ -0,0 +1,65 @@
1
+ # implements subdirs as used by the Courier Mail Server (courier-mta.org)
2
+ require 'maildir'
3
+ module Maildir::Subdirs
4
+ ROOT_NAME = 'INBOX'
5
+ DELIM = '.'
6
+
7
+ def self.included(base)
8
+ base.instance_eval do
9
+ alias_method :inspect_without_subdirs, :inspect
10
+ alias_method :inspect, :inspect_with_subdirs
11
+ end
12
+ end
13
+
14
+ def create_subdir(name)
15
+ raise ArgumentError.new("'name' may not contain delimiter character (#{DELIM})") if name.include?(DELIM)
16
+ full_name = (root? ? [] : subdir_parts(File.basename(path))).push(name).unshift('').join(DELIM)
17
+ md = Maildir.new(File.join(path, full_name), true)
18
+ @subdirs << md if @subdirs
19
+ md
20
+ end
21
+
22
+ # returns the logical mailbox path
23
+ def mailbox_path
24
+ @mailbox_path ||= root? ? ROOT_NAME : subdir_parts(File.basename(path)).unshift(ROOT_NAME).join(DELIM)
25
+ end
26
+
27
+ # returns an array of Maildir objects representing the direct subdirectories of this Maildir
28
+ def subdirs(only_direct=true)
29
+ if root?
30
+ @subdirs ||= (Dir.entries(path) - %w(. ..)).select {|e|
31
+ e =~ /^\./ && File.directory?(File.join(path, e)) && (only_direct ? subdir_parts(e).size == 1 : true)
32
+ }.map { |e| Maildir.new(File.join(path, e), false) }
33
+ else
34
+ my_parts = subdir_parts(File.basename(path))
35
+ @subdirs ||= root.subdirs(false).select { |md| subdir_parts(File.basename(md.path))[0..-2] == my_parts }
36
+ end
37
+ end
38
+
39
+ # Friendly inspect method
40
+ def inspect_with_subdirs
41
+ "#<#{self.class} path=#{@path} mailbox_path=#{mailbox_path}>"
42
+ end
43
+
44
+ # returns the Maildir representing the root directory
45
+ def root
46
+ root? ? self : Maildir.new(File.dirname(path), false)
47
+ end
48
+
49
+ # returns true if the parent directory doesn't look like a maildir
50
+ def root?
51
+ ! ((Dir.entries(File.dirname(path)) & %w(cur new tmp)).size == 3)
52
+ end
53
+
54
+ private
55
+
56
+ def subdir_parts(path)
57
+ parts = (path.split(DELIM) - [''])
58
+ # some clients (e.g. Thunderbird) mess up namespaces so subdirs
59
+ # end up looking like '.INBOX.Trash' instead of '.Trash'
60
+ parts.shift if parts.first == ROOT_NAME
61
+ parts
62
+ end
63
+ end
64
+
65
+ Maildir.send(:include, Maildir::Subdirs)
data/maildir.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{maildir}
8
- s.version = "0.3.0"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Aaron Suggs"]
12
- s.date = %q{2010-01-08}
12
+ s.date = %q{2010-01-24}
13
13
  s.description = %q{A ruby library for reading and writing arbitrary messages in DJB's maildir format}
14
14
  s.email = %q{aaron@ktheory.com}
15
15
  s.extra_rdoc_files = [
@@ -24,12 +24,14 @@ Gem::Specification.new do |s|
24
24
  "VERSION",
25
25
  "benchmarks/runner",
26
26
  "lib/maildir.rb",
27
+ "lib/maildir/keywords.rb",
27
28
  "lib/maildir/message.rb",
28
29
  "lib/maildir/serializer/base.rb",
29
30
  "lib/maildir/serializer/json.rb",
30
31
  "lib/maildir/serializer/mail.rb",
31
32
  "lib/maildir/serializer/marshal.rb",
32
33
  "lib/maildir/serializer/yaml.rb",
34
+ "lib/maildir/subdirs.rb",
33
35
  "lib/maildir/unique_name.rb",
34
36
  "maildir.gemspec",
35
37
  "test/test_helper.rb",
@@ -59,15 +61,18 @@ Gem::Specification.new do |s|
59
61
  s.add_development_dependency(%q<shoulda>, [">= 0"])
60
62
  s.add_development_dependency(%q<mail>, [">= 0"])
61
63
  s.add_development_dependency(%q<json>, [">= 0"])
64
+ s.add_development_dependency(%q<ktheory-fakefs>, [">= 0"])
62
65
  else
63
66
  s.add_dependency(%q<shoulda>, [">= 0"])
64
67
  s.add_dependency(%q<mail>, [">= 0"])
65
68
  s.add_dependency(%q<json>, [">= 0"])
69
+ s.add_dependency(%q<ktheory-fakefs>, [">= 0"])
66
70
  end
67
71
  else
68
72
  s.add_dependency(%q<shoulda>, [">= 0"])
69
73
  s.add_dependency(%q<mail>, [">= 0"])
70
74
  s.add_dependency(%q<json>, [">= 0"])
75
+ s.add_dependency(%q<ktheory-fakefs>, [">= 0"])
71
76
  end
72
77
  end
73
78
 
data/test/test_helper.rb CHANGED
@@ -1,24 +1,26 @@
1
1
  require 'rubygems'
2
2
  require 'test/unit'
3
3
  require 'shoulda'
4
- require 'tmpdir'
5
- require 'tempfile'
6
4
  require 'fileutils'
7
5
  require 'maildir'
8
6
 
7
+ # Require all the serializers
8
+ serializers = File.join(File.dirname(__FILE__), "..","lib","maildir","serializer","*")
9
+ Dir.glob(serializers).each do |serializer|
10
+ require serializer
11
+ end
12
+
13
+ # Require 'ktheory-fakefs' until issues 28, 29, and 30 are resolved in
14
+ # defunkt/fakefs. See http://github.com/defunkt/fakefs/issues
15
+ gem "ktheory-fakefs"
16
+ require 'fakefs'
17
+
9
18
  # Create a reusable maildir that's cleaned up when the tests are done
10
19
  def temp_maildir
11
- return $maildir if $maildir
12
-
13
- dir_path = Dir.mktmpdir("maildir_test")
14
- at_exit do
15
- puts "Cleaning up temp maildir"
16
- FileUtils.rm_r(dir_path)
17
- end
18
- $maildir = Maildir.new(dir_path)
20
+ Maildir.new("/tmp/maildir_test")
19
21
  end
20
22
 
21
23
  # Useful for testing that strings defined & not empty
22
24
  def assert_not_empty(obj, msg='')
23
25
  assert !obj.nil? && !obj.empty?, msg
24
- end
26
+ end
data/test/test_maildir.rb CHANGED
@@ -2,6 +2,10 @@ require 'test_helper'
2
2
  class TestMaildir < Test::Unit::TestCase
3
3
 
4
4
  context "A maildir" do
5
+ setup do
6
+ FakeFS::FileSystem.clear
7
+ end
8
+
5
9
  should "have a path" do
6
10
  assert_not_empty temp_maildir.path
7
11
  end
@@ -13,31 +17,38 @@ class TestMaildir < Test::Unit::TestCase
13
17
  end
14
18
  end
15
19
 
20
+ should "expand paths" do
21
+ maildir = Maildir.new("~/test_maildir/")
22
+ expanded_path = File.expand_path("~/test_maildir")
23
+ expanded_path = File.join(expanded_path, "/")
24
+ assert_equal expanded_path, maildir.path
25
+ end
26
+
16
27
  should "not create directories if specified" do
17
- tmp_dir = Dir.mktmpdir('new_maildir_test')
18
- maildir = Maildir.new(tmp_dir, false)
28
+ maildir = Maildir.new("/maildir_without_subdirs", false)
19
29
  %w(tmp new cur).each do |subdir|
20
30
  subdir_path = maildir.send("#{subdir}_path")
21
31
  assert !File.directory?(subdir_path), "Subdir #{subdir} exists"
22
32
  end
23
- FileUtils.rm_r(tmp_dir)
24
33
  end
25
-
34
+
26
35
  should "be identical to maildirs with the same path" do
27
36
  new_maildir = Maildir.new(temp_maildir.path)
28
37
  assert_equal temp_maildir.path, new_maildir.path
29
38
  assert_equal temp_maildir, new_maildir
30
39
  end
31
-
32
- context "with a message" do
33
- setup do
34
- @message = temp_maildir.add("")
35
- end
36
40
 
37
- should "list the message in it's keys" do
38
- messages = temp_maildir.list(:new)
39
- assert_equal messages, [@message]
40
- end
41
+ should "list a new message" do
42
+ @message = temp_maildir.add("")
43
+ messages = temp_maildir.list(:new)
44
+ assert_equal messages, [@message]
45
+ end
46
+
47
+ should "list a cur message" do
48
+ @message = temp_maildir.add("")
49
+ @message.process
50
+ messages = temp_maildir.list(:cur)
51
+ assert_equal messages, [@message]
41
52
  end
42
53
  end
43
54
 
@@ -48,4 +59,15 @@ class TestMaildir < Test::Unit::TestCase
48
59
  end
49
60
  end
50
61
 
62
+ context "Maildirs with stale messages in tmp" do
63
+ should "be found" do
64
+ stale_path = File.join(temp_maildir.path, "tmp", "stale_message")
65
+ File.open(stale_path, "w"){|f| f.write("")}
66
+ stale_time = Time.now - 30*24*60*60 # 1 month ago
67
+ File.utime(stale_time, stale_time, stale_path)
68
+
69
+ stale_tmp = [temp_maildir.get("tmp/stale_message")]
70
+ assert_equal stale_tmp, temp_maildir.get_stale_tmp
71
+ end
72
+ end
51
73
  end
data/test/test_message.rb CHANGED
@@ -3,13 +3,10 @@ class TestMessage < Test::Unit::TestCase
3
3
 
4
4
  context "An new, unwritten message" do
5
5
  setup do
6
+ FakeFS::FileSystem.clear
6
7
  @message = Maildir::Message.new(temp_maildir)
7
8
  end
8
9
 
9
- should "be instantiated" do
10
- assert @message
11
- end
12
-
13
10
  should "be in :tmp" do
14
11
  assert_equal :tmp, @message.dir
15
12
  assert_match(/tmp/, @message.path)
@@ -32,138 +29,198 @@ class TestMessage < Test::Unit::TestCase
32
29
  @message.info= "2,FRS"
33
30
  end
34
31
  end
32
+ end
35
33
 
36
- context "when written with a string" do
37
- setup do
38
- @data = "foo\n"
39
- @message.write(@data)
40
- end
34
+ context "A written message" do
35
+ setup do
36
+ FakeFS::FileSystem.clear
37
+ @message = Maildir::Message.new(temp_maildir)
38
+ @data = "foo"
39
+ @message.write(@data)
40
+ end
41
41
 
42
- should "not be writable" do
43
- assert_raise RuntimeError do
44
- @message.write("nope!")
45
- end
42
+ should "not be writable" do
43
+ assert_raise RuntimeError do
44
+ @message.write("nope!")
46
45
  end
46
+ end
47
47
 
48
- should "have no info" do
49
- assert_nil @message.info
50
- end
48
+ should "have no info" do
49
+ assert_nil @message.info
50
+ end
51
51
 
52
- should "not be able to set info" do
53
- assert_raises RuntimeError do
54
- @message.info= "2,FRS"
55
- end
52
+ should "not be able to set info" do
53
+ assert_raises RuntimeError do
54
+ @message.info= "2,FRS"
56
55
  end
56
+ end
57
57
 
58
- should "be in new dir" do
59
- assert_equal :new, @message.dir
60
- assert_match(/new/, @message.path)
61
- end
58
+ should "be in new" do
59
+ assert_equal :new, @message.dir
60
+ assert_match(/new/, @message.path)
61
+ end
62
62
 
63
- should "have have a file" do
64
- assert File.exists?(@message.path)
65
- end
63
+ should "have a file" do
64
+ assert File.exists?(@message.path)
65
+ end
66
66
 
67
- should "have the correct data" do
68
- assert_equal @data, @message.data
69
- end
67
+ should "have the correct data" do
68
+ assert_equal @data, @message.data
70
69
  end
71
70
  end
72
71
 
73
- context "A created message" do
72
+ context "A processed message" do
74
73
  setup do
75
- @data = "foo\n"
74
+ FakeFS::FileSystem.clear
75
+ @data = "foo"
76
76
  @message = Maildir::Message.create(temp_maildir, @data)
77
+ @message.process
77
78
  end
78
79
 
79
- should "have the correct data" do
80
- assert_equal @data, @message.data
81
- end
82
-
83
- context "when processed" do
84
- setup do
85
- @message.process
80
+ should "not be writable" do
81
+ assert_raise RuntimeError do
82
+ @message.write("nope!")
86
83
  end
84
+ end
87
85
 
88
- should "not be writable" do
89
- assert_raise RuntimeError do
90
- @message.write("nope!")
91
- end
92
- end
86
+ should "be in cur" do
87
+ assert_equal :cur, @message.dir
88
+ end
93
89
 
94
- should "be in cur" do
95
- assert_equal :cur, @message.dir
96
- end
90
+ should "have info" do
91
+ assert_equal Maildir::Message::INFO, @message.info
92
+ end
97
93
 
98
- should "have info" do
99
- assert_equal Maildir::Message::INFO, @message.info
100
- end
94
+ should "set info" do
95
+ info = "2,FRS"
96
+ @message.info = "2,FRS"
97
+ assert_equal @message.info, info
98
+ assert_match /#{info}$/, @message.path
99
+ end
101
100
 
102
- should "set info" do
103
- info = "2,FRS"
104
- @message.info = "2,FRS"
105
- assert_equal @message.info, info
106
- assert_match /#{info}$/, @message.path
107
- end
101
+ should "add and remove flags" do
102
+ @message.add_flag('S')
103
+ assert_equal ['S'], @message.flags
108
104
 
109
- should "add and remove flags" do
110
- @message.add_flag('S')
111
- assert_equal ['S'], @message.flags
105
+ # Test lowercase
106
+ @message.add_flag('r')
107
+ assert_equal ['R', 'S'], @message.flags
112
108
 
113
- # Test lowercase
114
- @message.add_flag('r')
115
- assert_equal ['R', 'S'], @message.flags
109
+ @message.remove_flag('S')
110
+ assert_equal ['R'], @message.flags
116
111
 
117
- @message.remove_flag('S')
118
- assert_equal ['R'], @message.flags
112
+ # Test lowercase
113
+ @message.remove_flag('r')
114
+ assert_equal [], @message.flags
115
+ end
119
116
 
120
- # Test lowercase
121
- @message.remove_flag('r')
122
- assert_equal [], @message.flags
117
+ flag_tests = {
118
+ "FRS" => ['F', 'R', 'S'],
119
+ "Sr" => ['R', 'S'], # test capitalization & sorting
120
+ '' => []
121
+ }
122
+ flag_tests.each do |arg, results|
123
+ should "set flags: #{arg}" do
124
+ @message.flags = arg
125
+ assert_equal results, @message.flags
126
+ path_suffix = "#{Maildir::Message::INFO}#{results.join('')}"
127
+ assert_match /#{path_suffix}$/, @message.path
123
128
  end
129
+ end
130
+ end
124
131
 
125
- flag_tests = {
126
- "FRS" => ['F', 'R', 'S'],
127
- "Sr" => ['R', 'S'], # test capitalization & sorting
128
- '' => []
129
- }
130
- flag_tests.each do |arg, results|
131
- should "set flags: #{arg}" do
132
- @message.flags = arg
133
- assert_equal results, @message.flags
134
- path_suffix = "#{Maildir::Message::INFO}#{results.join('')}"
135
- assert_match /#{path_suffix}$/, @message.path
136
- end
137
- end
138
- context "when destroyed" do
139
- setup { @message.destroy }
140
- should "be frozen" do
141
- assert @message.frozen?, "Message is not frozen"
142
- end
143
- should "have a nonexistant path" do
144
- assert !File.exists?(@message.path), "Message path exists"
145
- end
132
+ context "A destroyed message" do
133
+ setup do
134
+ FakeFS::FileSystem.clear
135
+ @message = Maildir::Message.create(temp_maildir, "foo")
136
+ @message.destroy
137
+ end
138
+ should "be frozen" do
139
+ assert @message.frozen?, "Message is not frozen"
140
+ end
141
+ should "have a nonexistant path" do
142
+ assert !File.exists?(@message.path), "Message path exists"
143
+ end
144
+ end
145
+
146
+ context "A message with a bad path" do
147
+ setup do
148
+ FakeFS::FileSystem.clear
149
+ @message = temp_maildir.add("")
150
+ File.delete(@message.path)
151
+ end
152
+
153
+ should "raise error for data" do
154
+ assert_raise Errno::ENOENT do
155
+ @message.data
146
156
  end
157
+ assert @message.frozen?
158
+ end
159
+
160
+ should "not be processed" do
161
+ old_key = @message.key
162
+ assert_equal false, @message.process
163
+ assert @message.frozen?
164
+ end
165
+
166
+
167
+ should "reset to the old key after attempt to process" do
168
+ old_key = @message.key
169
+ @message.process
170
+ assert_equal old_key, @message.key
147
171
  end
148
172
  end
149
173
 
150
- context "Messages" do
174
+ context "Different messages" do
151
175
  setup do
152
- @message1 = temp_maildir.add("")
176
+ FakeFS::FileSystem.clear
153
177
  end
154
-
178
+
155
179
  should "differ" do
156
- @message2 = Maildir::Message.new(temp_maildir)
180
+ @message1 = temp_maildir.add("")
181
+ @message2 = temp_maildir.add("")
157
182
  assert_equal -1, @message1 <=> @message2
158
183
  assert_equal 1, @message2 <=> @message1
159
184
  assert_not_equal @message1, @message2
160
-
161
185
  end
162
-
186
+ end
187
+
188
+ context "Identical messages" do
189
+ setup do
190
+ FakeFS::FileSystem.clear
191
+ end
192
+
163
193
  should "be identical" do
194
+ @message1 = temp_maildir.add("")
164
195
  another_message1 = temp_maildir.get(@message1.key)
165
196
  assert_equal @message1, another_message1
166
197
  end
167
198
  end
168
199
 
200
+ context "Message#utime" do
201
+ setup do
202
+ FakeFS::FileSystem.clear
203
+ end
204
+
205
+ should "update the messages mtime" do
206
+ @message = temp_maildir.add("")
207
+ time = Time.now - 60
208
+
209
+ @message.utime(time, time)
210
+
211
+ # Time should be within 1 second of each other
212
+ assert_in_delta time, @message.mtime, 1
213
+ end
214
+
215
+ # atime not currently supported in FakeFS
216
+ # should "update the messages atime" do
217
+ # @message = temp_maildir.add("")
218
+ # time = Time.now - 60
219
+ #
220
+ # @message.utime(time, time)
221
+ #
222
+ # # Time should be within 1 second of each other
223
+ # assert_in_delta time, @message.atime, 1
224
+ # end
225
+ end
169
226
  end
@@ -1,11 +1,4 @@
1
1
  require 'test_helper'
2
-
3
- # Require all the serializers
4
- path = File.join(File.dirname(__FILE__), "..","lib","maildir","serializer","*")
5
- Dir.glob(path).each do |file|
6
- require file
7
- end
8
-
9
2
  class TestSerializers < Test::Unit::TestCase
10
3
 
11
4
  serializers = [
@@ -18,6 +11,7 @@ class TestSerializers < Test::Unit::TestCase
18
11
  serializers.each do |klass, dumper|
19
12
  context "A message serialized with #{klass}" do
20
13
  setup do
14
+ FakeFS::FileSystem.clear
21
15
  @data = case klass.new
22
16
  when Maildir::Serializer::Mail
23
17
  Mail.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maildir
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Suggs
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-08 00:00:00 -05:00
12
+ date: 2010-01-24 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -42,6 +42,16 @@ dependencies:
42
42
  - !ruby/object:Gem::Version
43
43
  version: "0"
44
44
  version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: ktheory-fakefs
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
45
55
  description: A ruby library for reading and writing arbitrary messages in DJB's maildir format
46
56
  email: aaron@ktheory.com
47
57
  executables: []
@@ -59,12 +69,14 @@ files:
59
69
  - VERSION
60
70
  - benchmarks/runner
61
71
  - lib/maildir.rb
72
+ - lib/maildir/keywords.rb
62
73
  - lib/maildir/message.rb
63
74
  - lib/maildir/serializer/base.rb
64
75
  - lib/maildir/serializer/json.rb
65
76
  - lib/maildir/serializer/mail.rb
66
77
  - lib/maildir/serializer/marshal.rb
67
78
  - lib/maildir/serializer/yaml.rb
79
+ - lib/maildir/subdirs.rb
68
80
  - lib/maildir/unique_name.rb
69
81
  - maildir.gemspec
70
82
  - test/test_helper.rb