mail_to_hip_chat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
6
+ /doc
7
+ .yardoc
data/.rbenv-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.2-p290
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --protected
2
+ --no-private
3
+ -
4
+ ChangeLog
5
+ LICENSE
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in mail_to_hip_chat.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ Copyright (C) 2011, Gabriel Gironda
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a
4
+ copy of this software and associated documentation files (the "Software"),
5
+ to deal in the Software without restriction, including without limitation
6
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
7
+ and/or sell copies of the Software, and to permit persons to whom the
8
+ Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19
+ DEALINGS IN THE SOFTWARE.
20
+
21
+ Except as contained in this notice, the name of Gabriel Gironda shall not
22
+ be used in advertising or otherwise to promote the sale, use or other
23
+ dealings in this Software without prior written authorization from Gabriel
24
+ Gironda.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ _
2
+ [_| . . . . .
3
+ .-----|--, |-. . ,-. ,-. |-. ,-. |- ,-,-. ,-. . | ," . . ,-. ,-. ,-. |
4
+ / | /_\_ | | | | | | | | ,-| | -- | | | ,-| | | -- |- | | | | | | |-' |
5
+ | |__.-| ' ' ' |-' `-' ' ' `-^ `' ' ' ' `-^ ' `' | `-^ ' ' ' ' `-' `'
6
+ | |__\_| | '
7
+ '---,-,-;---' '
8
+ | |
9
+
10
+ # Mail To HipChat
11
+
12
+ Mail To HipChat lets you wire up email notifications to [HipChat](http://hipchat.com/r/30ad1). This is useful in situations where a third-party service doesn't have a real [WebHook](http://www.webhooks.org/) available, but you still want to be able to dump notifications into HipChat. The [Airbrake](http://airbrake.io) exception notification service is one example.
13
+
14
+ ## How It Works
15
+
16
+ Mail To HipChat is designed to be deployed on [Heroku](http://heroku.com) and used with the [CloudMailIn](http://cloudmailin.com/) add-on. Adding the CloudMailIn incoming email address to the recipients list of the service you wish to integrate with will shuffle emails off to your instance of this tool as a HTTP POST request. A list of mail handlers is checked, and the appropriate one is invoked to format a message and send it off to one or more HipChat rooms.
17
+
18
+ ## Prerequisites
19
+
20
+ You will need an account on Heroku and admin access to your HipChat group.
21
+
22
+ ## Initial Setup
23
+
24
+ 1. Make a home for your shiny new Mail To HipChat instance to live in.
25
+
26
+ $ mkdir my-mail_to_hip_chat
27
+ $ cd my-mail_to_hip_chat
28
+ $ git init
29
+
30
+ 2. Do the bundler dance.
31
+
32
+ $ echo 'source "http://rubygems.org"' >> Gemfile
33
+ $ echo 'gem "mail_to_hip_chat"' >> Gemfile
34
+ $ bundle install
35
+ $ git commit -a -m "Getting down with bundler"
36
+
37
+ 3. Copy over the default config.ru.
38
+
39
+ $ cp "`bundle show mail_to_hip_chat`/support/config.ru" .
40
+ $ git commit -a -m "Adding default config.ru"
41
+
42
+ 4. Set up a new Heroku application with CloudMailIn and Piggyback SSL.
43
+
44
+ $ heroku create --stack cedar
45
+ $ heroku addons:add cloudmailin
46
+ $ heroku addons:add ssl:piggyback
47
+
48
+ 5. Setup the CloudMailIn target address to point at your app.
49
+
50
+ Get the target address for CloudMailIn to hit when it receives an email.
51
+
52
+ $ echo "`heroku info -r | grep web_url | cut -d '=' -f 2 | sed 's/^http/https/'`notifications/create"
53
+
54
+ Get your CloudMailIn username and password from the Heroku app config.
55
+
56
+ $ heroku config | grep CLOUDMAILIN
57
+
58
+ [Log in to CloudMailIn](https://cloudmailin.com/users/sign_in) using the given `CLOUDMAILIN_USERNAME` and `CLOUDMAILIN_PASSWORD`. You'll see the entry for `CLOUDMAILIN_FORWARD_ADDRESS` in the list. Hit "Manage" and then "Edit Target", and set the target to the target address we retrieved above.
59
+
60
+ 6. Get a HipChat API token and the ID for the Room(s) to send messages to.
61
+
62
+ Visit your [HipChat API Admin page](http://hipchat.com/group_admin/api). If there's a token you want to already use, use that one. If not, create a new token of type "Notification". Once you have the token, set it as an environment variable on Heroku.
63
+
64
+ $ heroku config:add HIPCHAT_API_TOKEN=fd3deeef7b88b95c1780e6237c41c30f
65
+
66
+ Visit your [HipChat Chat History page](https://hipchat.com/history). The integer in the URL for the history of each room (for example, `https://gabe.hipchat.com/history/room/31373`) is the room ID. Once you have the ID(s) of the rooms you wish to send messages to, set it as a comma separated environment variable on Heroku.
67
+
68
+ $ heroku config:add HIPCHAT_ROOMS=31373,31374
69
+
70
+ 7. Deploy this sucker.
71
+
72
+ $ git push heroku master
73
+
74
+ 8. Get your CloudMailIn forwarding address and send a test email.
75
+
76
+ $ heroku config --long | grep CLOUDMAILIN_FORWARD_ADDRESS
77
+
78
+ Send an email to the `CLOUDMAILIN_FORWARD_ADDRESS`, with a subject of "Testing Setup". The message should appear in the rooms you've configured Mail To HipChat to send messages to.
79
+
80
+ ## Hooking it up to Airbrake
81
+
82
+ Just add a new user to your project, set their email address to `CLOUDMAILIN_FORWARD_ADDRESS`, and watch the exceptions roll in. Then turn your face from God and weep in abject horror.
83
+
84
+ ## Adding new message handlers
85
+
86
+ ## FAQ
87
+
88
+ ## TODO
89
+
90
+ * There are too many steps for initial setup. This needs to be cut down.
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+ require "yard"
4
+ require "mail_to_hip_chat"
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList['test/{unit,integration}/**/*_test.rb']
10
+ t.verbose = true
11
+ end
12
+
13
+ YARD::Rake::YardocTask.new do |t|
14
+ t.options += ['--title', "Mail To HipChat #{MailToHipChat::VERSION} Documentation"]
15
+ end
16
+
17
+ task :default => :test
@@ -0,0 +1,32 @@
1
+ module MailToHipChat
2
+ # A {ChuteChain} is used to hold a collection of chutes and to check whether any of those chutes is
3
+ # able to handle a message to hand off to HipChat. Anything that responds to #call can be pushed
4
+ # onto the chain.
5
+ class ChuteChain
6
+
7
+ def initialize
8
+ @chutes = []
9
+ end
10
+
11
+ # Pushes a chute onto the chain.
12
+ #
13
+ # @param [#call] chute The chute to push onto the chain.
14
+ #
15
+ # @return [self]
16
+ def push(chute)
17
+ @chutes.push(chute)
18
+ self
19
+ end
20
+
21
+ # Takes in a message and traverses the chain looking for a chute that will handle it.
22
+ #
23
+ # @param [Hash] message The message to give to the chutes in the chain.
24
+ #
25
+ # @return [true] True if a chute accepts the message.
26
+ # @return [false] False if no chute can accept the message.
27
+ def accept(message)
28
+ @chutes.any? { |chute| chute.call(message) }
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module MailToHipChat
2
+ # Used to tag exceptions being raised from inside this library before they hit client code.
3
+ module InternalError; end
4
+ end
@@ -0,0 +1,20 @@
1
+ require "hipchat-api"
2
+
3
+ module MailToHipChat
4
+ module MessageChute
5
+
6
+ def initialize(opts)
7
+ @rooms = Array(opts[:rooms])
8
+ @hipchat_api = opts[:hipchat_api] || HipChat::API.new(opts[:api_token])
9
+ end
10
+
11
+ private
12
+
13
+ def message_rooms(from, message)
14
+ @rooms.each do |room_id|
15
+ @hipchat_api.rooms_message(room_id, from, message)
16
+ end
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,58 @@
1
+ require "mail_to_hip_chat/message_chute"
2
+ require "mustache"
3
+
4
+ module MailToHipChat
5
+ module MessageChutes
6
+ class Airbrake
7
+ include MessageChute
8
+
9
+ def call(params)
10
+ return false unless process_message(params["plain"])
11
+ true
12
+ end
13
+
14
+ private
15
+
16
+ def airbrake_domain
17
+ "airbrake.io"
18
+ end
19
+
20
+ def extraction_expression
21
+ %r[\A\n+Project:\s([^\n]+)\n+ # Pull out the project name
22
+ Environment:\s([^\n]+)\n+ # Pull out the project environment
23
+ # Pull out the URL to the exception
24
+ ^(http://[^.]+\.#{Regexp.escape(airbrake_domain)}/errors/\d+)\n+
25
+ # Pull out the first line of the error message
26
+ Error\sMessage:\n-{14}\n([^\n]+)]mx
27
+ end
28
+
29
+ def hipchat_sender
30
+ "Airbrake"
31
+ end
32
+
33
+ def message_template
34
+ %q[<b>{{project}} - {{environment}}</b><br /><a href="{{url}}">{{message}}</a>]
35
+ end
36
+
37
+ def process_message(plaintext)
38
+ return nil unless parts = extract_parts(plaintext)
39
+ send_notifications(parts)
40
+ true
41
+ end
42
+
43
+ def extract_parts(plaintext)
44
+ return nil unless parts = plaintext.match(extraction_expression)
45
+ [:project, :environment, :url, :message].each_with_index.inject({}) do |hash,(key,idx)|
46
+ hash[key] = parts[idx + 1]
47
+ hash
48
+ end
49
+ end
50
+
51
+ def send_notifications(message_parts)
52
+ hipchat_message = Mustache.render(message_template, message_parts)
53
+ message_rooms(hipchat_sender, hipchat_message)
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ require "mail_to_hip_chat/message_chute"
2
+ require "mustache"
3
+
4
+ module MailToHipChat
5
+ module MessageChutes
6
+ class TestEmail
7
+ include MessageChute
8
+
9
+ def call(params)
10
+ return false unless params["subject"] =~ /testing setup/i
11
+ message = Mustache.render("Message:<br />{{message}}", :message => params["plain"])
12
+ message_rooms("Testing", message)
13
+ true
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,47 @@
1
+ require "mail_to_hip_chat/rack_app"
2
+ require "rack/builder"
3
+ require "rack/urlmap"
4
+ require "rack/commonlogger"
5
+
6
+ module MailToHipChat
7
+ class RackApp
8
+ class Builder
9
+ attr_accessor :secret, :rooms, :api_token, :mount_point
10
+
11
+ def initialize
12
+ @chutes = []
13
+ @mount_point = '/notifications/create'
14
+ yield(self) if block_given?
15
+ end
16
+
17
+ def use_chute(chute_klass)
18
+ @chutes << chute_klass
19
+ end
20
+
21
+ def to_app
22
+ return @app if @app
23
+ app = build_app
24
+ mnt = mount_point
25
+ @app = Rack::Builder.new do
26
+ use Rack::CommonLogger
27
+ map(mnt) { run app }
28
+ end.to_app
29
+ end
30
+
31
+ private
32
+
33
+ def build_app
34
+ MailToHipChat::RackApp.new(:secret => @secret) do |f|
35
+ @chutes.each do |chute_klass|
36
+ f.use_chute(chute_klass.new(:api_token => @api_token, :rooms => split_rooms))
37
+ end
38
+ end
39
+ end
40
+
41
+ def split_rooms
42
+ @rooms.split(/,\s*/)
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,57 @@
1
+ require "mail_to_hip_chat/rack_app/builder"
2
+ require "mail_to_hip_chat/chute_chain"
3
+ require "mail_to_hip_chat/exceptions"
4
+ require "rack/request"
5
+ require "digest/md5"
6
+
7
+ module MailToHipChat
8
+ class RackApp
9
+ SIGNATURE_PARAM_NAME = "signature"
10
+
11
+ def initialize(opts = {})
12
+ @secret = opts[:secret]
13
+ @chute_chain = opts[:chute_chain] || ChuteChain.new
14
+
15
+ yield(self) if block_given?
16
+ end
17
+
18
+ def call(rack_env)
19
+ request = Rack::Request.new(rack_env)
20
+ return [400, {}, 'Bad Request.'] unless valid_request?(request)
21
+
22
+ if @chute_chain.accept(request.params)
23
+ [200, {}, 'OK']
24
+ else
25
+ [404, {}, 'Not Found.']
26
+ end
27
+
28
+ rescue Exception => error
29
+ error.extend(InternalError)
30
+ raise error
31
+ end
32
+
33
+ def use_chute(chute)
34
+ @chute_chain.push(chute)
35
+ end
36
+
37
+ private
38
+
39
+ def valid_request?(request)
40
+ computed_sig = create_sig_from_params(request.params)
41
+
42
+ computed_sig == request.params[SIGNATURE_PARAM_NAME]
43
+ end
44
+
45
+
46
+ def create_sig_from_params(params)
47
+ sorted_param_names = params.keys.sort
48
+ sorted_param_names.delete(SIGNATURE_PARAM_NAME)
49
+
50
+ param_vals_with_secret = params.values_at(*sorted_param_names)
51
+ param_vals_with_secret.push(@secret)
52
+
53
+ Digest::MD5.hexdigest(param_vals_with_secret.join)
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module MailToHipChat
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,2 @@
1
+ require "mail_to_hip_chat/version"
2
+ require "mail_to_hip_chat/exceptions"
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mail_to_hip_chat/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "mail_to_hip_chat"
7
+ s.version = MailToHipChat::VERSION
8
+ s.authors = ["Gabriel Gironda"]
9
+ s.email = ["gabriel@gironda.org"]
10
+ s.homepage = ""
11
+ s.summary = %q{Funnels email into HipChat}
12
+ s.description = %q{Funnels email into HipChat using CloudMailIn}
13
+
14
+ s.rubyforge_project = "mail_to_hip_chat"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "http_parser.rb", "~> 0.5"
22
+ s.add_development_dependency "mocha", "~> 0.10"
23
+ s.add_development_dependency "yard", "~> 0.7"
24
+ s.add_development_dependency "rdiscount", "~> 1.6"
25
+ s.add_development_dependency "webmock", "~> 1.7"
26
+ s.add_development_dependency "rake"
27
+
28
+ s.add_runtime_dependency "rack", "~> 1.3"
29
+ s.add_runtime_dependency "hipchat-api", "~> 1.0"
30
+ s.add_runtime_dependency "mustache", "~> 0.99"
31
+ end
data/support/config.ru ADDED
@@ -0,0 +1,18 @@
1
+ require "mail_to_hip_chat"
2
+ require "mail_to_hip_chat/rack_app"
3
+ require "mail_to_hip_chat/message_chutes/airbrake"
4
+ require "mail_to_hip_chat/message_chutes/test_email"
5
+
6
+ app = MailToHipChat::RackApp::Builder.new do |builder|
7
+ builder.secret = ENV['CLOUDMAILIN_SECRET']
8
+ builder.rooms = ENV['HIPCHAT_ROOMS']
9
+ builder.api_token = ENV['HIPCHAT_API_TOKEN']
10
+
11
+ # An included chute to process messages from Airbrake.
12
+ builder.use_chute MailToHipChat::MessageChutes::Airbrake
13
+
14
+ # This chute should be removed once you've confirmed your setup works.
15
+ builder.use_chute MailToHipChat::MessageChutes::TestEmail
16
+ end.to_app
17
+
18
+ run app
@@ -0,0 +1,27 @@
1
+
2
+
3
+ Project: Echelon
4
+ Environment: Staging
5
+
6
+
7
+ http://the-nsa.airbrake.io/errors/11082122
8
+
9
+ Error Message:
10
+ --------------
11
+ AirbrakeTestingException: Testing airbrake via "rake airbrake:test". If you can see this, it works.
12
+
13
+ Where:
14
+ ------
15
+ application#verify
16
+ [PROJECT_ROOT]/vendor/bundle/ruby/1.9.1/gems/activesupport-3.1.0/lib/active_support/callbacks.rb, line 412
17
+
18
+ URL:
19
+ ----
20
+ http://example.org/verify
21
+
22
+ Backtrace Summary:
23
+ ------------------
24
+ [PROJECT_ROOT]/lib/sha1_unhasher.rb:13:in `block in _call'
25
+ [PROJECT_ROOT]/lib/sha1_unhasher.rb.rb:12:in `_call'
26
+ [PROJECT_ROOT]/lib/sha1_unhasher.rb.rb:7:in `call'
27
+