kns_email_endpoint 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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