sortah 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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,10 @@
1
+ require 'sortah/version'
2
+ require 'sortah/parser'
3
+ require 'sortah/patches'
4
+ require 'sortah/errors'
5
+ require 'sortah/cleanroom'
6
+ require 'sortah/handler'
7
+ require 'sortah/components'
8
+ require 'sortah/email'
9
+
10
+ require 'mail'
@@ -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,3 @@
1
+ require 'sortah/components/destination'
2
+ require 'sortah/components/lens'
3
+ require 'sortah/components/router'
@@ -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
@@ -0,0 +1,13 @@
1
+ require 'sortah/util/component_collection'
2
+ require 'sortah/util/component'
3
+
4
+ module Sortah
5
+ class Routers < ComponentCollection
6
+ def has_root?
7
+ self.fetch(:root, false)
8
+ end
9
+ end
10
+
11
+ class Router < Component
12
+ end
13
+ end
@@ -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,7 @@
1
+ module Kernel
2
+ def sortah(&block)
3
+ instance = Sortah::Parser.instance
4
+ instance.handle &block if block_given?
5
+ Sortah::Handler.build_from(instance)
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ module Sortah
2
+ VERSION = "0.5.0"
3
+ 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