multi_mail 0.1.0 → 0.1.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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +57 -34
  3. data/bin/multi_mail_post +71 -0
  4. data/lib/mail_ext/message.rb +11 -0
  5. data/lib/multi_mail.rb +25 -8
  6. data/lib/multi_mail/cloudmailin/receiver.rb +24 -3
  7. data/lib/multi_mail/mailgun/message.rb +6 -1
  8. data/lib/multi_mail/mailgun/sender.rb +4 -4
  9. data/lib/multi_mail/mandrill/message.rb +4 -3
  10. data/lib/multi_mail/mandrill/sender.rb +9 -2
  11. data/lib/multi_mail/message/base.rb +14 -0
  12. data/lib/multi_mail/postmark/message.rb +74 -0
  13. data/lib/multi_mail/postmark/receiver.rb +2 -1
  14. data/lib/multi_mail/postmark/sender.rb +54 -24
  15. data/lib/multi_mail/sendgrid/message.rb +4 -4
  16. data/lib/multi_mail/sendgrid/sender.rb +2 -2
  17. data/lib/multi_mail/simple/receiver.rb +31 -1
  18. data/lib/multi_mail/version.rb +1 -1
  19. data/multi_mail.gemspec +2 -1
  20. data/spec/cloudmailin/receiver_spec.rb +89 -84
  21. data/spec/fixtures/cloudmailin/json/attachment_store.txt +65 -0
  22. data/spec/fixtures/cloudmailin/multipart/attachment_store.txt +174 -0
  23. data/spec/fixtures/cloudmailin/raw/attachment_store.txt +162 -0
  24. data/spec/fixtures/mailgun/parsed/valid.txt +107 -101
  25. data/spec/fixtures/simple/invalid.txt +4 -0
  26. data/spec/fixtures/simple/missing.txt +4 -0
  27. data/spec/fixtures/simple/valid.txt +1 -1
  28. data/spec/mail_ext/message_spec.rb +45 -0
  29. data/spec/mailgun/message_spec.rb +38 -8
  30. data/spec/mailgun/receiver_spec.rb +104 -110
  31. data/spec/mailgun/sender_spec.rb +13 -7
  32. data/spec/mandrill/message_spec.rb +25 -1
  33. data/spec/mandrill/receiver_spec.rb +81 -83
  34. data/spec/mandrill/sender_spec.rb +13 -6
  35. data/spec/message/base_spec.rb +33 -1
  36. data/spec/postmark/message_spec.rb +292 -0
  37. data/spec/postmark/receiver_spec.rb +46 -48
  38. data/spec/postmark/sender_spec.rb +10 -10
  39. data/spec/sendgrid/message_spec.rb +6 -1
  40. data/spec/sendgrid/receiver_spec.rb +56 -58
  41. data/spec/sendgrid/sender_spec.rb +9 -7
  42. data/spec/service_spec.rb +1 -1
  43. data/spec/simple/receiver_spec.rb +38 -25
  44. data/spec/spec_helper.rb +6 -8
  45. metadata +185 -203
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 10a52d2bbde07c200870939a097077677734ec7f
4
+ data.tar.gz: 41aeb775d3c1cf6b1fefb20799756a78297eecbb
5
+ SHA512:
6
+ metadata.gz: ea709588481915a7bc64e7402eb1dffc43ddfc470f0f79e7417f17c594fb29b73721953c1d1364117e5e3ac2201a8526fb3cf168ecb58e60a3c4529a33cc33af
7
+ data.tar.gz: 2a2bf5311c4098c2255ab698fcd5c6498350d2adb601b1d8aff2de79dcc5683b45ea2d9a157961b3f5153ad729cf61c1d5b4b100a0204ca3b62fbb1f07242c43
data/README.md CHANGED
@@ -12,6 +12,7 @@ Many providers offer APIs to send, receive, and parse email. MultiMail lets you
12
12
  * [Mandrill](http://mandrill.com/): [Example](#mandrill)
13
13
  * [Postmark](http://postmarkapp.com/): [Example](#postmark)
14
14
  * [SendGrid](http://sendgrid.com/): [Example](#sendgrid)
15
+ * MTA like [Postfix](http://en.wikipedia.org/wiki/Postfix_\(software\)) or [qmail](http://en.wikipedia.org/wiki/Qmail): [Example](#mta)
15
16
 
16
17
  ## Usage
17
18
 
@@ -37,7 +38,6 @@ With MultiMail, you send a message the same way you do with the [Mail](https://g
37
38
 
38
39
  ```ruby
39
40
  require 'multi_mail'
40
- require 'multi_mail/postmark/sender'
41
41
 
42
42
  message = Mail.new do
43
43
  delivery_method MultiMail::Sender::Postmark, :api_key => 'your-api-key'
@@ -67,39 +67,38 @@ Mail.defaults do
67
67
  end
68
68
  ```
69
69
 
70
- #### Track opens and clicks
70
+ #### Tagging
71
71
 
72
- Mailgun and Mandrill allow you to set open tracking and click tracking on a per-message basis:
72
+ Mailgun, Mandrill and Postmark allow you to tag messages in order to accumulate statistics by tag, which will be accessible through their user interface:
73
73
 
74
74
  ```ruby
75
75
  require 'multi_mail'
76
- require 'multi_mail/mailgun/sender'
77
76
 
78
77
  message = Mail.new do
79
- delivery_method MultiMail::Sender::Mailgun,
80
- :api_key => 'your-api-key',
81
- :domain => 'your-domain.mailgun.org',
82
- :track => {
83
- :opens => true,
84
- :clicks => false,
85
- }
78
+ delivery_method MultiMail::Sender::Mandrill, :api_key => 'your-api-key'
79
+
80
+ tag 'signup'
81
+ tag 'promotion'
82
+
86
83
  ...
87
84
  end
88
85
 
89
86
  message.deliver
90
87
  ```
91
88
 
92
- [Mailgun](http://documentation.mailgun.com/user_manual.html#tracking-clicks) and [Mandrill](http://help.mandrill.com/entries/21721852-Why-aren-t-clicks-being-tracked-) track whether a recipient has clicked a link in a message by rewriting its URL.
89
+ Mailgun accepts at most [3 tags](http://documentation.mailgun.com/user_manual.html#tagging) and Postmark at most one tag.
93
90
 
94
- If want to rewrite URLs in HTML parts only – leaving URLs as-is in text parts – use `:clicks => 'htmlonly'` if you are using Mailgun; if you are using Mandrill, do not set `:clicks` and instead configure click tracking globally in your [Mandrill sending options](https://mandrillapp.com/settings/sending-options).
91
+ #### Track opens and clicks
92
+
93
+ Mailgun and Mandrill allow you to set open tracking and click tracking on a per-message basis:
95
94
 
96
95
  ```ruby
97
96
  require 'multi_mail'
98
- require 'multi_mail/mandrill/sender'
99
97
 
100
98
  message = Mail.new do
101
- delivery_method MultiMail::Sender::Mandrill,
99
+ delivery_method MultiMail::Sender::Mailgun,
102
100
  :api_key => 'your-api-key',
101
+ :domain => 'your-domain.mailgun.org',
103
102
  :track => {
104
103
  :opens => true,
105
104
  :clicks => false,
@@ -110,6 +109,8 @@ end
110
109
  message.deliver
111
110
  ```
112
111
 
112
+ [Mailgun](http://documentation.mailgun.com/user_manual.html#tracking-clicks) and [Mandrill](http://help.mandrill.com/entries/21721852-Why-aren-t-clicks-being-tracked-) track whether a recipient has clicked a link in a message by rewriting its URL. If want to rewrite URLs in HTML parts only (leaving URLs as-is in text parts) use `:clicks => 'htmlonly'` if you are using Mailgun; if you are using Mandrill, do not set `:clicks` and instead configure click tracking globally in your [Mandrill sending options](https://mandrillapp.com/settings/sending-options).
113
+
113
114
  #### Inspect the API response
114
115
 
115
116
  Pass `:return_response => true` to `delivery_method` and use the `deliver!` method to send the message:
@@ -144,13 +145,21 @@ service = MultiMail::Receiver.new({
144
145
  })
145
146
  ```
146
147
 
148
+ If you are using an [Amazon S3 attachment store](http://docs.cloudmailin.com/receiving_email/attachments/), add a `:attachment_store => true` option. You must set the attachment store's permission setting to "Public Read".
149
+
150
+ ```ruby
151
+ service = MultiMail::Receiver.new({
152
+ :provider => 'cloudmailin',
153
+ :http_post_format => 'multipart',
154
+ :attachment_store => true,
155
+ })
156
+ ```
157
+
147
158
  See [Cloudmailin's documentation](http://docs.cloudmailin.com/http_post_formats/) for these additional parameters provided by the API:
148
159
 
149
160
  * `reply_plain`
150
161
  * `spf-result`
151
162
 
152
- **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 default `raw` format).
153
-
154
163
  ## Mailgun
155
164
 
156
165
  ### Incoming
@@ -189,8 +198,6 @@ See [Mailgun's documentation](http://documentation.mailgun.net/user_manual.html#
189
198
  ### Outgoing
190
199
 
191
200
  ```ruby
192
- require 'multi_mail/mailgun/sender'
193
-
194
201
  Mail.deliver do
195
202
  delivery_method MultiMail::Sender::Mailgun, :api_key => 'your-api-key', :domain => 'your-domain.mailgun.org'
196
203
  ...
@@ -199,7 +206,6 @@ end
199
206
 
200
207
  You may pass additional arguments to `delivery_method` to use Mailgun-specific features ([see docs](http://documentation.mailgun.com/api-sending.html)):
201
208
 
202
- * `o:tag`
203
209
  * `o:campaign`
204
210
  * `o:dkim`
205
211
  * `o:deliverytime`
@@ -217,7 +223,7 @@ service = MultiMail::Receiver.new({
217
223
  })
218
224
  ```
219
225
 
220
- To check that a request originates from Mandrill, add `:mandrill_webhook_key` and `:mandrill_webhook_url` options:
226
+ To check that a request originates from Mandrill, add `:mandrill_webhook_key` and `:mandrill_webhook_url` options (you can get your webhook key from [Mandrill's Webhooks Settings](https://mandrillapp.com/settings/webhooks)):
221
227
 
222
228
  ```ruby
223
229
  service = MultiMail::Receiver.new({
@@ -226,7 +232,6 @@ service = MultiMail::Receiver.new({
226
232
  :mandrill_webhook_url => 'http://example.com/post',
227
233
  })
228
234
  ```
229
- You can get your webhook key from [Mandrill's Webhooks Settings](https://mandrillapp.com/settings/webhooks).
230
235
 
231
236
  The default SpamAssassin score needed to flag an email as spam is `5`. Add a `:spamassassin_threshold` option to increase or decrease it:
232
237
 
@@ -249,8 +254,6 @@ See [Mandrill's documentation](http://help.mandrill.com/entries/22092308-What-is
249
254
  ### Outgoing
250
255
 
251
256
  ```ruby
252
- require 'multi_mail/mandrill/sender'
253
-
254
257
  Mail.deliver do
255
258
  delivery_method MultiMail::Sender::Mandrill, :api_key => 'your-api-key'
256
259
  ...
@@ -267,7 +270,6 @@ You may pass additional arguments to `delivery_method` to use Mandrill-specific
267
270
  * `bcc_address`
268
271
  * `tracking_domain` and `signing_domain`
269
272
  * `merge`, `global_merge_vars` and `merge_vars`
270
- * `tags`
271
273
  * `google_analytics_domains` and `google_analytics_campaign`
272
274
  * `metadata` and `recipient_metadata`
273
275
  * `async`
@@ -287,24 +289,17 @@ service = MultiMail::Receiver.new({
287
289
  See [Postmark's documentation](http://developer.postmarkapp.com/developer-inbound-parse.html#mailboxhash) for these additional parameters provided by the API:
288
290
 
289
291
  * `MailboxHash`
290
- * `MessageID`
291
292
  * `Tag`
292
293
 
293
294
  ### Outgoing
294
295
 
295
296
  ```ruby
296
- require 'multi_mail/postmark/sender'
297
-
298
297
  Mail.deliver do
299
298
  delivery_method MultiMail::Sender::Postmark, :api_key => 'your-api-key'
300
299
  ...
301
300
  end
302
301
  ```
303
302
 
304
- MultiMail depends on the `postmark` gem for its Postmark integration.
305
-
306
- You may also pass a `Tag` option to `delivery_method` ([see Postmark's documentation](http://developer.postmarkapp.com/developer-build.html#message-format)).
307
-
308
303
  ## SendGrid
309
304
 
310
305
  ### Incoming
@@ -334,8 +329,6 @@ See [SendGrid's documentation](http://sendgrid.com/docs/API_Reference/Webhooks/p
334
329
  ### Outgoing
335
330
 
336
331
  ```ruby
337
- require 'multi_mail/sendgrid/sender'
338
-
339
332
  Mail.deliver do
340
333
  delivery_method MultiMail::Sender::SendGrid, :api_user => 'username', :api_key => 'password'
341
334
  ...
@@ -344,6 +337,36 @@ end
344
337
 
345
338
  You may also pass a `x-smtpapi` option to `delivery_method` ([see SendGrid's documentation](http://sendgrid.com/docs/API_Reference/Web_API/mail.html)).
346
339
 
340
+ ## MTA
341
+
342
+ ### Incoming
343
+
344
+ If you are switching from an email API to Postfix or qmail, the simplest option is to continue sending messages to your application's webhook URL.
345
+
346
+ Your Postfix configuration may look like:
347
+
348
+ # /etc/postfix/virtual
349
+ incoming@myapp.com myappalias
350
+
351
+ # /etc/mail/aliases
352
+ myappalias: "| multi_mail_post --secret my-secret-string http://www.myapp.com/post"
353
+
354
+ Your qmail configuration may look like:
355
+
356
+ # /var/qmail/mailnames/myapp.com/.qmail-incoming
357
+ | multi_mail_post --secret my-secret-string http://www.myapp.com/post
358
+
359
+ In your application, you would use the `simple` provider:
360
+
361
+ ```ruby
362
+ service = MultiMail::Receiver.new({
363
+ :provider => 'simple',
364
+ :secret => 'my-secret-string',
365
+ })
366
+ ```
367
+
368
+ It's recommended to use a secret key, to ensure that the requests are sent by Postfix and qmail and not by other sources on the internet.
369
+
347
370
  ## Bugs? Questions?
348
371
 
349
372
  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.
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'securerandom'
5
+
6
+ require 'multi_mail'
7
+ require 'multi_mail/version'
8
+
9
+ secret = nil
10
+
11
+ opts = OptionParser.new do |opts|
12
+ opts.banner = "Usage: #{opts.program_name} [options] URL < INPUT"
13
+ opts.on('-s', '--secret SECRET', 'A secret key to ensure that requests are authentic') do |v|
14
+ secret = v
15
+ end
16
+ opts.on_tail('-h', '--help', 'Show this message') do
17
+ puts opts
18
+ exit
19
+ end
20
+ opts.on_tail('-v', '--version', 'Show version') do
21
+ puts MultiMail::VERSION
22
+ exit
23
+ end
24
+ end
25
+
26
+ rest = opts.parse!(ARGV)
27
+
28
+ if rest.empty?
29
+ puts opts.banner
30
+ exit
31
+ end
32
+
33
+ url = rest.shift
34
+
35
+ begin
36
+ url = URI.parse(url)
37
+ rescue URI::InvalidURIError
38
+ abort "#{url} is not a valid URI"
39
+ end
40
+
41
+ unless url.scheme && url.host
42
+ abort "#{url} is not a valid URI"
43
+ end
44
+
45
+ service = MultiMail::Receiver.new({
46
+ :provider => :simple,
47
+ :secret => secret,
48
+ })
49
+
50
+ params = {
51
+ 'message' => STDIN.read,
52
+ 'timestamp' => Time.now.to_i,
53
+ 'token' => SecureRandom.hash,
54
+ }
55
+
56
+ params['signature'] = service.signature(params) if secret
57
+
58
+ connection = Faraday.new(:url => "#{url.scheme}://#{url.host}:#{url.port}") do |conn|
59
+ conn.basic_auth url.user, url.password if url.user || url.password
60
+ conn.request :url_encoded
61
+ conn.adapter Faraday.default_adapter
62
+ end
63
+
64
+ response = connection.post(url.path, params)
65
+
66
+ if response.status != 200
67
+ headers = response.headers.map do |key,value|
68
+ "#{key.gsub(/\b([a-z])/) {$1.capitalize}}: #{value}"
69
+ end
70
+ abort (headers + ['', response.body]).join("\r\n")
71
+ end
@@ -0,0 +1,11 @@
1
+ module Mail
2
+ class Message
3
+ def tag(val = nil)
4
+ default :tag, val
5
+ end
6
+
7
+ def tag=(val)
8
+ header[:tag] = val
9
+ end
10
+ end
11
+ end
data/lib/multi_mail.rb CHANGED
@@ -9,24 +9,33 @@ require 'mail'
9
9
  require 'multimap'
10
10
  require 'rack'
11
11
 
12
+ require 'mail_ext/message'
13
+
12
14
  module MultiMail
13
15
  # @see http://rdoc.info/gems/fog/Fog/Errors
14
16
  class Error < StandardError; end
17
+
15
18
  # Raise if an incoming POST request is forged.
16
19
  class ForgedRequest < MultiMail::Error; end
17
- # Raise if an API key is invalid.
18
- class InvalidAPIKey < MultiMail::Error; end
19
20
 
21
+ # Raise if an outgoing request is invalid.
22
+ class InvalidRequest < MultiMail::Error; end
23
+ # Raise if an API key is invalid.
24
+ class InvalidAPIKey < InvalidRequest; end
20
25
  # Raise if a message is invalid.
21
- class InvalidMessage < MultiMail::Error; end
26
+ class InvalidMessage < InvalidRequest; end
27
+
28
+ # Raise if a message header is invalid
29
+ class InvalidHeader < InvalidMessage; end
30
+ # Raise if a message has no body.
31
+ class MissingBody < InvalidMessage; end
32
+
22
33
  # Raise if a message has no sender.
23
- class MissingSender < InvalidMessage; end
34
+ class MissingSender < InvalidHeader; end
24
35
  # Raise if a message has no recipients.
25
- class MissingRecipients < InvalidMessage; end
36
+ class MissingRecipients < InvalidHeader; end
26
37
  # Raise if a message has no subject.
27
- class MissingSubject < InvalidMessage; end
28
- # Raise if a message has no body.
29
- class MissingBody < InvalidMessage; end
38
+ class MissingSubject < InvalidHeader; end
30
39
 
31
40
  class << self
32
41
  # @return [RegExp] a message whose subject matches this pattern will be
@@ -97,3 +106,11 @@ require 'multi_mail/receiver'
97
106
  require 'multi_mail/message/base'
98
107
  require 'multi_mail/receiver/base'
99
108
  require 'multi_mail/sender/base'
109
+ require 'multi_mail/mailgun/message'
110
+ require 'multi_mail/mailgun/sender'
111
+ require 'multi_mail/mandrill/message'
112
+ require 'multi_mail/mandrill/sender'
113
+ require 'multi_mail/postmark/message'
114
+ require 'multi_mail/postmark/sender'
115
+ require 'multi_mail/sendgrid/message'
116
+ require 'multi_mail/sendgrid/sender'
@@ -9,15 +9,18 @@ module MultiMail
9
9
  class Cloudmailin
10
10
  include MultiMail::Receiver::Base
11
11
 
12
- recognizes :http_post_format
12
+ recognizes :http_post_format, :attachment_store
13
13
 
14
14
  # Initializes a Cloudmailin incoming email receiver.
15
15
  #
16
16
  # @param [Hash] options required and optional arguments
17
17
  # @option options [String] :http_post_format "multipart", "json" or "raw"
18
+ # @option options [Boolean] :attachment_store whether attachments have
19
+ # been sent to an attachment store
18
20
  def initialize(options = {})
19
21
  super
20
22
  @http_post_format = options[:http_post_format]
23
+ @attachment_store = options[:attachment_store]
21
24
  end
22
25
 
23
26
  # @param [Hash] params the content of Cloudmailin's webhook
@@ -33,6 +36,12 @@ module MultiMail
33
36
  # Extra Cloudmailin parameters.
34
37
  message['spf-result'] = params['envelope']['spf']['result']
35
38
 
39
+ if @attachment_store
40
+ params['attachments'].each do |_,attachment|
41
+ message.add_file(:filename => attachment['file_name'], :content => Faraday.get(attachment['url']).body)
42
+ end
43
+ end
44
+
36
45
  # Discard rest of `envelope`: `from`, `to`, `recipients`,
37
46
  # `helo_domain` and `remote_ip`.
38
47
  [message]
@@ -40,6 +49,7 @@ module MultiMail
40
49
  # Mail changes `self`.
41
50
  headers = self.class.multimap(params['headers'])
42
51
  http_post_format = @http_post_format
52
+ attachment_store = @attachment_store
43
53
  this = self
44
54
 
45
55
  message = Mail.new do
@@ -57,13 +67,24 @@ module MultiMail
57
67
  end
58
68
 
59
69
  if params.key?('attachments')
70
+ # Using something like lazy.rb will not prevent the HTTP request,
71
+ # because the Mail gem must be able to call #valid_encoding? on
72
+ # the attachment body (in Ruby 1.9).
60
73
  if http_post_format == 'json'
61
74
  params['attachments'].each do |attachment|
62
- add_file(:filename => attachment['file_name'], :content => Base64.decode64(attachment['content']))
75
+ if attachment_store
76
+ add_file(:filename => attachment['file_name'], :content => Faraday.get(attachment['url']).body)
77
+ else
78
+ add_file(:filename => attachment['file_name'], :content => Base64.decode64(attachment['content']))
79
+ end
63
80
  end
64
81
  else
65
82
  params['attachments'].each do |_,attachment|
66
- add_file(this.class.add_file_arguments(attachment))
83
+ if attachment_store
84
+ add_file(:filename => attachment['file_name'], :content => Faraday.get(attachment['url']).body)
85
+ else
86
+ add_file(this.class.add_file_arguments(attachment))
87
+ end
67
88
  end
68
89
  end
69
90
  end
@@ -9,7 +9,7 @@ module MultiMail
9
9
  hash = Multimap.new
10
10
  header_fields.each do |field|
11
11
  key = field.name.downcase
12
- unless %w(from to cc bcc subject message-id).include?(key)
12
+ unless %w(from to cc bcc subject tag).include?(key)
13
13
  hash["h:#{field.name}"] = field.value
14
14
  end
15
15
  end
@@ -32,6 +32,7 @@ module MultiMail
32
32
  # Returns the message as parameters to POST to Mailgun.
33
33
  #
34
34
  # @return [Hash] the message as parameters to POST to Mailgun
35
+ # @see http://documentation.mailgun.com/user_manual.html#tagging
35
36
  def to_mailgun_hash
36
37
  hash = Multimap.new
37
38
 
@@ -60,6 +61,10 @@ module MultiMail
60
61
  hash['html'] = body_html
61
62
  end
62
63
 
64
+ tags.each do |tag|
65
+ hash['o:tag'] = tag
66
+ end
67
+
63
68
  normalize(hash.merge(mailgun_attachments).merge(mailgun_headers).to_hash)
64
69
  end
65
70
  end