multi_mail 0.0.1 → 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.
Files changed (59) hide show
  1. data/.travis.yml +4 -0
  2. data/.yardopts +4 -0
  3. data/Gemfile +1 -1
  4. data/README.md +107 -12
  5. data/Rakefile +75 -52
  6. data/lib/multi_mail/cloudmailin/receiver.rb +100 -0
  7. data/lib/multi_mail/mailgun/receiver.rb +74 -36
  8. data/lib/multi_mail/mailgun/sender.rb +14 -0
  9. data/lib/multi_mail/mandrill/receiver.rb +77 -35
  10. data/lib/multi_mail/mandrill/sender.rb +14 -0
  11. data/lib/multi_mail/postmark/receiver.rb +68 -0
  12. data/lib/multi_mail/postmark/sender.rb +14 -0
  13. data/lib/multi_mail/receiver/base.rb +125 -8
  14. data/lib/multi_mail/receiver.rb +10 -4
  15. data/lib/multi_mail/sender/base.rb +7 -0
  16. data/lib/multi_mail/sender.rb +10 -4
  17. data/lib/multi_mail/sendgrid/receiver.rb +42 -0
  18. data/lib/multi_mail/sendgrid/sender.rb +11 -0
  19. data/lib/multi_mail/service.rb +15 -4
  20. data/lib/multi_mail/simple/receiver.rb +15 -0
  21. data/lib/multi_mail/simple/sender.rb +14 -0
  22. data/lib/multi_mail/version.rb +1 -1
  23. data/lib/multi_mail.rb +71 -3
  24. data/multi_mail.gemspec +12 -5
  25. data/spec/cloudmailin/receiver_spec.rb +112 -0
  26. data/spec/fixtures/cloudmailin/json/spam.txt +59 -0
  27. data/spec/fixtures/cloudmailin/json/valid.txt +59 -0
  28. data/spec/fixtures/cloudmailin/multipart/spam.txt +135 -0
  29. data/spec/fixtures/cloudmailin/multipart/valid.txt +135 -0
  30. data/spec/fixtures/cloudmailin/raw/spam.txt +137 -0
  31. data/spec/fixtures/cloudmailin/raw/valid.txt +137 -0
  32. data/spec/fixtures/mailgun/parsed/invalid.txt +8 -0
  33. data/spec/fixtures/mailgun/parsed/missing.txt +8 -0
  34. data/spec/fixtures/mailgun/parsed/spam.txt +8 -0
  35. data/spec/fixtures/mailgun/parsed/valid.txt +187 -0
  36. data/spec/fixtures/mandrill/spam.txt +9 -0
  37. data/spec/fixtures/mandrill/valid.txt +10 -0
  38. data/spec/fixtures/multipart.txt +99 -0
  39. data/spec/fixtures/postmark/spam.txt +83 -0
  40. data/spec/fixtures/postmark/valid.txt +92 -0
  41. data/spec/fixtures/simple/valid.txt +4 -0
  42. data/spec/mailgun/receiver_spec.rb +105 -50
  43. data/spec/mailgun/sender_spec.rb +0 -0
  44. data/spec/mandrill/receiver_spec.rb +35 -35
  45. data/spec/mandrill/sender_spec.rb +0 -0
  46. data/spec/multi_mail_spec.rb +63 -0
  47. data/spec/postmark/receiver_spec.rb +60 -0
  48. data/spec/postmark/sender_spec.rb +0 -0
  49. data/spec/receiver/base_spec.rb +73 -8
  50. data/spec/sender/base_spec.rb +21 -0
  51. data/spec/service_spec.rb +2 -2
  52. data/spec/simple/receiver_spec.rb +36 -0
  53. data/spec/simple/sender_spec.rb +0 -0
  54. data/spec/spec_helper.rb +123 -10
  55. metadata +141 -21
  56. data/spec/fixtures/mailgun/invalid.txt +0 -8
  57. data/spec/fixtures/mailgun/missing.txt +0 -8
  58. data/spec/fixtures/mailgun/spam.txt +0 -8
  59. data/spec/fixtures/mailgun/valid.txt +0 -8
data/.travis.yml CHANGED
@@ -1,3 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 1.8.7
4
+ - 1.9.2
3
5
  - 1.9.3
