maildir 0.1.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 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/NOTES.txt ADDED
@@ -0,0 +1,17 @@
1
+ md = Maildir.new(path)
2
+
3
+ md.add(data) => message
4
+
5
+ md.list_new => array of messages
6
+
7
+ md.list_cur => array of messages
8
+
9
+ message.process(flags) => moves message from new to cur and adds flags
10
+
11
+ message.flags
12
+ message.flags=
13
+ message.add_flag
14
+ message.remove_flag
15
+
16
+ message.info
17
+ message.info=
data/README.rdoc ADDED
@@ -0,0 +1,9 @@
1
+ = Maildir
2
+
3
+ A ruby library for reading and writing messages in the maildir format.
4
+
5
+ See http://cr.yp.to/proto/maildir.html
6
+
7
+ == Copyright
8
+
9
+ Copyright (c) 2009 Aaron Suggs. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
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 = "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"]
19
+ gemspec.add_development_dependency "thoughtbot-shoulda", ">= 0"
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
24
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/benchmarks/runner ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'maildir'
data/lib/maildir.rb ADDED
@@ -0,0 +1,66 @@
1
+ class Maildir
2
+
3
+ SUBDIRS = [:tmp, :new, :cur].freeze
4
+ READABLE_DIRS = SUBDIRS.reject{|s| :tmp == s}.freeze
5
+
6
+ attr_reader :path
7
+ def initialize(path, create = true)
8
+ @path = File.join(path, '/') # Ensure path has a trailing slash
9
+ create_subdirectories if create
10
+ end
11
+
12
+ # Maildirs are indentical if they have the same path
13
+ def ==(maildir)
14
+ return false unless Maildir === maildir
15
+ maildir.path == self.path
16
+ end
17
+
18
+ # define methods tmp_path, new_path, & cur_path
19
+ SUBDIRS.each do |subdir|
20
+ define_method "#{subdir}_path" do
21
+ File.join(path, subdir.to_s)
22
+ end
23
+ end
24
+
25
+ # Ensure subdirectories exist. This can safely be called multiple times, but
26
+ # must hit the disk. Avoid calling this if you're certain the directories
27
+ # exist.
28
+ def create_subdirectories
29
+ SUBDIRS.each do |subdir|
30
+ FileUtils.mkdir_p(self.send("#{subdir}_path"))
31
+ end
32
+ end
33
+
34
+ def list(new_or_cur)
35
+ list_keys(new_or_cur).map{|key| get_message(key)}
36
+ end
37
+
38
+ def list_keys(new_or_cur)
39
+ new_or_cur = new_or_cur.to_sym
40
+ unless [:new, :cur].include? new_or_cur
41
+ raise ArgumentError, "first arg must be new or cur"
42
+ end
43
+ get_dir_listing(new_or_cur)
44
+ end
45
+
46
+ # Writes IO object out as a new message. See Maildir::Message.create for
47
+ # more.
48
+ def add_message(io)
49
+ Maildir::Message.create(self, io)
50
+ end
51
+
52
+ def get_message(key)
53
+ Maildir::Message.new(self, key)
54
+ end
55
+
56
+ protected
57
+ def get_dir_listing(new_or_cur)
58
+ search_path = File.join(self.path, new_or_cur.to_s, '*')
59
+ results = Dir.glob(search_path)
60
+ # Remove the maildir's path from the beginning of the message path
61
+ results.map!{|message_path| message_path.sub!(self.path, '')}
62
+ end
63
+ end
64
+
65
+ require 'maildir/unique_name'
66
+ require 'maildir/message'
@@ -0,0 +1,131 @@
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
+ class << self
8
+ # Create a new message in maildir with the contents of io. The message is
9
+ # first written to the tmp dir, then moved to new. This is a shortcut for:
10
+ # message = Maildir::Message.new(maildir)
11
+ # message.write(io)
12
+ def create(maildir, io)
13
+ message = self.new(maildir)
14
+ message.write(io)
15
+ message
16
+ end
17
+ end
18
+
19
+ attr_reader :dir, :unique_name, :info, :old_key
20
+
21
+ # Create a new, unwritten message:
22
+ # Message.new(maildir)
23
+ #
24
+ # Instantiate an existing message:
25
+ # Message.new(maildir, key)
26
+ def initialize(maildir, key=nil)
27
+ @maildir = maildir
28
+ if key.nil?
29
+ @dir = :tmp
30
+ else
31
+ parse_key(key)
32
+ end
33
+
34
+ raise ArgumentError, "State must be in #{Maildir::SUBDIRS.inspect}" unless Maildir::SUBDIRS.include? dir
35
+
36
+ if :tmp == dir
37
+ @unique_name = Maildir::UniqueName.create
38
+ end
39
+ end
40
+
41
+ # Writes io to disk. Can only be called on messages instantiated without a
42
+ # key (which haven't been written to disk). If the io object has a 'read'
43
+ # method, calls io.read. Otherwise, calls io.to_s.
44
+ def write(io)
45
+ raise "Can only write to messages in tmp" unless :tmp == @dir
46
+ # Write out contents to tmp
47
+ File.open(path, 'w') do |file|
48
+ file.puts io.respond_to?(:read) ? io.read : io.to_s
49
+ end
50
+
51
+ # Rename to new
52
+ rename(:new)
53
+
54
+ end
55
+
56
+ # Move a message from new to cur, add info
57
+ def process
58
+ rename(:cur, INFO)
59
+ end
60
+
61
+ def info=(info)
62
+ raise "Can only set info on cur messages" unless :cur == @dir
63
+ rename(:cur, info)
64
+ end
65
+
66
+ # Returns an array of single letter flags applied to the message
67
+ def flags
68
+ @info.sub(INFO,'').split('')
69
+ end
70
+
71
+ def flags=(*flags)
72
+ self.info = INFO + sort_flags(flags.flatten.join(''))
73
+ end
74
+
75
+ def add_flag(flag)
76
+ self.flags = (flags << flag.upcase)
77
+ end
78
+
79
+ def remove_flag(flag)
80
+ self.flags = flags.delete_if{|f| f == flag.upcase}
81
+ end
82
+
83
+
84
+ # Returns the filename of the message
85
+ def filename
86
+ [unique_name, info].compact.join(COLON)
87
+ end
88
+
89
+ # Returns the key to identify the message
90
+ def key
91
+ File.join(dir.to_s, filename)
92
+ end
93
+
94
+ # Returns the full path to the message
95
+ def path
96
+ File.join(@maildir.path, key)
97
+ end
98
+
99
+ def old_path
100
+ File.join(@maildir.path, old_key)
101
+ end
102
+
103
+ protected
104
+ # Sets dir, unique_name, and info based in key
105
+ def parse_key(key)
106
+ @dir, filename = key.split(File::SEPARATOR)
107
+ @dir = @dir.to_sym
108
+ @unique_name, @info = filename.split(COLON)
109
+ end
110
+
111
+ def sort_flags(flags)
112
+ flags.split('').map{|f| f.upcase}.sort!.uniq.join('')
113
+ end
114
+
115
+ def rename(new_dir, new_info=nil)
116
+ # Safe the old key so we can revert to the old state
117
+ @old_key = key
118
+
119
+ # Set the new state
120
+ @dir = new_dir
121
+ @info = new_info if new_info
122
+
123
+ begin
124
+ File.rename(old_path, path) unless old_path == path
125
+ rescue Errno::ENOENT
126
+ # Restore ourselves to the old state
127
+ parse_key(@old_key)
128
+ raise
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,72 @@
1
+ # Class for generating unique file names for new messages
2
+ class Maildir::UniqueName
3
+ require 'thread' # For mutex support
4
+ require 'socket' # For getting the hostname
5
+ class << self
6
+ # Return a thread-safe increasing counter
7
+ def counter
8
+ @counter_mutex ||= Mutex.new
9
+ @counter_mutex.synchronize do
10
+ @counter = @counter.to_i + 1
11
+ end
12
+ end
13
+
14
+ def create
15
+ self.new.to_s
16
+ end
17
+ end
18
+
19
+ # Return a unique file name based on strategy
20
+ def initialize
21
+ # Use the same time object
22
+ @now = Time.now
23
+ end
24
+
25
+ # Return the name as a string
26
+ def to_s
27
+ [left, middle, right].join(".")
28
+ end
29
+
30
+ protected
31
+ # The left part of the unique name is the number of seconds from since the
32
+ # UNIX epoch
33
+ def left
34
+ @now.to_i.to_s
35
+ end
36
+
37
+ # The middle part contains the microsecond, the process id, and a
38
+ # per-process incrementing counter
39
+ def middle
40
+ "M#{microsecond}P#{process_id}Q#{delivery_count}"
41
+ end
42
+
43
+ # The right part is the hostname
44
+ def right
45
+ Socket.gethostname
46
+ end
47
+
48
+ def secure_random(bytes=8)
49
+ # File.read("/dev/urandom", bytes).unpack("H*")[0]
50
+ raise "Not implemented"
51
+ end
52
+
53
+ def inode
54
+ raise "Not implemented"
55
+ end
56
+
57
+ def device_number
58
+ raise "Not implemented"
59
+ end
60
+
61
+ def microsecond
62
+ @now.usec.to_s
63
+ end
64
+
65
+ def process_id
66
+ Process.pid.to_s
67
+ end
68
+
69
+ def delivery_count
70
+ self.class.counter.to_s
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'tmpdir'
5
+ require 'tempfile'
6
+ require 'fileutils'
7
+ require 'maildir'
8
+
9
+ # Create a reusable maildir that's cleaned up when the tests are done
10
+ 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)
19
+ end
20
+
21
+ # Useful for testing that strings defined & not empty
22
+ def assert_not_empty(obj, msg='')
23
+ assert !obj.nil? && !obj.empty?, msg
24
+ end
@@ -0,0 +1,38 @@
1
+ require 'test_helper'
2
+ class TestMaildir < Test::Unit::TestCase
3
+
4
+ context "A maildir" do
5
+
6
+ should "be initialized" do
7
+ assert temp_maildir
8
+ end
9
+
10
+ should "have a path" do
11
+ assert_not_empty temp_maildir.path
12
+ end
13
+
14
+ should "create subdirectories by default" do
15
+ %w(tmp new cur).each do |subdir|
16
+ subdir_path = temp_maildir.send("#{subdir}_path")
17
+ assert File.directory?(subdir_path), "Subdir #{subdir} does not exist"
18
+ end
19
+ end
20
+
21
+ should "not create directories if specified" do
22
+ tmp_dir = Dir.mktmpdir('new_maildir_test')
23
+ maildir = Maildir.new(tmp_dir, false)
24
+ %w(tmp new cur).each do |subdir|
25
+ subdir_path = maildir.send("#{subdir}_path")
26
+ assert !File.directory?(subdir_path), "Subdir #{subdir} exists"
27
+ end
28
+ FileUtils.rm_r(tmp_dir)
29
+ end
30
+
31
+ should "be identical to maildirs with the same path" do
32
+ new_maildir = Maildir.new(temp_maildir.path)
33
+ assert_equal temp_maildir.path, new_maildir.path
34
+ assert_equal temp_maildir, new_maildir
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,152 @@
1
+ require 'test_helper'
2
+ class TestMessage < Test::Unit::TestCase
3
+
4
+
5
+ context "An new, unwritten message" do
6
+ setup do
7
+ @message = Maildir::Message.new(temp_maildir)
8
+ end
9
+
10
+ should "be instantiated" do
11
+ assert @message
12
+ end
13
+
14
+ should "be in :tmp" do
15
+ assert_equal :tmp, @message.dir
16
+ assert_match(/tmp/, @message.path)
17
+ end
18
+
19
+ should "have a unique name" do
20
+ assert_not_empty @message.unique_name
21
+ end
22
+
23
+ should "have a file name" do
24
+ assert_not_empty @message.filename
25
+ end
26
+
27
+ should "have no info" do
28
+ assert_nil @message.info
29
+ end
30
+
31
+ should "not be able to set info" do
32
+ assert_raises RuntimeError do
33
+ @message.info= "2,FRS"
34
+ end
35
+ end
36
+
37
+ context "when written with a string" do
38
+ setup do
39
+ @data = "foo\n"
40
+ @message.write(@data)
41
+ end
42
+
43
+ should "not be writable" do
44
+ assert_raise RuntimeError do
45
+ @message.write("nope!")
46
+ end
47
+ end
48
+
49
+ should "have no info" do
50
+ assert_nil @message.info
51
+ end
52
+
53
+ should "not be able to set info" do
54
+ assert_raises RuntimeError do
55
+ @message.info= "2,FRS"
56
+ end
57
+ end
58
+
59
+ should "be in new dir" do
60
+ assert_equal :new, @message.dir
61
+ assert_match(/new/, @message.path)
62
+ end
63
+
64
+ should "have have a file" do
65
+ assert File.exists?(@message.path)
66
+ end
67
+
68
+ should "have the correct data" do
69
+ assert @data == File.open(@message.path).read
70
+ end
71
+ end
72
+
73
+ context "when written with an IO object" do
74
+ setup do
75
+ @data = "foo\n"
76
+ @message.write(StringIO.open(@data))
77
+ end
78
+
79
+ should "have the correct data" do
80
+ assert @data == File.open(@message.path).read
81
+ end
82
+ end
83
+ end
84
+
85
+ context "A created message" do
86
+ setup do
87
+ @data = "foo\n"
88
+ @message = Maildir::Message.create(temp_maildir, @data)
89
+ end
90
+
91
+ should "have the correct data" do
92
+ assert @data == File.open(@message.path).read
93
+ end
94
+
95
+ context "when processed" do
96
+ setup do
97
+ @message.process
98
+ end
99
+
100
+ should "not be writable" do
101
+ assert_raise RuntimeError do
102
+ @message.write("nope!")
103
+ end
104
+ end
105
+
106
+ should "be in cur" do
107
+ assert_equal :cur, @message.dir
108
+ end
109
+
110
+ should "have info" do
111
+ assert_equal Maildir::Message::INFO, @message.info
112
+ end
113
+
114
+ should "set info" do
115
+ info = "2,FRS"
116
+ @message.info = "2,FRS"
117
+ assert_equal @message.info, info
118
+ assert_match /#{info}$/, @message.path
119
+ end
120
+
121
+ should "add and remove flags" do
122
+ @message.add_flag('S')
123
+ assert_equal ['S'], @message.flags
124
+
125
+ # Test lowercase
126
+ @message.add_flag('r')
127
+ assert_equal ['R', 'S'], @message.flags
128
+
129
+ @message.remove_flag('S')
130
+ assert_equal ['R'], @message.flags
131
+
132
+ # Test lowercase
133
+ @message.remove_flag('r')
134
+ assert_equal [], @message.flags
135
+ end
136
+
137
+ flag_tests = {
138
+ "FRS" => ['F', 'R', 'S'],
139
+ "Sr" => ['R', 'S'], # test capitalization & sorting
140
+ '' => []
141
+ }
142
+ flag_tests.each do |arg, results|
143
+ should "set flags: #{arg}" do
144
+ @message.flags = arg
145
+ assert_equal results, @message.flags
146
+ path_suffix = "#{Maildir::Message::INFO}#{results.join('')}"
147
+ assert_match /#{path_suffix}$/, @message.path
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+ class TestUniqueName < Test::Unit::TestCase
3
+
4
+ context "A UniqueName" do
5
+ setup do
6
+ @raw_name = Maildir::UniqueName.new
7
+ @name = @raw_name.to_s
8
+ @now = @raw_name.send(:instance_variable_get, :@now)
9
+ end
10
+
11
+ should "be initialized" do
12
+ assert @raw_name
13
+ end
14
+
15
+ should "have a name" do
16
+ assert_not_empty @name
17
+ end
18
+
19
+ should "begin with timestamp" do
20
+ assert_match /^#{@now.to_i}/, @name
21
+ end
22
+
23
+ should "end with hostname" do
24
+ assert_match /#{Socket.gethostname}$/, @name
25
+ end
26
+
27
+ should "be unique when created in the same microsecond" do
28
+ @new_name = Maildir::UniqueName.new
29
+ # Set @now be identical in both UniqueName instances
30
+ @new_name.send(:instance_variable_set, :@now, @now)
31
+ assert_not_equal @name, @new_name.to_s
32
+ end
33
+
34
+ end
35
+
36
+ context "The UniqueName counter" do
37
+ should "increment when called" do
38
+ value1 = Maildir::UniqueName.counter
39
+ value2 = Maildir::UniqueName.counter
40
+ assert_equal value1+1, value2
41
+ end
42
+ end
43
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: maildir
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Suggs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-05 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: A ruby library for reading and writing arbitrary messages in DJB's maildir format
26
+ email: aaron@ktheory.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - LICENSE
36
+ - NOTES.txt
37
+ - README.rdoc
38
+ - Rakefile
39
+ - VERSION
40
+ - benchmarks/runner
41
+ - lib/maildir.rb
42
+ - lib/maildir/message.rb
43
+ - lib/maildir/unique_name.rb
44
+ - test/test_helper.rb
45
+ - test/test_maildir.rb
46
+ - test/test_message.rb
47
+ - test/test_unique_name.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/ktheory/maildir
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --charset=UTF-8
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.5
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Read & write messages in the maildir format
76
+ test_files:
77
+ - test/test_helper.rb
78
+ - test/test_maildir.rb
79
+ - test/test_message.rb
80
+ - test/test_unique_name.rb