witch 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f4aec34f53b5eefdb9e1ef701556c0b9834f23ae
4
+ data.tar.gz: 99bf4a228960cc0e830136b57e00562e4254b5fc
5
+ SHA512:
6
+ metadata.gz: 3b6c2d058d1fc8c524756b0662b65f7d7d4a601902e96c699b886b96348a47e9e2291f2514df161e95e13d4368d411b3ae3828c7ccad8726e5fcc6e3952bdcd2
7
+ data.tar.gz: a55381d307d1c6cea404640a135da9d553730074b39807d5397e5f35c6b13cba06e3c87b8f0c80ef554bcb08878ab58654fcb3df1567d936f1f7ff5ecc810ad8
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+
3
+ # specified in tuktuk.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ Tuktuk - SMTP client for Ruby
2
+ =============================
3
+
4
+ Unlike famous ol' Pony gem (which is friggin' awesome by the way), Tuktuk does not rely on
5
+ `sendmail` or a separate SMTP server in order to deliver email. Tuktuk looks up the
6
+ MX servers of the destination address and connects directly using Net::SMTP.
7
+ This way you don't need to install Exim or Postfix and you can actually handle
8
+ response status codes -- like bounces, 5xx -- within your application.
9
+
10
+ Plus, it supports DKIM out of the box.
11
+
12
+ Delivering mail
13
+ ---------------
14
+
15
+ ``` ruby
16
+ require 'tuktuk'
17
+
18
+ message = {
19
+ :from => 'you@username.com',
20
+ :to => 'user@yoursite.com',
21
+ :body => 'Hello there',
22
+ :subject => 'Hiya'
23
+ }
24
+
25
+ response, email = Tuktuk.deliver(message)
26
+ ```
27
+
28
+ The `response` is either a Net::SMTP::Response object, or a Bounce exception (HardBounce or SoftBounce, depending on the cause). `email` is a [mail](https://github.com/mikel/mail) object. So, to handle bounces you'd do:
29
+
30
+ ``` ruby
31
+ [...]
32
+
33
+ response, email = Tuktuk.deliver(message)
34
+
35
+ if response.is_a?(Tuktuk::Bounce)
36
+ puts 'Email bounced. Type: ' + response.class.name # => HardBounce or SoftBounce
37
+ else
38
+ puts 'Email delivered!'
39
+ end
40
+ ```
41
+
42
+ Delivering multiple
43
+ -------------------
44
+
45
+ With Tuktuk, you can also deliver multiple messages at once. Depending on the `max_workers` config parameter, Tuktuk will either connect sequentially to the target domain's MX servers, or do it in parallel by spawning threads.
46
+
47
+ Tuktuk will try to send all emails targeted for a specific domain on the same SMTP session. If a MX server is not responding -- or times out in the middle --, Tuktuk will try to deliver the remaining messages to next MX server, and so on.
48
+
49
+ To #deliver_many, you need to pass an array of messages, and you'll receive an array of [response, email] elements, just as above.
50
+
51
+ ``` ruby
52
+ messages = [ { ... }, { ... }, { ... }, { ... } ] # array of messages
53
+
54
+ result = Tuktuk.deliver_many(messages)
55
+
56
+ result.each do |response, email|
57
+
58
+ if response.is_a?(Tuktuk::Bounce)
59
+ puts 'Email bounced. Type: ' + response.class.name
60
+ else
61
+ puts 'Email delivered!'
62
+ end
63
+
64
+ end
65
+ ```
66
+
67
+ Options & DKIM
68
+ --------------
69
+
70
+ Now, if you want to enable DKIM (and you should):
71
+
72
+ ``` ruby
73
+ require 'tuktuk'
74
+
75
+ Tuktuk.options = {
76
+ :dkim => {
77
+ :domain => 'yoursite.com',
78
+ :selector => 'mailer',
79
+ :private_key => IO.read('ssl/yoursite.com.key')
80
+ }
81
+ }
82
+
83
+ message = { ... }
84
+
85
+ response, email = Tuktuk.deliver(message)
86
+ ```
87
+
88
+ For DKIM to work, you need to set up some TXT records in your domain's DNS. You can use [this tool](http://www.socketlabs.com/domainkey-dkim-generation-wizard/) to generate the key. You should also create [SPF records](http://www.spfwizard.net/) if you haven't.
89
+
90
+ All available options, with their defaults:
91
+
92
+ ``` ruby
93
+ Tuktuk.options = {
94
+ :log_to => nil, # e.g. log/mailer.log or STDOUT
95
+ :helo_domain => nil, # your server's domain goes here
96
+ :max_workers => 0, # controls number of threads for delivering_many emails (read below)
97
+ :open_timeout => 20, # max seconds to wait for opening a connection
98
+ :read_timeout => 20, # 20 seconds to wait for a response, once connected
99
+ :verify_ssl => true, # whether to skip SSL keys verification or not
100
+ :debug => false, # connects and delivers email to localhost, instead of real target server. CAUTION!
101
+ :dkim => { ... }
102
+ }
103
+ ```
104
+
105
+ You can set the `max_threads` option to `auto`, which will spawn the necessary threads to connect in paralell to all target MX servers when delivering multiple messages. When set to `0`, these batches will be delivered sequentially.
106
+
107
+ In other words, if you have three emails targeted to Gmail users and two for Hotmail users, using `auto` Tuktuk will spawn two threads and connect to both servers at once. Using `0` will have email deliveried to one host and then the other.
108
+
109
+ Using with Rails
110
+ ----------------
111
+
112
+ Tuktuk comes with ActionMailer support out of the box. In your environment.rb or environments/{env}.rb:
113
+
114
+ ``` ruby
115
+ require 'tuktuk/rails'
116
+
117
+ [...]
118
+
119
+ config.action_mailer.delivery_method = :tuktuk
120
+ ```
121
+
122
+ Since Tuktuk delivers email directly to the user's MX servers, it's probably a good idea to set `config.action_mailer.raise_delivery_errors` to true. That way you can actually know if an email couldn't make it to its destination.
123
+
124
+ When used with ActionMailer, you can pass options using ActionMailer's interface, like this:
125
+
126
+ ``` ruby
127
+
128
+ config.action_mailer.delivery_method = :tuktuk
129
+
130
+ config.action_mailer.tuktuk_settings = {
131
+ :log_to => 'log/mailer.log', # when not set, Tuktuk will use Rails.logger
132
+ :dkim => {
133
+ :domain => 'yoursite.com',
134
+ :selector => 'mailer',
135
+ :private_key => IO.read('ssl/yoursite.com.key')
136
+ }
137
+ }
138
+ ```
139
+
140
+ # Contributions
141
+
142
+ You're more than welcome. Send a pull request, including tests, and make sure you don't break anything. That's it.
143
+
144
+ # Copyright
145
+
146
+ (c) Fork Limited. MIT license.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module Witch
2
+ VERSION = '0.0.2'
3
+ end
data/lib/witch.rb ADDED
@@ -0,0 +1,154 @@
1
+ require 'redis'
2
+ require 'json'
3
+
4
+ module Witch
5
+
6
+ class ConsumerDisconnected < Exception; end
7
+
8
+ class << self
9
+
10
+ def init(options = {})
11
+ raise 'Already initialized!' if @queue
12
+ @queue = options.delete(:queue) || 'eventstream'
13
+ @options = options
14
+ @callbacks = {:once => {}, :on => {}}
15
+ @channels = []
16
+ @id = Socket.gethostname + '-' + $$.to_s
17
+ end
18
+
19
+ def once(*events, &block)
20
+ raise 'Not initialized' unless @callbacks
21
+ events.each do |event|
22
+ @callbacks[:once][event.to_sym] ||= []
23
+ @callbacks[:once][event.to_sym].push(block)
24
+ register_for(event)
25
+ end
26
+ listen(:once, @id) unless listening_on?(@id)
27
+ end
28
+
29
+ def on(*events, &block)
30
+ raise 'Not initialized' unless @callbacks
31
+ events.each do |event|
32
+ @callbacks[:on][event.to_sym] ||= []
33
+ @callbacks[:on][event.to_sym].push(block)
34
+ end
35
+ listen(:on, @queue) unless listening_on?(@queue)
36
+ end
37
+
38
+ def off(*events)
39
+ raise 'Not initialized' unless @callbacks
40
+ events.each do |event|
41
+ @callbacks[:on].delete(event.to_sym)
42
+ end
43
+ end
44
+
45
+ def publish(event, data = nil)
46
+ str = event + '___' + data.to_s
47
+
48
+ lock.synchronize do
49
+ # first, send to everyone on the main queue
50
+ puts '[publisher] Publishing on queue ' + @queue
51
+ connection.publish(@queue, str)
52
+
53
+ # if one or more clients are subscribed to the 'once' queue, choose one and notify.
54
+ if consumer = connection.smembers('subscribers:' + event.to_s).shuffle.first
55
+ # connection.publish(['events', event, subscriber].join(':'), data)
56
+ puts "[publisher] Found consumer for #{event}: #{consumer}"
57
+ count = connection.publish(consumer, str)
58
+
59
+ if count == 0
60
+ puts "[publisher] Consumer #{consumer} disconnected! Removing and retrying..."
61
+ connection.srem('subscribers:' + event.to_s, consumer)
62
+ raise ConsumerDisconnected, "name: #{consumer}"
63
+ end
64
+ end
65
+
66
+ end # lock
67
+
68
+ rescue ConsumerDisconnected => e
69
+ retry
70
+ end
71
+
72
+ def disconnect
73
+ return unless @id
74
+ @callbacks[:once].keys.each do |event|
75
+ unregister_from(event)
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def register_for(event)
82
+ lock.synchronize do
83
+ count = connection.sadd('subscribers:' + event.to_s, @id)
84
+ puts "[consumer] Registered. Queue contains #{count} listeners."
85
+ end
86
+ end
87
+
88
+ def unregister_from(event)
89
+ lock.synchronize do
90
+ puts "[consumer] Unregistering from #{event} processing queue."
91
+ connection.srem('subscribers:' + event.to_s, @id)
92
+ end
93
+ rescue => e
94
+ puts e.inspect
95
+ end
96
+
97
+ def listen(type, queue)
98
+ lock.synchronize do
99
+ puts "[consumer] Subscribing to queue #{queue} on new thread..."
100
+ @channels.push(queue)
101
+ Thread.new do
102
+ # connection.subscribe(['events' + event + id].join(':')) do |on|
103
+ connection.subscribe(queue) do |on|
104
+ on.subscribe do |channel, subscriptions|
105
+
106
+ puts "[consumer] Subscribed to #{channel} (#{subscriptions} subscriptions)"
107
+ raise "Existing subscribers!" if subscriptions > 1
108
+ end
109
+ on.message do |channel, msg|
110
+ process(type, msg)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ rescue => e
116
+ @channels.delete(queue)
117
+ puts e.message
118
+ end
119
+
120
+ def process(type, msg)
121
+ parts = msg.split('___')
122
+ data = parts[1] ? JSON.parse(parts[1]) : nil
123
+ fire_callbacks(type, parts[0], data)
124
+ rescue => e
125
+ puts "[consumer] Error parsing message: #{e.message}"
126
+ end
127
+
128
+ def fire_callbacks(type, event, data)
129
+ return if @callbacks.nil?
130
+ lock.synchronize do
131
+ (@callbacks[type][event.to_sym] || []).each do |callback|
132
+ callback.call(data)
133
+ end
134
+ end
135
+ rescue => e
136
+ puts "[consumer] Error firing callback: #{e.message}"
137
+ end
138
+
139
+ def lock
140
+ @lock ||= Mutex.new
141
+ end
142
+
143
+ def connection
144
+ raise 'Not initialized!' unless @options
145
+ Thread.current[:redis] ||= Redis.new(@options)
146
+ end
147
+
148
+ def listening_on?(queue)
149
+ @channels.include?(queue)
150
+ end
151
+
152
+ end
153
+
154
+ end
data/sandbox/api.rb ADDED
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'witch'
4
+
5
+ Witch.init
6
+
7
+ puts 'API server...'
8
+
9
+ Signal.trap 'EXIT' do
10
+ Thread.new { Witch.disconnect }.join
11
+ end
12
+
13
+ module Events
14
+ def self.device_dead
15
+ puts Time.now.to_s + ' - marking device dead'
16
+ end
17
+ end
18
+
19
+ Redis.new.subscribe('test') do |on|
20
+ on.message do |channel, message|
21
+ puts 'got message: ' + message
22
+ Events.send(message) if Events.respond_to?(message)
23
+ Witch.publish(message)
24
+ end
25
+ end
data/sandbox/mailer.rb ADDED
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'witch'
4
+
5
+ Witch.init
6
+
7
+ puts 'mailer server...'
8
+
9
+ Witch.once 'device_dead' do |data|
10
+ puts Time.now.to_s + ' sending email to user'
11
+ end
12
+
13
+ Signal.trap 'EXIT' do
14
+ Thread.new { Witch.disconnect }.join
15
+ end
16
+
17
+ loop { sleep 1; }
data/sandbox/test.rb ADDED
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'witch'
4
+
5
+ Witch.init
6
+
7
+ puts 'API server...'
8
+
9
+ Witch.once 'foo' do |data|
10
+ puts 'got message: ' + data.inspect
11
+ end
12
+
13
+ Signal.trap 'EXIT' do
14
+ Thread.new { Witch.disconnect }.join
15
+ end
16
+
17
+ loop { sleep 1; }
data/sandbox/web.rb ADDED
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'witch'
4
+
5
+ Witch.init
6
+
7
+ puts 'web server...'
8
+
9
+ Witch.on 'device_dead', 'device_away' do |data|
10
+ puts Time.now.to_s + ' - showing device dead message in UI '
11
+ end
12
+
13
+ Signal.trap 'EXIT' do
14
+ Thread.new { Witch.disconnect }.join
15
+ end
16
+
17
+ loop { sleep 1; }
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+
3
+ this_path = File.dirname(__FILE__)
4
+ require File.join(this_path, '..', 'lib', 'witch.rb')
@@ -0,0 +1,57 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe 'witch' do
4
+
5
+ before do
6
+ @called = {}
7
+ end
8
+
9
+ after(:all) do
10
+ # Witch.disconnect
11
+ end
12
+
13
+ def register(type)
14
+ Witch.init
15
+ Witch.send(type, 'some_event') do
16
+ # puts 'calledxxxxxxxxx'
17
+ @called[$$] = true
18
+ end
19
+ sleep 0.1
20
+ end
21
+
22
+ describe 'once' do
23
+
24
+ describe 'single process' do
25
+
26
+ before do
27
+ register :once
28
+ end
29
+
30
+ it 'fires event once' do
31
+ puts 'publishing'
32
+ Witch.publish 'some_event'
33
+ puts @called.inspect
34
+ # @called.keys.should == $$
35
+ end
36
+
37
+ end
38
+
39
+ describe 'multi process' do
40
+
41
+ it 'fires event once' do
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ describe 'on' do
50
+
51
+ end
52
+
53
+ describe 'off' do
54
+
55
+ end
56
+
57
+ end
data/witch.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "witch"
6
+ s.version = Witch::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Tomás Pollak']
9
+ s.email = ['tomas@forkhq.com']
10
+ s.homepage = "https://github.com/tomas/witch"
11
+ s.summary = "Reliable pub/sub for multi-process architectures."
12
+ s.description = "Reliable pub/sub for multi-process architectures."
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+ s.rubyforge_project = "tuktuk"
16
+
17
+ s.add_development_dependency "bundler", ">= 1.0.0"
18
+ s.add_development_dependency "rspec", '~> 3.0', '>= 3.0.0'
19
+ s.add_runtime_dependency "redis", ">= 3.0"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
23
+ s.require_path = 'lib'
24
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: witch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Tomás Pollak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.0.0
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '3.0'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: redis
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ description: Reliable pub/sub for multi-process architectures.
62
+ email:
63
+ - tomas@forkhq.com
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - Gemfile
69
+ - README.md
70
+ - Rakefile
71
+ - lib/version.rb
72
+ - lib/witch.rb
73
+ - sandbox/api.rb
74
+ - sandbox/mailer.rb
75
+ - sandbox/test.rb
76
+ - sandbox/web.rb
77
+ - spec/spec_helper.rb
78
+ - spec/witch/witch_spec.rb
79
+ - witch.gemspec
80
+ homepage: https://github.com/tomas/witch
81
+ licenses: []
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 1.3.6
97
+ requirements: []
98
+ rubyforge_project: tuktuk
99
+ rubygems_version: 2.2.0
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Reliable pub/sub for multi-process architectures.
103
+ test_files: []
104
+ has_rdoc: