entp-astrotrain 0.2.0

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.
Files changed (57) hide show
  1. data/.gitignore +26 -0
  2. data/LICENSE +20 -0
  3. data/README +47 -0
  4. data/Rakefile +145 -0
  5. data/VERSION +1 -0
  6. data/astrotrain.gemspec +96 -0
  7. data/config/sample.rb +12 -0
  8. data/lib/astrotrain/api.rb +53 -0
  9. data/lib/astrotrain/logged_mail.rb +41 -0
  10. data/lib/astrotrain/mapping/http_post.rb +18 -0
  11. data/lib/astrotrain/mapping/jabber.rb +23 -0
  12. data/lib/astrotrain/mapping/transport.rb +55 -0
  13. data/lib/astrotrain/mapping.rb +157 -0
  14. data/lib/astrotrain/message.rb +313 -0
  15. data/lib/astrotrain/tmail.rb +48 -0
  16. data/lib/astrotrain.rb +55 -0
  17. data/lib/vendor/rest-client/README.rdoc +104 -0
  18. data/lib/vendor/rest-client/Rakefile +84 -0
  19. data/lib/vendor/rest-client/bin/restclient +65 -0
  20. data/lib/vendor/rest-client/foo.diff +66 -0
  21. data/lib/vendor/rest-client/lib/rest_client/net_http_ext.rb +21 -0
  22. data/lib/vendor/rest-client/lib/rest_client/payload.rb +185 -0
  23. data/lib/vendor/rest-client/lib/rest_client/request_errors.rb +75 -0
  24. data/lib/vendor/rest-client/lib/rest_client/resource.rb +103 -0
  25. data/lib/vendor/rest-client/lib/rest_client.rb +189 -0
  26. data/lib/vendor/rest-client/rest-client.gemspec +18 -0
  27. data/lib/vendor/rest-client/spec/base.rb +5 -0
  28. data/lib/vendor/rest-client/spec/master_shake.jpg +0 -0
  29. data/lib/vendor/rest-client/spec/payload_spec.rb +71 -0
  30. data/lib/vendor/rest-client/spec/request_errors_spec.rb +44 -0
  31. data/lib/vendor/rest-client/spec/resource_spec.rb +52 -0
  32. data/lib/vendor/rest-client/spec/rest_client_spec.rb +219 -0
  33. data/tasks/doc.thor +149 -0
  34. data/tasks/merb.thor +2020 -0
  35. data/test/api_test.rb +28 -0
  36. data/test/fixtures/apple_multipart.txt +100 -0
  37. data/test/fixtures/basic.txt +14 -0
  38. data/test/fixtures/custom.txt +15 -0
  39. data/test/fixtures/fwd.txt +0 -0
  40. data/test/fixtures/gb2312_encoding.txt +16 -0
  41. data/test/fixtures/gb2312_encoding_invalid.txt +15 -0
  42. data/test/fixtures/html.txt +16 -0
  43. data/test/fixtures/iso-8859-1.txt +13 -0
  44. data/test/fixtures/mapped.txt +13 -0
  45. data/test/fixtures/multipart.txt +213 -0
  46. data/test/fixtures/multipart2.txt +213 -0
  47. data/test/fixtures/multiple.txt +13 -0
  48. data/test/fixtures/multiple_delivered_to.txt +14 -0
  49. data/test/fixtures/multiple_with_body_recipients.txt +15 -0
  50. data/test/fixtures/reply.txt +16 -0
  51. data/test/fixtures/utf-8.txt +13 -0
  52. data/test/logged_mail_test.rb +63 -0
  53. data/test/mapping_test.rb +129 -0
  54. data/test/message_test.rb +424 -0
  55. data/test/test_helper.rb +54 -0
  56. data/test/transport_test.rb +111 -0
  57. metadata +115 -0
