postmark-inbound 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +79 -0
- data/bin/pin-server +20 -0
- data/lib/postmark-inbound/config.rb +41 -0
- data/lib/postmark-inbound/handler.rb +55 -0
- data/lib/postmark-inbound/helper.rb +99 -0
- data/lib/postmark-inbound/logger.rb +24 -0
- data/lib/postmark-inbound/server.rb +57 -0
- data/lib/postmark-inbound/version.rb +5 -0
- data/lib/postmark-inbound.rb +2 -0
- data/postmark-inbound.gemspec +23 -0
- metadata +85 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d30cef83d4df28de873f3d1d929ee705f27a86b4
|
4
|
+
data.tar.gz: e915c307737eccfcae23d32e4abaa332d9576237
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6d9eff2ee5a31ef257445750f70a8c8b1c652787463272bc30ecc7c9995cd9486e6fc80472fad47c22a254acf1866b4d6829de55534ece6235a17264b3cf46a4
|
7
|
+
data.tar.gz: 485870ff33fd174d41d147c1ccaeefd519099d2fe1e14b64086ac27c2a9c1c85c6a5bf73e385e3cd46fd50c64287f114bf4c2b31395afa88da4c801707f72f78
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2016 Ken J.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
# postmark-inbound
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/postmark-inbound.svg)](http://badge.fury.io/rb/postmark-inbound) [![Code Climate](https://codeclimate.com/github/kenjij/postmark-inbound/badges/gpa.svg)](https://codeclimate.com/github/kenjij/postmark-inbound) [![security](https://hakiri.io/github/kenjij/postmark-inbound/master.svg)](https://hakiri.io/github/kenjij/postmark-inbound/master)
|
4
|
+
|
5
|
+
A Ruby server for Postmark inbound webhook.
|
6
|
+
|
7
|
+
## Requirements
|
8
|
+
|
9
|
+
- Ruby 2.1 <=
|
10
|
+
- Kajiki 1.1 <=
|
11
|
+
- Sinatra 1.4 <=
|
12
|
+
|
13
|
+
## Getting Started
|
14
|
+
|
15
|
+
### Install
|
16
|
+
|
17
|
+
```
|
18
|
+
$ gem install postmark-inbound
|
19
|
+
```
|
20
|
+
|
21
|
+
### Configure
|
22
|
+
|
23
|
+
Create a configuration file following the example below.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# Configure application logging
|
27
|
+
PINS.logger = Logger.new(STDOUT)
|
28
|
+
PINS.logger.level = Logger::DEBUG
|
29
|
+
|
30
|
+
PINS::Config.setup do |c|
|
31
|
+
# User custom data
|
32
|
+
c.user = {my_data1: 'Something', my_data2: 'Somethingelse'}
|
33
|
+
# If any number of strings are set, it will require a matching "?auth=" parameter in the incoming request
|
34
|
+
c.auth_tokens = [
|
35
|
+
'adc9c81e23cd4ed785038028e69f7c54',
|
36
|
+
'4e1bca06d99f477f89c110e12e45f8f4'
|
37
|
+
]
|
38
|
+
# HTTP server (Sinatra) settings
|
39
|
+
c.dump_errors = true
|
40
|
+
c.logging = true
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add and register handler blocks; all handlers will be called for every incoming request
|
44
|
+
PINS::Handler.add(:my_handler1) do |h|
|
45
|
+
h.set_block do |pin|
|
46
|
+
break unless pin[:originalrecipient] == 'myinbox@pm.example.com'
|
47
|
+
puts "It's a match!"
|
48
|
+
# Do something more
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
PINS::Handler.add(:my_handler2) do |h|
|
53
|
+
h.set_block do |pin|
|
54
|
+
break unless pin[:spam_status]
|
55
|
+
puts "We've got a spam."
|
56
|
+
# Do something more
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
### Use
|
62
|
+
|
63
|
+
To see help:
|
64
|
+
|
65
|
+
```
|
66
|
+
$ pin-server -h
|
67
|
+
Usage: pin-server [options] {start|stop}
|
68
|
+
-c, --config=<s> Load config from file
|
69
|
+
-d, --daemonize Run in the background
|
70
|
+
-l, --log=<s> Log output to file
|
71
|
+
-P, --pid=<s> Store PID to file
|
72
|
+
-p, --port=<i> Use port (default: 4567)
|
73
|
+
```
|
74
|
+
|
75
|
+
The minimum to start a server:
|
76
|
+
|
77
|
+
```
|
78
|
+
$ pin-server -c config.rb start
|
79
|
+
```
|
data/bin/pin-server
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'kajiki'
|
3
|
+
require 'postmark-inbound'
|
4
|
+
|
5
|
+
|
6
|
+
opts = Kajiki.preset_options(:server, {config: true})
|
7
|
+
|
8
|
+
Kajiki.run(opts) do |cmd|
|
9
|
+
case cmd
|
10
|
+
when 'start'
|
11
|
+
PINS::Config.load_config(opts[:config]) if opts[:config]
|
12
|
+
require 'postmark-inbound/server'
|
13
|
+
PINS.logger.warn('Postmark Inbound Server starting...')
|
14
|
+
Rack::Server.start({
|
15
|
+
app: PINS::Server.new,
|
16
|
+
Host: opts[:address],
|
17
|
+
Port: opts[:port]
|
18
|
+
})
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'postmark-inbound/handler'
|
2
|
+
|
3
|
+
module PINS
|
4
|
+
|
5
|
+
class Config
|
6
|
+
|
7
|
+
# Load Ruby config file
|
8
|
+
# @param path [String] config file
|
9
|
+
def self.load_config(path)
|
10
|
+
raise 'config file missing' unless path
|
11
|
+
PINS.logger.debug("Loading config file: #{path}")
|
12
|
+
require File.expand_path(path)
|
13
|
+
PINS.logger.info('Config.load_config done.')
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the shared instance
|
17
|
+
# @return [PINS::Config]
|
18
|
+
def self.shared
|
19
|
+
@shared_config ||= Config.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Call this from your config file
|
23
|
+
def self.setup(&block)
|
24
|
+
yield Config.shared
|
25
|
+
PINS.logger.debug('Config.setup block executed.')
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :user
|
29
|
+
attr_accessor :auth_tokens
|
30
|
+
attr_accessor :dump_errors
|
31
|
+
attr_accessor :logging
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@auth_tokens = []
|
35
|
+
@dump_errors = false
|
36
|
+
@logging = false
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module PINS
|
2
|
+
|
3
|
+
class Handler
|
4
|
+
|
5
|
+
# Call as often as necessary to add handlers, usually from the config file; each call creates a PINS::Handler object
|
6
|
+
# @param name [Symbol] or a String
|
7
|
+
def self.add(name)
|
8
|
+
@catalog ||= {}
|
9
|
+
if @catalog[name]
|
10
|
+
PINS.logger.warn("Handler not added, duplicate name: #{name}")
|
11
|
+
return nil
|
12
|
+
end
|
13
|
+
handler = Handler.new(name)
|
14
|
+
yield handler if block_given?
|
15
|
+
@catalog[name] = handler
|
16
|
+
PINS.logger.info("Added handler: #{name}")
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Hash] containing all the handlers
|
20
|
+
def self.catalog
|
21
|
+
return @catalog
|
22
|
+
end
|
23
|
+
|
24
|
+
# Run the handlers, typically called by the server
|
25
|
+
# @param pin [Hash] from Postmark inbound webhook JSON
|
26
|
+
def self.run(pin, names = Handler.catalog.keys)
|
27
|
+
PINS.logger.info('Running handlers...')
|
28
|
+
names.each do |name|
|
29
|
+
(Handler.catalog)[name].run(pin)
|
30
|
+
end
|
31
|
+
PINS.logger.info('Done running handlers.')
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :myname
|
35
|
+
|
36
|
+
def initialize(name)
|
37
|
+
PINS.logger.debug("Initializing handler: #{name}")
|
38
|
+
@myname = name
|
39
|
+
end
|
40
|
+
|
41
|
+
# When adding a handler, call this to register a block
|
42
|
+
def set_block(&block)
|
43
|
+
PINS.logger.debug("Registering block for handler: #{}")
|
44
|
+
@block = block
|
45
|
+
end
|
46
|
+
|
47
|
+
def run(obj)
|
48
|
+
PINS.logger.warn("No block to execute for handler: #{myname}") unless @block
|
49
|
+
PINS.logger.debug("Running handler: #{myname}")
|
50
|
+
@block.call(obj)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module PINS
|
5
|
+
|
6
|
+
module Helper
|
7
|
+
|
8
|
+
def parse_json(json)
|
9
|
+
obj = JSON.parse(json)
|
10
|
+
return parse_pm(keys_to_sym(obj)).merge!(catch_pm_headers(obj[:headers]))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Convert all Hash keys to lowercase symbols
|
14
|
+
# @param obj [Object] any Ruby object
|
15
|
+
def keys_to_sym(obj)
|
16
|
+
case obj
|
17
|
+
when Array
|
18
|
+
obj.each do |v|
|
19
|
+
keys_to_sym(v)
|
20
|
+
end
|
21
|
+
when Hash
|
22
|
+
obj.keys.each do |k|
|
23
|
+
if k.class == String
|
24
|
+
obj[k.downcase.to_sym] = keys_to_sym(obj.delete(k))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
return obj
|
29
|
+
end
|
30
|
+
|
31
|
+
# Additional parsing/conversions
|
32
|
+
def parse_pm(hash)
|
33
|
+
hash[:date] = Time.parse(hash[:date]) if hash[:date]
|
34
|
+
return hash
|
35
|
+
end
|
36
|
+
|
37
|
+
# Extract Postmark related headers into a Hash
|
38
|
+
# @param headers [Array]
|
39
|
+
# @return [Hash]
|
40
|
+
def catch_pm_headers(headers)
|
41
|
+
return nil unless Array === headers
|
42
|
+
caught = {}
|
43
|
+
headers.each do |h|
|
44
|
+
case h[:name]
|
45
|
+
when 'X-Spam-Status'
|
46
|
+
caught[:spam_status] = h[:value].downcase.start_with?('yes')
|
47
|
+
when 'X-Spam-Score'
|
48
|
+
caught[:spam_score] = h[:value].to_f
|
49
|
+
when 'Received-SPF'
|
50
|
+
caught[:received_spf_status] = (h[:value].split)[0].downcase
|
51
|
+
end
|
52
|
+
end
|
53
|
+
return caught
|
54
|
+
end
|
55
|
+
|
56
|
+
def lookup_email_headers(headers, name)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Convert object into JSON, optionally pretty-format
|
60
|
+
# @param obj [Object] any Ruby object
|
61
|
+
# @param opts [Hash] any JSON options
|
62
|
+
# @return [String] JSON string
|
63
|
+
def json_with_object(obj, pretty: true, opts: nil)
|
64
|
+
return '{}' if obj.nil?
|
65
|
+
if pretty
|
66
|
+
opts = {
|
67
|
+
indent: ' ',
|
68
|
+
space: ' ',
|
69
|
+
object_nl: "\n",
|
70
|
+
array_nl: "\n"
|
71
|
+
}
|
72
|
+
end
|
73
|
+
JSON.fast_generate(json_format_value(obj), opts)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return Ruby object/value to JSON standard format
|
77
|
+
# @param val [Object]
|
78
|
+
# @return [Object]
|
79
|
+
def json_format_value(val)
|
80
|
+
case val
|
81
|
+
when Array
|
82
|
+
val.map { |v| json_format_value(v) }
|
83
|
+
when Hash
|
84
|
+
val.reduce({}) { |h, (k, v)| h.merge({k => json_format_value(v)}) }
|
85
|
+
when String
|
86
|
+
val.encode!('UTF-8', {invalid: :replace, undef: :replace})
|
87
|
+
when Time
|
88
|
+
val.utc.iso8601
|
89
|
+
else
|
90
|
+
val
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
[{'A' => 1, 'Base' => 'Text', 'He' => {'llo' => 2, 'NONE' => 0}, 'aRRAy' => [1,2,{a: 1, 'BB' => "two"}]}]
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
|
4
|
+
module PINS
|
5
|
+
|
6
|
+
def self.logger=(logger)
|
7
|
+
@logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.logger
|
11
|
+
@logger ||= NullLogger.new()
|
12
|
+
end
|
13
|
+
|
14
|
+
class NullLogger < Logger
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(*args, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'postmark-inbound/helper'
|
3
|
+
|
4
|
+
module PINS
|
5
|
+
|
6
|
+
# The Sinatra server
|
7
|
+
class Server < Sinatra::Base
|
8
|
+
|
9
|
+
helpers Helper
|
10
|
+
|
11
|
+
configure do
|
12
|
+
set :environment, :production
|
13
|
+
disable :static
|
14
|
+
c = Config.shared
|
15
|
+
set :dump_errors, c.dump_errors
|
16
|
+
set :logging, c.logging
|
17
|
+
PINS.logger.info('Sinatra server configured.')
|
18
|
+
end
|
19
|
+
|
20
|
+
before do
|
21
|
+
tokens = Config.shared.auth_tokens
|
22
|
+
halt 401 unless tokens.empty? || tokens.include?(params[:auth])
|
23
|
+
end
|
24
|
+
|
25
|
+
post '/' do
|
26
|
+
PINS.logger.info('Incoming request received.')
|
27
|
+
PINS.logger.debug("Body size: #{request.content_length} bytes")
|
28
|
+
request.body.rewind
|
29
|
+
Handler.run(parse_json(request.body.read))
|
30
|
+
body ''
|
31
|
+
end
|
32
|
+
|
33
|
+
not_found do
|
34
|
+
PINS.logger.info('Invalid request.')
|
35
|
+
PINS.logger.debug("Request method and path: #{request.request_method} #{request.path}")
|
36
|
+
json_with_object({message: 'Huh, nothing here.'})
|
37
|
+
end
|
38
|
+
|
39
|
+
error 401 do
|
40
|
+
PINS.logger.info(params[:auth] ? 'Invalid auth token provided.' : 'Missing auth token.')
|
41
|
+
PINS.logger.debug("Provided auth token: #{params[:auth]}") if params[:auth]
|
42
|
+
json_with_object({message: 'Oops, need a valid auth.'})
|
43
|
+
end
|
44
|
+
|
45
|
+
error do
|
46
|
+
status 500
|
47
|
+
err = env['sinatra.error']
|
48
|
+
PINS.logger.error "#{err.class.name} - #{err}"
|
49
|
+
json_with_object({message: 'Yikes, internal error.'})
|
50
|
+
end
|
51
|
+
|
52
|
+
after do
|
53
|
+
content_type 'application/json'
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __FILE__))
|
2
|
+
require 'postmark-inbound/version'
|
3
|
+
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'postmark-inbound'
|
7
|
+
s.version = PINS::Version
|
8
|
+
s.authors = ['Ken J.']
|
9
|
+
s.email = ['kenjij@gmail.com']
|
10
|
+
s.summary = %q{A Ruby server for Postmark inbound webhook.}
|
11
|
+
s.description = %q{A programable Ruby server for Postmark inbound webhook. For example, trigger notifications or automated response.}
|
12
|
+
s.homepage = 'https://github.com/kenjij/postmark-inbound'
|
13
|
+
s.license = 'MIT'
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split($/)
|
16
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.required_ruby_version = '>= 2.1.0'
|
20
|
+
|
21
|
+
s.add_runtime_dependency 'kajiki', '~> 1.1'
|
22
|
+
s.add_runtime_dependency 'sinatra', '~> 1.4'
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: postmark-inbound
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ken J.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: kajiki
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sinatra
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.4'
|
41
|
+
description: A programable Ruby server for Postmark inbound webhook. For example,
|
42
|
+
trigger notifications or automated response.
|
43
|
+
email:
|
44
|
+
- kenjij@gmail.com
|
45
|
+
executables:
|
46
|
+
- pin-server
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- LICENSE
|
51
|
+
- README.md
|
52
|
+
- bin/pin-server
|
53
|
+
- lib/postmark-inbound.rb
|
54
|
+
- lib/postmark-inbound/config.rb
|
55
|
+
- lib/postmark-inbound/handler.rb
|
56
|
+
- lib/postmark-inbound/helper.rb
|
57
|
+
- lib/postmark-inbound/logger.rb
|
58
|
+
- lib/postmark-inbound/server.rb
|
59
|
+
- lib/postmark-inbound/version.rb
|
60
|
+
- postmark-inbound.gemspec
|
61
|
+
homepage: https://github.com/kenjij/postmark-inbound
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata: {}
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 2.1.0
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 2.4.8
|
82
|
+
signing_key:
|
83
|
+
specification_version: 4
|
84
|
+
summary: A Ruby server for Postmark inbound webhook.
|
85
|
+
test_files: []
|