sortah 0.5.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 +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
|