data/.gitignore ADDED
@@ -0,0 +1,26 @@
1
+ .DS_Store
2
+ .rake_tasks
3
+ log/*
4
+ tmp/*
5
+ TAGS
6
+ *~
7
+ .#*
8
+ pkg
9
+ schema/schema.rb
10
+ schema/*_structure.sql
11
+ schema/*.sqlite3
12
+ schema/*.sqlite
13
+ schema/*.db
14
+ *.sqlite
15
+ *.sqlite3
16
+ *.db
17
+ src/*
18
+ .hgignore
19
+ .hg/*
20
+ .svn/*
21
+ doc
22
+ config/database.yml
23
+ merb/custom.rb
24
+ test/fixtures/queue
25
+ messages
26
+ test/messages
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-* ENTP
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,47 @@
1
+ astrotrain
2
+ ==========
3
+
4
+ NOTE: Astrotrain is a full gem now. If you're looking for the old Astrotrain on merb:
5
+ http://github.com/entp/astrotrain/tree/merb
6
+ git://github.com/entp/astrotrain.git (merb branch)
7
+
8
+ Scans incoming emails for mapped recipients and sends an HTTP POST somewhere.
9
+
10
+ # setup a config file.
11
+ # Point the queue_path at a directory that your mail server dumps each raw incoming mail.
12
+ require 'astrotrain'
13
+
14
+ Astrotrain.load path do
15
+ DataMapper.setup(:default, {
16
+ :adapter => "mysql",
17
+ :database => "astrotrain",
18
+ :username => "root",
19
+ :host => "localhost"
20
+ })
21
+ end
22
+ Astrotrain::Message.queue_path = "/path/to/maildir"
23
+ Astrotrain::Mapping::Transport.processing = true
24
+
25
+ # start up IRB
26
+ irb -I /var/astrotrain/lib -r config.rb
27
+
28
+ # manage mappings
29
+ LIB=/var/astrotrain/lib CONFIG=config.rb rake at:mappings
30
+ LIB=/var/astrotrain/lib CONFIG=config.rb rake at:map EMAIL=support@foo.com DEST=http://foo.com/email
31
+ LIB=/var/astrotrain/lib CONFIG=config.rb rake at:unmap MAP=123
32
+
33
+ # start the server that runs over the queue directory
34
+ LIB=/var/astrotrain/lib CONFIG=config.rb rake at:process
35
+
36
+ # start the sinatra API
37
+ ruby -I /var/astrotrain/lib /var/astrotrain/lib/astrotrain/api.rb config.rb
38
+
39
+ A single Astrotrain process currently handles email for two production applications, processing thousands daily.
40
+ It's far from perfect, but definitely usable.
41
+
42
+ TODO
43
+ ====
44
+
45
+ Gem package - I realize the load paths could be simplified a bunch, so a gem package will come soon.
46
+ Docs
47
+ bounced emails
data/Rakefile ADDED
@@ -0,0 +1,145 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "astrotrain"
8
+ gem.summary = %Q{email => http post}
9
+ gem.email = "technoweenie@gmail.com"
10
+ gem.homepage = "http://github.com/entp/astrotrain"
11
+ gem.authors = ["technoweenie"]
12
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
13
+ end
14
+
15
+ rescue LoadError
16
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
17
+ end
18
+
19
+ namespace :at do
20
+ task :init do
21
+ $LOAD_PATH.unshift File.expand_path(ENV['LIB']) if ENV['LIB']
22
+ require ENV['CONFIG'] if ENV['CONFIG']
23
+
24
+ if !Object.const_defined?(:Astrotrain)
25
+ require 'astrotrain'
26
+ Astrotrain.load
27
+ end
28
+
29
+ Astrotrain::Message.queue_path = ENV['QUEUE'] if ENV['QUEUE']
30
+ if !File.exist?(Astrotrain::Message.queue_path)
31
+ puts "No Queue: #{Astrotrain.queue_path.inspect}"
32
+ exit
33
+ end
34
+ end
35
+
36
+ desc "List Mappings"
37
+ task :mappings => :init do
38
+ Astrotrain::Mapping.all.each do |map|
39
+ puts "##{map.id}: #{map.full_email} => #{map.destination}"
40
+ puts "Separated by: #{map.separator.inspect}" if !map.separator.blank?
41
+ puts
42
+ end
43
+ end
44
+
45
+ desc "Add mapping EMAIL=ticket*@foo.com DEST=http://foo.com/email [TRANS=(http_post|jabber)] [SEP=REPLYABOVEHERE] [HEADERS=delivered_to,to,original_to]"
46
+ task :map => :init do
47
+ map = Astrotrain::Mapping.new
48
+ map.email_user, map.email_domain = ENV['EMAIL'].to_s.split('@')
49
+ map.destination = ENV['DEST']
50
+ map.transport = ENV['TRANS'] if ENV['TRANS']
51
+ map.separator = ENV['SEP'] if ENV['SEP']
52
+ if map.save
53
+ puts "Mapping created for #{map.full_email} => #{map.destination}"
54
+ Rake::Task['at:mappings'].invoke
55
+ else
56
+ puts map.inspect
57
+ puts map.errors.inspect
58
+ end
59
+ end
60
+
61
+ desc "Remove mapping MAP=123"
62
+ task :unmap => :init do
63
+ map = Astrotrain::Mapping.get(ENV["MAP"])
64
+ map.destroy if map
65
+ Rake::Task['at:mappings'].invoke
66
+ end
67
+
68
+ desc "Start astrotrain DRb server."
69
+ task :process => :init do
70
+ pid_filename = File.join(Astrotrain.root, 'log', 'astrotrain_job.pid')
71
+
72
+ FileUtils.mkdir_p File.dirname(pid_filename)
73
+ require 'benchmark'
74
+
75
+ begin
76
+ File.open(pid_filename, 'w') { |f| f << Process.pid.to_s }
77
+ SLEEP = 5
78
+
79
+ trap('TERM') { puts 'Exiting...'; $exit = true }
80
+ trap('INT') { puts 'Exiting...'; $exit = true }
81
+
82
+ loop do
83
+ count = nil
84
+
85
+ realtime = Benchmark.realtime do
86
+ files = Dir["#{Astrotrain::Message.queue_path}/*"]
87
+ files.each do |mail|
88
+ Astrotrain::Message.receive_file(mail)
89
+ end
90
+ count = files.size
91
+ end
92
+
93
+ break if $exit
94
+
95
+ if count.zero?
96
+ sleep(SLEEP)
97
+ else
98
+ puts "#{count} mails processed at %.4f m/s ..." % [count / realtime]
99
+ end
100
+
101
+ break if $exit
102
+ end
103
+ ensure
104
+ FileUtils.rm(pid_filename) rescue nil
105
+ end
106
+ end
107
+ end
108
+
109
+ require 'rake/testtask'
110
+ Rake::TestTask.new do |t|
111
+ t.test_files = FileList['test/*_test.rb']
112
+ t.verbose = true
113
+ end
114
+
115
+ begin
116
+ require 'rcov/rcovtask'
117
+ Rcov::RcovTask.new do |test|
118
+ test.libs << 'test'
119
+ test.pattern = 'test/**/*_test.rb'
120
+ test.verbose = true
121
+ end
122
+ rescue LoadError
123
+ task :rcov do
124
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
125
+ end
126
+ end
127
+
128
+ require 'rake/rdoctask'
129
+ Rake::RDocTask.new do |rdoc|
130
+ if File.exist?('VERSION.yml')
131
+ config = YAML.load(File.read('VERSION.yml'))
132
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
133
+ else
134
+ version = ""
135
+ end
136
+
137
+ rdoc.rdoc_dir = 'rdoc'
138
+ rdoc.title = "astrotrain #{version}"
139
+ rdoc.rdoc_files.include('README*')
140
+ rdoc.rdoc_files.include('lib/**/*.rb')
141
+ end
142
+
143
+
144
+ desc 'Default: run test examples'
145
+ task :default => 'test'
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,96 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{astrotrain}
5
+ s.version = "0.2.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["technoweenie"]
9
+ s.date = %q{2009-09-21}
10
+ s.email = %q{technoweenie@gmail.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "LICENSE",
18
+ "README",
19
+ "Rakefile",
20
+ "VERSION",
21
+ "astrotrain.gemspec",
22
+ "config/sample.rb",
23
+ "lib/astrotrain.rb",
24
+ "lib/astrotrain/api.rb",
25
+ "lib/astrotrain/logged_mail.rb",
26
+ "lib/astrotrain/mapping.rb",
27
+ "lib/astrotrain/mapping/http_post.rb",
28
+ "lib/astrotrain/mapping/jabber.rb",
29
+ "lib/astrotrain/mapping/transport.rb",
30
+ "lib/astrotrain/message.rb",
31
+ "lib/astrotrain/tmail.rb",
32
+ "lib/vendor/rest-client/README.rdoc",
33
+ "lib/vendor/rest-client/Rakefile",
34
+ "lib/vendor/rest-client/bin/restclient",
35
+ "lib/vendor/rest-client/foo.diff",
36
+ "lib/vendor/rest-client/lib/rest_client.rb",
37
+ "lib/vendor/rest-client/lib/rest_client/net_http_ext.rb",
38
+ "lib/vendor/rest-client/lib/rest_client/payload.rb",
39
+ "lib/vendor/rest-client/lib/rest_client/request_errors.rb",
40
+ "lib/vendor/rest-client/lib/rest_client/resource.rb",
41
+ "lib/vendor/rest-client/rest-client.gemspec",
42
+ "lib/vendor/rest-client/spec/base.rb",
43
+ "lib/vendor/rest-client/spec/master_shake.jpg",
44
+ "lib/vendor/rest-client/spec/payload_spec.rb",
45
+ "lib/vendor/rest-client/spec/request_errors_spec.rb",
46
+ "lib/vendor/rest-client/spec/resource_spec.rb",
47
+ "lib/vendor/rest-client/spec/rest_client_spec.rb",
48
+ "tasks/doc.thor",
49
+ "tasks/merb.thor",
50
+ "test/api_test.rb",
51
+ "test/fixtures/apple_multipart.txt",
52
+ "test/fixtures/basic.txt",
53
+ "test/fixtures/custom.txt",
54
+ "test/fixtures/fwd.txt",
55
+ "test/fixtures/gb2312_encoding.txt",
56
+ "test/fixtures/gb2312_encoding_invalid.txt",
57
+ "test/fixtures/html.txt",
58
+ "test/fixtures/iso-8859-1.txt",
59
+ "test/fixtures/mapped.txt",
60
+ "test/fixtures/multipart.txt",
61
+ "test/fixtures/multipart2.txt",
62
+ "test/fixtures/multiple.txt",
63
+ "test/fixtures/multiple_delivered_to.txt",
64
+ "test/fixtures/multiple_with_body_recipients.txt",
65
+ "test/fixtures/reply.txt",
66
+ "test/fixtures/utf-8.txt",
67
+ "test/logged_mail_test.rb",
68
+ "test/mapping_test.rb",
69
+ "test/message_test.rb",
70
+ "test/test_helper.rb",
71
+ "test/transport_test.rb"
72
+ ]
73
+ s.homepage = %q{http://github.com/entp/astrotrain}
74
+ s.rdoc_options = ["--charset=UTF-8"]
75
+ s.require_paths = ["lib"]
76
+ s.rubygems_version = %q{1.3.4}
77
+ s.summary = %q{email => http post}
78
+ s.test_files = [
79
+ "test/api_test.rb",
80
+ "test/logged_mail_test.rb",
81
+ "test/mapping_test.rb",
82
+ "test/message_test.rb",
83
+ "test/test_helper.rb",
84
+ "test/transport_test.rb"
85
+ ]
86
+
87
+ if s.respond_to? :specification_version then
88
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
89
+ s.specification_version = 3
90
+
91
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
92
+ else
93
+ end
94
+ else
95
+ end
96
+ end
data/config/sample.rb ADDED
@@ -0,0 +1,12 @@
1
+ path = File.join(File.dirname(__FILE__), '..')
2
+ $LOAD_PATH.unshift File.join(path, 'lib')
3
+ require 'astrotrain'
4
+
5
+ Astrotrain.load path do
6
+ DataMapper.setup(:default, {
7
+ :adapter => "mysql",
8
+ :database => "astrotrain",
9
+ :username => "root",
10
+ :host => "localhost"
11
+ })
12
+ end
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'sinatra'
3
+
4
+ before do
5
+ response['Content-Type'] = 'text/plain'
6
+ end
7
+
8
+ get '/queue_size' do
9
+ if File.exist?(Astrotrain::Message.queue_path)
10
+ (Dir.entries(Astrotrain::Message.queue_path).size - 2).to_s
11
+ else
12
+ '0'
13
+ end
14
+ end
15
+
16
+ get '/queue/*' do
17
+ path = params[:splat].first
18
+ path.gsub! /^\/+/, ''
19
+ file = File.join(Astrotrain::Message.queue_path, path)
20
+ if File.exist?(file)
21
+ IO.read(file)
22
+ else
23
+ halt 404, "#{path.inspect} was not found."
24
+ end
25
+ end
26
+
27
+ get '/queue' do
28
+ data = []
29
+ Dir.entries(Astrotrain::Message.queue_path).each do |e|
30
+ data << e unless e =~ /^\.{1,2}$/
31
+ end
32
+ data * "\n"
33
+ end
34
+
35
+ unless $testing
36
+ configure do
37
+ # the idea being, you pass the name of the config file
38
+ #
39
+ # # loads File.join(Dir.pwd, 'config.rb')
40
+ # CONFIG=config ruby lib/astrotrain/api.rb
41
+ #
42
+ # # loads File.join(Dir.pwd, 'config.rb')
43
+ # ruby lib/astrotrain/api.rb config
44
+ #
45
+ # That file contains the Greymalkin.load block that initializes Sequel
46
+ #
47
+ path = ENV['CONFIG'] || ARGV.shift
48
+ if path[0..0] != '/'
49
+ path = File.join(Dir.pwd, path)
50
+ end
51
+ require File.expand_path(path)
52
+ end
53
+ end
@@ -0,0 +1,41 @@
1
+ module Astrotrain
2
+ # Logs details of each incoming message.
3
+ class LoggedMail
4
+ include DataMapper::Resource
5
+
6
+ class << self
7
+ attr_accessor :log_path, :log_processed
8
+ end
9
+
10
+ # Enabling this will save records for every processed email, not just the errored emails.
11
+ self.log_processed = false
12
+ self.log_path = File.join(Astrotrain.root, 'messages')
13
+
14
+ property :id, Serial
15
+ property :mapping_id, Integer, :index => true
16
+ property :sender, String, :index => true, :size => 255, :length => 1..255
17
+ property :recipient, String, :index => true, :size => 255, :length => 1..255
18
+ property :subject, String, :index => true, :size => 255, :length => 1..255
19
+ property :created_at, DateTime
20
+ property :delivered_at, DateTime
21
+ property :error_message, String, :size => 255, :length => 1..255
22
+
23
+ belongs_to :mapping
24
+
25
+ def self.from(message)
26
+ logged = new
27
+ begin
28
+ logged.sender = Message.parse_email_addresses(message.sender).first
29
+ logged.subject = message.subject
30
+ end
31
+ if !block_given? || yield(logged)
32
+ begin
33
+ logged.save
34
+ rescue
35
+ puts $!.inspect
36
+ end
37
+ end
38
+ logged
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ module Astrotrain
2
+ class Mapping
3
+ class HttpPost < Transport
4
+ def process
5
+ return unless Transport.processing
6
+ RestClient.post @mapping.destination, fields.merge(:emails => fields[:emails].join(","))
7
+ end
8
+
9
+ def fields
10
+ super
11
+ @message.attachments.each_with_index do |att, index|
12
+ @fields[:"attachments_#{index}"] = att
13
+ end
14
+ @fields
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ module Astrotrain
2
+ class Mapping
3
+ # This is experimental. No attachments are supported.
4
+ class Jabber < Transport
5
+ class << self
6
+ attr_accessor :login, :password
7
+ end
8
+
9
+ def process
10
+ return unless Transport.processing
11
+ connection.deliver(@mapping.destination, content)
12
+ end
13
+
14
+ def connection
15
+ @connection ||= ::Jabber::Simple.new(self.class.login, self.class.password)
16
+ end
17
+
18
+ def content
19
+ @content ||= "From: %s\nTo: %s\nSubject: %s\nEmails: %s\n%s" % [fields[:from], fields[:to], fields[:subject], fields[:emails] * ", ", fields[:body]]
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,55 @@
1
+ module Astrotrain
2
+ class Mapping
3
+ class Transport
4
+ class << self
5
+ attr_accessor :processing
6
+ end
7
+
8
+ # Enable this turn on processing.
9
+ self.processing = false
10
+
11
+ attr_reader :message, :mapping
12
+
13
+ # process a given message against the mapping. The mapping transport is checked,
14
+ # and the appropirate transport class handles the request.
15
+ def self.process(message, mapping, recipient)
16
+ case mapping.transport
17
+ when 'http_post' then HttpPost.process(message, mapping, recipient)
18
+ when 'jabber' then Jabber.process(message, mapping, recipient)
19
+ end
20
+ end
21
+
22
+ def initialize(message, mapping, recipient)
23
+ message.body = mapping.find_reply_from(message.body)
24
+ @message = message
25
+ @mapping = mapping
26
+ @recipient = recipient
27
+ end
28
+
29
+ def process
30
+ raise UnimplementedError
31
+ end
32
+
33
+ def fields
34
+ @fields ||= begin
35
+ all_emails = @message.recipients - [@recipient]
36
+ f = {:subject => @message.subject, :to => @recipient, :from => @message.sender, :body => @message.body, :emails => all_emails, :html => @message.html}
37
+ @message.headers.each do |key, value|
38
+ f["headers[#{key}]"] = value
39
+ end
40
+ f
41
+ end
42
+ end
43
+
44
+ # defines custom #process class methods that instantiate the class and calls a #process instance method
45
+ def self.inherited(child)
46
+ super
47
+ class << child
48
+ def process(message, mapping, recipient)
49
+ new(message, mapping, recipient).process
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end