witch 0.0.2

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.
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: