kns_email_endpoint 0.1.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.
Files changed (39) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +15 -0
  3. data/Gemfile.lock +71 -0
  4. data/Guardfile +10 -0
  5. data/LICENCE +21 -0
  6. data/README.md +1 -0
  7. data/Rakefile +2 -0
  8. data/kns_email_endpoint.gemspec +39 -0
  9. data/lib/kns_email_endpoint/configuration.rb +87 -0
  10. data/lib/kns_email_endpoint/connection.rb +97 -0
  11. data/lib/kns_email_endpoint/core_extensions/class.rb +44 -0
  12. data/lib/kns_email_endpoint/core_extensions/hash.rb +14 -0
  13. data/lib/kns_email_endpoint/core_extensions/imap.rb +36 -0
  14. data/lib/kns_email_endpoint/core_extensions/message.rb +26 -0
  15. data/lib/kns_email_endpoint/core_extensions/pop3.rb +0 -0
  16. data/lib/kns_email_endpoint/core_extensions/test_retriever.rb +42 -0
  17. data/lib/kns_email_endpoint/email_endpoint.rb +88 -0
  18. data/lib/kns_email_endpoint/message_state.rb +89 -0
  19. data/lib/kns_email_endpoint/process_email.rb +123 -0
  20. data/lib/kns_email_endpoint/storage/abstract_storage.rb +93 -0
  21. data/lib/kns_email_endpoint/storage/file_storage.rb +74 -0
  22. data/lib/kns_email_endpoint/storage/memcache_storage.rb +72 -0
  23. data/lib/kns_email_endpoint/storage/storage.rb +15 -0
  24. data/lib/kns_email_endpoint/version.rb +3 -0
  25. data/lib/kns_email_endpoint.rb +19 -0
  26. data/spec/a18x34/.app +4 -0
  27. data/spec/a18x34/a18x34.krl +91 -0
  28. data/spec/lib/kns_email_endpoint/configuration_spec.rb +44 -0
  29. data/spec/lib/kns_email_endpoint/connection_spec.rb +133 -0
  30. data/spec/lib/kns_email_endpoint/email_endpoint_spec.rb +97 -0
  31. data/spec/lib/kns_email_endpoint/message_state_spec.rb +69 -0
  32. data/spec/lib/kns_email_endpoint/process_email_spec.rb +67 -0
  33. data/spec/lib/kns_email_endpoint/storage/file_storage_spec.rb +16 -0
  34. data/spec/lib/kns_email_endpoint/storage/memcache_storage_spec.rb +17 -0
  35. data/spec/lib/kns_email_endpoint/storage/shared_storage.rb +65 -0
  36. data/spec/lib/kns_email_endpoint/storage/storage_spec.rb +14 -0
  37. data/spec/test_config_file.yml +65 -0
  38. data/spec/test_email.eml +43 -0
  39. metadata +181 -0
@@ -0,0 +1,89 @@
1
+ require 'digest/sha1'
2
+ module KNSEmailEndpoint
3
+ class MessageState
4
+
5
+ class << self
6
+ attr_accessor :storage
7
+
8
+ def set_storage(storage, opts)
9
+ @storage = Storage.get_storage(storage, opts)
10
+ return @storage
11
+ end
12
+
13
+ def gen_unique_id(conn,msg)
14
+ raise "Message must have a valid message_id" unless msg.message_id.to_s != ""
15
+ Digest::SHA1.hexdigest "#{conn}::#{msg.message_id}::K-KEY"
16
+ end
17
+ end
18
+
19
+
20
+ attr_reader :message, :message_id, :retry_count, :unique_id, :state
21
+
22
+ def initialize(conn_name, message)
23
+ # setup the state
24
+ @conn_name = conn_name
25
+ @message = message
26
+ @message_id = get_message_id
27
+ @unique_id = get_unique_id
28
+ @storage = self.class.storage
29
+ raise "Unknown Storage" unless @storage
30
+
31
+ # get from storage
32
+ @storage.find_or_create(:unique_id => @unique_id, :message_id => @message_id)
33
+ @state = @storage.state
34
+ @retry_count = @storage.retry_count
35
+
36
+ # Do not auto delete email.
37
+ @message.mark_for_delete = false
38
+
39
+ end
40
+
41
+ def state=(s)
42
+ @state = s
43
+ @storage.state = @state if @storage.unique_id
44
+ @message.mark_for_delete = @state == :deleted
45
+ return @state
46
+ end
47
+
48
+ def retry
49
+ @retry_count += 1
50
+ @storage.retry_count = @retry_count
51
+ return @retry_count
52
+ end
53
+
54
+ def reset
55
+ @retry_count = 0
56
+ @storage.retry_count = @retry_count
57
+ return @retry_count
58
+ end
59
+
60
+ def reset_state
61
+ @storage.delete
62
+ @storage.create(:unique_id => @unique_id, :message_id => @message_id)
63
+ @retry_count = @storage.retry_count
64
+ @state = @storage.state
65
+ end
66
+
67
+ def delete
68
+ @storage.delete
69
+ self.state = :deleted
70
+ end
71
+
72
+
73
+ private
74
+
75
+ def get_message_id
76
+ return @message_id if @message_id
77
+ if @message.has_message_id?
78
+ @message_id = @message.message_id
79
+ else
80
+ raise "The mail message does not have a valid message_id."
81
+ end
82
+ return @message_id
83
+ end
84
+
85
+ def get_unique_id
86
+ return @unique_id ||= self.class.gen_unique_id(@conn_name, @message)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,123 @@
1
+ require 'work_queue'
2
+ module KNSEmailEndpoint
3
+
4
+ class ProcessEmail
5
+ # if in repeat mode, these states will be processed
6
+ $REPEATABLE_STATES = [:forwarded, :replied, :unprocessed, :error]
7
+ # if in single mode, these states will be processed
8
+ $SINGLE_STATES = [:unprocessed, :error]
9
+
10
+ class << self
11
+
12
+ def go(conn)
13
+ raise "Need config" unless Configuration[conn.name]
14
+ log = conn.conn_log
15
+ 3.times { log.debug "" }
16
+ log.info "Processing Email from connection: #{conn.name}"
17
+ endpoint_opts = {
18
+ :ruleset => conn.appid,
19
+ :environment => conn.environment,
20
+ :use_session => true,
21
+ :logging => log.debug?
22
+ }
23
+
24
+ begin
25
+ queue = WorkQueue.new(Configuration.work_threads)
26
+ email_processed_count = 0
27
+ email_errors_count = 0
28
+ conn.retriever.find({
29
+ :count => :all,
30
+ :delete_after_find => true,
31
+ :what => :first,
32
+ :order => :asc
33
+ }) do |msg|
34
+ # worker enqueue
35
+ queue.enqueue_b {
36
+ begin
37
+ msg_state = MessageState.new(conn.name, msg)
38
+ log.debug "Processing Message #{msg_state.unique_id}"
39
+ log.debug "STATE: #{msg_state.state}"
40
+ if (conn.process_mode == :single && $SINGLE_STATES.include?(msg_state.state)) ||
41
+ (conn.process_mode == :repeat && $REPEATABLE_STATES.include?(msg_state.state))
42
+
43
+ ee = EmailEndpoint.new(conn.name, endpoint_opts, conn.sender)
44
+ event_args = {
45
+ :msg => msg,
46
+ :unique_id => msg_state.unique_id
47
+ }.merge! conn.event_args
48
+
49
+ log.debug "Raising Event\n #{event_args.inspect}"
50
+ result = ee.received(event_args)
51
+ if log.debug?
52
+ log.debug "--- Endpoint Log ---"
53
+ log.debug ee.log.join("\n")
54
+ log.debug "--------------------"
55
+ end
56
+ if ee.message_state.state == :processing
57
+ # there was no directive returned or endpoint failed.
58
+ log.debug "UNEXPECTED DIRECTIVE RECEIVED: \n#{result.inspect}"
59
+ raise "No directive matched message (#{msg.message_id})"
60
+
61
+ end
62
+ log.debug "NEW STATE: " + ee.message_state.state.to_s
63
+ log.debug "Delete message? #{msg.is_marked_for_delete?}"
64
+ else
65
+ log.debug "Skipping #{msg.message_id} (#{msg_state.state})"
66
+ log.debug "Delete message? #{msg.is_marked_for_delete?}"
67
+ end
68
+ email_processed_count += 1
69
+
70
+ rescue => e
71
+ rc = msg_state.retry
72
+ if rc >= (conn.max_retry_count - 1)
73
+ msg_state.state = :failed
74
+ else
75
+ msg_state.state = :error
76
+ end
77
+ log.error e.message
78
+ log.error "RETRY COUNT: #{rc}"
79
+ log.error "NEW STATE: #{msg_state.state}"
80
+ log.error "Delete message? #{msg.is_marked_for_delete?}"
81
+ email_errors_count += 1
82
+ end
83
+
84
+ }
85
+ queue.join
86
+ end
87
+ log.info "Number of email successfully processed for connection #{conn.name}: #{email_processed_count}"
88
+ log.info "Number of email unsuccesfully processed for connection #{conn.name}: #{email_errors_count}"
89
+ rescue => e
90
+ log.error "There was an error processing email for #{conn.name}: #{e.message}"
91
+ end
92
+
93
+
94
+ end
95
+
96
+ def go_async
97
+ threads = []
98
+ begin
99
+ Configuration.each_connection do |conn|
100
+ threads << Thread.new { go conn }
101
+ end
102
+ threads.each { |t| t.join }
103
+ rescue => e
104
+ Configuration.log.error e.message
105
+ end
106
+ end
107
+
108
+ def go_all
109
+ begin
110
+ Configuration.each_connection { |conn| go conn }
111
+ rescue => e
112
+ Configuration.log.error e.message
113
+ end
114
+ end
115
+
116
+ def flush
117
+ # flush all message states
118
+ Configuration.storage_engine.delete_all
119
+ end
120
+ end
121
+
122
+ end
123
+ end
@@ -0,0 +1,93 @@
1
+ module KNSEmailEndpoint
2
+ module Storage
3
+ class AbstractStorage
4
+ attr_reader :message_id, :unique_id, :retry_count, :state
5
+
6
+ def initialize(*args)
7
+
8
+ reset_storage
9
+ end
10
+
11
+ # delete should return either true if successful, or false if unsuccessful
12
+ # delete should also call reset_storage if successful
13
+ def delete
14
+ return false # override me
15
+
16
+ end
17
+
18
+ # create should call set_vars and save! if successful and return true
19
+ # otherwise, it should raise an exception
20
+ def create(opts={})
21
+ return false #override me
22
+
23
+ end
24
+
25
+ # find should return self if a record is found after calling set_vars
26
+ # it should return nil if not found and call reset_storage
27
+ def find(unique_id)
28
+ return nil #override me
29
+ end
30
+
31
+ def retry_count=(r)
32
+ raise "Unknown unique_id" unless @unique_id
33
+ @retry_count = r
34
+ save!
35
+ end
36
+
37
+ def state=(s)
38
+ raise "Unknown unique_id" unless @unique_id
39
+ @state = s
40
+ save!
41
+ end
42
+
43
+ def to_h
44
+ return {} unless @unique_id
45
+ return {
46
+ :unique_id => @unique_id,
47
+ :message_id => @message_id,
48
+ :retry_count => @retry_count,
49
+ :state => @state
50
+ }
51
+ end
52
+
53
+ def find_or_create(opts={})
54
+ unless opts[:unique_id] && opts[:message_id]
55
+ raise "Must provide at least a unique_id and message_id"
56
+ end
57
+
58
+ s = find(opts[:unique_id])
59
+ return s if s
60
+
61
+ return create(opts) ? self : nil
62
+ end
63
+
64
+ private
65
+
66
+ def save!
67
+ # override me
68
+ end
69
+
70
+ def set_vars(h)
71
+ h.symbolize_keys!
72
+ raise "Invalid unique_id" unless h[:unique_id]
73
+ raise "Invalid message_id" unless h[:message_id]
74
+ raise "Invalid retry_count" unless h[:retry_count]
75
+ raise "Invalid state" unless h[:state]
76
+ @unique_id = h[:unique_id]
77
+ @message_id = h[:message_id]
78
+ @retry_count = h[:retry_count]
79
+ @state = h[:state].to_sym
80
+
81
+ end
82
+
83
+ def reset_storage
84
+ @unique_id = nil
85
+ @message_id = nil
86
+ @retry_count = nil
87
+ @state = nil
88
+ @current_file = nil
89
+ end
90
+
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,74 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+ module KNSEmailEndpoint
4
+ module Storage
5
+ class FileStorage < AbstractStorage
6
+ attr_reader :name
7
+
8
+ def initialize(settings={})
9
+ @dir = settings[:file_location]
10
+ raise "Unknown file_location" unless @dir
11
+ FileUtils.mkdir_p @dir
12
+
13
+ super(settings)
14
+ @name = "file"
15
+
16
+ end
17
+
18
+ def delete
19
+ return false unless @current_file
20
+ FileUtils.rm @current_file if File.exists? @current_file
21
+ reset_storage
22
+ return true
23
+ end
24
+
25
+ def create(opts={})
26
+ options = {
27
+ :state => :unprocessed,
28
+ :retry_count => 0
29
+ }.merge! opts
30
+ raise ":unique_id is required" unless options[:unique_id]
31
+ raise ":message_id is required" unless options[:message_id]
32
+
33
+ @current_file = File.join(@dir, options[:unique_id])
34
+
35
+ if File.exists? @current_file
36
+ raise "unique_id #{options[:unique_id]} already exists"
37
+ end
38
+
39
+ set_vars(options)
40
+ save!
41
+
42
+ return true
43
+ end
44
+
45
+ def find(unique_id)
46
+ lookup_file = File.join(@dir, unique_id)
47
+ if File.exists? lookup_file
48
+ @current_file = lookup_file
49
+ set_vars JSON.parse(File.open(@current_file, 'r').read)
50
+ return self
51
+ else
52
+ reset_storage
53
+ return nil
54
+ end
55
+ end
56
+
57
+ def delete_all
58
+ FileUtils.remove_dir(@dir, true)
59
+ FileUtils.mkdir_p @dir
60
+ end
61
+
62
+
63
+ private
64
+
65
+ def save!
66
+ File.open(@current_file, "w") do |f|
67
+ f.write to_h.to_json
68
+ end
69
+ end
70
+
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,72 @@
1
+ require 'dalli'
2
+ module KNSEmailEndpoint
3
+ module Storage
4
+ class MemcacheStorage < AbstractStorage
5
+ class << self
6
+ attr_accessor :client
7
+ end
8
+
9
+ def initialize(settings)
10
+ options = {
11
+ :host => "localhost",
12
+ :port => 11211,
13
+ :ttl => nil
14
+ }.merge!(settings)
15
+
16
+
17
+ # This simple bit of magic allows usage of
18
+ # a class connection rather than an instance connection
19
+ # so we never have more than one active connection to memcache
20
+ self.class.client ||= Dalli::Client.new("#{options[:host]}:#{options[:port]}")
21
+ @client = self.class.client
22
+ @ttl = options[:ttl]
23
+
24
+ end
25
+
26
+ def create(opts={})
27
+ options = {
28
+ :state => :unprocessed,
29
+ :retry_count => 0
30
+ }.merge! opts
31
+ raise ":unique_id is required" unless options[:unique_id]
32
+ raise ":message_id is required" unless options[:message_id]
33
+
34
+ set_vars(options)
35
+ save!
36
+
37
+ return true
38
+ end
39
+
40
+ def find(unique_id)
41
+ s = @client.get(unique_id)
42
+ if s
43
+ set_vars(s)
44
+ return self
45
+ else
46
+ reset_storage
47
+ return nil
48
+ end
49
+ end
50
+
51
+ def delete
52
+ return false unless @unique_id
53
+ @client.delete @unique_id
54
+ reset_storage
55
+ return true
56
+ end
57
+
58
+ def delete_all
59
+ @client.flush
60
+ reset_storage
61
+ return true
62
+ end
63
+
64
+ private
65
+
66
+ def save!
67
+ @client.set(@unique_id, to_h, @ttl)
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,15 @@
1
+ require 'kns_email_endpoint/storage/abstract_storage'
2
+ module KNSEmailEndpoint
3
+ module Storage
4
+ autoload :FileStorage, 'kns_email_endpoint/storage/file_storage'
5
+ autoload :MemcacheStorage, 'kns_email_endpoint/storage/memcache_storage'
6
+
7
+ def self.get_storage(engine, settings)
8
+ case engine.to_sym
9
+ when :file then return FileStorage.new(settings)
10
+ when :memcache then return MemcacheStorage.new(settings)
11
+ else return nil
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module KnsEmailEndpoint
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+
3
+ # require some extensions
4
+ require 'kns_email_endpoint/core_extensions/class'
5
+ require 'kns_email_endpoint/core_extensions/hash'
6
+ require 'kns_email_endpoint/core_extensions/imap'
7
+ require 'kns_email_endpoint/core_extensions/message'
8
+ require 'kns_email_endpoint/core_extensions/test_retriever'
9
+
10
+ # all the gem pieces
11
+ require 'kns_endpoint'
12
+ require 'kns_email_endpoint/connection'
13
+ require 'kns_email_endpoint/configuration'
14
+ require 'kns_email_endpoint/email_endpoint'
15
+ require 'kns_email_endpoint/storage/storage'
16
+ require 'kns_email_endpoint/message_state'
17
+ require 'kns_email_endpoint/process_email'
18
+
19
+
data/spec/a18x34/.app ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :ruleset_id: a18x34
3
+ :role: owner
4
+ :name: Testing KRL CLI
@@ -0,0 +1,91 @@
1
+ ruleset a18x34 {
2
+ meta {
3
+ name "Test App for Email Endpoint"
4
+ description <<
5
+ Testing application for the Email Endpoint
6
+ >>
7
+ author "Michael Farmer"
8
+ // Uncomment this line to require Marketplace purchase to use this app.
9
+ // authz require user
10
+ logging on
11
+ }
12
+
13
+
14
+ global {
15
+
16
+ }
17
+
18
+ rule receive_new_email is active {
19
+ select when mail received test_rule "parts"
20
+ pre {
21
+ envelope = event:param("msg");
22
+ from = event:param("from");
23
+ to = event:param("to");
24
+ subject = event:param("subject");
25
+ label = event:param("label");
26
+ unique_id = event:param("unique_id");
27
+ collection = {
28
+ "from": from,
29
+ "to": to,
30
+ "subject": subject,
31
+ "label": label,
32
+ "unique_id": unique_id
33
+ }
34
+ }
35
+ {
36
+ send_directive("processed");
37
+ }
38
+
39
+ fired {
40
+ log collection.encode();
41
+ }
42
+
43
+ }
44
+
45
+ rule delete_mail is active {
46
+ select when mail received test_rule "delete_me"
47
+ {
48
+ email:delete();
49
+ }
50
+
51
+ }
52
+
53
+ rule reply_mail is active {
54
+ select when mail received test_rule "reply"
55
+
56
+ {
57
+ email:reply() with body = "This is a reply message";
58
+ }
59
+ }
60
+
61
+ rule reply_and_delete_mail is active {
62
+ select when mail received test_rule "reply and delete"
63
+
64
+ {
65
+ email:reply() with body = "This is a reply message" and delete_message = true;
66
+ }
67
+ }
68
+
69
+
70
+ rule forward_mail is active {
71
+ select when mail received test_rule "forward"
72
+ pre {
73
+ fwd_to = event:param("forward_to");
74
+ }
75
+ {
76
+ email:forward() with to = fwd_to and body = "This is a forwarded message"
77
+ }
78
+
79
+ }
80
+
81
+ rule forward_and_delete_mail is active {
82
+ select when mail received test_rule "forward and delete"
83
+ pre {
84
+ fwd_to = event:param("forward_to");
85
+ }
86
+ {
87
+ email:forward() with to = fwd_to and body = "This is a forwarded message" and delete_message = true;
88
+ }
89
+
90
+ }
91
+ }
@@ -0,0 +1,44 @@
1
+ require 'lib/kns_email_endpoint'
2
+
3
+ $CONFIG_FILE = File.join(File.dirname(__FILE__), '../..', 'test_config_file.yml')
4
+
5
+ module KNSEmailEndpoint
6
+ describe Configuration do
7
+
8
+ Configuration.load_from_file $CONFIG_FILE
9
+ let(:c) { Configuration }
10
+
11
+ it "should have a storage engine" do
12
+ c.storage_engine.should_not be_nil
13
+ end
14
+
15
+ describe "loaded" do
16
+
17
+ specify { c.work_threads.should eql 5}
18
+ specify { c.poll_delay.should eql 30}
19
+ specify { c.logdir.should eql "/tmp/email_endpoint"}
20
+ specify { c.connections.should_not be_empty }
21
+ specify { c.storage.should_not be_empty }
22
+ specify { c.log.class.should == Logger }
23
+ specify { c.storage_engine.class.should == KNSEmailEndpoint::Storage::MemcacheStorage }
24
+ specify { c.log.level.should == 0 }
25
+
26
+ end
27
+
28
+
29
+ describe "access connections" do
30
+
31
+ it 'should return a connection by name' do
32
+ c["test"]["name"].should eql "test"
33
+ end
34
+
35
+ it "should let me loop through the connections" do
36
+ c.each_connection do |conn|
37
+ ["test", "gmail"].should include(conn.name)
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ end
44
+ end