maildir 0.3.0 → 0.4.0

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