maildir 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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