6
+ - 2.0.0
7
+ - ree
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --no-private
2
+ --hide-void-return
3
+ --embed-mixin ClassMethods
4
+ --markup=markdown
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in scraperwiki-api.gemspec
3
+ # Specify your gem's dependencies in the gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -1,22 +1,19 @@
1
1
  # MultiMail: easily switch between email APIs
2
2
 
3
+ [![Build Status](https://secure.travis-ci.org/opennorth/multi_mail.png)](http://travis-ci.org/opennorth/multi_mail)
3
4
  [![Dependency Status](https://gemnasium.com/opennorth/multi_mail.png)](https://gemnasium.com/opennorth/multi_mail)
4
- [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/opennorth/multi_mail)
5
+ [![Coverage Status](https://coveralls.io/repos/opennorth/multi_mail/badge.png?branch=master)](https://coveralls.io/r/opennorth/multi_mail)
6
+ [![Code Climate](https://codeclimate.com/github/opennorth/multi_mail.png)](https://codeclimate.com/github/opennorth/multi_mail)
5
7
 
6
8
  Many providers – including [Cloudmailin](http://www.cloudmailin.com/), [Mailgun](http://www.mailgun.com/), [Mandrill](http://mandrill.com/), [Postmark](http://postmarkapp.com/) and [SendGrid](http://sendgrid.com/) – offer APIs to send, receive, parse and forward email. MultiMail lets you easily switch between these APIs.
7
9
 
8
10
  ## Usage
9
11
 
10
- ```ruby
11
- require 'multi_mail'
12
-
13
- service = MultiMail::Receiver.new({
14
- :provider => 'mailgun',
15
- :mailgun_api_key => 'key-xxxxxxxxxxxxxxxxxxxxxxx-x-xxxxxx',
16
- })
17
-
18
- message = service.process data # raw POST data or params hash
19
- ```
12
+ require 'multi_mail'
13
+
14
+ service = MultiMail::Receiver.new(:provider => 'mandrill')
15
+
16
+ message = service.process data # raw POST data or params hash
20
17
 
21
18
  `message` is an array of [Mail::Message](https://github.com/mikel/mail) instances.
22
19
 
@@ -26,13 +23,111 @@ Incoming email:
26
23
 
27
24
  * [Mailgun](http://www.mailgun.com/)
28
25
  * [Mandrill](http://mandrill.com/)
26
+ * [Postmark](http://postmarkapp.com/)
27
+ * [Cloudmailin](http://www.cloudmailin.com/)
28
+
29
+ Any additional information provided by an API is added to the message as a header. For example, Mailgun provides `stripped-text`, which is the message body without quoted parts or signature block. You can access it with `message['stripped-text'].value`.
30
+
31
+ ## Cloudmailin
32
+
33
+ service = MultiMail::Receiver.new({
34
+ :provider => 'cloudmailin',
35
+ })
36
+
37
+ The default HTTP POST format is `raw`. Add a `:http_post_format` option to change the HTTP POST format, with possible values of `"multipart"`, `"json"` or `"raw"` (default). (The [original format](http://docs.cloudmailin.com/http_post_formats/original/) is deprecated.) For example:
38
+
39
+ service = MultiMail::Receiver.new({
40
+ :provider => 'cloudmailin',
41
+ :http_post_format => 'raw',
42
+ })
43
+
44
+ **Note:** [MultiMail doesn't yet support Cloudmailin's URL attachments (attachment stores).](https://github.com/opennorth/multi_mail/issues/11) Please use regular attachments (always the case if you use the `raw` format) if you are using MultiMail.
45
+
46
+ **2013-04-15:** If an email contains multiple HTML parts and you are using the `multipart` or `json` HTTP POST formats, Cloudmailin will only include the first HTML part in its `html` parameter. Use the `raw` format to avoid data loss. Cloudmailin also removes a newline from the end of each attachment.
47
+
48
+ ### Additional information provided by the API
49
+
50
+ See [Cloudmailin's documentation](http://docs.cloudmailin.com/http_post_formats/):
51
+
52
+ * `reply_plain`
53
+ * `spf-result`
54
+
55
+ ## Mailgun
56
+
57
+ service = MultiMail::Receiver.new({
58
+ :provider => 'mailgun',
59
+ :mailgun_api_key => 'key-xxxxxxxxxxxxxxxxxxxxxxx-x-xxxxxx',
60
+ })
61
+
62
+ If you have a route with a URL ending with "mime" and you are using the raw MIME format, add a `:http_post_format => 'raw'` option. For example:
63
+
64
+ service = MultiMail::Receiver.new({
65
+ :provider => 'mailgun',
66
+ :mailgun_api_key => 'key-xxxxxxxxxxxxxxxxxxxxxxx-x-xxxxxx',
67
+ :http_post_format => 'raw',
68
+ })
69
+
70
+ **2013-04-15:** Mailgun's `stripped-text` and `stripped-html` parameters do not return the same parts of the message. `stripped-html` sometimes incorrectly drops non-quoted, non-signature parts of the message; `stripped-text` doesn't.
71
+
72
+ ### Additional information provided by the API
29
73
 
30
- [Attachment parsing](https://github.com/mikel/mail#attaching-and-detaching-files) on incoming email is not implemented yet. No outgoing email services are implemented yet.
74
+ See [Mailgun's documentation](http://documentation.mailgun.net/user_manual.html#parsed-messages-parameters):
75
+
76
+ * `stripped-text`
77
+ * `stripped-signature`
78
+ * `stripped-html`
79
+ * `content-id-map`
80
+
81
+ ## Mandrill
82
+
83
+ service = MultiMail::Receiver.new({
84
+ :provider => 'mandrill',
85
+ })
86
+
87
+ The default SpamAssassin score needed to flag an email as spam is `5`. Add a `:spamassassin_threshold` option to increase or decrease it. For example:
88
+
89
+ service = MultiMail::Receiver.new({
90
+ :provider => 'mandrill',
91
+ :spamassassin_threshold => 4.5,
92
+ })
93
+
94
+ **2013-04-15:** If an email contains multiple HTML parts, Mandrill will only include the first HTML part in its `html` parameter. We therefore parse its `raw_msg` parameter to set the HTML part correctly. Mandrill also adds a newline to the end of each message part.
95
+
96
+ ### Additional information provided by the API
97
+
98
+ See [Mandrill's documentation](http://help.mandrill.com/entries/22092308-What-is-the-format-of-inbound-email-webhooks-):
99
+
100
+ * `ts`
101
+ * `email`
102
+ * `dkim-signed`
103
+ * `dkim-valid`
104
+ * `spam_report-score`
105
+ * `spf-result`
106
+
107
+ ## Postmark
108
+
109
+ service = MultiMail::Receiver.new({
110
+ :provider => 'postmark',
111
+ })
112
+
113
+ **2013-05-15:** If an email contains multiple HTML parts, Postmark will only include the first HTML part in its `HtmlBody` parameter. You cannot avoid this loss of data. Postmark is therefore not recommended.
114
+
115
+ ### Additional information provided by the API
116
+
117
+ See [Postmark's documentation](http://developer.postmarkapp.com/developer-inbound-parse.html#mailboxhash):
118
+
119
+ * `MailboxHash`
120
+ * `MessageID`
121
+ * `Tag`
31
122
 
32
123
  ## Bugs? Questions?
33
124
 
34
125
  This gem's main repository is on GitHub: [http://github.com/opennorth/multi_mail](http://github.com/opennorth/multi_mail), where your contributions, forks, bug reports, feature requests, and feedback are greatly welcomed.
35
126
 
127
+ ## Acknowledgements
128
+
129
+ This gem is developed by [Open North](http://www.opennorth.ca/) through a partnership with the [Participatory Politics Foundation](http://www.participatorypolitics.org/).
130
+
36
131
  ## Copyright
37
132
 
38
133
  This gem re-uses code from [fog](https://github.com/fog/fog), released under the MIT license.
data/Rakefile CHANGED
@@ -21,80 +21,103 @@ def credentials
21
21
  @credentials ||= YAML.load_file File.expand_path(File.join(File.dirname(__FILE__), 'api_keys.yml'))
22
22
  end
23
23
 
24
- namespace :mailgun do
24
+ desc 'Create a Mailgun catch-all route forwarding to a postbin'
25
+ task :mailgun do
26
+ require 'securerandom'
25
27
  require 'json'
26
28
  require 'rest-client'
27
29
 
28
- desc 'Create a Mailgun catch-all route forwarding to a postbin'
29
- task :postbin do
30
-
31
- bin_name = ENV['BIN_NAME'] || JSON.load(RestClient.post('http://requestb.in/api/v1/bins', {}))['name']
32
- bin_url = "http://requestb.in/#{bin_name}"
30
+ def bin_url_and_action
31
+ bin_name = JSON.load(RestClient.post('http://requestb.in/api/v1/bins', {}))['name']
32
+ ["http://requestb.in/#{bin_name}", %(forward("#{bin_url}"))]
33
+ end
33
34
 
34
- base_url = "https://api:#{credentials[:mailgun_api_key]}@api.mailgun.net/v2"
35
- action = %(forward("#{bin_url}"))
35
+ base_url = "https://api:#{credentials[:mailgun_api_key]}@api.mailgun.net/v2"
36
+ domain = JSON.load(RestClient.get("#{base_url}/domains"))['items'].first
36
37
 
37
- domain = ENV['DOMAIN'] || "#{SecureRandom.base64(4).tr('+/=lIO0', 'pqrsxyz')}.mailgun.com"
38
+ if domain
39
+ domain_name = domain['name']
40
+ else
41
+ domain_name = "#{SecureRandom.base64(4).tr('+/=lIO0', 'pqrsxyz')}.mailgun.com"
42
+ puts "Creating the #{domain_name} domain..."
43
+ RestClient.post("#{base_url}/domains", :name => domain_name)
44
+ end
38
45
 
39
- if JSON.load(RestClient.get("#{base_url}/domains"))['items'].empty?
40
- puts "Creating the #{domain} domain..."
41
- RestClient.post("#{base_url}/domains", :name => domain)
42
- end
46
+ catch_all_route = JSON.load(RestClient.get("#{base_url}/routes"))['items'].find do |route|
47
+ route['expression'] == 'catch_all()'
48
+ end
43
49
 
44
- route = JSON.load(RestClient.get("#{base_url}/routes"))['items'].find do |route|
45
- route['expression'] == 'catch_all()'
50
+ if catch_all_route
51
+ action = catch_all_route['actions'].find do |action|
52
+ action[%r{\Aforward\("(http://requestb\.in/\w+)"\)\z}]
46
53
  end
47
54
 
48
- if route
49
- unless route['action'] == action
50
- puts "Updating the catch_all() route..."
51
- JSON.load(RestClient.put("#{base_url}/routes/#{route['id']}", :action => action))
52
- end
55
+ if action
56
+ bin_url = $1
53
57
  else
54
- puts "Creating a catch_all() route..."
55
- JSON.load(RestClient.post("#{base_url}/routes", :expression => 'catch_all()', :action => action))
58
+ bin_url, action = bin_url_and_action
59
+ puts "Updating the catch_all() route..."
60
+ JSON.load(RestClient.put("#{base_url}/routes/#{catch_all_route['id']}", :action => action))
56
61
  end
57
-
58
- puts "#{bin_url}?inspect"
62
+ else
63
+ bin_url, action = bin_url_and_action
64
+ puts "Creating a catch_all() route..."
65
+ JSON.load(RestClient.post("#{base_url}/routes", :expression => 'catch_all()', :action => action))
59
66
  end
67
+
68
+ puts "The catchall route for #{domain_name} POSTs to #{bin_url}?inspect"
60
69
  end
61
70
 
62
- namespace :mandrill do
71
+ desc 'Ensure a Mandrill catch-all route forwarding to a postbin'
72
+ task :mandrill do
63
73
  require 'mandrill'
64
74
 
65
- def mandrill_api
66
- @mandrill_api ||= Mandrill::API.new credentials[:mandrill_api_key]
67
- end
75
+ api = Mandrill::API.new(credentials[:mandrill_api_key])
76
+ domain = api.inbound.domains.first
68
77
 
69
- def mandrill_domains
70
- @mandrill_domains ||= mandrill_api.inbound.domains.each_with_object({}) do |domain,domains|
71
- domains[domain['domain']] = domain
72
- end
73
- end
78
+ if domain
79
+ domain_name = domain['domain']
80
+ routes = api.inbound.routes domain_name
81
+ match = routes.find{|route| route['pattern'] == '*'}
74
82
 
75
- def mandrill_domain
76
- @mandrill_domain ||= ENV['DOMAIN'] || mandrill_domains.keys.first
83
+ puts "The MX for #{domain_name} is not valid" unless domain['valid_mx']
84
+ puts "Add a catchall (*) route for #{domain_name}" if match.nil?
85
+ puts "The catchall route for #{domain_name} POSTs to #{match['url']}?inspect"
86
+ else
87
+ abort 'Add an inbound domain at https://mandrillapp.com/ or, if you already have your MX records set up, by sending an email through Mandrill'
77
88
  end
89
+ end
78
90
 
79
- desc 'Create a Mandrill catch-all route forwarding to a postbin'
80
- task :validate do
81
- if mandrill_domains.empty?
82
- abort 'Add an inbound domain'
83
- elsif mandrill_domains.size > 1 && ENV['DOMAIN'].nil?
84
- abort "ENV['DOMAIN'] must be one of #{mandrill_domains.keys.join ', '}"
85
- end
91
+ desc 'Create a Postmark route forwarding to a postbin'
92
+ task :postmark do
93
+ require 'json'
94
+ require 'postmark'
95
+ require 'rest-client'
86
96
 
87
- if ENV['DOMAIN'] && !mandrill_domains.keys.include?(ENV['DOMAIN'])
88
- abort "#{ENV['DOMAIN']} must be one of #{mandrill_domains.keys.join ', '}"
89
- end
97
+ api = Postmark::ApiClient.new(credentials[:postmark_api_key])
90
98
 
91
- unless mandrill_domains[mandrill_domain]['valid_mx']
92
- puts "The MX for #{mandrill_domain} is not valid"
93
- end
99
+ info = api.server_info
94
100
 
95
- routes = mandrill_api.inbound.routes mandrill_domain
96
- if routes.empty? || routes.none?{|route| route['pattern'] == '*'}
97
- puts "Add a catchall (*) route for #{mandrill_domain}"
98
- end
101
+ if info.key?(:inbound_hook_url)
102
+ url = info[:inbound_hook_url]
103
+ else
104
+ bin_name = JSON.load(RestClient.post('http://requestb.in/api/v1/bins', {}))['name']
105
+ url = "http://requestb.in/#{bin_name}"
106
+ api.update_server_info :inbound_hook_url => url
99
107
  end
108
+
109
+ puts "#{info[:inbound_hash]}@inbound.postmarkapp.com POSTs to #{url}?inspect"
110
+ end
111
+
112
+ desc 'POST a test fixture to an URL'
113
+ task :http_post, :url, :fixture do |t,args|
114
+ require 'rest-client'
115
+
116
+ contents = File.read(args[:fixture])
117
+ io = StringIO.new(contents)
118
+ socket = Net::BufferedIO.new(io)
119
+ response = Net::HTTPResponse.read_new(socket)
120
+ body = contents[/(?:\r?\n){2,}(.+)\z/m, 1]
121
+
122
+ puts RestClient.post(args[:url], body, :content_type => response.header['content-type'])
100
123
  end
@@ -0,0 +1,100 @@
1
+ module MultiMail
2
+ module Receiver
3
+ # Cloudmailin's incoming email receiver.
4
+ #
5
+ # Cloudmailin recommends using basic authentication over HTTPS to ensure
6
+ # that a request originates from Cloudmailin.
7
+ #
8
+ # @see http://docs.cloudmailin.com/receiving_email/securing_your_email_url_target/
9
+ class Cloudmailin < MultiMail::Service
10
+ include MultiMail::Receiver::Base
11
+
12
+ recognizes :http_post_format
13
+
14
+ # Initializes a Cloudmailin incoming email receiver.
15
+ #
16
+ # @param [Hash] options required and optional arguments
17
+ # @option options [String] :http_post_format "multipart", "json" or "raw"
18
+ def initialize(options = {})
19
+ super
20
+ @http_post_format = options[:http_post_format]
21
+ end
22
+
23
+ # @param [Hash] params the content of Cloudmailin's webhook
24
+ # @return [Array<Mail::Message>] messages
25
+ # @see http://docs.cloudmailin.com/http_post_formats/multipart/
26
+ # @see http://docs.cloudmailin.com/http_post_formats/json/
27
+ # @see http://docs.cloudmailin.com/http_post_formats/raw/
28
+ def transform(params)
29
+ case @http_post_format
30
+ when 'raw', '', nil
31
+ message = self.class.condense(Mail.new(params['message']))
32
+
33
+ # Extra Cloudmailin parameters.
34
+ message['spf-result'] = params['envelope']['spf']['result']
35
+
36
+ # Discard rest of `envelope`: `from`, `to`, `recipients`,
37
+ # `helo_domain` and `remote_ip`.
38
+ [message]
39
+ when 'multipart', 'json'
40
+ headers = Multimap.new
41
+ params['headers'].each do |key,value|
42
+ if Array === value
43
+ value.each do |v|
44
+ headers[key] = v
45
+ end
46
+ else
47
+ headers[key] = value
48
+ end
49
+ end
50
+
51
+ # Mail changes `self`.
52
+ http_post_format = @http_post_format
53
+
54
+ this = self
55
+ message = Mail.new do
56
+ headers headers
57
+
58
+ text_part do
59
+ body params['plain']
60
+ end
61
+
62
+ if params.key?('html')
63
+ html_part do
64
+ content_type 'text/html; charset=UTF-8'
65
+ body params['html']
66
+ end
67
+ end
68
+
69
+ if params.key?('attachments')
70
+ if http_post_format == 'json'
71
+ params['attachments'].each do |attachment|
72
+ add_file(:filename => attachment['file_name'], :content => Base64.decode64(attachment['content']))
73
+ end
74
+ else
75
+ params['attachments'].each do |_,attachment|
76
+ add_file(this.class.add_file_arguments(attachment))
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Extra Cloudmailin parameters. The multipart format uses CRLF whereas
83
+ # the JSON format uses LF. Normalize to LF.
84
+ message['reply_plain'] = params['reply_plain'].gsub("\r\n", "\n")
85
+ message['spf-result'] = params['envelope']['spf']['result']
86
+
87
+ [message]
88
+ else
89
+ raise ArgumentError, "Can't handle Cloudmailin #{@http_post_format} HTTP POST format"
90
+ end
91
+ end
92
+
93
+ # @param [Mail::Message] message a message
94
+ # @return [Boolean] whether the message is spam
95
+ def spam?(message)
96
+ message['spf-result'] && message['spf-result'].value == 'fail'
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,20 +1,28 @@
1
1
  module MultiMail
2
2
  module Receiver
3
+ # Mailgun's incoming email receiver.
3
4
  class Mailgun < MultiMail::Service
4
5
  include MultiMail::Receiver::Base
5
6
 
6
7
  requires :mailgun_api_key
8
+ recognizes :http_post_format
7
9
 
10
+ # Initializes a Mailgun incoming email receiver.
11
+ #
8
12
  # @param [Hash] options required and optional arguments
9
- # @option opts [String] :mailgun_api_key a Mailgun API key
13
+ # @option options [String] :mailgun_api_key a Mailgun API key
14
+ # @option options [String] :http_post_format "parsed" or "raw"
10
15
  def initialize(options = {})
11
16
  super
12
17
  @mailgun_api_key = options[:mailgun_api_key]
18
+ @http_post_format = options[:http_post_format]
13
19
  end
14
20
 
21
+ # Returns whether a request originates from Mailgun.
22
+ #
15
23
  # @param [Hash] params the content of Mailgun's webhook
16
24
  # @return [Boolean] whether the request originates from Mailgun
17
- # @raises [KeyError] if the request is missing parameters
25
+ # @raise [IndexError] if the request is missing parameters
18
26
  # @see http://documentation.mailgun.net/user_manual.html#securing-webhooks
19
27
  def valid?(params)
20
28
  params.fetch('signature') == OpenSSL::HMAC.hexdigest(
@@ -22,53 +30,83 @@ module MultiMail
22
30
  '%s%s' % [params.fetch('timestamp'), params.fetch('token')])
23
31
  end
24
32
 
33
+ # Transforms the content of Mailgun's webhook into a list of messages.
34
+ #
25
35
  # @param [Hash] params the content of Mailgun's webhook
26
36
  # @return [Array<Mail::Message>] messages
27
- # @note Mailgun sends the message headers both individually and in the
28
- # `message-headers` parameter. Only `message-headers` is documented.
29
- # @todo parse attachments properly
37
+ # @see http://documentation.mailgun.net/user_manual.html#mime-messages-parameters
38
+ # @see http://documentation.mailgun.net/user_manual.html#parsed-messages-parameters
30
39
  def transform(params)
31
- headers = Multimap.new
32
- JSON.parse(params['message-headers']).each do |key,value|
33
- headers[key] = value
34
- end
40
+ case @http_post_format
41
+ when 'parsed', '', nil
42
+ headers = Multimap.new
43
+ JSON.parse(params['message-headers']).each do |key,value|
44
+ headers[key] = value
45
+ end
46
+
47
+ this = self
48
+ message = Mail.new do
49
+ headers headers
50
+
51
+ # The following are redundant with `body-mime` in raw MIME format
52
+ # and with `message-headers` in fully parsed format.
53
+ #
54
+ # from params['from']
55
+ # sender params['sender']
56
+ # to params['recipient']
57
+ # subject params['subject']
58
+ #
59
+ # Mailgun POSTs all MIME headers both individually and in
60
+ # `message-headers`.
35
61
 
36
- message = Mail.new do
37
- headers headers
62
+ text_part do
63
+ body params['body-plain']
64
+ end
38
65
 
39
- # The following are redundant with `message-headers`:
40
- #
41
- # from params['from']
42
- # sender params['sender']
43
- # to params['recipient']
44
- # subject params['subject']
66
+ if params.key?('body-html')
67
+ html_part do
68
+ content_type 'text/html; charset=UTF-8'
69
+ body params['body-html']
70
+ end
71
+ end
45
72
 
46
- text_part do
47
- body params['body-plain']
73
+ if params.key?('attachment-count')
74
+ 1.upto(params['attachment-count'].to_i).each do |n|
75
+ attachment = params["attachment-#{n}"]
76
+ add_file(this.class.add_file_arguments(attachment))
77
+ end
78
+ end
48
79
  end
49
80
 
50
- html_part do
51
- content_type 'text/html; charset=UTF-8'
52
- body params['body-html']
81
+ # Extra Mailgun parameters.
82
+ extra = [
83
+ 'stripped-text',
84
+ 'stripped-signature',
85
+ 'stripped-html',
86
+ 'content-id-map',
87
+ ]
88
+
89
+ # Non-plain, non-HTML body parts.
90
+ extra += params.keys.select do |key|
91
+ key[/\Abody-(?!html|plain)/]
53
92
  end
54
- end
55
93
 
56
- # Extra Mailgun parameters.
57
- [ 'stripped-text',
58
- 'stripped-signature',
59
- 'stripped-html',
60
- 'attachment-count',
61
- 'attachment-x',
62
- 'content-id-map',
63
- ].each do |key|
64
- if !params[key].nil? && !params[key].empty?
65
- message[key] = params[key]
94
+ extra.each do |key|
95
+ if params.key?(key) && !params[key].empty?
96
+ message[key] = params[key]
97
+ end
66
98
  end
67
- end
68
99
 
69
- [message]
100
+ [message]
101
+ when 'raw'
102
+ [Mail.new(params['body-mime'])]
103
+ else
104
+ raise ArgumentError, "Can't handle Mailgun #{@http_post_format} HTTP POST format"
105
+ end
70
106
  end
71
107
 
108
+ # Returns whether a message is spam.
109
+ #
72
110
  # @param [Mail::Message] message a message
73
111
  # @return [Boolean] whether the message is spam
74
112
  # @see http://documentation.mailgun.net/user_manual.html#spam-filter
@@ -77,7 +115,7 @@ module MultiMail
77
115
  # @note We may also inspect `X-Mailgun-SScore` and `X-Mailgun-Spf`, whose
78
116
  # possible values are "Pass", "Neutral", "Fail" and "SoftFail".
79
117
  def spam?(message)
80
- message['X-Mailgun-Sflag'].value == 'Yes'
118
+ message['X-Mailgun-Sflag'] && message['X-Mailgun-Sflag'].value == 'Yes'
81
119
  end
82
120
  end
83
121
  end
@@ -0,0 +1,14 @@
1
+ module MultiMail
2
+ module Sender
3
+ class Mailgun < MultiMail::Service
4
+ include MultiMail::Sender::Base
5
+
6
+ #requires :
7
+
8
+ # @param [Hash] options required and optional arguments
9
+ def initialize(options = {})
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end