surenotify_rails 0.1.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2ff790368b45481416ff8b497480f097a3698ccc3c04272c2e70eaf2599a043
4
- data.tar.gz: d286745c8c606c2ba902584663586b3b8e40b17b7ee5145d36b46f5657f38c03
3
+ metadata.gz: 9dad2191d0d3d3b681ff8b9bff90a6bceda7beeb403178a97660ab08f88288f2
4
+ data.tar.gz: 01ec276d067483b5076f2acc95d9447a02f17c2f125d78987cddab30cc71e608
5
5
  SHA512:
6
- metadata.gz: b11f4579e8c6235c0084cc4739fd406b95f07129c67c246764d8e65b079c1d1b487d59c1da2e100abf734762de67d0e33ab96117c03324ad051e5fbf585f678e
7
- data.tar.gz: a5c327ba693a6a5e6cb36f388939f38cea51ce58be9b5499833f19b54200296d38106f1c56022f222241e1f8d270cc41281f1b7f7dba52d4ac06a3c05924f7ce
6
+ metadata.gz: 6f858a3f49158c9b398fa7782f35d51d858b2826a3b907812c97ea4dfbfaec306ac4d1b9c2792a98f579e2b8ff21118a52bfa63db82dc640f6e78d19a71763b7
7
+ data.tar.gz: 2773f90590e251631519e0dfd6d8bf69689911ed957c88d98afd8e1b73652f313eb4b1352a218b2cafa767ec30a5be99916fe3bb0dc685cade52e6004725b427
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # surenotify_rails (電子豹非官方 Rails 套件)
2
+
3
+ *surenotify_rails* is an Action Mailer adapter for using 電子豹 [Surenotify](https://newsleopard.com/surenotify) in Rails apps. It uses the [Surenotify API v1](https://newsleopard.com/surenotify/api/v1/) internally.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby >= 3.0
8
+ - Rails (Action Mailer) >= 6.0 — tested against Rails 6.1, 7.1, 7.2 and 8.0
9
+
10
+ ## Installing
11
+
12
+ In your `Gemfile`
13
+
14
+ ```ruby
15
+ gem 'surenotify_rails'
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ To configure your Surenotify credentials place the following code in the corresponding environment file (`development.rb`, `production.rb`...)
21
+
22
+ ```ruby
23
+ config.action_mailer.delivery_method = :surenotify
24
+ config.action_mailer.surenotify_settings = {
25
+ api_key: '<surenotify api key>',
26
+ unsubscribed_link: 'https://example.com/unsubscribe' # optional default
27
+ }
28
+ ```
29
+
30
+ Now you can send emails using plain Action Mailer:
31
+
32
+ ```ruby
33
+ email = mail from: 'Your Name <sender@email.com>', to: 'receiver@email.com', subject: 'this is an email'
34
+ ```
35
+
36
+ ### Per-recipient merge variables
37
+
38
+ Set variables per recipient address (rendered by Surenotify templates, max 100 characters per value):
39
+
40
+ ```ruby
41
+ def welcome_email
42
+ message = mail from: 'sender@email.com', to: 'receiver@email.com', subject: 'welcome'
43
+ message.surenotify_recipient_variables = {
44
+ 'receiver@email.com' => { 'name' => '小明', 'coupon' => 'ABC123' }
45
+ }
46
+ message
47
+ end
48
+ ```
49
+
50
+ ### Unsubscribe link
51
+
52
+ Override the default unsubscribe link per message:
53
+
54
+ ```ruby
55
+ message.surenotify_unsubscribed_link = 'https://example.com/custom-unsubscribe'
56
+ ```
57
+
58
+ ### Querying message events
59
+
60
+ ```ruby
61
+ client = SurenotifyRails::Client.new('<surenotify api key>')
62
+ client.events(status: 'open', recipient: 'receiver@email.com')
63
+ # => { "items" => [...] }
64
+ ```
65
+
66
+ Raises `SurenotifyRails::APIError` on API errors. Events are limited to the past 30 days (max 50 results) by the API.
67
+
68
+ ## Notes and limitations
69
+
70
+ - The Surenotify API allows at most **100 recipients per request**; `SurenotifyRails::TooManyRecipientsError` is raised beyond that (split batches yourself).
71
+ - The API has no cc/bcc concept: cc/bcc addresses are merged into `recipients` (deduplicated), so everyone receives an individual copy.
72
+ - Attachments are not supported by the Surenotify API.
73
+ - `deliver!` raises `SurenotifyRails::APIError` (with `code` and `body`) when the API rejects the request; it no longer silently returns the failed response.
74
+
75
+ Pull requests are welcomed
76
+
@@ -1,8 +1,10 @@
1
- require 'rest_client'
2
-
1
+ require "net/http"
2
+ require "json"
3
3
 
4
4
  module SurenotifyRails
5
5
  class Client
6
+ API_URL = "https://mail.surenotifyapi.com/v1".freeze
7
+
6
8
  attr_reader :api_key, :verify_ssl
7
9
 
8
10
  def initialize(api_key, verify_ssl = true)
@@ -11,25 +13,41 @@ module SurenotifyRails
11
13
  end
12
14
 
13
15
  def send_message(options)
14
- RestClient::Request.execute(
15
- method: :post,
16
- url: surenotify_url,
17
- payload: JSON::dump(options),
18
- verify_ssl: verify_ssl,
19
- headers: {
20
- content_type: 'application/json',
21
- accept: 'application/json',
22
- x_api_key: @api_key
23
- }
24
- )
16
+ uri = URI("#{API_URL}/messages")
17
+ request = Net::HTTP::Post.new(uri)
18
+ apply_headers(request)
19
+ request.body = JSON.dump(options)
20
+ perform(uri, request)
25
21
  end
26
22
 
27
- def surenotify_url
28
- api_url + "/messages"
23
+ def events(filters = {})
24
+ uri = URI("#{API_URL}/events")
25
+ uri.query = URI.encode_www_form(filters) unless filters.empty?
26
+ request = Net::HTTP::Get.new(uri)
27
+ apply_headers(request)
28
+ response = perform(uri, request)
29
+
30
+ unless response.is_a?(Net::HTTPSuccess)
31
+ raise APIError.new("Surenotify API error: #{response.code}",
32
+ code: response.code, body: response.body)
33
+ end
34
+
35
+ JSON.parse(response.body)
36
+ end
37
+
38
+ private
39
+
40
+ def apply_headers(request)
41
+ request["Content-Type"] = "application/json"
42
+ request["Accept"] = "application/json"
43
+ request["x-api-key"] = api_key
29
44
  end
30
45
 
31
- def api_url
32
- 'https://mail.surenotifyapi.com/v1'
46
+ def perform(uri, request)
47
+ http = Net::HTTP.new(uri.host, uri.port)
48
+ http.use_ssl = uri.scheme == "https"
49
+ http.verify_mode = verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
50
+ http.request(request)
33
51
  end
34
52
  end
35
53
  end
@@ -1,5 +1,6 @@
1
1
  module SurenotifyRails
2
2
  class Deliverer
3
+ MAX_RECIPIENTS = 100
3
4
 
4
5
  attr_accessor :settings
5
6
 
@@ -8,73 +9,70 @@ module SurenotifyRails
8
9
  end
9
10
 
10
11
  def api_key
11
- self.settings[:api_key]
12
+ settings[:api_key]
12
13
  end
13
14
 
14
15
  def verify_ssl
15
- #default value = true
16
- self.settings[:verify_ssl] != false
16
+ # default value = true
17
+ settings[:verify_ssl] != false
17
18
  end
18
19
 
19
20
  def deliver!(rails_message)
20
21
  response = surenotify_client.send_message build_surenotify_message_for(rails_message)
21
- if response.code == 200
22
- surenotify_message_id = JSON.parse(response.to_str)["id"]
23
- rails_message.message_id = surenotify_message_id
22
+ unless response.is_a?(Net::HTTPSuccess)
23
+ raise APIError.new("Surenotify API error: #{response.code}",
24
+ code: response.code, body: response.body)
24
25
  end
26
+ rails_message.message_id = JSON.parse(response.body)["id"]
25
27
  response
26
28
  end
27
29
 
28
30
  private
29
31
 
30
32
  def build_surenotify_message_for(rails_message)
31
- surenotify_message = build_basic_surenotify_message_for rails_message
32
- # transform_surenotify_attributes_from_rails rails_message, surenotify_message
33
- remove_empty_values surenotify_message
34
-
35
- surenotify_message
36
- end
37
-
38
- def transform_surenotify_attributes_from_rails(rails_message, surenotify_message)
39
- transform_reply_to rails_message, surenotify_message if rails_message.reply_to
40
- transform_surenotify_variables rails_message, surenotify_message
41
- transform_surenotify_options rails_message, surenotify_message
42
- transform_surenotify_recipient_variables rails_message, surenotify_message
43
- transform_custom_headers rails_message, surenotify_message
33
+ from = rails_message[:from].addrs.first
34
+ message = {
35
+ fromName: from.display_name,
36
+ fromAddress: from.address,
37
+ recipients: recipients_for(rails_message),
38
+ subject: rails_message.subject,
39
+ content: extract_html(rails_message),
40
+ unsubscribedLink: unsubscribed_link_for(rails_message)
41
+ }
42
+ remove_empty_values(message)
44
43
  end
45
44
 
46
- def build_basic_surenotify_message_for(rails_message)
47
- surenotify_message = {
48
- fromName: rails_message[:from].addrs.first.name,
49
- fromAddress: rails_message[:from].addrs.first.address,
50
- recipients: formatted_receivers(rails_message[:to]),
51
- subject: rails_message.subject,
52
- content: extract_html(rails_message)
53
- }
45
+ def recipients_for(rails_message)
46
+ addrs = [:to, :cc, :bcc].flat_map do |field|
47
+ rails_message[field] ? rails_message[field].addrs : []
48
+ end
49
+ recipients = addrs.uniq(&:address).map { |addr| recipient_for(addr, rails_message) }
54
50
 
55
- [:cc, :bcc].each do |key|
56
- surenotify_message[key] = rails_message[key].formatted if rails_message[key]
51
+ if recipients.empty?
52
+ raise NoRecipientsError, "the message has no recipients (to/cc/bcc are all empty)"
57
53
  end
58
54
 
59
- return surenotify_message if rails_message.attachments.empty?
60
-
61
- # RestClient requires attachments to be in file format, use a temp directory and the decoded attachment
62
- surenotify_message[:attachment] = []
63
- surenotify_message[:inline] = []
64
- rails_message.attachments.each do |attachment|
65
- # then add as a file object
66
- if attachment.inline?
67
- surenotify_message[:inline] << surenotifyRails::Attachment.new(attachment, encoding: 'ascii-8bit', inline: true)
68
- else
69
- surenotify_message[:attachment] << surenotifyRails::Attachment.new(attachment, encoding: 'ascii-8bit')
70
- end
55
+ if recipients.size > MAX_RECIPIENTS
56
+ raise TooManyRecipientsError,
57
+ "Surenotify API allows at most #{MAX_RECIPIENTS} recipients per request " \
58
+ "(got #{recipients.size})"
71
59
  end
72
60
 
73
- return surenotify_message
61
+ recipients
74
62
  end
75
63
 
76
- def transform_reply_to(rails_message, surenotify_message)
77
- surenotify_message['h:Reply-To'] = rails_message[:reply_to].formatted.first
64
+ def recipient_for(addr, rails_message)
65
+ recipient = {
66
+ name: addr.display_name || addr.address.split("@").first,
67
+ address: addr.address
68
+ }
69
+ variables = (rails_message.surenotify_recipient_variables || {})[addr.address]
70
+ recipient[:variables] = variables if variables
71
+ recipient
72
+ end
73
+
74
+ def unsubscribed_link_for(rails_message)
75
+ rails_message.surenotify_unsubscribed_link || settings[:unsubscribed_link]
78
76
  end
79
77
 
80
78
  # @see http://stackoverflow.com/questions/4868205/rails-mail-getting-the-body-as-plain-text
@@ -86,48 +84,15 @@ module SurenotifyRails
86
84
  end
87
85
  end
88
86
 
89
- def extract_text(rails_message)
90
- if rails_message.multipart?
91
- rails_message.text_part ? rails_message.text_part.body.decoded : nil
92
- else
93
- rails_message.content_type =~ /text\/plain/ ? rails_message.body.decoded : nil
94
- end
95
- end
96
-
97
- def transform_surenotify_variables(rails_message, surenotify_message)
98
- rails_message.surenotify_variables.try(:each) do |name, value|
99
- surenotify_message["v:#{name}"] = value
87
+ def remove_empty_values(message)
88
+ message.delete_if do |_key, value|
89
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
100
90
  end
101
91
  end
102
92
 
103
- def transform_surenotify_options(rails_message, surenotify_message)
104
- rails_message.surenotify_options.try(:each) do |name, value|
105
- surenotify_message["o:#{name}"] = value
106
- end
107
- end
108
-
109
- def transform_custom_headers(rails_message, surenotify_message)
110
- rails_message.surenotify_headers.try(:each) do |name, value|
111
- surenotify_message["h:#{name}"] = value
112
- end
113
- end
114
-
115
- def transform_surenotify_recipient_variables(rails_message, surenotify_message)
116
- surenotify_message['recipient-variables'] = rails_message.surenotify_recipient_variables.to_json if rails_message.surenotify_recipient_variables
117
- end
118
-
119
- def remove_empty_values(surenotify_message)
120
- surenotify_message.delete_if { |key, value| value.nil? or
121
- value.respond_to?(:empty?) && value.empty? }
122
- end
123
-
124
93
  def surenotify_client
125
94
  @surenotify_client ||= Client.new(api_key, verify_ssl)
126
95
  end
127
-
128
- def formatted_receivers(receivers)
129
- receivers.addrs.map { |r| { address: r.address } }
130
- end
131
96
  end
132
97
  end
133
98
 
@@ -0,0 +1,17 @@
1
+ module SurenotifyRails
2
+ class Error < StandardError; end
3
+
4
+ class TooManyRecipientsError < Error; end
5
+
6
+ class NoRecipientsError < Error; end
7
+
8
+ class APIError < Error
9
+ attr_reader :code, :body
10
+
11
+ def initialize(message, code: nil, body: nil)
12
+ super(message)
13
+ @code = code
14
+ @body = body
15
+ end
16
+ end
17
+ end
@@ -4,5 +4,6 @@ module Mail
4
4
  attr_accessor :surenotify_options
5
5
  attr_accessor :surenotify_recipient_variables
6
6
  attr_accessor :surenotify_headers
7
+ attr_accessor :surenotify_unsubscribed_link
7
8
  end
8
9
  end
@@ -1,3 +1,3 @@
1
1
  module SurenotifyRails
2
- VERSION = "0.1.0"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,8 +1,11 @@
1
- require 'action_mailer'
2
- require 'json'
1
+ require "action_mailer"
2
+ require "json"
3
3
 
4
-
5
- Dir[File.dirname(__FILE__) + '/surenotify_rails/*.rb'].each {|file| require file }
4
+ require "surenotify_rails/version"
5
+ require "surenotify_rails/errors"
6
+ require "surenotify_rails/mail_ext"
7
+ require "surenotify_rails/client"
8
+ require "surenotify_rails/deliverer"
6
9
 
7
10
  module SurenotifyRails
8
11
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surenotify_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leo Chen
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-01-25 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: actionmailer
@@ -16,70 +15,56 @@ dependencies:
16
15
  requirements:
17
16
  - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: 4.2.11
18
+ version: '6.0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: 4.2.11
25
+ version: '6.0'
27
26
  - !ruby/object:Gem::Dependency
28
- name: json
27
+ name: rake
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: 2.1.0
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: 2.1.0
41
- - !ruby/object:Gem::Dependency
42
- name: rest-client
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: 2.0.2
48
- type: :runtime
32
+ version: '0'
33
+ type: :development
49
34
  prerelease: false
50
35
  version_requirements: !ruby/object:Gem::Requirement
51
36
  requirements:
52
37
  - - ">="
53
38
  - !ruby/object:Gem::Version
54
- version: 2.0.2
39
+ version: '0'
55
40
  - !ruby/object:Gem::Dependency
56
41
  name: rspec
57
42
  requirement: !ruby/object:Gem::Requirement
58
43
  requirements:
59
44
  - - "~>"
60
45
  - !ruby/object:Gem::Version
61
- version: 2.14.1
46
+ version: '3.13'
62
47
  type: :development
63
48
  prerelease: false
64
49
  version_requirements: !ruby/object:Gem::Requirement
65
50
  requirements:
66
51
  - - "~>"
67
52
  - !ruby/object:Gem::Version
68
- version: 2.14.1
53
+ version: '3.13'
69
54
  - !ruby/object:Gem::Dependency
70
- name: rails
55
+ name: webmock
71
56
  requirement: !ruby/object:Gem::Requirement
72
57
  requirements:
73
- - - ">="
58
+ - - "~>"
74
59
  - !ruby/object:Gem::Version
75
- version: 4.2.11
60
+ version: '3.0'
76
61
  type: :development
77
62
  prerelease: false
78
63
  version_requirements: !ruby/object:Gem::Requirement
79
64
  requirements:
80
- - - ">="
65
+ - - "~>"
81
66
  - !ruby/object:Gem::Version
82
- version: 4.2.11
67
+ version: '3.0'
83
68
  description: An adapter for using Surenotify with Rails and Action Mailer
84
69
  email:
85
70
  - pominx@gmail.com
@@ -88,19 +73,20 @@ extensions: []
88
73
  extra_rdoc_files: []
89
74
  files:
90
75
  - MIT-LICENSE
91
- - README.rdoc
92
- - Rakefile
76
+ - README.md
93
77
  - lib/surenotify_rails.rb
94
- - lib/surenotify_rails/attachment.rb
95
78
  - lib/surenotify_rails/client.rb
96
79
  - lib/surenotify_rails/deliverer.rb
80
+ - lib/surenotify_rails/errors.rb
97
81
  - lib/surenotify_rails/mail_ext.rb
98
82
  - lib/surenotify_rails/version.rb
99
83
  homepage: https://github.com/pominx/surenotify_rails
100
84
  licenses:
101
85
  - MIT
102
- metadata: {}
103
- post_install_message:
86
+ metadata:
87
+ homepage_uri: https://github.com/pominx/surenotify_rails
88
+ source_code_uri: https://github.com/pominx/surenotify_rails
89
+ rubygems_mfa_required: 'true'
104
90
  rdoc_options: []
105
91
  require_paths:
106
92
  - lib
@@ -108,15 +94,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
94
  requirements:
109
95
  - - ">="
110
96
  - !ruby/object:Gem::Version
111
- version: '0'
97
+ version: '3.0'
112
98
  required_rubygems_version: !ruby/object:Gem::Requirement
113
99
  requirements:
114
100
  - - ">="
115
101
  - !ruby/object:Gem::Version
116
102
  version: '0'
117
103
  requirements: []
118
- rubygems_version: 3.1.4
119
- signing_key:
104
+ rubygems_version: 3.7.1
120
105
  specification_version: 4
121
106
  summary: Rails Action Mailer adapter for Surenotify (NewsLeopard)
122
107
  test_files: []
data/README.rdoc DELETED
@@ -1,3 +0,0 @@
1
- = SurenotifyRails
2
-
3
- This project rocks and uses MIT-LICENSE.
data/Rakefile DELETED
@@ -1,23 +0,0 @@
1
- begin
2
- require 'bundler/setup'
3
- rescue LoadError
4
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
- end
6
-
7
- require 'rdoc/task'
8
-
9
- RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'SurenotifyRails'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.rdoc')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
15
- end
16
-
17
- Bundler::GemHelper.install_tasks
18
-
19
- require 'rspec/core/rake_task'
20
-
21
- RSpec::Core::RakeTask.new(:spec)
22
-
23
- task default: :spec
@@ -1,16 +0,0 @@
1
- module SurenotifyRails
2
- class Attachment < StringIO
3
- attr_reader :original_filename, :content_type, :path
4
-
5
- def initialize (attachment, *rest)
6
- @path = ''
7
- if rest.detect {|opt| opt[:inline] }
8
- basename = @original_filename = attachment.cid
9
- else
10
- basename = @original_filename = attachment.filename
11
- end
12
- @content_type = attachment.content_type.split(';')[0]
13
- super attachment.body.decoded
14
- end
15
- end
16
- end