multi_mail 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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