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/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