multi_mail 0.1.0 → 0.1.1

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