postmark-inbound 0.0.1

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: 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,5 @@
1
+ module PINS
2
+
3
+ Version = '0.0.1'
4
+
5
+ end
@@ -0,0 +1,2 @@
1
+ require 'postmark-inbound/logger'
2
+ require 'postmark-inbound/config'
@@ -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: []