sortah 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.rvmrc +2 -0
- data/.travis.yml +3 -0
- data/CONTRIBUTION.md +23 -0
- data/FUTURE_PLANS.md +73 -0
- data/Gemfile +10 -0
- data/KNOWN_BUGS.md +12 -0
- data/LICENSE +27 -0
- data/README.md +216 -0
- data/Rakefile +14 -0
- data/TUTORIAL.md +43 -0
- data/bin/sortah +33 -0
- data/lib/sortah.rb +10 -0
- data/lib/sortah/cleanroom.rb +47 -0
- data/lib/sortah/components.rb +3 -0
- data/lib/sortah/components/destination.rb +41 -0
- data/lib/sortah/components/lens.rb +49 -0
- data/lib/sortah/components/router.rb +13 -0
- data/lib/sortah/email.rb +31 -0
- data/lib/sortah/errors.rb +15 -0
- data/lib/sortah/handler.rb +46 -0
- data/lib/sortah/parser.rb +57 -0
- data/lib/sortah/patches.rb +7 -0
- data/lib/sortah/util/component.rb +28 -0
- data/lib/sortah/util/component_collection.rb +22 -0
- data/lib/sortah/version.rb +3 -0
- data/sortah.gemspec +31 -0
- data/spec/bin_spec.rb +54 -0
- data/spec/destination_spec.rb +42 -0
- data/spec/email_spec.rb +13 -0
- data/spec/fixtures/rc +8 -0
- data/spec/parser_spec.rb +270 -0
- data/spec/semantic_spec.rb +310 -0
- data/spec/sortah_handler_spec.rb +21 -0
- data/spec/spec_helper.rb +3 -0
- metadata +117 -0
data/bin/sortah
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'trollop'
|
3
|
+
require 'sortah'
|
4
|
+
require 'digest/sha1'
|
5
|
+
|
6
|
+
opts = Trollop::options do
|
7
|
+
banner 'Sortah'
|
8
|
+
opt :rc, "path to your sortah files", :default => "#{ENV['HOME']}/.sortahrc"
|
9
|
+
opt :"dry-run", "Do not execute any changes"
|
10
|
+
opt :verbose , "Increase verbosity"
|
11
|
+
end
|
12
|
+
|
13
|
+
puts "Dry-run mode" if opts[:"dry-run"]
|
14
|
+
|
15
|
+
puts "Reading mail" if opts[:verbose]
|
16
|
+
email = Mail.new do
|
17
|
+
body ARGF.read
|
18
|
+
end
|
19
|
+
|
20
|
+
load File.expand_path(opts[:rc])
|
21
|
+
|
22
|
+
dest = sortah.sort(email).full_destination
|
23
|
+
puts "writing email to: #{dest}" if opts[:"dry-run"] || opts[:verbose]
|
24
|
+
|
25
|
+
exit 0 if opts[:"dry-run"]
|
26
|
+
|
27
|
+
#no need to check for dry-run here, we would have exited otherwise
|
28
|
+
system "mkdir -p #{dest}"
|
29
|
+
File.open("#{dest}/#{Digest::SHA1.hexdigest(email.to_s)}.eml", 'w') do |f|
|
30
|
+
f << email.to_s
|
31
|
+
end
|
32
|
+
puts "wrote file to #{dest}" if opts[:verbose]
|
33
|
+
|
data/lib/sortah.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Sortah
|
2
|
+
|
3
|
+
class FinishedExecution < Exception
|
4
|
+
end
|
5
|
+
|
6
|
+
|
7
|
+
class CleanRoom < BasicObject
|
8
|
+
def self.sort(email, context)
|
9
|
+
new(email, context).sort
|
10
|
+
end
|
11
|
+
|
12
|
+
def sort
|
13
|
+
until @pointer.is_a?(Destination) do
|
14
|
+
run!(@pointer) rescue FinishedExecution
|
15
|
+
end
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def metadata(key)
|
20
|
+
email.send(key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def destination
|
24
|
+
@pointer if @pointer.is_a? Destination
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def email; @__email__; end
|
30
|
+
|
31
|
+
def run!(component)
|
32
|
+
@pointer.run_dependencies!(email, @__context__.lenses)
|
33
|
+
self.instance_eval &component.block
|
34
|
+
end
|
35
|
+
|
36
|
+
def send_to(dest)
|
37
|
+
@pointer = @__context__.routers[dest] || @__context__.destinations[dest]
|
38
|
+
throw FinishedExecution
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(email, context)
|
42
|
+
@__email__ = Email.wrap(email)
|
43
|
+
@__context__ = context
|
44
|
+
@pointer = context.routers[:root]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'sortah/util/component_collection'
|
2
|
+
|
3
|
+
module Sortah
|
4
|
+
class Destinations < ComponentCollection
|
5
|
+
def [](key)
|
6
|
+
value = self.fetch(key)
|
7
|
+
if value.alias?
|
8
|
+
self.fetch(value.path)
|
9
|
+
else
|
10
|
+
value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Destination
|
16
|
+
attr_reader :name, :path
|
17
|
+
|
18
|
+
def initialize(name, path)
|
19
|
+
@name = name
|
20
|
+
@path = if path.class == Hash then path[:abs] else path end
|
21
|
+
end
|
22
|
+
|
23
|
+
def defined?(context)
|
24
|
+
context.include?(@name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def alias?
|
28
|
+
@path.class == Symbol
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
(other.class == Destination && other.name == @name && other.path == @path) ||
|
33
|
+
@path == other ||
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
@path
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'sortah/util/component_collection'
|
2
|
+
require 'sortah/util/component'
|
3
|
+
|
4
|
+
module Sortah
|
5
|
+
class Lenses < ComponentCollection
|
6
|
+
def clear_state!
|
7
|
+
self.each_value { |lens| lens.clear_state! }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Lens < Component
|
12
|
+
def provides_value?
|
13
|
+
!@opts[:pass_through]
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?(context)
|
17
|
+
dependencies.each do |lens|
|
18
|
+
raise ParseErrorException unless context.include? lens
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def run!(email, context)
|
23
|
+
@email = email
|
24
|
+
run_dependencies!(email, context)
|
25
|
+
return if already_ran?
|
26
|
+
result = run_block!
|
27
|
+
email.metadata(name, result) if provides_value?
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear_state!; @ran = false; end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def mark_as_run!; @ran = true end
|
35
|
+
def already_ran?; @ran end
|
36
|
+
|
37
|
+
# used for context evaluation
|
38
|
+
def email; @email; end
|
39
|
+
|
40
|
+
def provides_value?
|
41
|
+
!@opts[:pass_through]
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_block!
|
45
|
+
mark_as_run!
|
46
|
+
self.instance_eval &block
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/sortah/email.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'mail'
|
3
|
+
module Sortah
|
4
|
+
class Email < DelegateClass(Mail)
|
5
|
+
def self.wrap(context, metadata = {})
|
6
|
+
Email.new(context, metadata)
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(meth, *args, &blk)
|
10
|
+
return @metadata[meth] if has_data_for?(meth)
|
11
|
+
super rescue nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def metadata(key, value)
|
15
|
+
@metadata[key] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def has_data_for?(meth)
|
21
|
+
@metadata.keys.include?(meth) and
|
22
|
+
@metadata[meth] != :pass_through
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(context, metadata)
|
26
|
+
@metadata = metadata
|
27
|
+
super(context)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Sortah
|
2
|
+
class ParseErrorException < Exception
|
3
|
+
alias_method :to_s, :inspect
|
4
|
+
def inspect
|
5
|
+
"<Sortah::ParseErrorException>"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class NoRootRouterException < Exception
|
10
|
+
alias_method :to_s, :inspect
|
11
|
+
def inspect
|
12
|
+
"<Sortah::NoRootRouterException>"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'sortah/components'
|
2
|
+
module Sortah
|
3
|
+
class Handler
|
4
|
+
def self.build_from(context)
|
5
|
+
new(context)
|
6
|
+
end
|
7
|
+
|
8
|
+
def sort(context)
|
9
|
+
raise NoRootRouterException unless @routers.has_root?
|
10
|
+
clear_state!
|
11
|
+
@result = CleanRoom.sort(context, self)
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :destinations, :lenses, :routers, :maildir
|
16
|
+
|
17
|
+
def metadata(key)
|
18
|
+
@result.metadata(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def destination
|
22
|
+
@result.destination
|
23
|
+
end
|
24
|
+
|
25
|
+
def full_destination
|
26
|
+
maildir + destination.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def clear_state!
|
32
|
+
@lenses.clear_state!
|
33
|
+
end
|
34
|
+
|
35
|
+
def clear_state!
|
36
|
+
@lenses.clear_state!
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(context)
|
40
|
+
@destinations = context.destinations
|
41
|
+
@lenses = context.lenses
|
42
|
+
@routers = context.routers
|
43
|
+
@maildir = context.maildir
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'sortah/components'
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
module Sortah
|
5
|
+
class Parser
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def self.clear!
|
9
|
+
self.instance.clear!
|
10
|
+
end
|
11
|
+
|
12
|
+
##object-level interaction
|
13
|
+
attr_reader :destinations, :lenses, :routers
|
14
|
+
|
15
|
+
def clear!
|
16
|
+
@destinations = Destinations.new
|
17
|
+
@lenses = Lenses.new
|
18
|
+
@routers = Routers.new
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
clear!
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle(&block)
|
26
|
+
self.instance_eval &block
|
27
|
+
valid?
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid?
|
31
|
+
@lenses.valid?
|
32
|
+
@routers.valid?
|
33
|
+
@destinations.valid?
|
34
|
+
end
|
35
|
+
|
36
|
+
## metadata/config data
|
37
|
+
|
38
|
+
#double-duty getter/setter
|
39
|
+
def maildir(maildir_path = nil)
|
40
|
+
@maildir = maildir_path if maildir_path
|
41
|
+
@maildir
|
42
|
+
end
|
43
|
+
|
44
|
+
## language elements
|
45
|
+
def destination(name, args)
|
46
|
+
@destinations << Destination.new(name, args)
|
47
|
+
end
|
48
|
+
|
49
|
+
def lens(name, opts = {}, &block)
|
50
|
+
@lenses << Lens.new(name, opts, block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def router(name = :root, opts = {}, &block)
|
54
|
+
@routers << Router.new(name, opts, block)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Sortah
|
2
|
+
class Component
|
3
|
+
attr_reader :name, :block
|
4
|
+
|
5
|
+
def initialize(name, opts = {}, *potential_block)
|
6
|
+
@name = name
|
7
|
+
@opts = opts
|
8
|
+
@block = potential_block.first unless potential_block.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
def run_dependencies!(email, context)
|
12
|
+
dependencies(context).each { |lens| lens.run!(email, context) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def defined?(context)
|
16
|
+
!!context[name]
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def dependencies(context = nil)
|
22
|
+
lenses = (@opts[:lenses] || [])
|
23
|
+
return lenses if context.nil?
|
24
|
+
lenses.map { |lens| context[lens] }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Sortah
|
2
|
+
class ComponentCollection < Hash
|
3
|
+
def <<(component)
|
4
|
+
return unless component.respond_to? :name
|
5
|
+
raise ParseErrorException if component.defined?(self)
|
6
|
+
self[component.name] = component
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
return if self.empty?
|
11
|
+
self.each_value do |value|
|
12
|
+
# someone might have registered a singleton method?
|
13
|
+
next unless value.respond_to? :valid?
|
14
|
+
value.valid?(self)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def defined?(dest)
|
19
|
+
self.keys.include?(dest)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/sortah.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "sortah/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "sortah"
|
7
|
+
s.version = Sortah::VERSION
|
8
|
+
s.authors = ["Joe Fredette"]
|
9
|
+
s.email = ["jfredett@gmail.com"]
|
10
|
+
s.homepage = "http://www.github.com/jfredett/sortah"
|
11
|
+
s.summary = %q{For sortin' your email}
|
12
|
+
s.description = %q{
|
13
|
+
Sortah provides a simple, declarative internal DSL for sorting
|
14
|
+
your email. It provides an executable which may serve as an external
|
15
|
+
mail delivery agent for such programs as `getmail`. Finally, since
|
16
|
+
your sorting logic is just Plain Old Ruby Code (PORC, as I like to call it).
|
17
|
+
You have access to 100% of ruby as needed, including all of it's
|
18
|
+
object oriented goodness, it's wonderful community of gems, and it's
|
19
|
+
powerful metaprogramming ability.
|
20
|
+
}
|
21
|
+
|
22
|
+
s.rubyforge_project = "sortah"
|
23
|
+
|
24
|
+
s.add_dependency "mail"
|
25
|
+
s.add_dependency "trollop"
|
26
|
+
|
27
|
+
s.files = `git ls-files`.split("\n")
|
28
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
29
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
30
|
+
s.require_paths = ["lib"]
|
31
|
+
end
|